diff --git a/acceptance-tests/src/main/java/net/zscript/acceptance/BasicSequenceSteps.java b/acceptance-tests/src/main/java/net/zscript/acceptance/BasicSequenceSteps.java new file mode 100644 index 00000000..b09aa248 --- /dev/null +++ b/acceptance-tests/src/main/java/net/zscript/acceptance/BasicSequenceSteps.java @@ -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 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())); + } +} diff --git a/acceptance-tests/src/main/java/net/zscript/acceptance/CommandSteps.java b/acceptance-tests/src/main/java/net/zscript/acceptance/CommandSteps.java index 7eb4ace5..2364b6b8 100644 --- a/acceptance-tests/src/main/java/net/zscript/acceptance/CommandSteps.java +++ b/acceptance-tests/src/main/java/net/zscript/acceptance/CommandSteps.java @@ -3,6 +3,7 @@ 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; @@ -10,6 +11,8 @@ 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; @@ -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); @@ -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 future = connectionSteps.getTestDeviceHandle().send(combineCommands()); - connectionSteps.progressLocalDevice(); + final CommandSequenceNode cmdSeq = combineCommands(); + final Future future = connectionSteps.getTestDeviceHandle().send(cmdSeq); + connectionSteps.progressLocalDeviceIfRequired(); + final ResponseSequenceCallback responseResult = future.get(10, SECONDS); currentResponseIndex = 0; responses = responseResult.getResponses(); @@ -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 actualFields = currentResponse.expression().fields().collect(toSet()); + final Set 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 actualFields = currentResponse.expression().fields().collect(toSet()); + final Set 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. */ diff --git a/acceptance-tests/src/main/java/net/zscript/acceptance/ConnectionSteps.java b/acceptance-tests/src/main/java/net/zscript/acceptance/ConnectionSteps.java index bad2be16..c7bbcaef 100644 --- a/acceptance-tests/src/main/java/net/zscript/acceptance/ConnectionSteps.java +++ b/acceptance-tests/src/main/java/net/zscript/acceptance/ConnectionSteps.java @@ -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; @@ -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; @@ -38,30 +39,46 @@ public class ConnectionSteps { private final CollectingConsumer deviceBytesCollector = new CollectingConsumer<>(); private final CollectingConsumer 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 @@ -73,13 +90,23 @@ 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); @@ -87,18 +114,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"); + 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()); } @@ -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}") @@ -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)); - // } - } diff --git a/acceptance-tests/src/main/java/net/zscript/acceptance/LocalDeviceSteps.java b/acceptance-tests/src/main/java/net/zscript/acceptance/LocalJavaReceiverSteps.java similarity index 50% rename from acceptance-tests/src/main/java/net/zscript/acceptance/LocalDeviceSteps.java rename to acceptance-tests/src/main/java/net/zscript/acceptance/LocalJavaReceiverSteps.java index 75b5788c..591a83c6 100644 --- a/acceptance-tests/src/main/java/net/zscript/acceptance/LocalDeviceSteps.java +++ b/acceptance-tests/src/main/java/net/zscript/acceptance/LocalJavaReceiverSteps.java @@ -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; @@ -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 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); @@ -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; + } } diff --git a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature index 659b6521..40da5741 100644 --- a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature +++ b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature @@ -4,8 +4,7 @@ Feature: Zscript Connection SO THAT I can perform basic sanity checking and logic Background: - Given a locally running zscript device - And a connection to that local device + Given a connection to the receiver Scenario: Echo Command responds When I send "Z1A5" as a command to the connection diff --git a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-device.feature b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-javareceiver.feature similarity index 58% rename from acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-device.feature rename to acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-javareceiver.feature index 97f0d217..40e3f9dd 100644 --- a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-device.feature +++ b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-local-javareceiver.feature @@ -1,11 +1,11 @@ -Feature: Zscript Device Representation +Feature: Zscript Local Device Representation for Java Receiver AS A Zscript client developer - I WANT to define a Device - SO THAT my code has entities that represent the physical hardware architecture + I WANT to run a local Java Receiver + SO THAT I can easily test commands on a reference implementation Background: - Given a locally running zscript device - And a connection to that local device + Given a locally running zscript receiver + And a connection to that local receiver And a connected device handle Scenario: Simple textual command responds with text @@ -14,7 +14,7 @@ Feature: Zscript Device Representation Then device should answer with response status 0 and field A = 7 - Scenario: Command Sequence sent, and Response Sequence returns + Scenario: Command Sequence sent using generated command API, and Response Sequence is processed with generated response parser Given the capabilities command from the Core module in net.zscript.model.modules.base And it has the VersionType field set to 0 When I send the command sequence to the device and receive a response sequence @@ -24,14 +24,12 @@ Feature: Zscript Device Representation And there should be no additional responses - Scenario: Two Command Sequences sent, and Response Sequences returned + Scenario: Two Commands in sequence sent using generated command API, and Response Sequences is processed with generated response parser Given the capabilities command from the Core module in net.zscript.model.modules.base And it has the VersionType field set to enum value UserFirmware And if successful, it is followed by the echo command And it has the field key 'A' set to 0x14 - When I send the command sequence to the device and receive a response sequence - Then it should include a Status field set to 0 And it should include a Version field set to 1 And it should include a bigfield Ident set to "" @@ -39,3 +37,15 @@ Feature: Zscript Device Representation And it should include a Status field set to 0 And it should include a field key 'A' set to 0x14 And there should be no additional responses + + + Scenario: Two Command Sequences sent, and Response Sequences matching as required + Given the capabilities command from the Core module in net.zscript.model.modules.base + And it has the VersionType field set to enum value UserFirmware + And if successful, it is followed by the echo command + And it has the field key 'A' set to 0x14 + When I send the command sequence to the device and receive a response sequence + Then it should match "C3117 M1 V1 S0" + And as successful, there should be a following response + And it should match "A14 S0" + And there should be no additional responses diff --git a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-logic.feature b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-logic.feature new file mode 100644 index 00000000..7bbe4470 --- /dev/null +++ b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-logic.feature @@ -0,0 +1,103 @@ +Feature: Zscript Logic Handling + AS A Zscript client developer + I WANT to chain commands using consistent logical sequencing operators + SO THAT my commands are known to run in a predictable way + + Background: + Given a connected device handle + + + Scenario Outline: Sequences of empty commands + When the command sequence "" is sent to the device + Then the response should match "" + + # Empty command sequences joined by &, | and grouped () + Examples: + | commands | responses | description | + | | ! | empty command sequence | + | & | ! & | 2x ANDed empty commands | + | &&& | ! &&& | 4x ANDed empty commands | + | \| | ! \| | 2x ORed empty commands | + | \|\|\| | ! \|\|\| | 4x ORed empty commands | + | ( ) | ! && | empty parens - implies ∅ & ( ∅ ) & ∅ | + | ( & ) | ! &&& | ANDed empty commands in parens | + | ( \| ) | ! ( ) | ORed empty commands in parens | + | ( \| ) & | ! ( ) & | ORed empty commands in parens with ANDed successor | + | ( \| ) &( \| ) | ! ( ) & ( ) | 2x ORed empty commands in parens | + | \| ) &( \| | ! ) & ( | 2x ORed empty commands in minimal parens | + + + Scenario Outline: Sequences of successful echo commands + When the command sequence "" is sent to the device + Then the response should match "" + + # Echo command sequences joined by &, | and grouped () - all successful + Examples: + | commands | responses | description | + | Z1A1 | ! S0A1 | single successful cmd, should just execute | + | Z1A1 & Z1A2 | ! S0A1 & S0A2 | 2x ANDed cmds, should execute both | + | Z1A1 & Z1A2 & Z1A3 & Z1A4 | ! S0A1 & S0A2 & S0A3 & S0A4 | 4x ANDed cmds, should execute all | + | Z1A1 \| Z1A2 | ! S0A1 | 2x ORed cmds, should execute first only | + | Z1A1 \| Z1A2 \| Z1A3 \| Z1A4 | ! S0A1 | 4x ORed cmds, should execute first only | + | ( Z1A1 ) | ! & S0A1 & | Paren'ed cmd, should treat excess parens as implying empty cmd, "AND with ∅" | + | ( Z1A1 & Z1A2) | ! & S0A1 & S0A2 & | Paren'ed ANDed cmds, should treat excess parens as implying empty cmd, "AND with ∅" | + | ( Z1A1 \| Z1A2) | ! ( S0A1 ) | ORed echo commands in parens | + | ( Z1A1 \| Z1A2 ) & Z1A3 | ! ( S0A1 ) & S0A3 | ORed echo commands in parens with ANDed successor | + | ( Z1A1 \| Z1A2 ) & ( Z1A3 \| Z1A4 ) | ! ( S0A1 ) & ( S0A3 ) | 2x ORed echo commands in parens | + | Z1A1 \| Z1A2 ) & ( Z1A3 \| Z1A4 | ! S0A1 ) & ( S0A3 | 2x ORed echo commands in minimal parens | + + + Scenario Outline: Logic sequences of AND/ORed success/failure echo commands + When the command sequence "" is sent to the device + Then the response should match "" + + # Echo command sequences joined by &, | and grouped () - various combinations of failure using S2 "programmatic" fail + Examples: + | commands | responses | description | + | Z1A1 S2 | ! S2A1 | single echo, failed | + | Z1A1 S2 & Z1A2 | ! S2A1 | 2x ANDed cmds, 1st failed, should skip 2nd | + | Z1A1 & Z1A2 S2 | ! S0A1 & S2A2 | 2x ANDed cmds, 2nd failed, should run 1st and fail 2nd | + | Z1A1 S2 & Z1A2 & Z1A3 & Z1A4 | ! S2A1 | 4x ANDed cmds, 1st failed | + | Z1A1 & Z1A2 & Z1A3 S2 & Z1A4 | ! S0A1 & S0A2 & S2A3 | 4x ANDed cmds, 3rd failed, should run only 1/2/3 with 3rd failing | + | Z1A1 & Z1A2 & Z1A3 & Z1A4 S2 | ! S0A1 & S0A2 & S0A3 & S2A4 | 4x ANDed cmds, 4th failed, should run all with 4th failing | + | Z1A1 S2 \| Z1A2 | ! S2A1 \| S0A2 | 2x ORed cmds, 1st failed, should run both | + | Z1A1 \| Z1A2 S2 | ! S0A1 | 2x ORed cmds, 2nd failed, 1st success should prevent others | + | Z1A1 \| Z1A2 \| Z1A3 S2 \| Z1A4 | ! S0A1 | 4x ORed cmds, 3rd failed, 1st success should prevent others | + | Z1A1 S2 \| Z1A2 S2 \| Z1A3 S2 \| Z1A4 S2 | ! S2A1 \| S2A2 \| S2A3 \| S2A4 | 4x ORed cmds, ALL failed, should all execute | + + Scenario Outline: Logic sequences of AND/ORed success/failure echo commands with more complex parens + When the command sequence "" is sent to the device + Then the response should match "" + + Examples: + | commands | responses | description | + | ( Z1A1 S2) | ! & S2A1 | Single failing paren'ed cmd | + | ( Z1A1 S2 & Z1A2) | ! & S2A1 | Paren'ed 2x ANDed cmds, 1st failed | + | ( Z1A1 & Z1A2 S2) | ! & S0A1 & S2A2 | Paren'ed 2x ANDed cmds, 2nd failed | + | ( Z1A1 S2 \| Z1A2) | ! ( S2A1 \| S0A2 ) | ORed echo commands in parens | + | ( Z1A1 S2 \| Z1A2 ) & Z1A3 | ! ( S2A1 \| S0A2) & S0A3 | ORed echo commands in parens with ANDed successor | + | ( Z1A1 S2 \| Z1A2 S2 ) & Z1A3 | ! ( S2A1 \| S2A2 ) | ORed echo commands in parens with ANDed successor | + | ( Z1A1 S2 \| Z1A2 ) & ( Z1A3 S2 \| Z1A4 ) | ! ( S2A1 \| S0A2 ) & ( S2A3 \| S0A4 ) | 2x ORed cmds in parens, ANDed, should run first pair and then second pair | + | ( Z1A1 S2 \| Z1A2 ) \| ( Z1A3 S2 \| Z1A4 ) | ! ( S2A1 \| S0A2 ) | 2x ORed cmds in parens, ORed; should run 1st pair and skip 2nd pair | + + Scenario Outline: Logic sequences of AND/ORed echo commands including ERROR responses + When the command sequence "" is sent to the device + Then the response should match "" + + Examples: + | commands | responses | description | + | Z1A1 S13 | ! S13A1 | Single errored cmd, should return status | + | Z1A1 S13 & Z1A2 | ! S13A1 | Error before AND, should skip 2nd | + | Z1A1 S13 \| Z1A2 | ! S13A1 | Error before OR, should skip 2nd | + | Z1A1 S2 & Z1A2 S13 \| Z1A3 | ! S2A1 \| S0A3 | 3x cmds, 1st fails and error after AND, should run 1st+3rd only | + | Z1A1 S2 \| Z1A2 S13 \| Z1A3 | ! S2A1 \| S13A2 | 3x cmds, 1st fails OR 2nd err, should run 1st+2nd only | + + Scenario Outline: Logic sequences of AND/ORed echo commands including ERROR responses, in more complex parens + When the command sequence "" is sent to the device + Then the response should match "" + + # note error expressions don't bother closing parens (instant death) + Examples: + | commands | responses | description | + | ( Z1A1 S2 \| Z1A2 S13 ) & Z1A3 | ! ( S2A1 \| S13A2 | ORed echo commands in parens with ANDed successor | + | ( Z1A1 S2 \| Z1A2 S13 ) \| ( Z1A3 S2 \| Z1A4 ) | ! ( S2A1 \| S13A2 | 2x ORed cmds in parens, ORed; should run 1st pair and skip 2nd pair | diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ValidatingResponse.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ValidatingResponse.java index a6f6f1c2..2261f44e 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ValidatingResponse.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ValidatingResponse.java @@ -14,7 +14,8 @@ public ValidatingResponse(final ZscriptExpression expression, final byte[] requi this.requiredKeys = requiredKeys; } - public ZscriptExpression getExpression() { + @Override + public ZscriptExpression expression() { return expression; } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java index 61f9139e..9ca67eee 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java @@ -4,6 +4,7 @@ import net.zscript.model.components.Zchars; import net.zscript.model.components.ZscriptStatus; +import net.zscript.tokenizer.ZscriptExpression; public interface ZscriptResponse { /** @@ -13,6 +14,13 @@ public interface ZscriptResponse { */ boolean isValid(); + /** + * Accesses this response in expression form. + * + * @return a ZscriptExpression representation of this response + */ + ZscriptExpression expression(); + OptionalInt getField(byte key); default boolean succeeded() { diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java index 0e87a3c5..2feb45a6 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java @@ -12,6 +12,11 @@ public DefaultResponse(ZscriptExpression expression) { this.expression = expression; } + @Override + public ZscriptExpression expression() { + return expression; + } + @Override public boolean isValid() { return true; diff --git a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/DemoActivateCommand.java b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/DemoActivateCommand.java index 55c2429a..be6181f4 100644 --- a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/DemoActivateCommand.java +++ b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/DemoActivateCommand.java @@ -50,7 +50,7 @@ public ZscriptCommandBuilder setField(char key, int } } - static class DemoActivateCommandResponse extends ValidatingResponse { + public static class DemoActivateCommandResponse extends ValidatingResponse { private final boolean alreadyActivated; public DemoActivateCommandResponse(ZscriptExpression resp, byte[] requiredKeys, boolean alreadyActivated) { diff --git a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/ValidatingResponseTest.java b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/ValidatingResponseTest.java index 6e1f958d..cda81b9d 100644 --- a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/ValidatingResponseTest.java +++ b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/ValidatingResponseTest.java @@ -18,7 +18,7 @@ public void shouldValidateExpression() { Map.of((byte) 'A', 12, (byte) 'Z', 7, (byte) 'C', 0, (byte) 'D', 0x123)); ValidatingResponse vr1 = new ValidatingResponse(fieldSet, new byte[] { 'A', 'D' }); - assertThat(vr1.getExpression()).isSameAs(fieldSet); + assertThat(vr1.expression()).isSameAs(fieldSet); assertThat(vr1.isValid()).isTrue(); assertThat(vr1.getField((byte) 'A')).isPresent().hasValue(0xc); diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java index 334d568d..48b450ea 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java @@ -227,7 +227,7 @@ public boolean matchesResponses(ResponseExecutionPath resps) { try { compareResponses(resps); return true; - } catch (IllegalArgumentException ex) { + } catch (ZscriptParseException ex) { return false; } } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/tokens/ExtendingTokenBuffer.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/tokens/ExtendingTokenBuffer.java index a5eddaf0..7ff20f9c 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/tokens/ExtendingTokenBuffer.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/tokens/ExtendingTokenBuffer.java @@ -40,13 +40,16 @@ public static ExtendingTokenBuffer tokenize(ByteString sequence, boolean autoNew final Tokenizer tok = new Tokenizer(buf.getTokenWriter()); final TokenBufferFlags flags = buf.getTokenReader().getFlags(); - for (int i = 0; i < sequence.size(); i++) { + for (int i = 0, n = sequence.size(); i < n; i++) { tok.accept(sequence.get(i)); if (flags.getAndClearSeqMarkerWritten()) { - byte key = buf.getTokenReader().tokenIterator().stream().filter(ReadToken::isSequenceEndMarker).findFirst().orElseThrow().getKey(); + final byte key = buf.getTokenReader().tokenIterator().stream() + .filter(ReadToken::isSequenceEndMarker) + .findFirst().orElseThrow().getKey(); if (key != Tokenizer.NORMAL_SEQUENCE_END) { throw new ZscriptParseException("Syntax error [index=%d, err=%02x, text='%s']", i, key, sequence); - } else if (i < sequence.size() - 1) { + } else if (i < n - 1) { + // is this ok? There could be insignificant chars (eg spaces) after the '\n'. Better to read whole thing and check if there are following tokens. throw new ZscriptParseException("Extra characters found beyond first complete sequence[text='%s']", sequence); } return buf; @@ -59,17 +62,35 @@ public static ExtendingTokenBuffer tokenize(ByteString sequence, boolean autoNew return buf; } + /** + * Initializes a new Buffer with exactly the bytes supplied, representing data in token form (validated). The writer is preset to the end of the written bytes ready for + * appending. + * + * @param tokenBytes bytes representing a token sequence + * @return a token buffer containing the supplied bytes + * @see #getTokenBytes() + */ + public static ExtendingTokenBuffer fromTokenBytes(ByteString tokenBytes) { + return new ExtendingTokenBuffer(tokenBytes); + } + + /** + * Creates an empty token buffer with an initial capacity of 20 bytes. + */ public ExtendingTokenBuffer() { super(20); } - public ExtendingTokenBuffer(ByteString tokenBytes) { + /** + * @param tokenBytes bytes representing a token sequence + */ + private ExtendingTokenBuffer(ByteString tokenBytes) { super(validateTokens(tokenBytes).toByteArray(), tokenBytes.size()); } /** - * Validates a ByteString as containing a legitimate set of token bytes, as if generated by the tokenizer for some input. Note that the structure of the token sequence means - * that we can only really check that the Marker/token lengths create a valid chain to the end of the bytes. + * Validates a ByteString as containing a legitimate set of token bytes, as if generated by the tokenizer for some input. Note that the general structure of the token sequence + * means that we can only really check that the Marker/token lengths create a valid chain to the end of the bytes. * * @param tokenBytes the bytes to validate * @return the input ByteString, unaltered diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CommandExecutionPathRegenerationTest.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CommandExecutionPathRegenerationTest.java index a46592d3..49a49176 100644 --- a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CommandExecutionPathRegenerationTest.java +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CommandExecutionPathRegenerationTest.java @@ -1,8 +1,8 @@ package net.zscript.javaclient.commandPrinting; -import java.nio.charset.StandardCharsets; import java.util.stream.Stream; +import static net.zscript.util.ByteString.byteStringUtf8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.of; @@ -12,19 +12,14 @@ import net.zscript.javaclient.commandPaths.CommandExecutionPath; import net.zscript.javaclient.tokens.ExtendingTokenBuffer; -import net.zscript.tokenizer.Tokenizer; public class CommandExecutionPathRegenerationTest { @ParameterizedTest @MethodSource public void shouldProduceActionsForLogicalCommandSeries(final String input, final String output) { - ExtendingTokenBuffer bufferCmd = new ExtendingTokenBuffer(); - Tokenizer tokenizerCmd = new Tokenizer(bufferCmd.getTokenWriter(), 2); - for (byte b : input.getBytes(StandardCharsets.UTF_8)) { - tokenizerCmd.accept(b); - } - CommandExecutionPath path = CommandExecutionPath.parse(bufferCmd.getTokenReader().getFirstReadToken()); + ExtendingTokenBuffer bufferCmd = ExtendingTokenBuffer.tokenize(byteStringUtf8(input), false); + CommandExecutionPath path = CommandExecutionPath.parse(bufferCmd.getTokenReader().getFirstReadToken()); assertThat(path.toByteString().asString()).isEqualTo(output); } diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CompleteCommandGrapherTest.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CompleteCommandGrapherTest.java index 49ce85db..9de68d81 100644 --- a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CompleteCommandGrapherTest.java +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/CompleteCommandGrapherTest.java @@ -1,9 +1,9 @@ package net.zscript.javaclient.commandPrinting; -import java.nio.charset.StandardCharsets; -import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.stream.Collectors.joining; +import static net.zscript.util.ByteString.byteStringUtf8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.of; @@ -14,7 +14,6 @@ import net.zscript.ascii.AnsiCharacterStylePrinter; import net.zscript.javaclient.commandPaths.CommandExecutionPath; import net.zscript.javaclient.tokens.ExtendingTokenBuffer; -import net.zscript.tokenizer.Tokenizer; public class CompleteCommandGrapherTest { CommandGraph.GraphPrintSettings basicSettings = new CommandGraph.GraphPrintSettings(new StandardCommandGrapher.CommandPrintSettings(" ", VerbositySetting.NAME), true, 2, @@ -23,18 +22,16 @@ public class CompleteCommandGrapherTest { @ParameterizedTest @MethodSource public void shouldProduceGoodGraphs(final String input, final String output) { - ExtendingTokenBuffer bufferCmd = new ExtendingTokenBuffer(); - Tokenizer tokenizerCmd = new Tokenizer(bufferCmd.getTokenWriter(), 2); - for (byte b : input.getBytes(StandardCharsets.UTF_8)) { - tokenizerCmd.accept(b); - } - CommandExecutionPath path = CommandExecutionPath.parse(bufferCmd.getTokenReader().getFirstReadToken()); + final ExtendingTokenBuffer bufferCmd = ExtendingTokenBuffer.tokenize(byteStringUtf8(input), false); + final CommandExecutionPath path = CommandExecutionPath.parse(bufferCmd.getTokenReader().getFirstReadToken()); String ANSI_ANYTHING = "(\u001B[\\[\\d;]*m)*"; - assertThat(new StandardCommandGrapher().graph(path, null, basicSettings).generateString(new AnsiCharacterStylePrinter()).replaceAll(ANSI_ANYTHING, "").lines() - .map(s -> s.stripTrailing() + "\n").collect( - Collectors.joining())). - isEqualTo(output); + assertThat(new StandardCommandGrapher().graph(path, null, basicSettings) + .generateString(new AnsiCharacterStylePrinter()) + .replaceAll(ANSI_ANYTHING, "") + .lines() + .map(s -> s.stripTrailing() + "\n").collect(joining())) + .isEqualTo(output); } private static Stream shouldProduceGoodGraphs() { diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/Main.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/Main.java new file mode 100644 index 00000000..9b2fc29a --- /dev/null +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/commandPrinting/Main.java @@ -0,0 +1,25 @@ +package net.zscript.javaclient.commandPrinting; + +import static net.zscript.util.ByteString.byteStringUtf8; + +import net.zscript.ascii.AnsiCharacterStylePrinter; +import net.zscript.javaclient.commandPaths.CommandExecutionPath; +import net.zscript.javaclient.commandPaths.ResponseExecutionPath; +import net.zscript.javaclient.tokens.ExtendingTokenBuffer; + +public class Main { + public static void main(String[] args) { + CommandGraph.GraphPrintSettings basicSettings = new CommandGraph.GraphPrintSettings( + new StandardCommandGrapher.CommandPrintSettings(" ", VerbositySetting.FIELDS), true, 2, 80); + + ExtendingTokenBuffer bufferCmd = ExtendingTokenBuffer.tokenize(byteStringUtf8("Z1 A1 & Z1 B2 | Z1 C3"), false); + CommandExecutionPath cmdPath = CommandExecutionPath.parse(bufferCmd.getTokenReader().getFirstReadToken()); + + ExtendingTokenBuffer bufferResp = ExtendingTokenBuffer.tokenize(byteStringUtf8("SA1 & S1 | SC3"), false); + ResponseExecutionPath respPath = ResponseExecutionPath.parse(bufferResp.getTokenReader().getFirstReadToken()); + + String output = new StandardCommandGrapher().graph(cmdPath, null, basicSettings) + .generateString(new AnsiCharacterStylePrinter()); + System.out.println(output); + } +} diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/tokens/ExtendingTokenBufferTest.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/tokens/ExtendingTokenBufferTest.java index 104491e4..a47a3c99 100644 --- a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/tokens/ExtendingTokenBufferTest.java +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/tokens/ExtendingTokenBufferTest.java @@ -91,13 +91,13 @@ public void shouldTokenizeText() { @Test public void shouldCreateTokenBufferFromBytes() { - ExtendingTokenBuffer buf = new ExtendingTokenBuffer(byteString(new byte[] { '!', 0, 'S', 1, 5, Tokenizer.NORMAL_SEQUENCE_END })); + ExtendingTokenBuffer buf = ExtendingTokenBuffer.fromTokenBytes(byteString(new byte[] { '!', 0, 'S', 1, 5, Tokenizer.NORMAL_SEQUENCE_END })); assertThat(buf.getDataSize()).isEqualTo(6); } @Test public void shouldRejectTokenBufferFromIncorrectBytes() { - assertThatThrownBy(() -> new ExtendingTokenBuffer(byteString(new byte[] { '!', 0, 'S', 0, 5, Tokenizer.NORMAL_SEQUENCE_END }))) + assertThatThrownBy(() -> ExtendingTokenBuffer.fromTokenBytes(byteString(new byte[] { '!', 0, 'S', 0, 5, Tokenizer.NORMAL_SEQUENCE_END }))) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Not a valid token string"); } } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java index e4e4a9af..40837190 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java @@ -31,6 +31,7 @@ import net.zscript.javaclient.commandbuilder.notifications.NotificationId; import net.zscript.javaclient.nodes.ZscriptNode; import net.zscript.javaclient.sequence.CommandSequence; +import net.zscript.javaclient.sequence.ResponseSequence; import net.zscript.javaclient.tokens.ExtendingTokenBuffer; import net.zscript.model.ZscriptModel; import net.zscript.tokenizer.TokenBuffer; @@ -177,11 +178,7 @@ public void send(final byte[] cmdSeq, final Consumer callback) { * @param callback the response from that sequence */ public void send(final ByteString cmdSeq, final Consumer callback) { - final ExtendingTokenBuffer buffer = new ExtendingTokenBuffer(); - final Tokenizer tok = new Tokenizer(buffer.getTokenWriter()); - - cmdSeq.forEach(tok::accept); - + final ExtendingTokenBuffer buffer = ExtendingTokenBuffer.tokenize(cmdSeq); final TokenBuffer.TokenReader tokenReader = buffer.getTokenReader(); List sequenceMarkers = tokenReader.tokenIterator().stream().filter(ReadToken::isSequenceEndMarker).collect(toList()); @@ -204,6 +201,26 @@ public void send(final ByteString cmdSeq, final Consumer callback) { } } + /** + * Sends an unaddressed command sequence (optionally with locks and echo field), and posts the matching response sequence to the supplied callback. + * + * @param cmdSeq the sequence to send + * @param respCallback the handler for the response sequence + */ + public void send(final CommandSequence cmdSeq, final Consumer respCallback) { + node.send(cmdSeq, respCallback); + } + + /** + * Sends the supplied command path (without address, locks or echo), and posts the matching response to the supplied callback. + * + * @param cmdSeq the sequence to send + * @param respCallback the handler for the response sequence + */ + public void send(final CommandExecutionPath cmdSeq, final Consumer respCallback) { + node.send(cmdSeq, respCallback); + } + public static class CommandExecutionTask { private final CommandExecutionPath path; private final Consumer callback;