From 0688062dae04b8abb6aad8c09a4cdbd58e45fc8b Mon Sep 17 00:00:00 2001 From: susanw1 Date: Mon, 13 Jan 2025 19:39:04 +0000 Subject: [PATCH] [#175] Ensure correct Tokenizer address mode is used consistently, fix address and response parsing accordingly --- .../commandnodes/AndSequenceNode.java | 7 +- .../commandnodes/CommandSequenceNode.java | 7 +- .../commandnodes/OrSequenceNode.java | 15 ++- .../commandnodes/ZscriptCommandNode.java | 13 +-- .../commandNodes/SequenceNodeTest.java | 7 +- .../model/modules/base/CoreModuleTest.java | 31 +++--- .../test/JavaCommandBuilderBuildTest.java | 3 +- .../JavaCommandBuilderNotificationTest.java | 2 +- .../test/JavaCommandBuilderResponseTest.java | 2 +- .../addressing/CompleteAddressedResponse.java | 45 ++++----- .../javaclient/addressing/ZscriptAddress.java | 43 +++++---- .../javaclient/nodes/DirectConnection.java | 4 +- .../javaclient/sequence/ResponseSequence.java | 6 +- .../tokens/ExtendingTokenBuffer.java | 11 ++- .../CompleteAddressedResponseTest.java | 94 +++++++++++++++++++ .../addressing/ZscriptAddressTest.java | 63 +++++-------- .../tokens/ExtendingTokenBufferTest.java | 6 +- .../connection/LocalZscriptConnection.java | 2 +- .../javaclient/connection/SerialMain.java | 15 +-- .../javaclient/connection/TcpMain.java | 11 +-- .../scriptSpaces/ScriptSpaceWriteCommand.java | 2 +- .../scriptSpaces/ScriptSpace.java | 2 +- .../zscript/javareceiver/demoRun/Main.java | 4 +- .../zscript/javareceiver/demoRun2/Main.java | 2 +- .../semanticParser/EchoCommandTest.java | 2 +- .../semanticParser/LockEchoParserTest.java | 2 +- .../SemanticParserAddressingTest.java | 2 +- .../SemanticParserAsyncCommandTest.java | 2 +- .../SemanticParserMulticommandTest.java | 2 +- .../semanticParser/SemanticParserTest.java | 2 +- .../SemanticParserTokenWaitTest.java | 2 +- .../javareceiver/testing/LocalChannel.java | 2 +- .../javareceiver/testing/StringChannel.java | 2 +- .../java/net/zscript/tokenizer/Tokenizer.java | 29 ++++-- .../net/zscript/tokenizer/TokenizerTest.java | 2 +- .../tokenizer/ZscriptTokenExpressionTest.java | 2 +- .../javasimulator/zcode/i2c/I2cChannel.java | 2 +- .../java/net/zscript/util/ByteString.java | 34 +++++++ .../java/net/zscript/util/OptIterator.java | 54 +++++++++++ .../java/net/zscript/util/ByteStringTest.java | 9 ++ .../net/zscript/util/OptIteratorTest.java | 42 +++++++-- 41 files changed, 397 insertions(+), 192 deletions(-) create mode 100644 clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/CompleteAddressedResponseTest.java diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/AndSequenceNode.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/AndSequenceNode.java index 4d2556eb4..bafc9c638 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/AndSequenceNode.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/AndSequenceNode.java @@ -2,12 +2,12 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import net.zscript.javaclient.commandbuilder.defaultCommands.AbortCommandNode; import net.zscript.javaclient.commandbuilder.defaultCommands.BlankCommandNode; import net.zscript.javaclient.commandbuilder.defaultCommands.FailureCommandNode; import net.zscript.model.components.Zchars; +import net.zscript.util.ByteString.ByteStringBuilder; public class AndSequenceNode extends CommandSequenceNode { private final List elements; @@ -84,7 +84,8 @@ public List getChildren() { return elements; } - public String asString() { - return elements.stream().map(CommandSequenceNode::asString).collect(Collectors.joining("" + (char) Zchars.Z_ANDTHEN)); + @Override + public void appendTo(ByteStringBuilder builder) { + builder.appendJoining(elements, b -> b.appendByte(Zchars.Z_ANDTHEN)); } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/CommandSequenceNode.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/CommandSequenceNode.java index 0214f4c9c..fc1ab8851 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/CommandSequenceNode.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/CommandSequenceNode.java @@ -8,11 +8,12 @@ import net.zscript.javaclient.commandbuilder.defaultCommands.AbortCommandNode; import net.zscript.javaclient.commandbuilder.defaultCommands.BlankCommandNode; import net.zscript.javaclient.commandbuilder.defaultCommands.FailureCommandNode; +import net.zscript.util.ByteString.ByteAppendable; /** * An element of a Command Sequence under construction, representing a node in the Syntax Tree of a sequence during building. */ -public abstract class CommandSequenceNode implements Iterable> { +public abstract class CommandSequenceNode implements Iterable>, ByteAppendable { private CommandSequenceNode parent = null; void setParent(CommandSequenceNode parent) { @@ -149,7 +150,7 @@ public ZscriptCommandNode next() { if (!children.hasNext()) { throw new NoSuchElementException(); } - CommandSequenceNode node = children.next(); + final CommandSequenceNode node = children.next(); if (node instanceof ZscriptCommandNode) { return (ZscriptCommandNode) node; } @@ -159,6 +160,4 @@ public ZscriptCommandNode next() { } }; } - - public abstract String asString(); } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/OrSequenceNode.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/OrSequenceNode.java index 22516c78f..08d4408c5 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/OrSequenceNode.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/OrSequenceNode.java @@ -2,6 +2,10 @@ import java.util.List; +import net.zscript.model.components.Zchars; +import net.zscript.util.ByteString; +import net.zscript.util.ByteString.ByteAppendable; + public class OrSequenceNode extends CommandSequenceNode { final CommandSequenceNode before; final CommandSequenceNode after; @@ -22,7 +26,7 @@ protected boolean canFail() { boolean isCommand() { return false; } - + @Override CommandSequenceNode optimize() { if (!before.canFail()) { @@ -35,7 +39,12 @@ public List getChildren() { return List.of(before, after); } - public String asString() { - return "(" + before.asString() + "|" + after.asString() + ")"; + @Override + public void appendTo(ByteString.ByteStringBuilder builder) { + builder.appendByte(Zchars.Z_OPENPAREN) + .append((ByteAppendable) before) + .appendByte(Zchars.Z_ORELSE) + .append((ByteAppendable) after) + .appendByte(Zchars.Z_CLOSEPAREN); } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java index 3e2235b22..f2ce40b71 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java @@ -5,8 +5,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import static java.util.stream.Collectors.toList; import static net.zscript.javaclient.commandPaths.NumberField.fieldOf; import net.zscript.javaclient.commandPaths.BigField; @@ -14,7 +14,7 @@ import net.zscript.javaclient.commandbuilder.Respondable; import net.zscript.javaclient.commandbuilder.ZscriptResponse; import net.zscript.tokenizer.ZscriptExpression; -import net.zscript.util.ByteString; +import net.zscript.util.ByteString.ByteStringBuilder; public abstract class ZscriptCommandNode extends CommandSequenceNode implements Respondable { @@ -58,14 +58,12 @@ public List getChildren() { @Nonnull public ZscriptFieldSet asFieldSet() { - return ZscriptFieldSet.fromMap(bigFields.stream().map(BigField::getData).collect(Collectors.toList()), - bigFields.stream().map(BigField::isString).collect(Collectors.toList()), fields); + return ZscriptFieldSet.fromMap(bigFields.stream().map(BigField::getData).collect(toList()), + bigFields.stream().map(BigField::isString).collect(toList()), fields); } - @Nonnull @Override - public String asString() { - ByteString.ByteStringBuilder b = ByteString.builder(); + public void appendTo(ByteStringBuilder b) { for (int i = 'A'; i <= 'Z'; i++) { if (fields.get((byte) i) != null) { int value = fields.get((byte) i); @@ -75,6 +73,5 @@ public String asString() { for (BigField f : bigFields) { f.appendTo(b); } - return b.asString(); } } diff --git a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/commandNodes/SequenceNodeTest.java b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/commandNodes/SequenceNodeTest.java index 9fdcc7009..d1a5e74ea 100644 --- a/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/commandNodes/SequenceNodeTest.java +++ b/clients/java-client-lib/client-command-api/src/test/java/net/zscript/javaclient/commandbuilder/commandNodes/SequenceNodeTest.java @@ -1,13 +1,10 @@ package net.zscript.javaclient.commandbuilder.commandNodes; -import java.nio.charset.StandardCharsets; - import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import net.zscript.javaclient.commandbuilder.DemoActivateCommand; -import net.zscript.javaclient.commandbuilder.commandnodes.CommandSequenceNode; public class SequenceNodeTest { @Test @@ -16,7 +13,7 @@ public void shouldCompileBasicCommand() { .setVersionType(DemoCapabilitiesCommandBuilder.PLATFORM_FIRMWARE) .addBigField("abc") .build(); - assertThat(seq.asString()).isEqualTo("V2Z\"abc\""); + assertThat(seq.asStringUtf8()).isEqualTo("V2Z\"abc\""); } @Test @@ -28,7 +25,7 @@ public void shouldCompileAndedCommandSequence() { .andThen(DemoActivateCommand.builder() .setField('A', 0x1234) .build()); - assertThat(seq.asString()).isEqualTo("V2Z+0506077f&A1234Z2"); + assertThat(seq.asStringUtf8()).isEqualTo("V2Z+0506077f&A1234Z2"); } } diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/base/CoreModuleTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/base/CoreModuleTest.java index 7a4700be7..3333ff786 100644 --- a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/base/CoreModuleTest.java +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/base/CoreModuleTest.java @@ -6,52 +6,51 @@ import org.junit.jupiter.api.Test; -import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode; import net.zscript.javaclient.commandbuilder.ZscriptMissingFieldException; +import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode; +import net.zscript.model.modules.base.CoreModule.ActivateCommand.ActivateResponse; +import net.zscript.model.modules.base.CoreModule.CapabilitiesCommand.CapabilitiesResponse; +import net.zscript.model.modules.base.CoreModule.EchoCommand.EchoResponse; +import net.zscript.model.modules.base.CoreModule.ReadIdCommand.ReadIdResponse; public class CoreModuleTest { @Test public void shouldCreateCoreCapabilities() { - ZscriptCommandNode c = CoreModule.capabilitiesBuilder() + ZscriptCommandNode c = CoreModule.capabilitiesBuilder() .build(); - String ztext = c.asString(); - assertThat(ztext).isEqualTo("Z"); + assertThat(c.asStringUtf8()).isEqualTo("Z"); } @Test public void shouldCreateCoreActivate() { - ZscriptCommandNode c = CoreModule.activateBuilder() + ZscriptCommandNode c = CoreModule.activateBuilder() .build(); - String ztext = c.asString(); - assertThat(ztext).isEqualTo("Z2"); + assertThat(c.asStringUtf8()).isEqualTo("Z2"); } @Test public void shouldCreateCoreActivateWithField() { - ZscriptCommandNode c = CoreModule.ActivateCommand.builder() + ZscriptCommandNode c = CoreModule.ActivateCommand.builder() .setChallenge(3) .build(); - String ztext = c.asString(); - assertThat(ztext).isEqualTo("K3Z2"); + assertThat(c.asStringUtf8()).isEqualTo("K3Z2"); } @Test public void shouldCreateCoreEcho() { - ZscriptCommandNode c = CoreModule.echoBuilder() + ZscriptCommandNode c = CoreModule.echoBuilder() .setAny('J', 123) .build(); - String ztext = c.asString(); - assertThat(ztext).isEqualTo("J7bZ1"); + assertThat(c.asStringUtf8()).isEqualTo("J7bZ1"); } @Test public void shouldCreateCoreMatchCodeWithRequiredField() { - ZscriptCommandNode c = CoreModule.readIdBuilder() + ZscriptCommandNode c = CoreModule.readIdBuilder() .setIdType(TemporaryId) .setMatchId(new byte[] { 0x3a, 0x42 }) .build(); - String ztext = c.asString(); - assertThat(ztext).isEqualTo("IZ4+3a42"); + assertThat(c.asStringUtf8()).isEqualTo("IZ4+3a42"); } @Test diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderBuildTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderBuildTest.java index 7d5fa3937..09d9c74f3 100644 --- a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderBuildTest.java +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderBuildTest.java @@ -271,7 +271,6 @@ void shouldCreateCommandWithOptionalBytesAsEmptyBytes() { } private String build(ZscriptCommandBuilder b) { - // ISO8859, because we want an 8-bit byte in each char, and they *could* be non-printing / non-ascii if they're in Text - return b.build().asString(); + return b.build().asStringUtf8(); } } diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java index 738b7bcab..0486121c5 100644 --- a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderNotificationTest.java @@ -34,7 +34,7 @@ void shouldDefineNotificationClasses() { @Test void shouldCreateNotificationWithRequiredFields() { - final CompleteAddressedResponse car = CompleteAddressedResponse.parse(tokenize(byteStringUtf8("!234 Dab Lcd & Xef\n")).getTokenReader()); + final CompleteAddressedResponse car = CompleteAddressedResponse.parse(tokenize(byteStringUtf8("!234 Dab Lcd & Xef\n")).getTokenReader().getFirstReadToken()); final TestingModule.TestNtfBNotification.TestNtfBNotificationHandle handle = TestingModule.TestNtfBNotification.ID.newHandle(); diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java index dea15b73f..15218bdd1 100644 --- a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java @@ -25,7 +25,7 @@ public class JavaCommandBuilderResponseTest { final ExtendingTokenBuffer buffer = new ExtendingTokenBuffer(); - final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), true); final TokenReader tokenReader = buffer.getTokenReader(); @Test diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java index 48f4b790f..223cba276 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java @@ -1,16 +1,13 @@ package net.zscript.javaclient.addressing; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import static net.zscript.tokenizer.TokenBuffer.TokenReader; -import static net.zscript.tokenizer.TokenBuffer.TokenReader.ReadToken; - +import net.zscript.javaclient.ZscriptParseException; import net.zscript.javaclient.sequence.ResponseSequence; import net.zscript.model.components.Zchars; +import net.zscript.tokenizer.TokenBuffer.TokenReader.ReadToken; import net.zscript.tokenizer.Tokenizer; -import net.zscript.util.OptIterator; /** * Defines a Response sequence along with its address - this is the completed parse from a TokenReader. @@ -19,30 +16,22 @@ public class CompleteAddressedResponse { private final List addressSections; private final ResponseSequence content; - public static CompleteAddressedResponse parse(TokenReader reader) { - OptIterator iterEnding = reader.tokenIterator(); - for (Optional opt = iterEnding.next(); opt.isPresent(); opt = iterEnding.next()) { - if (opt.get().isSequenceEndMarker()) { - if (opt.get().getKey() != Tokenizer.NORMAL_SEQUENCE_END) { - throw new RuntimeException("Parse failed with Tokenizer error: " + Integer.toHexString(opt.get().getKey())); - } - } - } - OptIterator iter = reader.tokenIterator(); - List addresses = new ArrayList<>(); - ResponseSequence seq = null; - for (Optional opt = iter.next(); opt.isPresent(); opt = iter.next()) { - ReadToken token = opt.get(); - if (token.getKey() == Zchars.Z_ADDRESSING) { - addresses.add(ZscriptAddress.parse(token)); - } else { - seq = ResponseSequence.parse(token); - break; - } - } - if (seq == null) { - seq = ResponseSequence.empty(); + public static CompleteAddressedResponse parse(ReadToken start) { + final Optional endToken = start.tokenIterator().stream() + .filter(ReadToken::isSequenceEndMarker).findFirst(); + if (endToken.isEmpty()) { + throw new ZscriptParseException("Parse failed, no terminating sequence marker"); + } else if (endToken.get().getKey() != Tokenizer.NORMAL_SEQUENCE_END) { + throw new ZscriptParseException("Parse failed with Tokenizer error [marker=%s]", endToken.get()); } + + final List addresses = ZscriptAddress.parseAll(start); + final ResponseSequence seq = start.tokenIterator().stream() + .filter(t -> !Zchars.isAddressing(t.getKey())) + .findFirst() + .map(ResponseSequence::parse) + .orElseGet(ResponseSequence::empty); + return new CompleteAddressedResponse(addresses, seq); } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/ZscriptAddress.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/ZscriptAddress.java index 100c36db0..3cc8a414d 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/ZscriptAddress.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/ZscriptAddress.java @@ -1,8 +1,11 @@ package net.zscript.javaclient.addressing; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static java.util.stream.Collectors.toList; import static net.zscript.javaclient.commandPaths.NumberField.fieldOf; import static net.zscript.tokenizer.TokenBuffer.TokenReader.ReadToken; @@ -47,18 +50,31 @@ public static ZscriptAddress from(List addressParts) { return new ZscriptAddress(addressParts.stream().mapToInt(i -> i).toArray()); } - public static ZscriptAddress parse(ReadToken token) { - if (token.getKey() != Zchars.Z_ADDRESSING) { - throw new IllegalArgumentException("Cannot parse address from non-address fields"); + public static List parseAll(ReadToken start) { + if (start.getKey() != Zchars.Z_ADDRESSING) { + return Collections.emptyList(); } - // Note, tokenizer will only write one Address; subsequent addresses are inside the single token envelope. - int[] parts = token.tokenIterator().stream() - .takeWhile(t -> Zchars.isAddressing(t.getKey())) - .mapToInt(ReadToken::getData16) - .toArray(); + final List> addresses = new ArrayList<>(); + List elements = null; - return new ZscriptAddress(parts); + for (ReadToken token : start.tokenIterator().toIterable()) { + if (token.getKey() == Zchars.Z_ADDRESSING) { + elements = new ArrayList<>(); + addresses.add(elements); + elements.add(token.getData16()); + } else if (token.getKey() == Zchars.Z_ADDRESSING_CONTINUE) { + elements.add(token.getData16()); + } else { + break; + } + } + + return addresses.stream() + .map(list -> new ZscriptAddress(list.stream() + .mapToInt(i -> i) + .toArray())) + .collect(toList()); } private ZscriptAddress(int[] addr) { @@ -108,15 +124,6 @@ public final boolean equals(Object obj) { return Arrays.equals(addressParts, other.addressParts); } - /** - * Presents this address in conventional "@3.6a.1" format, including empty field for zero. - * - * @return the address as a string - */ - public String asString() { - return toByteString().asString(); - } - @Override public String toString() { return toStringImpl(); 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 ffa0247dc..b01c5ba9c 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 @@ -95,7 +95,7 @@ private void processReceivedBytes(byte[] bytes, Consumer resp receivedBytes.appendRaw(bytes); } else { final TokenBuffer buffer = new ExtendingTokenBuffer(); - final Tokenizer t = new Tokenizer(buffer.getTokenWriter(), 2); + final Tokenizer t = new Tokenizer(buffer.getTokenWriter(), true); // >0 incoming newlines, so we can use (and flush) any stashed bytes if (receivedBytes.size() > 0) { @@ -110,7 +110,7 @@ private void processReceivedBytes(byte[] bytes, Consumer resp t.accept(b); if (b == Zchars.Z_NEWLINE) { try { - AddressedResponse parsedResponse = CompleteAddressedResponse.parse(buffer.getTokenReader()).asResponse(); + AddressedResponse parsedResponse = CompleteAddressedResponse.parse(buffer.getTokenReader().getFirstReadToken()).asResponse(); responseHandler.accept(parsedResponse); } catch (Exception e) { parseFailHandler.accept(bytes, e); diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java index 71ac422b6..5ccd6e2cd 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java @@ -5,7 +5,7 @@ import net.zscript.javaclient.ZscriptParseException; import net.zscript.javaclient.commandPaths.ResponseExecutionPath; import net.zscript.model.components.Zchars; -import net.zscript.tokenizer.TokenBuffer; +import net.zscript.tokenizer.TokenBuffer.TokenReader.ReadToken; import net.zscript.tokenizer.TokenBufferIterator; import net.zscript.util.ByteString.ByteAppendable; import net.zscript.util.ByteString.ByteStringBuilder; @@ -25,13 +25,13 @@ public final class ResponseSequence implements ByteAppendable { private final int responseField; private final boolean timedOut; - public static ResponseSequence parse(TokenBuffer.TokenReader.ReadToken start) { + public static ResponseSequence parse(ReadToken start) { int echoField = -1; int responseField = -1; TokenBufferIterator iter = start.tokenIterator(); - TokenBuffer.TokenReader.ReadToken current = iter.next().orElse(null); + ReadToken current = iter.next().orElse(null); if (current == null || current.getKey() != Zchars.Z_RESPONSE_MARK) { throw new ZscriptParseException("Invalid response sequence without response marker [text=%s]", start); } 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 96a99cb1a..1adbf9e6d 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 @@ -36,7 +36,7 @@ public static ExtendingTokenBuffer tokenize(ByteString sequence) { * @throws ZscriptParseException if this buffer does not contain a single valid sequence */ public static ExtendingTokenBuffer tokenize(ByteString sequence, boolean autoNewline) { - return tokenizeImpl(sequence, autoNewline, true); + return tokenizeImpl(sequence, autoNewline, true, true); } /** @@ -48,12 +48,12 @@ public static ExtendingTokenBuffer tokenize(ByteString sequence, boolean autoNew * @return the buffer containing the tokens (possibly including error markers) */ public static ExtendingTokenBuffer tokenizeWithoutRejection(ByteString sequence) { - return tokenizeImpl(sequence, false, false); + return tokenizeImpl(sequence, false, false, true); } - private static ExtendingTokenBuffer tokenizeImpl(ByteString sequence, boolean autoNewline, boolean rejectErrors) { + private static ExtendingTokenBuffer tokenizeImpl(ByteString sequence, boolean autoNewline, boolean rejectErrors, boolean parseOutAddressing) { final ExtendingTokenBuffer buf = new ExtendingTokenBuffer(); - final Tokenizer tok = new Tokenizer(buf.getTokenWriter()); + final Tokenizer tok = new Tokenizer(buf.getTokenWriter(), Tokenizer.DEFAULT_MAX_NUMERIC_BYTES, parseOutAddressing); final TokenBufferFlags flags = buf.getTokenReader().getFlags(); for (int i = 0, n = sequence.size(); i < n; i++) { @@ -71,9 +71,10 @@ private static ExtendingTokenBuffer tokenizeImpl(ByteString sequence, boolean au return buf; } } - buf.getTokenWriter().endToken(); if (autoNewline) { tok.accept(Zchars.Z_NEWLINE); + } else { + buf.getTokenWriter().endToken(); } return buf; } diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/CompleteAddressedResponseTest.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/CompleteAddressedResponseTest.java new file mode 100644 index 000000000..b1f6ca3e5 --- /dev/null +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/CompleteAddressedResponseTest.java @@ -0,0 +1,94 @@ +package net.zscript.javaclient.addressing; + +import java.util.List; + +import static net.zscript.javaclient.tokens.ExtendingTokenBuffer.tokenize; +import static net.zscript.util.ByteString.byteStringUtf8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import net.zscript.javaclient.ZscriptParseException; +import net.zscript.javaclient.commandPaths.Response; +import net.zscript.tokenizer.TokenBuffer; + +class CompleteAddressedResponseTest { + @Test + public void shouldParseUnaddressedResponse() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("!2 A1S\n", "!2A1S"); + assertThat(r.getContent().getResponseValue()).isEqualTo(2); + assertThat(r.hasAddress(0)).isFalse(); + } + + @Test + public void shouldParseCompoundAddressedResponse() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("@3.2 ! A1S\n", "!A1S"); + assertThat(r.getAddressSection(0)).isEqualTo(ZscriptAddress.from(3, 2)); + assertThat(r.hasAddress(1)).isFalse(); + } + + @Test + public void shouldParseMultipleCompoundAddressedResponse() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("@3.2 @6.7 ! A1S\n", "!A1S"); + assertThat(r.getAddressSection(0)).isEqualTo(ZscriptAddress.from(3, 2)); + assertThat(r.getAddressSection(1)).isEqualTo(ZscriptAddress.from(6, 7)); + assertThat(r.hasAddress(2)).isFalse(); + } + + @Test + public void shouldParseEmptyResponse() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("!4 \n", "!4"); + assertThat(r.getContent().getResponseValue()).isEqualTo(4); + assertThat(r.hasAddress(0)).isFalse(); + + final List responses = r.getContent().getExecutionPath().getResponses(); + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getNext()).isNull(); + } + + @Test + public void shouldParseAndedEmptyResponses() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("!6 & \n", "!6&"); + assertThat(r.getContent().getResponseValue()).isEqualTo(6); + assertThat(r.hasAddress(0)).isFalse(); + } + + @Test + public void shouldParseEmptyResponsesWithAddress() { + CompleteAddressedResponse r = getAndCheckCommandExecutionPath("@12 !", "!"); + assertThat(r.getContent().getResponseValue()).isEqualTo(0); + assertThat(r.hasAddress(0)).isTrue(); + assertThat(r.getAddressSection(0)).isEqualTo(ZscriptAddress.from(0x12)); + } + + @Test + public void shouldFailResponseWithTokenizerError() { + assertThatThrownBy(() -> { + final TokenBuffer.TokenReader.ReadToken token = tokenize(byteStringUtf8("!2 \""), true).getTokenReader().getFirstReadToken(); + CompleteAddressedResponse.parse(token); + }).isInstanceOf(ZscriptParseException.class).hasMessageContaining("Tokenizer error"); + } + + @Test + public void shouldFailIncompleteResponse() { + assertThatThrownBy(() -> { + final TokenBuffer.TokenReader.ReadToken token = tokenize(byteStringUtf8("!2 \""), false).getTokenReader().getFirstReadToken(); + CompleteAddressedResponse.parse(token); + }).isInstanceOf(ZscriptParseException.class).hasMessageContaining("no terminating sequence"); + } + + @Test + public void shouldFailResponseWithoutResponseMark() { + assertThatThrownBy(() -> { + getAndCheckCommandExecutionPath("@1", ""); + }).isInstanceOf(ZscriptParseException.class).hasMessageContaining("without response marker"); + } + + private static CompleteAddressedResponse getAndCheckCommandExecutionPath(String cmds, String expectedSequence) { + TokenBuffer.TokenReader.ReadToken token = tokenize(byteStringUtf8(cmds), true).getTokenReader().getFirstReadToken(); + CompleteAddressedResponse cmdPath = CompleteAddressedResponse.parse(token); + assertThat(cmdPath.getContent().asStringUtf8()).isEqualTo(expectedSequence); + return cmdPath; + } +} diff --git a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/ZscriptAddressTest.java b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/ZscriptAddressTest.java index 718e75c27..32fee4e52 100644 --- a/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/ZscriptAddressTest.java +++ b/clients/java-client-lib/client-core/src/test/java/net/zscript/javaclient/addressing/ZscriptAddressTest.java @@ -1,49 +1,39 @@ package net.zscript.javaclient.addressing; -import java.nio.charset.StandardCharsets; import java.util.List; -import static java.util.stream.Collectors.toList; +import static net.zscript.javaclient.addressing.ZscriptAddress.address; +import static net.zscript.util.ByteString.byteStringUtf8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; -import net.zscript.model.components.Zchars; -import net.zscript.tokenizer.TokenBuffer; -import net.zscript.tokenizer.TokenBuffer.TokenReader; +import net.zscript.javaclient.tokens.ExtendingTokenBuffer; import net.zscript.tokenizer.TokenBuffer.TokenReader.ReadToken; -import net.zscript.tokenizer.TokenRingBuffer; -import net.zscript.tokenizer.Tokenizer; import net.zscript.util.ByteString; class ZscriptAddressTest { - TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); - TokenReader tokenReader = buffer.getTokenReader(); - @Test public void shouldConstructFromNumericParts() { ZscriptAddress address1 = ZscriptAddress.from(1, 0xa3b, 0, 0xffff); assertThat(address1.size()).isEqualTo(4); - assertThat(address1.asString()).isEqualTo("@1.a3b..ffff"); + assertThat(address1.asStringUtf8()).isEqualTo("@1.a3b..ffff"); ZscriptAddress address2 = ZscriptAddress.from(List.of(1, 0xa3b, 0, 0xffff)); assertThat(address2.size()).isEqualTo(4); - assertThat(address2.asString()).isEqualTo("@1.a3b..ffff"); + assertThat(address2.asStringUtf8()).isEqualTo("@1.a3b..ffff"); assertThat(address1).isEqualTo(address2); - - EqualsVerifier.forClass(ZscriptAddress.class).verify(); } @Test public void shouldParseSimpleAddress() { - tokenize("@12.34.ef Z1A2+34\n"); - ReadToken token = tokenReader.getFirstReadToken(); - final ZscriptAddress a = ZscriptAddress.parse(token); - assertThat(a.getAsInts()).containsExactly(0x12, 0x34, 0xef); + final ReadToken token = ExtendingTokenBuffer.tokenize(byteStringUtf8("@12.34.ef Z1A2+34")).getTokenReader().getFirstReadToken(); + final List a = ZscriptAddress.parseAll(token); + assertThat(a).hasSize(1); + assertThat(a.get(0).getAsInts()).containsExactly(0x12, 0x34, 0xef); } /** @@ -51,25 +41,20 @@ public void shouldParseSimpleAddress() { * token. That needs re-parsing in order to see the next level address. */ @Test - public void shouldNotParseMultipleAddresses() { - tokenize("@12.34.56 @78.ab Z1A2+34\n"); + public void shouldParseMultipleAddresses() { + final ReadToken token = ExtendingTokenBuffer.tokenize(byteStringUtf8("@12.34.56 @78.ab Z1A2+34")).getTokenReader().getFirstReadToken(); - List addresses = tokenReader.tokenIterator().stream() - .filter(t -> t.getKey() == Zchars.Z_ADDRESSING) - .map(ZscriptAddress::parse) - .collect(toList()); + List addresses = ZscriptAddress.parseAll(token); - // tokenReader.iterator().stream().forEach(System.out::println); - - assertThat(addresses).hasSize(1); + assertThat(addresses).hasSize(2); assertThat(addresses.get(0).getAsInts()).containsExactly(0x12, 0x34, 0x56); + assertThat(addresses.get(1).getAsInts()).containsExactly(0x78, 0xab); } @Test public void shouldNotParseNonAddresses() { - tokenize("A\n"); - ReadToken token = tokenReader.getFirstReadToken(); - assertThatIllegalArgumentException().isThrownBy(() -> ZscriptAddress.parse(token)); + final ReadToken token = ExtendingTokenBuffer.tokenize(byteStringUtf8("A\n")).getTokenReader().getFirstReadToken(); + assertThat(ZscriptAddress.parseAll(token)).isEmpty(); } @Test @@ -80,7 +65,7 @@ public void shouldFailOnOutOfRangeAddresses() { @Test public void shouldWriteToByteString() { - ZscriptAddress address = ZscriptAddress.address(2, 1); + ZscriptAddress address = address(2, 1); assertThat(address.toByteString()) .isEqualTo(ByteString.concat((object, builder) -> builder.appendUtf8(object), "@2.1")); } @@ -88,14 +73,18 @@ public void shouldWriteToByteString() { @Test public void shouldGetBufferLength() { // Number of bytes required in the TokenBuffer - 2 (tokens' key/len) + (0, 1, or 2) bytes, summed for each part. - ZscriptAddress address = ZscriptAddress.address(1, 0xa3b, 0, 0xffff); + ZscriptAddress address = address(1, 0xa3b, 0, 0xffff); // (a) 2+1, (b) 2+2, (c) 2+0, (d) 2+2 = 13 assertThat(address.getBufferLength()).isEqualTo(13); } - private void tokenize(String z) { - for (byte c : z.getBytes(StandardCharsets.ISO_8859_1)) { - tokenizer.accept(c); - } + @Test + public void shouldImplementEquals() { + EqualsVerifier.forClass(ZscriptAddress.class).verify(); + } + + @Test + public void shouldImplementToString() { + assertThat(address(1, 0xa3b, 0, 0xffff)).hasToString("ZscriptAddress:{'@1.a3b..ffff'}"); } } 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 a47a3c997..3b2a22769 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 @@ -44,7 +44,7 @@ public void tokenizerShouldRejectOverlongCommand() { @Test public void shouldTokenizeResponse() { final TokenBuffer buf = tokenize(byteStringUtf8("!S5")); - final AddressedResponse response = CompleteAddressedResponse.parse(buf.getTokenReader()).asResponse(); + final AddressedResponse response = CompleteAddressedResponse.parse(buf.getTokenReader().getFirstReadToken()).asResponse(); assertThat(response.hasAddress()).isFalse(); final ZscriptFieldSet fields = response.getContent().getExecutionPath().getFirstResponse().getFields(); assertThat(fields.getFieldCount()).isEqualTo(1); @@ -54,7 +54,7 @@ public void shouldTokenizeResponse() { @Test public void tokenizerShouldRejectBadResponse() { final TokenBuffer buf = tokenize(byteStringUtf8("S1 \n")); // missing '!' - assertThatThrownBy(() -> CompleteAddressedResponse.parse(buf.getTokenReader())) + assertThatThrownBy(() -> CompleteAddressedResponse.parse(buf.getTokenReader().getFirstReadToken())) .isInstanceOf(ZscriptParseException.class).hasMessageContaining("Invalid response sequence"); } @@ -73,7 +73,7 @@ public void shouldTokenizeIncompleteCommandWithNewline() { @Test public void shouldExpandBuffer() { final ExtendingTokenBuffer buf = new ExtendingTokenBuffer(); - final Tokenizer tok = new Tokenizer(buf.getTokenWriter()); + final Tokenizer tok = new Tokenizer(buf.getTokenWriter(), true); assertThat(buf.getDataSize()).isLessThan(500); tok.accept((byte) '"'); diff --git a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java index 5e70245c7..5d840315d 100644 --- a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java +++ b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java @@ -70,7 +70,7 @@ public void close() { }; zscript.addChannel(new ZscriptChannel(ringBuffer, outStream) { - final Tokenizer tokenizer = new Tokenizer(ringBuffer.getTokenWriter(), 2); + final Tokenizer tokenizer = new Tokenizer(ringBuffer.getTokenWriter(), false); byte[] current = null; int pos = 0; diff --git a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/SerialMain.java b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/SerialMain.java index 25aa8824f..72ce125dc 100644 --- a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/SerialMain.java +++ b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/SerialMain.java @@ -2,8 +2,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; import com.fazecast.jSerialComm.SerialPort; @@ -15,7 +13,7 @@ import net.zscript.javaclient.sequence.CommandSequence; import net.zscript.javaclient.tokens.ExtendingTokenBuffer; import net.zscript.model.modules.base.CoreModule; -import net.zscript.tokenizer.Tokenizer; +import net.zscript.util.ByteString; class SerialMain { public static void main(String[] args) throws IOException, InterruptedException { @@ -90,14 +88,11 @@ public static void main(String[] args) throws IOException, InterruptedException System.out.println("Response: " + response.toString()); }); for (int i = 0; i < 10; i++) { - byte[] ba = CoreModule.echoBuilder().setAny('A', 35).build().asString().getBytes(StandardCharsets.UTF_8); - ExtendingTokenBuffer buffer = new ExtendingTokenBuffer(); - Tokenizer t = new Tokenizer(buffer.getTokenWriter()); - for (byte b : ba) { - t.accept(b); - } + final ByteString command = CoreModule.echoBuilder().setAny('A', 35).build().toByteString(); + ExtendingTokenBuffer buffer = ExtendingTokenBuffer.tokenize(command); + conn.send(new AddressedCommand(CommandSequence.from(CommandExecutionPath.parse(buffer.getTokenReader().getFirstReadToken()), -1))); - System.out.println("Sending: " + Arrays.toString(ba)); + System.out.println("Sending: " + command); Thread.sleep(1000); } } catch (Exception e) { diff --git a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/TcpMain.java b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/TcpMain.java index a355b8209..f3afba657 100644 --- a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/TcpMain.java +++ b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/TcpMain.java @@ -1,7 +1,6 @@ package net.zscript.javaclient.connection; import java.net.Socket; -import java.nio.charset.StandardCharsets; import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.commandPaths.CommandExecutionPath; @@ -11,7 +10,7 @@ import net.zscript.javaclient.sequence.CommandSequence; import net.zscript.javaclient.tokens.ExtendingTokenBuffer; import net.zscript.model.modules.base.CoreModule; -import net.zscript.tokenizer.Tokenizer; +import net.zscript.util.ByteString; class TcpMain { public static void main(String[] args) throws Exception { @@ -23,11 +22,9 @@ public static void main(String[] args) throws Exception { conn.onReceive((response) -> { System.out.println("Response: " + response.toString()); }); - ExtendingTokenBuffer buffer = new ExtendingTokenBuffer(); - Tokenizer t = new Tokenizer(buffer.getTokenWriter(), 2); - for (byte b : CoreModule.echoBuilder().setAny('A', 1234).build().asString().getBytes(StandardCharsets.UTF_8)) { - t.accept(b); - } + + final ByteString command = CoreModule.echoBuilder().setAny('A', 35).build().toByteString(); + ExtendingTokenBuffer buffer = ExtendingTokenBuffer.tokenize(command); conn.send(new AddressedCommand(CommandSequence.from(CommandExecutionPath.parse(buffer.getTokenReader().getFirstReadToken()), -1))); } } diff --git a/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/modules/scriptSpaces/ScriptSpaceWriteCommand.java b/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/modules/scriptSpaces/ScriptSpaceWriteCommand.java index b60ea3520..584b39824 100644 --- a/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/modules/scriptSpaces/ScriptSpaceWriteCommand.java +++ b/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/modules/scriptSpaces/ScriptSpaceWriteCommand.java @@ -36,7 +36,7 @@ public static void execute(List spaces, CommandContext ctx) { writer = target.append(); } - Tokenizer tok = new Tokenizer(writer.getTokenWriter(), 2); + Tokenizer tok = new Tokenizer(writer.getTokenWriter(), false); for (Iterator iterator = ctx.bigFieldDataIterator(); iterator.hasNext(); ) { tok.accept(iterator.next()); } diff --git a/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/scriptSpaces/ScriptSpace.java b/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/scriptSpaces/ScriptSpace.java index 916c16c38..0e38cfec0 100644 --- a/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/scriptSpaces/ScriptSpace.java +++ b/receivers/jvm/java-receiver/src/main/java/net/zscript/javareceiver/scriptSpaces/ScriptSpace.java @@ -23,7 +23,7 @@ public class ScriptSpace implements ActionSource { public static ScriptSpace from(Zscript z, String str) { ScriptSpaceTokenBuffer buffer = new ScriptSpaceTokenBuffer(); ScriptSpaceWriterTokenBuffer writer = buffer.fromStart(); - Tokenizer tok = new Tokenizer(writer.getTokenWriter(), 2); + Tokenizer tok = new Tokenizer(writer.getTokenWriter(), false); for (byte b : str.getBytes(StandardCharsets.UTF_8)) { tok.accept(b); } diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun/Main.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun/Main.java index 4bac297e2..11fd00ad2 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun/Main.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun/Main.java @@ -31,8 +31,8 @@ public static void main(String[] args) { TokenRingBuffer rbuff = TokenRingBuffer.createBufferWithCapacity(100); SequenceOutStream out = new OutputStreamOutStream<>(System.out); zscript.addChannel(new ZscriptChannel(rbuff, out) { - final Tokenizer in = new Tokenizer(rbuff.getTokenWriter(), 2); - private int i = 0; + final Tokenizer in = new Tokenizer(rbuff.getTokenWriter(), false); + private int i = 0; @Override public void moveAlong() { diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun2/Main.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun2/Main.java index 5225f1155..55daedd95 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun2/Main.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/demoRun2/Main.java @@ -7,7 +7,7 @@ public class Main { public static void main(String[] args) { TokenRingBuffer buff = TokenRingBuffer.createBufferWithCapacity(100); - Tokenizer in = new Tokenizer(buff.getTokenWriter(), 2); + Tokenizer in = new Tokenizer(buff.getTokenWriter(), false); for (byte b : "A(Z0)&D1|(B0)\n".getBytes()) { in.accept(b); } diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/EchoCommandTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/EchoCommandTest.java index 0bbfb0833..5503476b8 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/EchoCommandTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/EchoCommandTest.java @@ -21,7 +21,7 @@ class EchoCommandTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/LockEchoParserTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/LockEchoParserTest.java index 1fe4c22e9..9d36b339f 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/LockEchoParserTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/LockEchoParserTest.java @@ -25,7 +25,7 @@ class LockEchoParserTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAddressingTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAddressingTest.java index 1ca3ce7f5..e22ae14dc 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAddressingTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAddressingTest.java @@ -21,7 +21,7 @@ public class SemanticParserAddressingTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAsyncCommandTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAsyncCommandTest.java index d9bae9b85..9dd0da0fc 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAsyncCommandTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserAsyncCommandTest.java @@ -25,7 +25,7 @@ class SemanticParserAsyncCommandTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserMulticommandTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserMulticommandTest.java index 44d4f7524..5a6902be8 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserMulticommandTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserMulticommandTest.java @@ -25,7 +25,7 @@ class SemanticParserMulticommandTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTest.java index 7ab379f7b..bc93f5aff 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTest.java @@ -29,7 +29,7 @@ class SemanticParserTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTokenWaitTest.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTokenWaitTest.java index c4d56f0de..283749775 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTokenWaitTest.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/semanticParser/SemanticParserTokenWaitTest.java @@ -18,7 +18,7 @@ class SemanticParserTokenWaitTest { private final TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + private final Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); private final SemanticParser parser = new SemanticParser(buffer.getTokenReader(), new ExecutionActionFactory()); 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 ce1ee823d..9825e2fed 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 @@ -89,7 +89,7 @@ public void close() { private LocalChannel(TokenBuffer buffer, OutputStreamOutStream out, boolean createPipe) { super(buffer, out); - this.tokenizer = new Tokenizer(buffer.getTokenWriter()); + this.tokenizer = new Tokenizer(buffer.getTokenWriter(), false); this.incoming = new ConcurrentLinkedQueue<>(); this.byteIterator = null; diff --git a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/StringChannel.java b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/StringChannel.java index 893f49e92..3884fafae 100644 --- a/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/StringChannel.java +++ b/receivers/jvm/java-receiver/src/test/java/net/zscript/javareceiver/testing/StringChannel.java @@ -23,7 +23,7 @@ public static StringChannel from(String input, SequenceOutStream out) { private StringChannel(TokenBuffer buffer, String input, SequenceOutStream out) { super(buffer, out); - this.in = new Tokenizer(buffer.getTokenWriter(), 2); + this.in = new Tokenizer(buffer.getTokenWriter(), false); this.input = input.getBytes(StandardCharsets.UTF_8); } diff --git a/receivers/jvm/java-tokenizer/src/main/java/net/zscript/tokenizer/Tokenizer.java b/receivers/jvm/java-tokenizer/src/main/java/net/zscript/tokenizer/Tokenizer.java index 13899be67..bd15a17f9 100644 --- a/receivers/jvm/java-tokenizer/src/main/java/net/zscript/tokenizer/Tokenizer.java +++ b/receivers/jvm/java-tokenizer/src/main/java/net/zscript/tokenizer/Tokenizer.java @@ -42,9 +42,10 @@ public class Tokenizer { // Token for whole of rest of sequence following an address, representing the message to be passed downstream. public static final byte ADDRESSING_FIELD_KEY = (byte) 0x80; - private static final int DEFAULT_MAX_NUMERIC_BYTES = 2; + // Normal max length of a numeric field's value (eg 0-0xffff) + public static final int DEFAULT_MAX_NUMERIC_BYTES = 2; - /////////////// Sequence marker values (top 4 bits set) - signify sequence end (either normal, or error during tokenization) + /// //////////// Sequence marker values (top 4 bits set) - signify sequence end (either normal, or error during tokenization) public static final byte NORMAL_SEQUENCE_END = (byte) 0xf0; public static final byte ERROR_BUFFER_OVERRUN = (byte) 0xf1; @@ -54,7 +55,7 @@ public class Tokenizer { public static final byte ERROR_CODE_STRING_ESCAPING = (byte) 0xf5; public static final byte ERROR_CODE_ILLEGAL_TOKEN = (byte) 0xf6; - /////////////// Normal marker values (top 3 bits set) - signify command/response end, implies read processing may proceed + /// //////////// Normal marker values (top 3 bits set) - signify command/response end, implies read processing may proceed public static final byte CMD_END_ANDTHEN = (byte) 0xe1; public static final byte CMD_END_ORELSE = (byte) 0xe2; @@ -75,14 +76,24 @@ public class Tokenizer { private boolean isNormalString; private int escapingCount; // 2 bit counter, from 2 to 0 - public Tokenizer(final TokenWriter writer) { - this(writer, DEFAULT_MAX_NUMERIC_BYTES, false); - } - - public Tokenizer(final TokenWriter writer, final int maxNumericBytes) { - this(writer, maxNumericBytes, false); + /** + * @param writer the writer to write tokens to + * @param parseOutAddressing true if addressed text should be written as normal tokens ("client"-style), false if they should be bundled into a singled ADDRESSING_FIELD_KEY + * token ("receiver"-style) ready for forwarding to its destination + */ + public Tokenizer(final TokenWriter writer, final boolean parseOutAddressing) { + this(writer, DEFAULT_MAX_NUMERIC_BYTES, parseOutAddressing); } + /** + * Creates a new Tokenizer to write chars into tokens on the supplied writer. There are two primary modes: "client"-mode, where all tokens are written normally, and + * "receiver"-mode. In receiver-mode, everything after the first address (eg "@a.b.c") is written as a single large text token marked with {@link #ADDRESSING_FIELD_KEY} + * + * @param writer the writer to write tokens to + * @param maxNumericBytes the width of the largest numeric token to write (beyond which we write ERROR_CODE_FIELD_TOO_LONG) + * @param parseOutAddressing true if addressed text should be written as normal tokens ("client"-mode), false if they should be bundled into a singled ADDRESSING_FIELD_KEY + * token ("receiver"-mode) ready for forwarding to its destination + */ public Tokenizer(final TokenWriter writer, final int maxNumericBytes, final boolean parseOutAddressing) { this.writer = writer; this.maxNumericBytes = maxNumericBytes; diff --git a/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/TokenizerTest.java b/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/TokenizerTest.java index 9a9185cc1..59974720c 100644 --- a/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/TokenizerTest.java +++ b/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/TokenizerTest.java @@ -42,7 +42,7 @@ class TokenizerTest { @BeforeEach void setUp() { - tokenizer = new Tokenizer(writer, 2); + tokenizer = new Tokenizer(writer, false); when(writer.checkAvailableCapacity(CAPACITY_CHECK_LENGTH)).thenReturn(true); } diff --git a/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/ZscriptTokenExpressionTest.java b/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/ZscriptTokenExpressionTest.java index d5075b7cd..c79a37c7c 100644 --- a/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/ZscriptTokenExpressionTest.java +++ b/receivers/jvm/java-tokenizer/src/test/java/net/zscript/tokenizer/ZscriptTokenExpressionTest.java @@ -12,7 +12,7 @@ class ZscriptTokenExpressionTest { TokenBuffer buffer = TokenRingBuffer.createBufferWithCapacity(256); - Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), 2); + Tokenizer tokenizer = new Tokenizer(buffer.getTokenWriter(), false); TokenBuffer.TokenReader tokenReader = buffer.getTokenReader(); ZscriptTokenExpression zscriptExpr = new ZscriptTokenExpression(() -> tokenReader.tokenIterator()); diff --git a/simulator/zscript-simulator-jvm/src/main/java/net/zscript/javasimulator/zcode/i2c/I2cChannel.java b/simulator/zscript-simulator-jvm/src/main/java/net/zscript/javasimulator/zcode/i2c/I2cChannel.java index 0a655b81b..09579c28e 100644 --- a/simulator/zscript-simulator-jvm/src/main/java/net/zscript/javasimulator/zcode/i2c/I2cChannel.java +++ b/simulator/zscript-simulator-jvm/src/main/java/net/zscript/javasimulator/zcode/i2c/I2cChannel.java @@ -84,7 +84,7 @@ private I2cChannel(Entity e, I2cAddress addr, TokenBuffer buffer, Queue super(buffer, out); this.e = e; this.addr = addr; - this.in = new Tokenizer(buffer.getTokenWriter(), 2); + this.in = new Tokenizer(buffer.getTokenWriter(), false); this.outQueue = outQueue; } diff --git a/util/misc/src/main/java/net/zscript/util/ByteString.java b/util/misc/src/main/java/net/zscript/util/ByteString.java index 6472520bb..2475db548 100644 --- a/util/misc/src/main/java/net/zscript/util/ByteString.java +++ b/util/misc/src/main/java/net/zscript/util/ByteString.java @@ -626,6 +626,18 @@ public ByteStringBuilder append(Iterable appendables) return append(ByteAppender.DEFAULT_APPENDER, appendables); } + /** + * Appends each of the Appendables in the supplied collection, in iterator order, separated by the supplied separator. + * + * @param appendables zero or more Appendables + * @param sep a separator object added between the main objects + * @return this builder, to facilitate chaining + */ + @Nonnull + public ByteStringBuilder appendJoining(Iterable appendables, ByteAppendable sep) { + return appendJoining(ByteAppender.DEFAULT_APPENDER, appendables, sep); + } + /** * Appends each of the supplied objects using the supplied appender, in iterator order. * @@ -640,6 +652,28 @@ public ByteStringBuilder append(ByteAppender appender, Iterable the type of the objects + * @return this builder, to facilitate chaining + */ + @Nonnull + public ByteStringBuilder appendJoining(ByteAppender appender, Iterable objects, ByteAppendable sep) { + boolean subsequent = false; + for (final T a : objects) { + if (subsequent) { + append(sep); + } + subsequent = true; + appender.append(a, this); + } + return this; + } + /** * Appends each of the supplied Appendables. * diff --git a/util/misc/src/main/java/net/zscript/util/OptIterator.java b/util/misc/src/main/java/net/zscript/util/OptIterator.java index a1bf5d85a..daf2119b5 100644 --- a/util/misc/src/main/java/net/zscript/util/OptIterator.java +++ b/util/misc/src/main/java/net/zscript/util/OptIterator.java @@ -2,9 +2,11 @@ import javax.annotation.Nonnull; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -72,4 +74,56 @@ public Optional next() { } }; } + + /** + * Drains this OptIterator's items into a list (as per {@link Collectors#toList()}). + * + * @return a list of the items + */ + @Nonnull + default List toList() { + return stream().collect(Collectors.toList()); + } + + /** + * Converts this OptIterator into a simple Iterable with a corresponding Iterator. + *

+ * This method is intended to facilitate usage in foreach loops: + *

+     * {@code
+     * for (Thing t : thingSource.things().toIterable()) {
+     *      System.out.println(t);
+     * }
+     * }
+     * 
+ * + * Warning: using the returned Iterable and its corresponding Iterator will drain this OptIterator's items (they aren't stored or copied)! Dependence on exactly how this + * behaves or interacts with use of the original OptIterator is fragile. + * + * @return this OptIterator in Iterable form + */ + default Iterable toIterable() { + return new Iterable<>() { + @Override + @Nonnull + public Iterator iterator() { + return new Iterator<>() { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + Optional cur = OptIterator.this.next(); + + @Override + public boolean hasNext() { + return cur.isPresent(); + } + + @Override + public T next() { + final T tmp = cur.orElseThrow(NoSuchElementException::new); + cur = OptIterator.this.next(); + return tmp; + } + }; + } + }; + } } diff --git a/util/misc/src/test/java/net/zscript/util/ByteStringTest.java b/util/misc/src/test/java/net/zscript/util/ByteStringTest.java index 3174ed4e7..c7345522c 100644 --- a/util/misc/src/test/java/net/zscript/util/ByteStringTest.java +++ b/util/misc/src/test/java/net/zscript/util/ByteStringTest.java @@ -8,6 +8,7 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1; import static net.zscript.util.ByteString.ByteAppender.ISOLATIN1_APPENDER; +import static net.zscript.util.ByteString.ByteAppender.UTF8_APPENDER; import static net.zscript.util.ByteString.byteString; import static net.zscript.util.ByteString.byteStringUtf8; import static org.assertj.core.api.Assertions.assertThat; @@ -179,6 +180,14 @@ public void appendTo(ByteString.ByteStringBuilder builder) { assertThat(new TestAppendable(6).toByteString().asString()).isEqualTo("x=06"); } + @Test + public void shouldJoin() { + ByteString.ByteStringBuilder builder = ByteString.builder(); + List list = List.of("green", "eggs", "and", "ham"); + builder.appendJoining(UTF8_APPENDER, list, ByteString.byteStringUtf8("|-|").asAppendable()); + assertThat(builder.asString()).isEqualTo("green|-|eggs|-|and|-|ham"); + } + @Test public void shouldCreateFromTypes() { assertThat(byteString((byte) 'a').toByteArray()).containsExactly('a'); diff --git a/util/misc/src/test/java/net/zscript/util/OptIteratorTest.java b/util/misc/src/test/java/net/zscript/util/OptIteratorTest.java index 7519f2a9e..0284d8d1d 100644 --- a/util/misc/src/test/java/net/zscript/util/OptIteratorTest.java +++ b/util/misc/src/test/java/net/zscript/util/OptIteratorTest.java @@ -1,21 +1,23 @@ package net.zscript.util; -import static java.util.stream.Collectors.toList; -import static org.assertj.core.api.Assertions.assertThat; - +import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + import org.junit.jupiter.api.Test; class OptIteratorTest { - final OptIterator simpleListIterator = OptIterator.of(List.of(1, 2, 3)); + final OptIterator simpleListIterator = OptIterator.of(List.of(1, 3, 5)); @Test void shouldIterateList() { assertThat(simpleListIterator.next()).isPresent().get().isEqualTo(1); - assertThat(simpleListIterator.next()).isPresent().get().isEqualTo(2); assertThat(simpleListIterator.next()).isPresent().get().isEqualTo(3); + assertThat(simpleListIterator.next()).isPresent().get().isEqualTo(5); assertThat(simpleListIterator.next()).isEmpty(); } @@ -27,15 +29,37 @@ void shouldIterateEmptyList() { @Test void shouldCreateStream() { - List filteredList = simpleListIterator.stream().filter(n -> n != 2).collect(toList()); - assertThat(filteredList).isEqualTo(List.of(1, 3)); + List filteredList = simpleListIterator.stream().filter(n -> n != 3).collect(toList()); + assertThat(filteredList).isEqualTo(List.of(1, 5)); } @Test void shouldExecuteForEach() { AtomicInteger total = new AtomicInteger(); // using this as a mutable int - simpleListIterator.forEach(n -> total.addAndGet(n)); - assertThat(total.get()).isEqualTo(6); + simpleListIterator.forEach(total::addAndGet); + assertThat(total.get()).isEqualTo(9); + } + + @Test + void shouldWorkInForeachLoops() { + AtomicInteger total = new AtomicInteger(); // using this as a mutable int + for (int i : simpleListIterator.toIterable()) { + total.addAndGet(i); + } + assertThat(total.get()).isEqualTo(9); + } + + @Test + void shouldCreateList() { + assertThat(simpleListIterator.toList()).isEqualTo(List.of(1, 3, 5)); + } + + @Test + void shouldWorkInForeachLoopsWhenEmpty() { + List collect = new ArrayList<>(); + for (int i : OptIterator.of(List.of()).toIterable()) { + fail("unexpectedly found " + i); + } } }