Skip to content

Commit

Permalink
[#145] Acceptance steps and general logic and device tests
Browse files Browse the repository at this point in the history
  • Loading branch information
susanw1 committed Dec 26, 2024
1 parent 9b3ee79 commit 24b0530
Show file tree
Hide file tree
Showing 19 changed files with 412 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.zscript.acceptance;

import java.util.concurrent.atomic.AtomicReference;

import static java.time.Duration.ofSeconds;
import static net.zscript.util.ByteString.byteString;
import static net.zscript.util.ByteString.byteStringUtf8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.zscript.javaclient.commandPaths.CommandExecutionPath;
import net.zscript.javaclient.commandPaths.ResponseExecutionPath;
import net.zscript.javaclient.sequence.ResponseSequence;
import net.zscript.javaclient.tokens.ExtendingTokenBuffer;

public class BasicSequenceSteps {
private static final Logger LOG = LoggerFactory.getLogger(BasicSequenceSteps.class);
private final ConnectionSteps connectionSteps;

private ResponseExecutionPath actualExecutionPath;

public BasicSequenceSteps(ConnectionSteps connectionSteps) {
this.connectionSteps = connectionSteps;
}

@When("the command sequence {string} is sent to the device")
public void sendCommandSequence(String commandSequenceAsText) {
final AtomicReference<ResponseExecutionPath> response = new AtomicReference<>();

final ExtendingTokenBuffer buffer = ExtendingTokenBuffer.tokenize(byteStringUtf8(commandSequenceAsText), true);
final CommandExecutionPath commandPath = CommandExecutionPath.parse(connectionSteps.getModel(), buffer.getTokenReader().getFirstReadToken());

connectionSteps.getTestDeviceHandle().send(commandPath, response::set);
connectionSteps.progressLocalDeviceIfRequired();
await().atMost(ofSeconds(10)).until(() -> response.get() != null);

actualExecutionPath = response.get();
}

@Then("the response should match {string}")
public void responseShouldMatch(String responseSequenceAsText) {
final ExtendingTokenBuffer buffer = ExtendingTokenBuffer.tokenize(byteStringUtf8(responseSequenceAsText));
final ResponseSequence expectedResponseSeq = ResponseSequence.parse(buffer.getTokenReader().getFirstReadToken());

LOG.trace("sequence match: [actualExecutionPath={}, expectedResponseSeq = {}", actualExecutionPath, expectedResponseSeq);
assertThat(byteString(actualExecutionPath)).isEqualTo(byteString(expectedResponseSeq.getExecutionPath()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;

import static java.util.Arrays.stream;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toSet;
import static net.zscript.util.ByteString.byteStringUtf8;
import static org.assertj.core.api.Assertions.assertThat;

import io.cucumber.java.en.And;
Expand All @@ -18,14 +21,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.zscript.javaclient.commandPaths.ZscriptFieldSet;
import net.zscript.javaclient.commandbuilder.ZscriptResponse;
import net.zscript.javaclient.commandbuilder.commandnodes.CommandSequenceNode;
import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandBuilder;
import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode;
import net.zscript.javaclient.devices.ResponseSequenceCallback;
import net.zscript.javaclient.tokens.ExtendingTokenBuffer;
import net.zscript.tokenizer.ZscriptExpression;
import net.zscript.tokenizer.ZscriptField;

/**
* Steps that relate to building commands and decoding responses.
* Steps that relate to building commands and decoding responses using the generated command API.
*/
public class CommandSteps {
private static final Logger LOG = LoggerFactory.getLogger(CommandSteps.class);
Expand Down Expand Up @@ -103,8 +110,10 @@ private CommandSequenceNode combineCommands() {

@When("I send the command sequence to the device and receive a response sequence")
public void sendCommandToDeviceAndResponse() throws InterruptedException, ExecutionException, TimeoutException {
final Future<ResponseSequenceCallback> future = connectionSteps.getTestDeviceHandle().send(combineCommands());
connectionSteps.progressLocalDevice();
final CommandSequenceNode cmdSeq = combineCommands();
final Future<ResponseSequenceCallback> future = connectionSteps.getTestDeviceHandle().send(cmdSeq);
connectionSteps.progressLocalDeviceIfRequired();

final ResponseSequenceCallback responseResult = future.get(10, SECONDS);
currentResponseIndex = 0;
responses = responseResult.getResponses();
Expand Down Expand Up @@ -132,6 +141,40 @@ public void shouldIncludeKeyedFieldSetTo(String key, String value) {
assertThat(Integer.decode(value)).isEqualTo(actualValue.orElseThrow());
}

/**
* Checks for field equivalence (eg every listed field must have same value as that returned, though not necessarily same order or big-field configuration)
*
* @param responseExpression the expression to check against
*/
@And("it should match {string}")
public void shouldMatch(String responseExpression) {
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);
final ExtendingTokenBuffer tokenize = ExtendingTokenBuffer.tokenize(byteStringUtf8(responseExpression));
final ZscriptExpression expected = ZscriptFieldSet.fromTokens(tokenize.getTokenReader().getFirstReadToken());

final Set<ZscriptField> actualFields = currentResponse.expression().fields().collect(toSet());
final Set<ZscriptField> expectedFields = expected.fields().collect(toSet());
assertThat(actualFields).containsExactlyInAnyOrderElementsOf(expectedFields);

assertThat(currentResponse.expression().hasBigField()).isEqualTo(expected.hasBigField());
assertThat(currentResponse.expression().getBigFieldAsByteString()).isEqualTo(expected.getBigFieldAsByteString());
}

@And("it should contain at least {string}")
public void shouldContainAtLeast(String responseExpression) {
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);
final ExtendingTokenBuffer tokenize = ExtendingTokenBuffer.tokenize(byteStringUtf8(responseExpression));
final ZscriptExpression expected = ZscriptFieldSet.fromTokens(tokenize.getTokenReader().getFirstReadToken());

final Set<ZscriptField> actualFields = currentResponse.expression().fields().collect(toSet());
final Set<ZscriptField> expectedFields = expected.fields().collect(toSet());
assertThat(actualFields).containsAll(expectedFields);

if (expected.hasBigField()) {
assertThat(currentResponse.expression().getBigFieldAsByteString()).isEqualTo(expected.getBigFieldAsByteString());
}
}

/**
* This method moves us to the next response in the sequence.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;

import static java.time.Duration.ofSeconds;
import static java.util.Objects.requireNonNull;
import static net.zscript.javaclient.tokens.ExtendingTokenBuffer.tokenize;
import static net.zscript.util.ByteString.byteStringUtf8;
import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -29,7 +30,7 @@
public class ConnectionSteps {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionSteps.class);

private final LocalDeviceSteps localDeviceSteps;
private final LocalJavaReceiverSteps localJavaReceiverSteps;

private ZscriptModel model;
private Device testDevice;
Expand All @@ -38,30 +39,46 @@ public class ConnectionSteps {
private final CollectingConsumer<ByteString> deviceBytesCollector = new CollectingConsumer<>();
private final CollectingConsumer<byte[]> connectionBytesCollector = new CollectingConsumer<>();

public ConnectionSteps(LocalDeviceSteps localDeviceSteps) {
this.localDeviceSteps = localDeviceSteps;
public ConnectionSteps(LocalJavaReceiverSteps localJavaReceiverSteps) {
this.localJavaReceiverSteps = localJavaReceiverSteps;
}

public Device getTestDeviceHandle() {
return testDevice;
return requireNonNull(testDevice);
}

public void progressLocalDevice() {
// allow any "not progressing" to burn away, before trying to actually progress
await().atMost(ofSeconds(10)).until(localDeviceSteps::progressZscriptDevice);
while (localDeviceSteps.progressZscriptDevice()) {
LOG.trace("Progressed Zscript device...");
public ZscriptModel getModel() {
return requireNonNull(model);
}

public void progressLocalDeviceIfRequired() {
if (localJavaReceiverSteps != null) {
// allow any "not progressing" to burn away, before trying to actually progress
await().atMost(ofSeconds(10)).until(localJavaReceiverSteps::progressZscriptDevice);
while (localJavaReceiverSteps.progressZscriptDevice()) {
LOG.trace("Progressed Zscript device...");
}
}
}

private DirectConnection createConnection() {
if (Boolean.getBoolean("zscript.acceptance.conn.tcp")) {
throw new PendingException("Support for network interfaces is not implemented yet");
// If local device setup has been done, assume it overrides system-property settings
if (localJavaReceiverSteps.isConnected()) {
return localJavaReceiverSteps.getLocalConnection();
}
if (Boolean.getBoolean("zscript.acceptance.conn.serial")) {
throw new PendingException("Support for network interfaces is not implemented yet");

// Otherwise use those system settings
if (Boolean.getBoolean("zscript.acceptance.conn.tcp")) {
throw new PendingException("Support for tests over network interfaces is not implemented yet");
} else if (Boolean.getBoolean("zscript.acceptance.conn.serial")) {
throw new PendingException("Support for tests over serial interfaces is not implemented yet");
} else {
if (!Boolean.getBoolean("zscript.acceptance.conn.local.java")) {
LOG.warn("No acceptance test connection defined - defaulting to zscript.acceptance.conn.local.java");
}
localJavaReceiverSteps.runAndConnectToZscriptReceiver();
return localJavaReceiverSteps.getLocalConnection();
}
return localDeviceSteps.getLocalConnection();
}

@After
Expand All @@ -73,32 +90,40 @@ public void closeConnection() throws IOException {
}
}

@Given("a connected device handle")
public void deviceHandleConnected() {
if (testDevice != null || model != null || conn != null) {
@Given("a connection to the receiver")
public void connectionToReceiver() {
if (conn != null) {
throw new IllegalStateException("Device/model/connection already initialized");
}
conn = createConnection();
}

@Given("a connected device handle")
public void deviceHandleConnected() {
if (testDevice != null || model != null) {
throw new IllegalStateException("Device/model already initialized");
}
if (conn != null) {
throw new IllegalStateException("connection already initialized");
}
conn = createConnection();
final ZscriptNode node = ZscriptNode.newNode(conn);
model = ZscriptModel.standardModel();
testDevice = new Device(model, node);
}

@When("I send {string} as a command to the connection")
public void sendCommandToConnection(String command) throws IOException {
if (conn != null) {
throw new IllegalStateException("Connection already initialized");
if (conn == null) {
throw new IllegalStateException("Connection not initialized");
}

conn = createConnection();
conn.onReceiveBytes(connectionBytesCollector);
conn.sendBytes(byteStringUtf8(command + "\n").toByteArray());
}

@Then("connection should receive exactly {string} in response")
public void shouldReceiveConnectionResponse(String response) {
await().atMost(ofSeconds(10)).until(() -> !localDeviceSteps.progressZscriptDevice() && !connectionBytesCollector.isEmpty());
await().atMost(ofSeconds(10)).until(() -> !localJavaReceiverSteps.progressZscriptDevice() && !connectionBytesCollector.isEmpty());
assertThat(connectionBytesCollector.next().get()).contains(byteStringUtf8(response + "\n").toByteArray());
}

Expand All @@ -108,7 +133,7 @@ public void sendCommandToDevice(String command) {
}

private void progressDeviceUntilResponseReceived() {
await().atMost(ofSeconds(10)).until(() -> !localDeviceSteps.progressZscriptDevice() && !deviceBytesCollector.isEmpty());
await().atMost(ofSeconds(10)).until(() -> !localJavaReceiverSteps.progressZscriptDevice() && !deviceBytesCollector.isEmpty());
}

@Then("device should answer with response sequence {string}")
Expand All @@ -127,20 +152,4 @@ public void shouldReceiveThisResponse(int status, String field, String value) {
assertThat(response.getFields().getField(Zchars.Z_STATUS)).hasValue(status);
assertThat(response.getFields().getField(field.charAt(0))).hasValue(Integer.decode(value));
}

// @When("I send {string} to the device using the high level interface")
// public void sendCommandToDeviceUsingHLI(String command) {
// ExtendingTokenBuffer buf = new ExtendingTokenBuffer();
// Tokenizer tok = new Tokenizer(buf.getTokenWriter());
// byteStringUtf8(command).forEach(tok::accept);
// CommandSequence seq = CommandSequence.parse(model, buf.getTokenReader().getFirstReadToken(), false);
// testDevice.send(seq);
// }

// @Then("device should supply response sequence {string} in response")
// public void shouldReceiveThisResponse(String response) {
// await().atMost(ofSeconds(10)).until(() -> !zscript.progress() && !deviceBytesCollector.isEmpty());
// assertThat(deviceBytesCollector.next().get()).isEqualTo(byteStringUtf8(response));
// }

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.zscript.acceptance;

import java.util.Optional;

import io.cucumber.java.en.Given;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -13,37 +15,41 @@
import net.zscript.tokenizer.TokenRingBuffer;

/**
* Steps that relate to a locally running zscript device Java emulation, see {@link Zscript}.
* Steps that relate to a locally running zscript-receiving Java emulation, see {@link Zscript}.
*/
public class LocalDeviceSteps {
private static final Logger LOG = LoggerFactory.getLogger(LocalDeviceSteps.class);
public class LocalJavaReceiverSteps {
private static final Logger LOG = LoggerFactory.getLogger(LocalJavaReceiverSteps.class);

private Zscript zscript;
private DirectConnection conn;
private DirectConnection localConn;

public boolean progressZscriptDevice() {
return zscript.progress();
}

public Optional<DirectConnection> getLocalConnectionIfSet() {
return Optional.ofNullable(localConn);
}

public DirectConnection getLocalConnection() {
if (zscript == null) {
throw new IllegalStateException("You need 'a locally running zscript device'");
throw new IllegalStateException("You need 'a locally running zscript receiver'");
}
if (conn == null) {
throw new IllegalStateException("You need 'a connection to that local device'");
if (localConn == null) {
throw new IllegalStateException("You need 'a connection to that local receiver'");
}
return conn;
return localConn;
}

@Given("a locally running zscript device")
public void runningZscriptDevice() {
runningZscriptDevice(128);
@Given("a locally running zscript receiver")
public void runningZscriptReceiver() {
runningZscriptReceiver(128);
}

@Given("a locally running zscript device with buffer size {int}")
public void runningZscriptDevice(int sz) {
@Given("a locally running zscript receiver with buffer size {int}")
public void runningZscriptReceiver(int sz) {
if (zscript != null) {
throw new IllegalStateException("zscript device already initialized");
throw new IllegalStateException("zscript receiver already initialized");
}

final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(sz);
Expand All @@ -54,19 +60,25 @@ public void runningZscriptDevice(int sz) {
zscript.addChannel(localChannel);
}

@Given("a connection to that local device")
public void connectionToDevice() {
LocalChannel channel = (LocalChannel) zscript.getChannels().get(0);
if (conn != null) {
@Given("a connection to that local receiver")
public void connectionToLocalJavaReceiver() {
if (zscript == null) {
throw new IllegalStateException("You need 'a locally running zscript receiver'");
}
if (localConn != null) {
throw new IllegalStateException("Connection already initialized");
}
conn = new LocalConnection(channel.getCommandOutputStream(), channel.getResponseInputStream());
LocalChannel channel = (LocalChannel) zscript.getChannels().get(0);
localConn = new LocalConnection(channel.getCommandOutputStream(), channel.getResponseInputStream());
}

@Given("a connection to a local zscript device")
public void connectionToZscriptDevice() {
runningZscriptDevice();
connectionToDevice();
@Given("a connection to a local zscript receiver")
public void runAndConnectToZscriptReceiver() {
runningZscriptReceiver();
connectionToLocalJavaReceiver();
}

public boolean isConnected() {
return localConn != null;
}
}
Loading

0 comments on commit 24b0530

Please sign in to comment.