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()");
     }
 }

Reply via email to