This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit e91f576e12fd2aa70c26b29966d4c5d7e2c99d0d Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Oct 25 04:54:12 2023 +0200 Refactor `ChannelImageOutputStream` as an implementation of `ImageOutputStream`. It allows us to test `ChannelData.yield(ChannelData)`. --- .../sis/io/stream/ChannelImageInputStream.java | 21 +- .../sis/io/stream/ChannelImageOutputStream.java | 186 +++++++++-- .../main/org/apache/sis/io/stream/IOUtilities.java | 75 ++--- .../apache/sis/io/stream/OutputStreamAdapter.java | 11 +- .../sis/io/stream/ChannelDataOutputTest.java | 340 ++++++++++----------- .../apache/sis/io/stream/ChannelDataTestCase.java | 53 +++- .../sis/io/stream/ChannelImageInputStreamTest.java | 97 ++++-- .../io/stream/ChannelImageOutputStreamTest.java | 254 ++++++++------- 8 files changed, 649 insertions(+), 388 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java index 04eeaead32..9426cb2caa 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java @@ -167,7 +167,8 @@ public class ChannelImageInputStream extends ChannelDataInput implements ImageIn /** * Reads up to {@code length} bytes from the stream, and modifies the supplied * {@code IIOByteBuffer} to indicate the byte array, offset, and length where - * the data may be found. + * the data may be found. This method may reference the internal buffer, thus + * avoiding a copy. * * @param dest the buffer to be written to. * @param length the maximum number of bytes to read. @@ -175,10 +176,22 @@ public class ChannelImageInputStream extends ChannelDataInput implements ImageIn */ @Override public final void readBytes(final IIOByteBuffer dest, int length) throws IOException { - final byte[] data = new byte[length]; - length = read(data); + clearBitOffset(); + final byte[] data; + final int offset; + if (buffer.hasArray()) { + ensureBufferContains(1); + data = buffer.array(); + offset = buffer.position(); + length = Math.min(buffer.remaining(), length); + buffer.position(offset + length); + } else { + data = new byte[length]; + length = read(data); + offset = 0; + } dest.setData(data); - dest.setOffset(0); + dest.setOffset(offset); dest.setLength(length); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java index a46152ca96..037b81ee80 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java @@ -16,63 +16,191 @@ */ package org.apache.sis.io.stream; -import java.io.Closeable; -import java.io.DataOutput; import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.WritableByteChannel; +import java.nio.ByteOrder; +import java.nio.channels.ByteChannel; +import javax.imageio.stream.IIOByteBuffer; import javax.imageio.stream.ImageOutputStream; /** - * Adds the missing methods in {@code ChannelDataOutput} for implementing the {@code DataOutput} interface. - * Current implementation does not yet implements the {@code ImageOutputStream} sub-interface, but a future - * implementation may do so. + * An {@code ImageOutputStream} backed by {@code ChannelDataInput} and {@code ChannelDataOutput}. + * Contrarily to most other I/O frameworks in the standard JDK, {@code ImageOutputStream} is read/write. * - * <p>We do not implement {@link ImageOutputStream} yet because the latter inherits all read operations from - * {@code ImageInputStream}, while the {@code org.apache.sis.storage.base} package keeps the concerns - * separated. Despite that, the name of this {@code ChannelImageOutputStream} anticipates a future version - * which would implement the image I/O interface.</p> - * - * <p>This class is a place-holder for future development.</p> + * @todo Not yet used in {@link org.apache.sis.storage.StorageConnector}. * * @author Rémi Maréchal (Geomatys) * @author Martin Desruisseaux (Geomatys) */ -public class ChannelImageOutputStream extends ChannelDataOutput implements DataOutput, Closeable { +@SuppressWarnings("deprecation") +public class ChannelImageOutputStream extends OutputStream implements ImageOutputStream, Markable { + /** + * The object to use for reading from the channel. + */ + private final ChannelImageInputStream input; + + /** + * The object to use for writing to the channel. + */ + private final ChannelDataOutput output; + /** - * Creates a new output stream for the given channel and using the given buffer. + * {@code false} if the stream is reading, or {@code true} if it is writing. + * + * @see #current() + */ + private boolean writing; + + /** + * Creates a new input/output stream for the given channel and using the given buffer. * * @param filename a file identifier used only for formatting error message. - * @param channel the channel where to write data. - * @param buffer the buffer from where to read the data. - * @throws IOException if an error occurred while writing into channel. + * @param channel the channel where to read and write data. + * @param buffer the buffer for data to read and write. + * @throws IOException if the stream cannot be created. */ - public ChannelImageOutputStream(final String filename, final WritableByteChannel channel, final ByteBuffer buffer) + public ChannelImageOutputStream(final String filename, final ByteChannel channel, final ByteBuffer buffer) throws IOException { - super(filename, channel, buffer); + input = new ChannelImageInputStream(filename, channel, buffer, true); + output = new ChannelDataOutput(filename, channel, buffer); + } + + /** Declares that this stream does not cache data itself. */ + @Override public boolean isCached() {return false;} + @Override public boolean isCachedMemory() {return false;} + @Override public boolean isCachedFile() {return false;} + + /** + * Returns the {@linkplain #input} or {@linkplain #output}, + * depending on whether this stream is reading or writing. + */ + private ChannelData current() { + return writing ? output : input; + } + + /** + * {@return the object to use for reading from the stream}. + * The returned object should not be used anymore after {@link #output()} + * has been invoked, until {@code input()} is invoked again. + * + * @throws IOException if an error occurred while flushing a buffer. + */ + public final ChannelImageInputStream input() throws IOException { + if (writing) { + output.yield(input); + writing = false; + } + return input; } /** - * Creates a new output source from the given {@code ChannelDataOutput}. - * This constructor is invoked when we need to change the implementation class - * from {@code ChannelDataOutput} to {@code ChannelImageOutputStream}. + * {@return the object to use for writing to the stream}. + * The returned object should not be used anymore after {@link #input()} + * has been invoked, until {@code output()} is invoked again. * - * @param output the existing instance from which to takes the channel and buffer. - * @throws IOException if an error occurred while writing into channel. + * @throws IOException if an error occurred while flushing a buffer. */ - public ChannelImageOutputStream(final ChannelDataOutput output) throws IOException { - super(output.filename, output.channel, output.buffer); + public final ChannelDataOutput output() throws IOException { + if (!writing) { + input.yield(output); + writing = true; + } + return output; + } + + /** Delegates to the reader or writer. */ + @Override public long length() throws IOException {return current().length();} + @Override public void mark() { current().mark();} + @Override public void reset() throws IOException { current().reset();} + @Override public void reset(long p) throws IOException { current().reset(p);} + @Override public void seek(long p) throws IOException { current().seek(p);} + @Override public void flushBefore(long p) throws IOException { current().flushBefore(p);} + @Override public long getFlushedPosition() {return current().getFlushedPosition();} + @Override public long getStreamPosition() {return current().getStreamPosition();} + @Override public int getBitOffset() {return current().getBitOffset();} + @Override public void setBitOffset(int n) { current().setBitOffset(n);} + @Override public ByteOrder getByteOrder() {return input.getByteOrder();} + @Override public void setByteOrder(ByteOrder v) { input.setByteOrder(v);} + @Override public void readBytes(IIOByteBuffer v, int n) throws IOException { input().readBytes(v, n);} + @Override public int read() throws IOException {return input().read();} + @Override public int readBit() throws IOException {return input().readBit();} + @Override public long readBits(int n) throws IOException {return input().readBits(n);} + @Override public boolean readBoolean() throws IOException {return input().readBoolean();} + @Override public byte readByte() throws IOException {return input().readByte();} + @Override public int readUnsignedByte() throws IOException {return input().readUnsignedByte();} + @Override public short readShort() throws IOException {return input().readShort();} + @Override public int readUnsignedShort() throws IOException {return input().readUnsignedShort();} + @Override public char readChar() throws IOException {return input().readChar();} + @Override public int readInt() throws IOException {return input().readInt();} + @Override public long readUnsignedInt() throws IOException {return input().readUnsignedInt();} + @Override public long readLong() throws IOException {return input().readLong();} + @Override public float readFloat() throws IOException {return input().readFloat();} + @Override public double readDouble() throws IOException {return input().readDouble();} + @Override public String readLine() throws IOException {return input().readLine();} + @Override public String readUTF() throws IOException {return input().readUTF();} + @Override public int read (byte[] v) throws IOException {return input().read(v);} + @Override public int read (byte[] v, int s, int n) throws IOException {return input().read(v, s, n);} + @Override public void readFully (byte[] v) throws IOException { input().readFully(v);} + @Override public void readFully (byte[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (short[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (char[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (int[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (long[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (float[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public void readFully (double[] v, int s, int n) throws IOException { input().readFully(v, s, n);} + @Override public int skipBytes (int n) throws IOException {return input().skipBytes(n);} + @Override public long skipBytes (long n) throws IOException {return input().skipBytes(n);} + @Override public void write (int v) throws IOException { output().write(v);} + @Override public void writeBit (int v) throws IOException { output().writeBit (v);} + @Override public void writeBits (long v, int n) throws IOException { output().writeBits (v, n);} + @Override public void writeBoolean(boolean v) throws IOException { output().writeBoolean(v);} + @Override public void writeByte (int v) throws IOException { output().writeByte (v);} + @Override public void writeShort (int v) throws IOException { output().writeShort (v);} + @Override public void writeChar (int v) throws IOException { output().writeChar (v);} + @Override public void writeInt (int v) throws IOException { output().writeInt (v);} + @Override public void writeLong (long v) throws IOException { output().writeLong (v);} + @Override public void writeFloat (float v) throws IOException { output().writeFloat (v);} + @Override public void writeDouble (double v) throws IOException { output().writeDouble (v);} + @Override public void writeBytes (String v) throws IOException { output().writeBytes (v);} + @Override public void writeChars (String v) throws IOException { output().writeChars (v);} + @Override public void writeUTF (String v) throws IOException { output().writeUTF (v);} + @Override public void write (byte[] v) throws IOException { output().write(v);} + @Override public void write (byte[] v, int s, int n) throws IOException { output().write(v, s, n);} + @Override public void writeShorts (short[] v, int s, int n) throws IOException { output().writeShorts (v, s, n);} + @Override public void writeChars (char[] v, int s, int n) throws IOException { output().writeChars (v, s, n);} + @Override public void writeInts (int[] v, int s, int n) throws IOException { output().writeInts (v, s, n);} + @Override public void writeLongs (long[] v, int s, int n) throws IOException { output().writeLongs (v, s, n);} + @Override public void writeFloats (float[] v, int s, int n) throws IOException { output().writeFloats (v, s, n);} + @Override public void writeDoubles(double[] v, int s, int n) throws IOException { output().writeDoubles(v, s, n);} + + /** + * Discards the initial position of the stream prior to the current stream position. + * + * @throws IOException if an I/O error occurred. + */ + @Override + public void flush() throws IOException { + if (writing) { + // Reproduce the behavior of Imava I/O implementation. + output.clearBitOffset(); + } + current().flushBefore(getStreamPosition()); } /** - * Closes the {@linkplain #channel}. + * Closes the channel. * * @throws IOException if an error occurred while closing the channel. */ @Override - public final void close() throws IOException { - channel.close(); + public void close() throws IOException { + try (output.channel) { + if (writing) { + output.flush(); + } + } } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java index 2cff111ba8..e2fbe4996a 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java @@ -31,6 +31,7 @@ import java.net.URL; import java.net.URLDecoder; import java.net.URISyntaxException; import java.net.MalformedURLException; +import java.nio.channels.Channel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.channels.SeekableByteChannel; @@ -552,25 +553,29 @@ public final class IOUtilities extends Static { * @return the input stream, or {@code null} if the given stream cannot be converted. * @throws IOException if an error occurred during input stream creation. */ - public static InputStream toInputStream(AutoCloseable stream) throws IOException { - if (stream != null) { - if (stream instanceof InputStream) { - return (InputStream) stream; - } - if (stream instanceof OutputStreamAdapter) { - stream = ((OutputStreamAdapter) stream).output; - } - if (stream instanceof ChannelDataOutput) { - final ChannelDataOutput c = (ChannelDataOutput) stream; - if (c.channel instanceof ReadableByteChannel) { - stream = new ChannelImageInputStream(c.filename, (ReadableByteChannel) c.channel, c.buffer, true); - } + public static InputStream toInputStream(final AutoCloseable stream) throws IOException { + if (stream instanceof InputStream) { + return (InputStream) stream; + } + final ImageInputStream input; + if (stream instanceof ImageInputStream) { + input = (ImageInputStream) stream; + } else { + final ChannelData output; + if (stream instanceof ChannelData) { + output = (ChannelData) stream; + } else if (stream instanceof OutputStreamAdapter) { + output = ((OutputStreamAdapter) stream).output; + } else { + return null; } - if (stream instanceof ImageInputStream) { - return new InputStreamAdapter((ImageInputStream) stream); + final Channel channel = output.channel(); + if (!(channel instanceof ReadableByteChannel)) { + return null; } + input = new ChannelImageInputStream(output.filename, (ReadableByteChannel) channel, output.buffer, true); } - return null; + return new InputStreamAdapter(input); } /** @@ -587,22 +592,23 @@ public final class IOUtilities extends Static { * @throws IOException if an error occurred during output stream creation. */ public static OutputStream toOutputStream(AutoCloseable stream) throws IOException { - if (stream != null) { - if (stream instanceof OutputStream) { - return (OutputStream) stream; - } - if (stream instanceof InputStreamAdapter) { - stream = ((InputStreamAdapter) stream).input; - } - if (stream instanceof ChannelDataInput) { - final ChannelDataInput c = (ChannelDataInput) stream; - if (c.channel instanceof WritableByteChannel) { - stream = new ChannelImageOutputStream(c.filename, (WritableByteChannel) c.channel, c.buffer); - } - } - if (stream instanceof ChannelImageOutputStream) { - return new OutputStreamAdapter((ChannelImageOutputStream) stream); + if (stream instanceof OutputStream) { + return (OutputStream) stream; + } + if (stream instanceof InputStreamAdapter) { + stream = ((InputStreamAdapter) stream).input; + } +check: if (stream instanceof ChannelData) { + final ChannelData data = (ChannelData) stream; + final ChannelDataOutput output; + if (stream instanceof ChannelDataOutput) { + output = (ChannelDataOutput) stream; + } else { + final Channel channel = data.channel(); + if (!(channel instanceof WritableByteChannel)) break check; + output = new ChannelDataOutput(data.filename, (WritableByteChannel) channel, data.buffer); } + return new OutputStreamAdapter(output); } return null; } @@ -618,10 +624,9 @@ public final class IOUtilities extends Static { */ public static boolean truncate(AutoCloseable stream) throws IOException { if (stream instanceof OutputStreamAdapter) { - stream = ((OutputStreamAdapter) stream).output; - } - if (stream instanceof ChannelDataOutput) { - stream = ((ChannelDataOutput) stream).channel; + stream = (((OutputStreamAdapter) stream).output).channel(); + } else if (stream instanceof ChannelData) { + stream = ((ChannelData) stream).channel(); } if (stream instanceof SeekableByteChannel) { final SeekableByteChannel s = (SeekableByteChannel) stream; diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/OutputStreamAdapter.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/OutputStreamAdapter.java index 74c08cba00..efa982cc4a 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/OutputStreamAdapter.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/OutputStreamAdapter.java @@ -32,18 +32,15 @@ final class OutputStreamAdapter extends OutputStream implements Markable { * The underlying data output stream. In principle, public access to this field breaks encapsulation. * But since {@code OutputStreamAdapter} does not hold any state and just forwards every method calls * to that {@code ChannelDataOutput}, using on object or the other does not make a difference. - * - * @todo to be replaced by a reference to {@link javax.imageio.stream.ImageOutputStream} if the - * {@link ChannelImageOutputStream} class implements that interface in a future version. */ - final ChannelImageOutputStream output; + final ChannelDataOutput output; /** * Constructs a new output stream. * * @param output the stream to wrap. */ - OutputStreamAdapter(final ChannelImageOutputStream output) { + OutputStreamAdapter(final ChannelDataOutput output) { this.output = output; } @@ -139,6 +136,8 @@ final class OutputStreamAdapter extends OutputStream implements Markable { */ @Override public void close() throws IOException { - output.close(); + try (output.channel) { + output.flush(); + } } } diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java index a7fe682c7c..763c4ecfdc 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java @@ -18,7 +18,6 @@ package org.apache.sis.io.stream; import java.util.Arrays; import java.io.ByteArrayOutputStream; -import java.io.DataOutput; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; @@ -28,7 +27,7 @@ import javax.imageio.stream.ImageOutputStream; import org.junit.Test; import org.apache.sis.test.DependsOnMethod; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** @@ -38,25 +37,24 @@ import static org.junit.Assert.*; * @author Rémi Maréchal (Geomatys) * @author Martin Desruisseaux (Geomatys) */ -public class ChannelDataOutputTest extends ChannelDataTestCase { +public final class ChannelDataOutputTest extends ChannelDataTestCase { /** - * The {@link DataOutput} implementation to test. This implementation will write data to - * {@link #testedStreamBackingArray}. The content of that array will be compared to - * {@link #expectedData} for verifying result correctness. + * The implementation to test. This implementation will write data to {@link #testedStreamBackingArray}. + * The content of that array will be compared to {@link #expectedData} for verifying result correctness. */ - ChannelDataOutput testedStream; + private ChannelDataOutput testedStream; /** * A stream to use as a reference implementation. Any data written in {@link #testedStream} * will also be written in {@code referenceStream}, for later comparison. */ - ImageOutputStream referenceStream; + private ImageOutputStream referenceStream; /** * Byte array which is filled by the {@linkplain #testedStream} implementation during write operations. * The content of this array will be compared to {@linkplain #expectedData}. */ - byte[] testedStreamBackingArray; + private byte[] testedStreamBackingArray; /** * Object which is filled by {@link #referenceStream} implementation during write operations. @@ -79,12 +77,12 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { * @param bufferLength length of the {@code ByteBuffer} to use for the tests. * @throws IOException should never happen since we read and write in memory only. */ - void initialize(final String testName, final int streamLength, final int bufferLength) throws IOException { + private void initialize(final String testName, final int streamLength, final int bufferLength) throws IOException { expectedData = new ByteArrayOutputStream(streamLength); referenceStream = new MemoryCacheImageOutputStream(expectedData); testedStreamBackingArray = new byte[streamLength]; - testedStream = new ChannelDataOutput(testName, - new ByteArrayChannel(testedStreamBackingArray, false), ByteBuffer.allocate(bufferLength)); + var channel = new ByteArrayChannel(testedStreamBackingArray, false); + testedStream = new ChannelDataOutput(testName, channel, ByteBuffer.allocate(bufferLength)); } /** @@ -119,7 +117,7 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { * Asserts that the content of {@link #testedStream} is equal to the content of {@link #referenceStream}. * This method closes the reference stream before to perform the comparison. */ - final void assertStreamContentEquals() throws IOException { + private void assertStreamContentEquals() throws IOException { testedStream.flush(); referenceStream.close(); final byte[] expectedArray = expectedData.toByteArray(); @@ -143,7 +141,7 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { for (int i=0; i<100; i++) { final int position = random.nextInt(seekRange); testedStream.seek(position); - assertEquals("getStreamPosition()", position, testedStream.getStreamPosition()); + assertEquals(position, testedStream.getStreamPosition(), "getStreamPosition()"); final long v = random.nextLong(); testedStream.writeLong(v); arrayView.putLong(position, v); @@ -178,7 +176,7 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { referenceStream.writeLong(v); testedStream.writeLong(v); } - assertEquals("getStreamPosition()", 24, testedStream.getStreamPosition()); + assertEquals(24, testedStream.getStreamPosition(), "getStreamPosition()"); testedStream.seek(40); // Move 2 long ahead. Space shall be filled by 0. referenceStream.writeLong(0); referenceStream.writeLong(0); @@ -197,36 +195,26 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { @Test public void testArgumentChecks() throws IOException { initialize("testArgumentChecks", 20, 10); - try { - testedStream.setBitOffset(9); - fail("Shall not accept invalid bitOffset."); - } catch (IllegalArgumentException e) { - final String message = e.getMessage(); - assertTrue(message, message.contains("bitOffset")); - } - try { - testedStream.reset(); - fail("Shall not accept reset without mark."); - } catch (IOException e) { - // This is the expected exception. - assertNotNull(e.getMessage()); - } - /* - * flushBefore(int). - */ + String message; + + // Shall not accept invalid bitOffset. + message = assertThrows(IllegalArgumentException.class, () -> testedStream.setBitOffset(9)).getMessage(); + assertTrue(message.contains("bitOffset"), message); + + // Shall not accept reset without mark. + message = assertThrows(IOException.class, () -> testedStream.reset()).getMessage(); + assertNotNull(message); + + // Shall not flush at a position greater than buffer limit. final int v = random.nextInt(); referenceStream.writeShort(v); testedStream.writeShort(v); - testedStream.flushBefore(0); // Valid. - try { - testedStream.flushBefore(3); - fail("Shall not flush at a position greater than buffer limit."); - } catch (IndexOutOfBoundsException e) { - final String message = e.getMessage(); - assertTrue(message, message.contains("position")); - } + testedStream.flushBefore(0); // Valid. + message = assertThrows(IndexOutOfBoundsException.class, () -> testedStream.flushBefore(3)).getMessage(); + assertTrue(message.contains("position"), message); + testedStream.flush(); - testedStream.flushBefore(0); // Should be a no-operation. + testedStream.flushBefore(0); // Should be a no-operation. assertStreamContentEquals(); } @@ -250,142 +238,32 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { final ImageOutputStream r = this.referenceStream; final ChannelDataOutput t = this.testedStream; switch (operation) { - case 0: { - final byte v = (byte) random.nextInt(1 << Byte.SIZE); - r.writeByte(v); - t.writeByte(v); - break; - } - case 1: { - final short v = (short) random.nextInt(1 << Short.SIZE); - r.writeShort(v); - t.writeShort(v); - break; - } - case 2: { - final char v = (char) random.nextInt(1 << Character.SIZE); - r.writeChar(v); - t.writeChar(v); - break; - } - case 3: { - final int v = random.nextInt(); - r.writeInt(v); - t.writeInt(v); - break; - } - case 4: { - final long v = random.nextLong(); - r.writeLong(v); - t.writeLong(v); - break; - } - case 5: { - final float v = random.nextFloat(); - r.writeFloat(v); - t.writeFloat(v); - break; - } - case 6: { - final double v = random.nextDouble(); - r.writeDouble(v); - t.writeDouble(v); - break; - } - case 7: { - final byte[] tmp = new byte[random.nextInt(ARRAY_MAX_LENGTH / Byte.BYTES)]; - random.nextBytes(tmp); - r.write(tmp); - t.write(tmp); - break; - } - case 8: { - final char[] tmp = new char[random.nextInt(ARRAY_MAX_LENGTH / Character.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = (char) random.nextInt(1 << Character.SIZE); - } - r.writeChars(tmp, 0, tmp.length); - t.writeChars(tmp); - break; - } - case 9: { - final short[] tmp = new short[random.nextInt(ARRAY_MAX_LENGTH / Short.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = (short) random.nextInt(1 << Short.SIZE); - } - r.writeShorts(tmp, 0, tmp.length); - t.writeShorts(tmp); - break; - } - case 10: { - final int[] tmp = new int[random.nextInt(ARRAY_MAX_LENGTH / Integer.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = random.nextInt(); - } - r.writeInts(tmp, 0, tmp.length); - t.writeInts(tmp); - break; - } - case 11: { - final long[] tmp = new long[random.nextInt(ARRAY_MAX_LENGTH / Long.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = random.nextLong(); - } - r.writeLongs(tmp, 0, tmp.length); - t.writeLongs(tmp); - break; - } - case 12: { - final float[] tmp = new float[random.nextInt(ARRAY_MAX_LENGTH / Float.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = random.nextFloat(); - } - r.writeFloats(tmp, 0, tmp.length); - t.writeFloats(tmp); - break; - } - case 13: { - final double[] tmp = new double[random.nextInt(ARRAY_MAX_LENGTH / Double.BYTES)]; - for (int i=0; i<tmp.length; i++) { - tmp[i] = random.nextDouble(); - } - r.writeDoubles(tmp, 0, tmp.length); - t.writeDoubles(tmp); - break; - } - case 14: { + default: throw new AssertionError(operation); + case 0: {byte v = (byte) random.nextInt(); r.writeByte (v); t.writeByte (v); break;} + case 1: {short v = (short) random.nextInt(); r.writeShort (v); t.writeShort (v); break;} + case 2: {char v = (char) random.nextInt(); r.writeChar (v); t.writeChar (v); break;} + case 3: {int v = random.nextInt(); r.writeInt (v); t.writeInt (v); break;} + case 4: {long v = random.nextLong(); r.writeLong (v); t.writeLong (v); break;} + case 5: {float v = random.nextFloat(); r.writeFloat (v); t.writeFloat (v); break;} + case 6: {double v = random.nextDouble(); r.writeDouble (v); t.writeDouble (v); break;} + case 7: {boolean v = random.nextBoolean(); r.writeBoolean(v); t.writeBoolean(v); break;} + case 8: {byte[] v = randomBytes(); r.write (v); t.write (v); break;} + case 9: {char[] v = randomChars(); r.writeChars (v, 0, v.length); t.writeChars (v); break;} + case 10: {short[] v = randomShorts(); r.writeShorts (v, 0, v.length); t.writeShorts (v); break;} + case 11: {int[] v = randomInts(); r.writeInts (v, 0, v.length); t.writeInts (v); break;} + case 12: {long[] v = randomLongs(); r.writeLongs (v, 0, v.length); t.writeLongs (v); break;} + case 13: {float[] v = randomFloats(); r.writeFloats (v, 0, v.length); t.writeFloats (v); break;} + case 14: {double[] v = randomDoubles(); r.writeDoubles(v, 0, v.length); t.writeDoubles(v); break;} + case 15: {String v = "Byte sequence"; r.writeBytes(v); t.writeBytes(v); break;} + case 16: {String v = "Character sequence"; r.writeChars(v); t.writeChars(v); break;} + case 17: {String v = "お元気ですか"; r.writeUTF (v); t.writeUTF (v); break;} + case 18: { final long v = random.nextLong(); final int numBits = random.nextInt(Byte.SIZE); r.writeBits(v, numBits); t.writeBits(v, numBits); break; } - case 15: { - final boolean v = random.nextBoolean(); - r.writeBoolean(v); - t.writeBoolean(v); - break; - } - case 16: { - final String s = "Byte sequence"; - r.writeBytes(s); - t.writeBytes(s); - break; - } - case 17: { - final String s = "Character sequence"; - r.writeChars(s); - t.writeChars(s); - break; - } - case 18: { - final String s = "お元気ですか"; - final byte[] array = s.getBytes("UTF-8"); - assertEquals(s.length() * 3, array.length); // Sanity check. - r.writeUTF(s); - t.writeUTF(s); - break; - } case 19: { long flushedPosition = StrictMath.max(r.getFlushedPosition(), t.getFlushedPosition()); flushedPosition += random.nextInt(1 + (int) (r.getStreamPosition() - flushedPosition)); @@ -398,9 +276,123 @@ public class ChannelDataOutputTest extends ChannelDataTestCase { t.flush(); break; } - default: throw new AssertionError(operation); } - assertEquals("getBitOffset()", r.getBitOffset(), t.getBitOffset()); - assertEquals("getStreamPosition()", r.getStreamPosition(), t.getStreamPosition()); + assertEquals(r.getBitOffset(), t.getBitOffset(), "getBitOffset()"); + assertEquals(r.getStreamPosition(), t.getStreamPosition(), "getStreamPosition()"); + } + + /** + * Test writing a sequence of bits. + * + * @throws IOException should never happen since we read and write in memory only. + */ + @Test + public void testWriteBits() throws IOException { + initialize("testWriteBits", STREAM_LENGTH, randomBufferCapacity()); + final int length = testedStreamBackingArray.length - ARRAY_MAX_LENGTH; // Keep a margin against buffer underflow. + while (testedStream.getStreamPosition() < length) { + final long v = random.nextLong(); + final int numBits = random.nextInt(Byte.SIZE); + referenceStream.writeBits(v, numBits); + testedStream.writeBits(v, numBits); + /* + * Randomly force flushing of bits. + */ + if (randomEvent()) { + final int f = random.nextInt(256); + referenceStream.writeByte(f); + testedStream.writeByte(f); + } + assertEquals(referenceStream.getBitOffset(), testedStream.getBitOffset(), "getBitOffset"); + assertEquals(referenceStream.getStreamPosition(), testedStream.getStreamPosition(), "getStreamPosition"); + } + assertStreamContentEquals(); + } + + /** + * Tests {@link ChannelImageOutputStream#mark()} and {@code reset()} methods. + * + * @throws IOException should never happen since we read and write in memory only. + */ + @Test + public void testMarkAndReset() throws IOException { + initialize("testMarkAndReset", STREAM_LENGTH, 1000); // We need a larger buffer for this test. + /* + * Fill both streams with random data. + * During this process, randomly takes mark. + */ + int nbMarks = 0; + for (int i=0; i<STREAM_LENGTH; i++) { + if (randomEvent() && i < STREAM_LENGTH - Long.BYTES) { + referenceStream.mark(); + testedStream.mark(); + nbMarks++; + } + final int v = random.nextInt(256); + referenceStream.writeByte(v); + testedStream.writeByte(v); + } + compareMarks(nbMarks); + } + + /** + * Invokes {@link ChannelImageOutputStream#reset()} {@code nbMarks} times and verify that the stream position + * is the expected one. This method will then write random values at those positions, and finally compare the + * stream content. + */ + private void compareMarks(int nbMarks) throws IOException { + while (--nbMarks >= 0) { + referenceStream.reset(); + testedStream.reset(); + assertEquals(referenceStream.getBitOffset(), testedStream.getBitOffset()); + assertEquals(referenceStream.getStreamPosition(), testedStream.getStreamPosition()); + final long v = random.nextLong(); + referenceStream.writeLong(v); + testedStream.writeLong(v); + } + /* + * Verify that we have no remaining marks, and finally compare stream content. + */ + String message = assertThrows(IOException.class, () -> testedStream.reset()).getMessage(); + assertNotNull(message); + assertStreamContentEquals(); + } + + /** + * Tests {@link ChannelImageOutputStream#flushBefore(long)}. + * + * @throws IOException should never happen since we read and write in memory only. + */ + @Test + @DependsOnMethod("testMarkAndReset") + public void testFlushBefore() throws IOException { + final int N = 50; // Number of long values to write. + initialize("testFlushBefore", N*Long.BYTES, 200); + for (int i=0; i<N; i++) { + switch (i) { + case 20: + case 30: + case 40: + case 45: { + referenceStream.mark(); + testedStream.mark(); + break; + } + case 10: { + referenceStream.flushBefore(5 * Long.BYTES); + testedStream.flushBefore(5 * Long.BYTES); + break; + } + case 35: { + referenceStream.flushBefore(32 * Long.BYTES); + testedStream.flushBefore(32 * Long.BYTES); + break; + } + } + final long v = random.nextLong(); + referenceStream.writeLong(v); + testedStream.writeLong(v); + } + compareMarks(2); } } diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataTestCase.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataTestCase.java index 8c6501259f..b02c10f04c 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataTestCase.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataTestCase.java @@ -145,9 +145,56 @@ abstract class ChannelDataTestCase extends TestCase { */ final byte[] createRandomArray(final int length) { final byte[] array = new byte[length]; - for (int i=0; i<length; i++) { - array[i] = (byte) random.nextInt(256); - } + random.nextBytes(array); + return array; + } + + /** {@return a new array of bytes or random length}. */ + final byte[] randomBytes() { + final byte[] array = new byte[random.nextInt(ARRAY_MAX_LENGTH / Byte.BYTES)]; + random.nextBytes(array); + return array; + } + + /** {@return a new array of characters or random length}. */ + final char[] randomChars() { + final char[] array = new char[random.nextInt(ARRAY_MAX_LENGTH / Character.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = (char) random.nextInt(1 << Character.SIZE); + return array; + } + + /** {@return a new array of short integers or random length}. */ + final short[] randomShorts() { + final short[] array = new short[random.nextInt(ARRAY_MAX_LENGTH / Short.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = (short) random.nextInt(1 << Short.SIZE); + return array; + } + + /** {@return a new array of integers or random length}. */ + final int[] randomInts() { + final int[] array = new int[random.nextInt(ARRAY_MAX_LENGTH / Integer.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = random.nextInt(); + return array; + } + + /** {@return a new array of long integers or random length}. */ + final long[] randomLongs() { + final long[] array = new long[random.nextInt(ARRAY_MAX_LENGTH / Long.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = random.nextLong(); + return array; + } + + /** {@return a new array of single-precision floating point values or random length}. */ + final float[] randomFloats() { + final float[] array = new float[random.nextInt(ARRAY_MAX_LENGTH / Float.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = random.nextFloat(); + return array; + } + + /** {@return a new array of double-precision floating point values or random length}. */ + final double[] randomDoubles() { + final double[] array = new double[random.nextInt(ARRAY_MAX_LENGTH / Double.BYTES)]; + for (int i=0; i<array.length; i++) array[i] = random.nextDouble(); return array; } diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageInputStreamTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageInputStreamTest.java index b1fae18292..4240b04213 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageInputStreamTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageInputStreamTest.java @@ -23,13 +23,14 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.channels.Channels; import javax.imageio.ImageIO; +import javax.imageio.stream.IIOByteBuffer; import javax.imageio.stream.ImageInputStream; // Test dependencies import org.junit.Test; import org.apache.sis.test.DependsOn; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** @@ -65,7 +66,24 @@ public final class ChannelImageInputStreamTest extends ChannelDataTestCase { */ @Test public void testWithRandomData() throws IOException { - final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_MAX_CAPACITY); + test(ByteBuffer.allocate(BUFFER_MAX_CAPACITY)); + } + + /** + * Same test, but using a direct buffer. + * Some code paths are different. + * + * @throws IOException should never happen since we read and write in memory only. + */ + @Test + public void testWithDirectBuffer() throws IOException { + test(ByteBuffer.allocateDirect(BUFFER_MAX_CAPACITY)); + } + + /** + * Runs the tests with the specified buffer. + */ + private void test(final ByteBuffer buffer) throws IOException { final ByteOrder byteOrder = random.nextBoolean() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; final byte[] data = createRandomArray(STREAM_LENGTH); referenceStream = ImageIO.createImageInputStream(new ByteArrayInputStream(data)); @@ -73,7 +91,7 @@ public final class ChannelImageInputStreamTest extends ChannelDataTestCase { testedStream = new ChannelImageInputStream("testWithRandomData", Channels.newChannel(new ByteArrayInputStream(data)), buffer, false); testedStream.setByteOrder(byteOrder); - transferRandomData(testedStream, data.length - ARRAY_MAX_LENGTH, 24); + transferRandomData(testedStream, data.length - ARRAY_MAX_LENGTH, 25); testedStream.close(); } @@ -87,104 +105,121 @@ public final class ChannelImageInputStreamTest extends ChannelDataTestCase { final ImageInputStream r = referenceStream; switch (operation) { default: throw new AssertionError(operation); - case 0: assertEquals("read()", r.read(), t.read()); break; - case 1: assertEquals("readBoolean()", r.readBoolean(), t.readBoolean()); break; - case 2: assertEquals("readChar()", r.readChar(), t.readChar()); break; - case 3: assertEquals("readByte()", r.readByte(), t.readByte()); break; - case 4: assertEquals("readShort()", r.readShort(), t.readShort()); break; - case 5: assertEquals("readUnsignedShort()", r.readUnsignedShort(), t.readUnsignedShort()); break; - case 6: assertEquals("readInt()", r.readInt(), t.readInt()); break; - case 7: assertEquals("readUnsignedInt()", r.readUnsignedInt(), t.readUnsignedInt()); break; - case 8: assertEquals("readLong()", r.readLong(), t.readLong()); break; - case 9: assertEquals("readFloat()", r.readFloat(), t.readFloat(), 0f); break; - case 10: assertEquals("readDouble()", r.readDouble(), t.readDouble(), 0d); break; - case 11: assertEquals("readBit()", r.readBit(), t.readBit()); break; + case 0: assertEquals(r.read(), t.read(), "read()"); break; + case 1: assertEquals(r.readBoolean(), t.readBoolean(), "readBoolean()"); break; + case 2: assertEquals(r.readChar(), t.readChar(), "readChar()"); break; + case 3: assertEquals(r.readByte(), t.readByte(), "readByte()"); break; + case 4: assertEquals(r.readShort(), t.readShort(), "readShort()"); break; + case 5: assertEquals(r.readUnsignedShort(), t.readUnsignedShort(), "readUnsignedShort()"); break; + case 6: assertEquals(r.readInt(), t.readInt(), "readInt()"); break; + case 7: assertEquals(r.readUnsignedInt(), t.readUnsignedInt(), "readUnsignedInt()"); break; + case 8: assertEquals(r.readLong(), t.readLong(), "readLong()"); break; + case 9: assertEquals(r.readFloat(), t.readFloat(), "readFloat()"); break; + case 10: assertEquals(r.readDouble(), t.readDouble(), "readDouble()"); break; + case 11: assertEquals(r.readBit(), t.readBit(), "readBit()"); break; case 12: { final int n = random.nextInt(Long.SIZE + 1); - assertEquals("readBits(" + n + ')', r.readBits(n), t.readBits(n)); + assertEquals(r.readBits(n), t.readBits(n), () -> "readBits(" + n + ')'); break; } case 13: { final int length = random.nextInt(ARRAY_MAX_LENGTH); final byte[] actual = new byte[length]; final int n = t.read(actual); - assertFalse("Reached EOF", n < 0); + assertFalse(n < 0, "Reached EOF"); final byte[] expected = new byte[n]; r.readFully(expected); - assertArrayEquals("read(byte[])", expected, actual); + assertArrayEquals(expected, actual, "read(byte[])"); break; } case 14: { final int length = random.nextInt(ARRAY_MAX_LENGTH); final byte[] expected = new byte[length]; r.readFully(expected); final byte[] actual = new byte[length]; t.readFully(actual); - assertArrayEquals("readFully(byte[])", expected, actual); + assertArrayEquals(expected, actual, "readFully(byte[])"); break; } case 15: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Character.BYTES); final char[] expected = new char[length]; r.readFully(expected, 0, length); final char[] actual = new char[length]; t.readFully(actual, 0, length); - assertArrayEquals("readFully(char[])", expected, actual); + assertArrayEquals(expected, actual, "readFully(char[])"); break; } case 16: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Short.BYTES); final short[] expected = new short[length]; r.readFully(expected, 0, length); final short[] actual = new short[length]; t.readFully(actual, 0, length); - assertArrayEquals("readFully(short[])", expected, actual); + assertArrayEquals(expected, actual, "readFully(short[])"); break; } case 17: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Integer.BYTES); final int[] expected = new int[length]; r.readFully(expected, 0, length); final int[] actual = new int[length]; t.readFully(actual, 0, length); - assertArrayEquals("readFully(int[])", expected, actual); + assertArrayEquals(expected, actual, "readFully(int[])"); break; } case 18: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Long.BYTES); final long[] expected = new long[length]; r.readFully(expected, 0, length); final long[] actual = new long[length]; t.readFully(actual, 0, length); - assertArrayEquals("readFully(long[])", expected, actual); + assertArrayEquals(expected, actual, "readFully(long[])"); break; } case 19: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Float.BYTES); final float[] expected = new float[length]; r.readFully(expected, 0, length); final float[] actual = new float[length]; t.readFully(actual, 0, length); - assertTrue("readFully(float[])", Arrays.equals(expected, actual)); + assertArrayEquals(expected, actual, "readFully(float[])"); break; } case 20: { final int length = random.nextInt(ARRAY_MAX_LENGTH / Double.BYTES); final double[] expected = new double[length]; r.readFully(expected, 0, length); final double[] actual = new double[length]; t.readFully(actual, 0, length); - assertTrue("readFully(double[])", Arrays.equals(expected, actual)); + assertArrayEquals(expected, actual, "readFully(double[])"); break; } case 21: { + final IIOByteBuffer buffer = new IIOByteBuffer(null, 0, 0); + t.readBytes(buffer, random.nextInt(ARRAY_MAX_LENGTH)); + final byte[] actual = data(buffer); + r.readBytes(buffer, actual.length); + final byte[] expected = data(buffer); + assertArrayEquals(expected, actual, "readBytes(IIOByteBuffer)"); + break; + } + case 22: { final long length = random.nextInt(ARRAY_MAX_LENGTH); final long n = t.skipBytes(length); - assertFalse("Reached EOF", n < 0); + assertFalse(n < 0, "Reached EOF"); r.readFully(new byte[(int) n]); - assertEquals("skipBytes(int)", r.getStreamPosition(), t.getStreamPosition()); + assertEquals(r.getStreamPosition(), t.getStreamPosition(), "skipBytes(int)"); break; } - case 22: { + case 23: { long flushedPosition = StrictMath.max(r.getFlushedPosition(), t.getFlushedPosition()); flushedPosition += random.nextInt(1 + (int) (r.getStreamPosition() - flushedPosition)); r.flushBefore(flushedPosition); t.flushBefore(flushedPosition); break; } - case 23: { + case 24: { r.flush(); t.flush(); break; } } - assertEquals("getStreamPosition()", r.getStreamPosition(), t.getStreamPosition()); - assertEquals("getBitOffset()", r.getBitOffset(), t.getBitOffset()); + assertEquals(r.getStreamPosition(), t.getStreamPosition(), "getStreamPosition()"); + assertEquals(r.getBitOffset(), t.getBitOffset(), "getBitOffset()"); + } + + /** + * Returns a copy of the data in the given buffer. + */ + private static byte[] data(final IIOByteBuffer buffer) { + final int offset = buffer.getOffset(); + return Arrays.copyOfRange(buffer.getData(), offset, offset + buffer.getLength()); } } diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java index 818a3feacd..1fd9c83a80 100644 --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java @@ -16,14 +16,18 @@ */ package org.apache.sis.io.stream; +import java.util.Arrays; import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import javax.imageio.stream.ImageOutputStream; // Test dependencies import org.junit.Test; -import org.apache.sis.test.DependsOnMethod; import org.apache.sis.test.DependsOn; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** @@ -33,139 +37,177 @@ import static org.junit.Assert.*; * @author Martin Desruisseaux (Geomatys) */ @DependsOn(ChannelDataOutputTest.class) -public final class ChannelImageOutputStreamTest extends ChannelDataOutputTest { +public final class ChannelImageOutputStreamTest extends ChannelDataTestCase { /** - * Creates a new test case. + * The implementation to test. This implementation will write data to {@link #testedStreamBackingArray}. + * The content of that array will be compared to {@link #expectedData} for verifying result correctness. */ - public ChannelImageOutputStreamTest() { - } + private ChannelImageOutputStream testedStream; /** - * Initializes all non-final fields before to execute a test. + * A wrapper around {@link #testedStream}, used only because required by the tests in super-class. */ - @Override - void initialize(final String testName, final int streamLength, final int bufferLength) throws IOException { - super.initialize(testName, streamLength, bufferLength); - testedStream = new ChannelImageOutputStream(testedStream); + private ChannelData testWrapper; + + /** + * A stream to use as a reference implementation. Any data written in {@link #testedStream} + * will also be written in {@code referenceStream}, for later comparison. + */ + private ImageOutputStream referenceStream; + + /** + * Byte array which is filled by the {@linkplain #testedStream} implementation during write operations. + * The content of this array will be compared to {@linkplain #expectedData}. + */ + private byte[] testedStreamBackingArray; + + /** + * Object which is filled by {@link #referenceStream} implementation during write operations. + * <b>Do not write to this stream</b> - this field is kept only for invocation of + * {@link ByteArrayOutputStream#toByteArray()}. + */ + private ByteArrayOutputStream expectedData; + + /** + * Creates a new test case. + */ + public ChannelImageOutputStreamTest() { } /** - * Test writing a sequence of bits. + * Initializes all non-final fields before to execute a test. * + * @param testName the name of the test method to be executed. + * @param streamLength length of stream to create. + * @param bufferLength length of the {@code ByteBuffer} to use for the tests. * @throws IOException should never happen since we read and write in memory only. */ - @Test - public void testWriteBits() throws IOException { - initialize("testWriteBits", STREAM_LENGTH, randomBufferCapacity()); - final int length = testedStreamBackingArray.length - ARRAY_MAX_LENGTH; // Keep a margin against buffer underflow. - while (testedStream.getStreamPosition() < length) { - final long v = random.nextLong(); - final int numBits = random.nextInt(Byte.SIZE); - referenceStream.writeBits(v, numBits); - testedStream.writeBits(v, numBits); - /* - * Randomly force flushing of bits. - */ - if (randomEvent()) { - final int f = random.nextInt(256); - referenceStream.writeByte(f); - testedStream.writeByte(f); - } - assertEquals("getBitOffset", referenceStream.getBitOffset(), testedStream.getBitOffset()); - assertEquals("getStreamPosition", referenceStream.getStreamPosition(), testedStream.getStreamPosition()); - } - assertStreamContentEquals(); + private void initialize(final String testName, final int streamLength, final int bufferLength) throws IOException { + expectedData = new ByteArrayOutputStream(streamLength); + referenceStream = new MemoryCacheImageOutputStream(expectedData); + testedStreamBackingArray = new byte[streamLength]; + var channel = new ByteArrayChannel(testedStreamBackingArray, false); + testedStream = new ChannelImageOutputStream(testName, channel, ByteBuffer.allocate(bufferLength)); + testWrapper = new ChannelData(testName, channel, testedStream.input().buffer) { + @Override public Channel channel() {return channel;} + @Override public long getStreamPosition() {return testedStream.getStreamPosition();} + @Override public void seek(long p) {fail("Should not be invoked.");} + @Override void flushNBytes(int n) {fail("Should not be invoked.");} + }; } /** - * Tests {@link ChannelImageOutputStream#mark()} and {@code reset()} methods. + * Fills a stream with random data and compares the result with a reference output stream. + * This method tests both read and write operations. * * @throws IOException should never happen since we read and write in memory only. */ @Test - public void testMarkAndReset() throws IOException { - initialize("testMarkAndReset", STREAM_LENGTH, 1000); // We need a larger buffer for this test. - /* - * Fill both streams with random data. - * During this process, randomly takes mark. - */ - int nbMarks = 0; - for (int i=0; i<STREAM_LENGTH; i++) { - if (randomEvent() && i < STREAM_LENGTH - Long.BYTES) { - referenceStream.mark(); - testedStream.mark(); - nbMarks++; - } - final int v = random.nextInt(256); - referenceStream.writeByte(v); - testedStream.writeByte(v); - } - compareMarks(nbMarks); + public void testAllMethods() throws IOException { + initialize("testAllMethods", STREAM_LENGTH, randomBufferCapacity()); + transferRandomData(testWrapper, testedStreamBackingArray.length - ARRAY_MAX_LENGTH, 21); // TODO: should be 22 + assertStreamContentEquals(); } /** - * Invokes {@link ChannelImageOutputStream#reset()} {@code nbMarks} times and verify that the stream position - * is the expected one. This method will then write random values at those positions, and finally compare the - * stream content. + * Asserts that the content of {@link #testedStream} is equal to the content of {@link #referenceStream}. + * This method closes the reference stream before to perform the comparison. */ - private void compareMarks(int nbMarks) throws IOException { - while (--nbMarks >= 0) { - referenceStream.reset(); - testedStream.reset(); - assertEquals(referenceStream.getBitOffset(), testedStream.getBitOffset()); - assertEquals(referenceStream.getStreamPosition(), testedStream.getStreamPosition()); - final long v = random.nextLong(); - referenceStream.writeLong(v); - testedStream.writeLong(v); - } - /* - * Verify that we have no remaining marks, and finally compare stream content. - */ - try { - testedStream.reset(); - fail("Expected no remaining marks."); - } catch (IOException e) { - // This is the expected exception. - assertNotNull(e.getMessage()); - } - assertStreamContentEquals(); + private void assertStreamContentEquals() throws IOException { + testedStream.flush(); + referenceStream.close(); + final byte[] expectedArray = expectedData.toByteArray(); + assertArrayEquals(expectedArray, Arrays.copyOf(testedStreamBackingArray, expectedArray.length)); } /** - * Tests {@link ChannelImageOutputStream#flushBefore(long)}. + * Writes a random unit of data using a method selected randomly. + * This method is invoked (indirectly) by {@link #writeInStreams()}. * - * @throws IOException should never happen since we read and write in memory only. + * @param operation numerical identifier of the operation to test. */ - @Test - @DependsOnMethod("testMarkAndReset") - public void testFlushBefore() throws IOException { - final int N = 50; // Number of long values to write. - initialize("testFlushBefore", N*Long.BYTES, 200); - for (int i=0; i<N; i++) { - switch (i) { - case 20: - case 30: - case 40: - case 45: { - referenceStream.mark(); - testedStream.mark(); - break; - } - case 10: { - referenceStream.flushBefore(5 * Long.BYTES); - testedStream.flushBefore(5 * Long.BYTES); - break; - } - case 35: { - referenceStream.flushBefore(32 * Long.BYTES); - testedStream.flushBefore(32 * Long.BYTES); - break; + @Override + final void transferRandomData(final int operation) throws IOException { + final ImageOutputStream r = this.referenceStream; + final ChannelImageOutputStream t = this.testedStream; + switch (operation) { + default: throw new AssertionError(operation); + case 0: {byte v = (byte) random.nextInt(); r.writeByte (v); t.writeByte (v); break;} + case 1: {short v = (short) random.nextInt(); r.writeShort (v); t.writeShort (v); break;} + case 2: {char v = (char) random.nextInt(); r.writeChar (v); t.writeChar (v); break;} + case 3: {int v = random.nextInt(); r.writeInt (v); t.writeInt (v); break;} + case 4: {long v = random.nextLong(); r.writeLong (v); t.writeLong (v); break;} + case 5: {float v = random.nextFloat(); r.writeFloat (v); t.writeFloat (v); break;} + case 6: {double v = random.nextDouble(); r.writeDouble (v); t.writeDouble (v); break;} + case 7: {boolean v = random.nextBoolean(); r.writeBoolean(v); t.writeBoolean(v); break;} + case 8: {byte[] v = randomBytes(); r.write (v); t.write (v); break;} + case 9: {char[] v = randomChars(); r.writeChars (v, 0, v.length); t.writeChars (v, 0, v.length); break;} + case 10: {short[] v = randomShorts(); r.writeShorts (v, 0, v.length); t.writeShorts (v, 0, v.length); break;} + case 11: {int[] v = randomInts(); r.writeInts (v, 0, v.length); t.writeInts (v, 0, v.length); break;} + case 12: {long[] v = randomLongs(); r.writeLongs (v, 0, v.length); t.writeLongs (v, 0, v.length); break;} + case 13: {float[] v = randomFloats(); r.writeFloats (v, 0, v.length); t.writeFloats (v, 0, v.length); break;} + case 14: {double[] v = randomDoubles(); r.writeDoubles(v, 0, v.length); t.writeDoubles(v, 0, v.length); break;} + case 15: {String v = "Byte sequence"; r.writeBytes(v); t.writeBytes(v); break;} + case 16: {String v = "Character sequence"; r.writeChars(v); t.writeChars(v); break;} + case 17: {String v = "お元気ですか"; r.writeUTF (v); t.writeUTF (v); break;} + case 18: { + final long v = random.nextLong(); + final int numBits = random.nextInt(Byte.SIZE); + r.writeBits(v, numBits); + t.writeBits(v, numBits); + break; + } + case 19: { + long flushedPosition = StrictMath.max(r.getFlushedPosition(), t.getFlushedPosition()); + flushedPosition += random.nextInt(1 + (int) (r.getStreamPosition() - flushedPosition)); + r.flushBefore(flushedPosition); + t.flushBefore(flushedPosition); + break; + } + case 20: { + r.flush(); + t.flush(); + break; + } + /* + * Seek operation, potentially followed by a few read operations. + * The seek is necessary for moving to a position where there is something to read. + */ + case 21: { + long length = r.length(); + assertTrue(length >= 0, "length"); + assertEquals(length, t.length(), "length"); + long position = Math.max(r.getFlushedPosition(), t.getFlushedPosition()); + if (position < length) { + position += random.nextInt(Math.toIntExact(length - position)); + r.seek(position); + t.seek(position); + length -= Double.BYTES; // Make room for the largest element that we will read. + for (int i = random.nextInt(5); --i >= 0;) { + position = r.getStreamPosition(); + assertEquals(position, t.getStreamPosition()); + if (position >= length) break; + switch (random.nextInt(12)) { + default: throw new AssertionError(); + case 0: assertEquals(r.read(), t.read(), "read()"); break; + case 1: assertEquals(r.readBoolean(), t.readBoolean(), "readBoolean()"); break; + case 2: assertEquals(r.readChar(), t.readChar(), "readChar()"); break; + case 3: assertEquals(r.readByte(), t.readByte(), "readByte()"); break; + case 4: assertEquals(r.readShort(), t.readShort(), "readShort()"); break; + case 5: assertEquals(r.readUnsignedShort(), t.readUnsignedShort(), "readUnsignedShort()"); break; + case 6: assertEquals(r.readInt(), t.readInt(), "readInt()"); break; + case 7: assertEquals(r.readUnsignedInt(), t.readUnsignedInt(), "readUnsignedInt()"); break; + case 8: assertEquals(r.readLong(), t.readLong(), "readLong()"); break; + case 9: assertEquals(r.readFloat(), t.readFloat(), "readFloat()"); break; + case 10: assertEquals(r.readDouble(), t.readDouble(), "readDouble()"); break; + case 11: assertEquals(r.readBit(), t.readBit(), "readBit()"); break; + } + } } + break; } - final long v = random.nextLong(); - referenceStream.writeLong(v); - testedStream.writeLong(v); } - compareMarks(2); + assertEquals(r.getBitOffset(), t.getBitOffset(), "getBitOffset()"); + assertEquals(r.getStreamPosition(), t.getStreamPosition(), "getStreamPosition()"); } }