Skip to content

Commit

Permalink
[#145] Full local device test capability
Browse files Browse the repository at this point in the history
  • Loading branch information
susanw1 committed Dec 16, 2024
1 parent 70feb83 commit fd579f0
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 88 deletions.
152 changes: 130 additions & 22 deletions acceptance-tests/src/main/java/net/zscript/acceptance/CommandSteps.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package net.zscript.acceptance;

import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.OptionalInt;
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 org.assertj.core.api.Assertions.assertThat;

import io.cucumber.java.en.And;
Expand All @@ -12,61 +19,162 @@
import org.slf4j.LoggerFactory;

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.model.modules.base.CoreModule;

/**
* Steps that relate to building commands and decoding responses.
*/
public class CommandSteps {
private static final Logger LOG = LoggerFactory.getLogger(CommandSteps.class);

ConnectionSteps connectionSteps;
private ConnectionSteps connectionSteps;

ZscriptCommandBuilder<?> builder;
CoreModule c;
private Class<?> currentModule;
private ZscriptCommandBuilder<?> nodeBuilder;
private CommandSequenceNode prevCommandNode;

ResponseSequenceCallback responseResult;
private BiFunction<CommandSequenceNode, CommandSequenceNode, CommandSequenceNode> prevCombiner;

private List<ZscriptResponse> responses;
private int currentResponseIndex;

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

@Given("a {string} command from the {string} module in {string}")
@Given("the {word} command from the {word} module in {word}")
public void createCommandBuilder(String cmdName, String moduleName, String pkg) throws Exception {
Class<?> module = Class.forName(pkg + "." + uc(moduleName) + "Module");
builder = (ZscriptCommandBuilder<?>) module.getMethod(lc(cmdName) + "Builder", new Class<?>[0]).invoke(null);
if (currentModule != null) {
throw new IllegalStateException("A command is already in progress");
}
currentModule = getModule(moduleName, pkg);
nodeBuilder = getBuilder(currentModule, cmdName);
}

@Given("it has the {word} field set to {word}")
public void addIntFieldToCommand(String fieldName, String value) throws Exception {
nodeBuilder.getClass().getMethod("set" + uc(fieldName), int.class).invoke(nodeBuilder, Integer.decode(value));
}

@And("it has the field key {string} set to {word}")
public void addNumberFieldToCommandByKey(String key, String value) {
nodeBuilder.setField(asKey(key), Integer.decode(value));
}

private static char asKey(String key) {
if (key.length() != 1 || !Character.isUpperCase(key.charAt(0))) {
throw new IllegalArgumentException("field key must be a single uppercase character, not: " + key);
}
return key.charAt(0);
}

@Given("it has the {word} field set to enum value {word}")
public void addEnumFieldToCommand(String fieldName, String value) throws Exception {
// The fieldName is the name of the enum class inside the command builder
final Class<?> enumClass = stream(nodeBuilder.getClass().getDeclaredClasses())
.filter(cls -> cls.getSimpleName().equals(fieldName))
.findFirst().orElseThrow();
final Object enumValue = Enum.valueOf((Class<Enum>) enumClass, value);
nodeBuilder.getClass().getMethod("set" + uc(fieldName), enumClass).invoke(nodeBuilder, enumValue);
}

@And("if successful, it is followed by the {word} command")
public void andedWithCommand(String cmdName) throws Exception {
prevCommandNode = combineCommands();
nodeBuilder = getBuilder(currentModule, cmdName);
prevCombiner = CommandSequenceNode::andThen;
}

@And("if failed, it is followed by the {word} command")
public void oredWithCommand(String cmdName) throws Exception {
prevCommandNode = combineCommands();
nodeBuilder = getBuilder(currentModule, cmdName);
prevCombiner = CommandSequenceNode::onFail;

}

@Given("it has field {string} set to {int}")
public void addFieldToCommand(String fieldName, int value) throws Exception {
builder.getClass().getMethod("set" + uc(fieldName), new Class<?>[] { int.class }).invoke(builder, value);
private CommandSequenceNode combineCommands() {
ZscriptCommandNode<?> commandNode = nodeBuilder.build();
return prevCommandNode == null ? commandNode : prevCombiner.apply(prevCommandNode, commandNode);
}

@When("I send the command to the device and receive a response")
public void sendCommandToDeviceAndResponse() throws InterruptedException, ExecutionException {
Future<ResponseSequenceCallback> future = connectionSteps.getTestDeviceHandle().send(builder.build());
@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();
responseResult = future.get();
final ResponseSequenceCallback responseResult = future.get(10, SECONDS);
currentResponseIndex = 0;
responses = responseResult.getResponses();
responses.forEach(r -> LOG.debug("Response: {}", r));
}

@And("it should include a {string} field set to {word}")
@And("it should include a {word} field set to {word}")
public void shouldIncludeFieldSetTo(String fieldName, String value) throws Exception {
ZscriptResponse r = responseResult.getResponses().get(0);
int actualValue = (Integer) r.getClass().getMethod("get" + uc(fieldName), new Class<?>[] {}).invoke(r);
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);
final int actualValue = (Integer) currentResponse.getClass().getMethod("get" + uc(fieldName)).invoke(currentResponse);
assertThat(Integer.decode(value)).isEqualTo(actualValue);
}

@And("it should include a bigfield {string} set to {string}")
@And("it should include a bigfield {word} set to {string}")
public void shouldIncludeBigFieldSetTo(String fieldName, String value) throws Exception {
ZscriptResponse r = responseResult.getResponses().get(0);
String actualValue = (String) r.getClass().getMethod("get" + uc(fieldName) + "AsString", new Class<?>[] {}).invoke(r);
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);
final String actualValue = (String) currentResponse.getClass().getMethod("get" + uc(fieldName) + "AsString").invoke(currentResponse);
assertThat(value).isEqualTo(actualValue);
}

// Text-bashing to ensure conformance with naming convention
@And("it should include a field key {string} set to {word}")
public void shouldIncludeKeyedFieldSetTo(String key, String value) {
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);
final OptionalInt actualValue = currentResponse.getField((byte) asKey(key));
assertThat(Integer.decode(value)).isEqualTo(actualValue.orElseThrow());
}

/**
* This method moves us to the next response in the sequence.
*/
@And("as successful, there should be a following response")
public void shouldContainFollowingSuccessResponse() {
final ZscriptResponse currentResponse = responses.get(currentResponseIndex);

currentResponseIndex++;
assertThat(responses.size()).isGreaterThan(currentResponseIndex);
}

/**
* This method moves us to the next response in the sequence.
*/
@And("as failed, there should be a following response")
public void shouldContainFollowingFailResponse() {
currentResponseIndex++;
assertThat(responses.size()).isGreaterThan(currentResponseIndex);
}

/**
* Verify that there are no more responses in the sequence.
*/
@And("there should be no additional responses")
public void shouldContainNoMoreResponses() {
currentResponseIndex++;
assertThat(responses.size()).isEqualTo(currentResponseIndex);
}

private ZscriptCommandBuilder<?> getBuilder(Class<?> currentModule, String cmdName) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return (ZscriptCommandBuilder<?>) currentModule.getMethod(lc(cmdName) + "Builder").invoke(null);
}

private Class<?> getModule(String moduleName, String pkg) throws ClassNotFoundException {
return Class.forName(pkg + "." + uc(moduleName) + "Module");
}

// Text-bashing to ensure conformance with naming convention - change first char to lower case
private String lc(String s) {
return s.isBlank() ? s : Character.toLowerCase(s.charAt(0)) + s.substring(1);
}

// Text-bashing to ensure conformance with naming convention - change first char to upper case
private String uc(String s) {
return s.isBlank() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import io.cucumber.java.After;
import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
Expand All @@ -30,27 +31,29 @@ public class ConnectionSteps {

private final LocalDeviceSteps localDeviceSteps;

ZscriptModel model;
Device testDevice;
DirectConnection conn;
private ZscriptModel model;
private Device testDevice;
private DirectConnection conn;

final CollectingConsumer<ByteString> deviceBytesCollector = new CollectingConsumer<>();
final CollectingConsumer<byte[]> connectionBytesCollector = new CollectingConsumer<>();
private final CollectingConsumer<ByteString> deviceBytesCollector = new CollectingConsumer<>();
private final CollectingConsumer<byte[]> connectionBytesCollector = new CollectingConsumer<>();

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

public Device getTestDeviceHandle() {
return 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()) {
// do nothing, just keep progressing
LOG.trace("Progressed Zscript device...");
}
}

public Device getTestDeviceHandle() {
return testDevice;
}

private DirectConnection createConnection() {
if (Boolean.getBoolean("zscript.acceptance.conn.tcp")) {
throw new PendingException("Support for network interfaces is not implemented yet");
Expand All @@ -61,8 +64,20 @@ private DirectConnection createConnection() {
return localDeviceSteps.getLocalConnection();
}

@After
public void closeConnection() throws IOException {
if (conn != null) {
LOG.trace("Closing direct connection...");
conn.close();
LOG.trace("Direct connection closed");
}
}

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

final ZscriptNode node = ZscriptNode.newNode(conn);
Expand All @@ -72,12 +87,16 @@ public void deviceHandleConnected() {

@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");
}

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

@Then("connection should receive {string} in response")
@Then("connection should receive exactly {string} in response")
public void shouldReceiveConnectionResponse(String response) {
await().atMost(ofSeconds(10)).until(() -> !localDeviceSteps.progressZscriptDevice() && !connectionBytesCollector.isEmpty());
assertThat(connectionBytesCollector.next().get()).contains(byteStringUtf8(response + "\n").toByteArray());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.zscript.acceptance;

import io.cucumber.java.en.Given;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.zscript.javaclient.nodes.DirectConnection;
import net.zscript.javaclient.testing.LocalConnection;
Expand All @@ -10,7 +12,12 @@
import net.zscript.tokenizer.TokenBuffer;
import net.zscript.tokenizer.TokenRingBuffer;

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

private Zscript zscript;
private DirectConnection conn;

Expand All @@ -35,6 +42,10 @@ public void runningZscriptDevice() {

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

final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(sz);
final LocalChannel localChannel = new LocalChannel(buffer);

Expand All @@ -46,6 +57,9 @@ public void runningZscriptDevice(int sz) {
@Given("a connection to that local device")
public void connectionToDevice() {
LocalChannel channel = (LocalChannel) zscript.getChannels().get(0);
if (conn != null) {
throw new IllegalStateException("Connection already initialized");
}
conn = new LocalConnection(channel.getCommandOutputStream(), channel.getResponseInputStream());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ Feature: Zscript Connection

Background:
Given a locally running zscript device
And a connection to a local zscript device
And a connection to that local device

Scenario: Echo Command responds
When I send "Z1A5" as a command to the connection
Then connection should receive "!A5S" in response
Then connection should receive exactly "!A5S" in response

Scenario: Two Echo Command responds
When I send "Z1A1 & Z1A2" as a command to the connection
Then connection should receive "!A1S&A2S" in response
Then connection should receive exactly "!A1S&A2S" in response

This file was deleted.

Loading

0 comments on commit fd579f0

Please sign in to comment.