TestMergedStream.java

package wstxtest.io;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import com.ctc.wstx.io.MergedStream;
import org.junit.jupiter.api.Test;

/**
 * Unit tests for {@link MergedStream}, the InputStream wrapper that serves
 * data from a "rewound" buffer first, then falls back to the underlying
 * stream once the buffer is exhausted.
 */
public class TestMergedStream extends wstxtest.BaseWstxTest
{
    // ---------- construction ----------

    @Test
    public void testNullUnderlyingRejected()
    {
        try {
            new MergedStream(null, null, new byte[]{1, 2}, 0, 2);
            fail("Expected IllegalArgumentException for null underlying stream");
        } catch (IllegalArgumentException e) {
            verifyException(e, "InputStream");
        }
    }

    // ---------- single-byte read() ----------

    @Test
    public void testReadDrainsBufferThenUnderlying() throws IOException
    {
        byte[] buf = "AB".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("CD".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            assertEquals('A', ms.read());
            assertEquals('B', ms.read());
            // Buffer exhausted; should now read from underlying
            assertEquals('C', ms.read());
            assertEquals('D', ms.read());
            assertEquals(-1, ms.read());
        }
    }

    @Test
    public void testReadHonorsStartOffset() throws IOException
    {
        byte[] buf = "XXAB".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream(new byte[0]);
        // Start at offset 2 ��� first two bytes ignored
        try (MergedStream ms = new MergedStream(null, underlying, buf, 2, buf.length)) {
            assertEquals('A', ms.read());
            assertEquals('B', ms.read());
            assertEquals(-1, ms.read());
        }
    }

    // ---------- bulk read() ----------

    @Test
    public void testReadIntoArrayFromBuffer() throws IOException
    {
        byte[] buf = "ABCDE".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream(new byte[0]);
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            byte[] out = new byte[10];
            int n = ms.read(out);
            // Bulk read is capped to remaining buffered bytes
            assertEquals(5, n);
            assertEquals("ABCDE", new String(out, 0, n, "ISO-8859-1"));
            // Subsequent read falls back to (empty) underlying
            assertEquals(-1, ms.read(out));
        }
    }

    @Test
    public void testReadIntoArrayPartialThenUnderlying() throws IOException
    {
        byte[] buf = "ABCDE".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("FGH".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            byte[] out = new byte[3];
            // First read pulls 3 bytes from the buffered segment
            assertEquals(3, ms.read(out, 0, 3));
            assertEquals("ABC", new String(out, 0, 3, "ISO-8859-1"));
            // Two left in buffer
            assertEquals(2, ms.read(out, 0, 3));
            assertEquals("DE", new String(out, 0, 2, "ISO-8859-1"));
            // Now from underlying
            int n = ms.read(out, 0, 3);
            assertEquals(3, n);
            assertEquals("FGH", new String(out, 0, n, "ISO-8859-1"));
        }
    }

    // ---------- available() ----------

    @Test
    public void testAvailableReportsBufferedThenUnderlying() throws IOException
    {
        byte[] buf = "ABC".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("DE".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            assertEquals(3, ms.available());
            ms.read(); // drain one
            assertEquals(2, ms.available());
            // Drain the rest of the buffer
            ms.read();
            ms.read();
            // Now reflects underlying stream's available()
            assertEquals(2, ms.available());
        }
    }

    // ---------- skip() ----------

    @Test
    public void testSkipWithinBuffer() throws IOException
    {
        byte[] buf = "ABCDEFGH".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("xyz".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            // Skip 3 within the buffered segment ��� return value == requested
            assertEquals(3L, ms.skip(3));
            assertEquals('D', ms.read());
        }
    }

    @Test
    public void testSkipCrossesBufferIntoUnderlying() throws IOException
    {
        byte[] buf = "ABCD".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("EFGHIJ".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            // Skip 6: 4 from buffer + 2 from underlying
            long n = ms.skip(6);
            assertEquals(6L, n);
            assertEquals('G', ms.read());
        }
    }

    @Test
    public void testSkipPastBufferOnlyUnderlying() throws IOException
    {
        byte[] buf = "AB".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("CDEF".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            // First fully drain the buffer
            ms.read();
            ms.read();
            // Now skip from underlying directly
            assertEquals(2L, ms.skip(2));
            assertEquals('E', ms.read());
        }
    }

    // ---------- mark / markSupported / reset ----------

    @Test
    public void testMarkSupportedFalseWhileBuffered() throws IOException
    {
        byte[] buf = "AB".getBytes("ISO-8859-1");
        // ByteArrayInputStream.markSupported() returns true by default
        InputStream underlying = new ByteArrayInputStream(new byte[]{ 'C' });
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            // While in buffered segment, marks are not supported
            assertFalse(ms.markSupported());
            ms.read();
            ms.read();
            // After buffer is drained, the underlying stream's capability is reported
            assertTrue(ms.markSupported());
        }
    }

    @Test
    public void testMarkNoOpWhileBuffered() throws IOException
    {
        byte[] buf = "AB".getBytes("ISO-8859-1");
        // ByteArrayInputStream supports mark/reset, but it should NOT be called
        // while we're still in the buffered segment.
        InputStream underlying = new ByteArrayInputStream("CD".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            ms.mark(10);   // should be no-op while buffered
            assertEquals('A', ms.read());
            // reset must also be a no-op while buffered (won't throw)
            ms.reset();
            // We continue from where we left off, NOT back to start
            assertEquals('B', ms.read());
        }
    }

    @Test
    public void testMarkAndResetDelegatedAfterBufferDrained() throws IOException
    {
        byte[] buf = "AB".getBytes("ISO-8859-1");
        InputStream underlying = new ByteArrayInputStream("CDE".getBytes("ISO-8859-1"));
        try (MergedStream ms = new MergedStream(null, underlying, buf, 0, buf.length)) {
            // Drain buffered segment
            ms.read();
            ms.read();
            ms.mark(10);
            assertEquals('C', ms.read());
            assertEquals('D', ms.read());
            ms.reset();
            assertEquals('C', ms.read());
        }
    }

    // ---------- close() ----------

    @Test
    public void testCloseClosesUnderlying() throws IOException
    {
        final boolean[] closed = { false };
        InputStream underlying = new ByteArrayInputStream(new byte[]{ 1 }) {
            @Override public void close() throws IOException {
                closed[0] = true;
                super.close();
            }
        };
        MergedStream ms = new MergedStream(null, underlying, new byte[]{ 0 }, 0, 1);
        ms.close();
        assertTrue("close() must propagate to underlying", closed[0]);
    }
}