diff --git a/acceptance-tests/src/main/java/net/zscript/acceptance/EchoCommandSteps.java b/acceptance-tests/src/main/java/net/zscript/acceptance/BasicConnectionSteps.java similarity index 83% rename from acceptance-tests/src/main/java/net/zscript/acceptance/EchoCommandSteps.java rename to acceptance-tests/src/main/java/net/zscript/acceptance/BasicConnectionSteps.java index 5d9576ab..daacf215 100644 --- a/acceptance-tests/src/main/java/net/zscript/acceptance/EchoCommandSteps.java +++ b/acceptance-tests/src/main/java/net/zscript/acceptance/BasicConnectionSteps.java @@ -23,8 +23,8 @@ import net.zscript.tokenizer.TokenBuffer; import net.zscript.tokenizer.TokenRingBuffer; -public class EchoCommandSteps { - private static final Logger LOG = LoggerFactory.getLogger(EchoCommandSteps.class); +public class BasicConnectionSteps { + private static final Logger LOG = LoggerFactory.getLogger(BasicConnectionSteps.class); private Zscript zscript; @@ -69,25 +69,24 @@ public void connectionToZscriptDevice() { connectionToDevice(); } - @When("I send an echo command") - public void sendAnEchoCommand() throws IOException { - LOG.debug("WHEN I send an echo command"); + @When("I send {string} as a command") + public void sendAnEchoCommand(String command) throws IOException { + LOG.debug("WHEN I send " + command + " as a command"); CoreModule c; LOG.debug("Sending... "); - - conn.sendBytes(byteStringUtf8("Z1 A8\n").toByteArray()); + conn.sendBytes(byteStringUtf8(command + "\n").toByteArray()); // testDevice.send(from("Z1\n", UTF8_APPENDER), bytesCollector); LOG.debug("Sent"); } - @Then("I should receive an echo response") - public void shouldReceiveAnEchoResponse() { - LOG.debug("THEN I should receive an echo response"); + @Then("I should receive {string} in response") + public void shouldReceiveAThisResponse(String response) { + LOG.debug("THEN I should receive " + response + " in response"); await().atMost(ofSeconds(10)).until(() -> !zscript.progress() && !bytesCollector2.isEmpty()); LOG.trace("bytesCollector2 empty? {}", bytesCollector2.isEmpty()); - assertThat(bytesCollector2.next().get()).contains(byteStringUtf8("!A8S\n").toByteArray()); + assertThat(bytesCollector2.next().get()).contains(byteStringUtf8(response + "\n").toByteArray()); } } diff --git a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-echo.feature b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature similarity index 75% rename from acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-echo.feature rename to acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature index ec06d202..75b978db 100644 --- a/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-echo.feature +++ b/acceptance-tests/src/main/resources/features/net/zscript/acceptance/zscript-connection.feature @@ -8,5 +8,5 @@ Feature: Zscript Echo Command And a connection to the device Scenario: Echo Command responds - When I send an echo command - Then I should receive an echo response + When I send "Z1A5" as a command + Then I should receive "!A5S" in response diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/DirectConnection.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/DirectConnection.java index 76be03ea..8db12626 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/DirectConnection.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/DirectConnection.java @@ -14,7 +14,6 @@ import static net.zscript.util.ByteString.byteString; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.addressing.AddressedResponse; @@ -37,8 +36,6 @@ * DirectConnections are responsible for ensuring that split-up sequences of bytes are bundled into newline-terminated response sequences. */ public abstract class DirectConnection implements Connection, Closeable { - private final Logger LOG = LoggerFactory.getLogger(getClass()); - /** General thread pool for connection-related actions, eg blocking reads */ private static final ScheduledExecutorService CONNECTION_EXEC = Executors.newScheduledThreadPool(0); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannel.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannel.java index 7524be8b..987600b4 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannel.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannel.java @@ -107,9 +107,9 @@ private LocalChannel(TokenBuffer buffer, OutputStreamOutStream out, boolean c } /** - * An OutputStream for writing commands to this Channel. Ensure that {@link OutputStream#flush()} is called if the command data is sufficiently complete for processing. + * An OutputStream for writing commands to this Channel. Ensure that {@link OutputStream#flush()} is called when the command data is sufficiently complete for processing. * - * @return an OutputStream + * @return an OutputStream for writing commands */ public OutputStream getCommandOutputStream() { return connCommandOutput; @@ -118,9 +118,13 @@ public OutputStream getCommandOutputStream() { /** * An InputStream for reading responses from this Channel. * - * @return an InputStream, or null if this Channel has been given a responseHandler + * @return an InputStream + * @throws IllegalStateException if this {@link LocalChannel} was created with a callback */ public InputStream getResponseInputStream() { + if (connResponseInput == null) { + throw new IllegalStateException("this LocalChannel provides a response callback, not a stream"); + } return connResponseInput; } @@ -165,14 +169,7 @@ public void moveAlong() { */ private void commandReader() { try (InputStream s = channelCommandInput) { - byte[] buf = new byte[128]; - int len; - while ((len = s.read(buf)) != -1) { - if (len > 0) { - incoming.add(byteString(buf, 0, len)); - } - } - this.status = ChannelStatus.CLOSED; + readBytesToQueue(s); } catch (InterruptedIOException ex) { LOG.error("commandReader() interrupted"); this.status = ChannelStatus.INTERRUPTED; @@ -182,6 +179,18 @@ private void commandReader() { } } + // VisibleForTesting + void readBytesToQueue(InputStream s) throws IOException { + byte[] buf = new byte[128]; + int len; + while ((len = s.read(buf)) != -1) { + if (len > 0) { + incoming.add(byteString(buf, 0, len)); + } + } + this.status = ChannelStatus.CLOSED; + } + @Override public void channelInfo(CommandContext ctx) { ctx.getOutStream().writeBigFieldQuoted("LocalChannel"); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannelTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannelTest.java index 3951cb12..51c9f7a0 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannelTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/LocalChannelTest.java @@ -103,38 +103,29 @@ public void shouldRegisterFailureOnReadError() throws IOException { // Force the InputStream to fail final InputStream cmdStream = mock(InputStream.class); when(cmdStream.read(Mockito.any(byte[].class))).thenReturn(0).thenThrow(new IOException("my test exception")); - - LocalChannel brokenChannel = new LocalChannel(buffer, responseCollector); - - brokenChannel.moveAlong(); - brokenChannel.moveAlong(); - - final CommandContext ctx = mock(CommandContext.class); - final SequenceOutStream sequenceOutStream = brokenChannel.getOutStream(null); - when(ctx.getOutStream()).thenReturn(sequenceOutStream.asCommandOutStream()); - - sequenceOutStream.open(); - brokenChannel.channelInfo(ctx); - sequenceOutStream.close(); - + makeItRun(cmdStream); // E3 implies ERROR assertThat(responseCollector.next()).isPresent().get().isEqualTo("\"LocalChannel\"E3".getBytes(UTF_8)); } - // - // @Test - // public void shouldRegisterClosureOnEOF() throws IOException { - // // Force the InputStream to fail - // final InputStream cmdStream = mock(InputStream.class); - // when(cmdStream.read(Mockito.any(byte[].class))).thenReturn(-1); - // - // LocalChannel brokenChannel = new LocalChannel(buffer, cmdStream, responseCollector); - // - // makeItRun(brokenChannel); - // // E1 implies CLOSED - // assertThat(responseCollector.next()).isPresent().get().isEqualTo("\"LocalChannel\"E1".getBytes(UTF_8)); - // } - - private void makeItRun(LocalChannel brokenChannel) throws IOException { + + @Test + public void shouldRegisterClosureOnEOF() throws IOException { + // Force the InputStream to fail + final InputStream cmdStream = mock(InputStream.class); + when(cmdStream.read(Mockito.any(byte[].class))).thenReturn(-1); + + makeItRun(cmdStream); + // E1 implies CLOSED + assertThat(responseCollector.next()).isPresent().get().isEqualTo("\"LocalChannel\"E1".getBytes(UTF_8)); + } + + private void makeItRun(InputStream cmdStream) throws IOException { + LocalChannel brokenChannel = new LocalChannel(buffer, responseCollector) { + @Override + void readBytesToQueue(InputStream s) throws IOException { + super.readBytesToQueue(cmdStream); + } + }; brokenChannel.moveAlong(); brokenChannel.moveAlong();