From bf0eb9b78c1d17bac0cabfb91e0391a0c6bdbb51 Mon Sep 17 00:00:00 2001 From: susanw1 Date: Thu, 11 Jul 2024 12:25:04 +0100 Subject: [PATCH] [#157] Improved ByteString comments and tests --- .../java/net/zscript/util/ByteString.java | 40 ++++++++++---- .../java/net/zscript/util/OptIterator.java | 8 +-- .../java/net/zscript/util/ByteStringTest.java | 52 +++++++++++++++++++ 3 files changed, 86 insertions(+), 14 deletions(-) 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 fadb771d2..b8c36e119 100644 --- a/util/misc/src/main/java/net/zscript/util/ByteString.java +++ b/util/misc/src/main/java/net/zscript/util/ByteString.java @@ -7,6 +7,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; +/** + * A buildable byte string to make it easy to work with byte-oriented text data. + */ public interface ByteString { /** * Returns (a copy of) the content of this ByteString. @@ -138,23 +141,37 @@ public B appendByte(int b) { return appendByte((byte) b); } + /** + * Appends the supplied single byte, without having to check for range. + * + * @param b any value 0x00-0xff + * @return this builder, to facilitate chaining + */ public B appendByte(byte b) { baos.write(b); return asTypeB(); } + /** + * Appends the supplied byte array. + * + * @param bytes any value 0x00-0xff + * @return this builder, to facilitate chaining + * @throws IllegalArgumentException if value is out of range 0-ff + */ public B appendRaw(byte[] bytes) { baos.writeBytes(bytes); return asTypeB(); } + /** + * Appends the supplied byte string (creates temporary copy unless it's immutable). + * + * @param byteString any existing ByteString + * @return this builder, to facilitate chaining + */ public B append(ByteString byteString) { - baos.writeBytes(byteString.toByteArray()); - return asTypeB(); - } - - public B append(ImmutableByteString byteString) { - baos.writeBytes(byteString.bytes); + baos.writeBytes(byteString instanceof ImmutableByteString ? ((ImmutableByteString) byteString).bytes : byteString.toByteArray()); return asTypeB(); } @@ -190,14 +207,17 @@ public B appendNumeric32(long value) { } /** - * Appends a number as up to 4 nibbles of hex. All leading zeroes EXCEPT ONE are suppressed. + * Appends a number as up to 8 nibbles of hex. All leading zeroes EXCEPT ONE are suppressed. * * @param value the value to append * @return this builder, to facilitate chaining * @throws IllegalArgumentException if value is out of range */ public B appendNumeric32KeepZero(long value) { - if (value > 0x10000) { + if ((value & ~0xffffffffL) != 0) { + throw new IllegalArgumentException("Numeric32 fields must be 0x0-0xffffffff: " + value); + } + if (value >= 0x10000) { appendNumericKeepZero((int) (value >>> 16)); appendHexPair((byte) (value >>> 8)); return appendHexPair((byte) (value)); @@ -226,7 +246,7 @@ public B appendNumeric(int value) { */ public B appendNumericKeepZero(int value) { if ((value & ~0xffff) != 0) { - throw new IllegalArgumentException("Numeric fields must be 0x0-0xffffffff: " + value); + throw new IllegalArgumentException("Numeric fields must be 0x0-0xffff: " + value); } if (value >= 0x1000) { baos.write(toHex(value >>> 12)); @@ -242,7 +262,7 @@ public B appendNumericKeepZero(int value) { } @SuppressWarnings("unchecked") - public B asTypeB() { + private B asTypeB() { return (B) this; } 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 10f6ed56b..7ff1aa468 100644 --- a/util/misc/src/main/java/net/zscript/util/OptIterator.java +++ b/util/misc/src/main/java/net/zscript/util/OptIterator.java @@ -25,7 +25,7 @@ public interface OptIterator { * to highlight improper use, but they might not. * * @return value items wrapped as Optional, or {@link java.util.Optional#empty()} to signal end of items - * @exception NoSuchElementException on going beyond the end (optional) + * @throws NoSuchElementException on going beyond the end (optional) */ Optional next(); @@ -42,12 +42,12 @@ default void forEach(final Consumer action) { } /** - * Exposes the emelements as a conventional {@link java.util.stream.Stream}. + * Exposes the elements as a conventional {@link java.util.stream.Stream}. * * @return a stream of elements */ default Stream stream() { - return Stream.iterate(next(), t -> t.isPresent(), t -> next()).map(t -> t.get()); + return Stream.iterate(next(), Optional::isPresent, t -> next()).map(Optional::get); } /** @@ -59,7 +59,7 @@ default Stream stream() { */ static OptIterator of(final Iterable iterable) { return new OptIterator() { - Iterator origIterator = iterable.iterator(); + private final Iterator origIterator = iterable.iterator(); @Override public Optional next() { 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 102d1ffab..505cbee28 100644 --- a/util/misc/src/test/java/net/zscript/util/ByteStringTest.java +++ b/util/misc/src/test/java/net/zscript/util/ByteStringTest.java @@ -2,12 +2,15 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import net.zscript.util.ByteString.ImmutableByteString; @@ -30,6 +33,22 @@ public void shouldAppendBytes() throws IOException { .isInstanceOf(IllegalArgumentException.class); } + @Test + public void shouldInitializeNewBuilderWithExistingContent() { + ByteString.ByteStringBuilder b = ByteString.builder().appendByte('Z').appendNumeric(0x01a2); + assertThat(ByteString.builder(b).appendByte('X').appendNumeric(0x1a2b).toByteArray()) + .containsExactly('Z', '1', 'a', '2', 'X', '1', 'a', '2', 'b'); + } + + @Test + public void shouldAppendExistingContent() { + ByteString.ByteStringBuilder b = ByteString.builder().appendByte('Z').appendNumeric(0xa2); + assertThat(ByteString.builder().appendByte('X').append(b).appendByte('Y').toByteArray()) + .containsExactly('X', 'Z', 'a', '2', 'Y'); + assertThat(ByteString.builder().appendByte('X').append(b.build()).appendByte('Y').toByteArray()) + .containsExactly('X', 'Z', 'a', '2', 'Y'); + } + @Test public void shouldWriteToByteArrayNumbers() { assertThat(ByteString.builder().appendByte('Z').appendNumeric(0x1a2b).toByteArray()) @@ -54,6 +73,33 @@ public void shouldThrowOnOutOfRangeNumericValue() { .isInstanceOf(IllegalArgumentException.class); } + @ParameterizedTest + @CsvSource({ "0xf1e2d3c4,Zf1e2d3c4", "0xe2d3c4,Ze2d3c4", "0x3c4,Z3c4", "0x10000,Z10000", "0xffff,Zffff", "0,Z" }) + public void shouldWriteToByteArrayNumbers32(long value, String expected) { + assertThat(ByteString.builder().appendByte('Z').appendNumeric32(value).toByteArray()) + .containsExactly(expected.getBytes(StandardCharsets.ISO_8859_1)); + } + + @Test + public void shouldOmitNumberZero32() { + assertThat(ByteString.builder().appendByte('Z').appendNumeric32(0).toByteArray()) + .containsExactly('Z'); + } + + @Test + public void shouldWriteNumberZero32() { + assertThat(ByteString.builder().appendByte('Z').appendNumeric32KeepZero(0).toByteArray()) + .containsExactly('Z', '0'); + } + + @Test + public void shouldThrowOnOutOfRangeNumericValue32() { + assertThatThrownBy(() -> ByteString.builder().appendByte('Z').appendNumeric32(0x123456789L).toByteArray()) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ByteString.builder().appendByte('Z').appendNumeric32(-1).toByteArray()) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void shouldWriteHex() { ByteString.ByteStringBuilder builder = ByteString.builder(); @@ -75,4 +121,10 @@ public void toStringContract() { assertThat(ByteString.builder().appendByte('Z').appendNumeric(0x12).build().toString()) .isEqualTo("ImmutableByteString[Z12]"); } + + @Test + public void builderToStringContract() { + assertThat(ByteString.builder().appendByte('Z').appendNumeric(0x12).toString()) + .isEqualTo("ByteStringBuilder[Z12]"); + } }