You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2019/08/07 01:16:43 UTC

[commons-io] branch master updated: [IO-612] Add class TeeReader. PR from Rob Spoor but modified.

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git


The following commit(s) were added to refs/heads/master by this push:
     new 1e37482  [IO-612] Add class TeeReader. PR from Rob Spoor but modified.
1e37482 is described below

commit 1e374820d0251105a87b9ac86f0c8fa32ce8031b
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Tue Aug 6 21:16:38 2019 -0400

    [IO-612] Add class TeeReader. PR from Rob Spoor but modified.
---
 pom.xml                                            |   6 +
 src/changes/changes.xml                            |   3 +
 .../apache/commons/io/input/NullInputStream.java   |  10 ++
 .../org/apache/commons/io/input/NullReader.java    |  13 +-
 .../org/apache/commons/io/input/TeeReader.java     | 168 +++++++++++++++++++
 .../commons/io/input/TeeInputStreamTest.java       |  57 +++++++
 .../org/apache/commons/io/input/TeeReaderTest.java | 182 +++++++++++++++++++++
 .../io/testtools/YellOnCloseInputStream.java       |  15 +-
 ...putStream.java => YellOnCloseOutputStream.java} |  26 +--
 ...loseInputStream.java => YellOnCloseReader.java} |  27 +--
 ...loseInputStream.java => YellOnCloseWriter.java} |  24 ++-
 11 files changed, 496 insertions(+), 35 deletions(-)

diff --git a/pom.xml b/pom.xml
index c63f2c1..b278282 100644
--- a/pom.xml
+++ b/pom.xml
@@ -233,6 +233,12 @@ file comparators, endian transformation classes, and much more.
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>3.0.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>com.google.jimfs</groupId>
       <artifactId>jimfs</artifactId>
       <version>1.1</version>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index b8ac8f1..875ea25 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -110,6 +110,9 @@ The <action> type attribute can be add,update,fix,remove.
       <action issue="IO-610" dev="ggregory" type="update" due-to="Sebastian">
         Remove throws IOException in method isSymlink() #80.
       </action>
+      <action issue="IO-612" dev="ggregory" type="add" due-to="Rob Spoor, Gary Gregory">
+        Add class TeeReader.
+      </action>
     </release>
 
     <release version="2.6" date="2017-10-15" description="Java 7 required, Java 9 supported.">
diff --git a/src/main/java/org/apache/commons/io/input/NullInputStream.java b/src/main/java/org/apache/commons/io/input/NullInputStream.java
index ad8be20..2df3e66 100644
--- a/src/main/java/org/apache/commons/io/input/NullInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/NullInputStream.java
@@ -73,6 +73,16 @@ public class NullInputStream extends InputStream {
     private final boolean markSupported;
 
     /**
+     * Create an {@link InputStream} that emulates a size 0 stream
+     * which supports marking and does not throw EOFException.
+     * 
+     * @since 2.7
+     */
+    public NullInputStream() {
+       this(0, true, false);
+    }
+
+    /**
      * Create an {@link InputStream} that emulates a specified size
      * which supports marking and does not throw EOFException.
      *
diff --git a/src/main/java/org/apache/commons/io/input/NullReader.java b/src/main/java/org/apache/commons/io/input/NullReader.java
index 6a24d25..b361e54 100644
--- a/src/main/java/org/apache/commons/io/input/NullReader.java
+++ b/src/main/java/org/apache/commons/io/input/NullReader.java
@@ -45,7 +45,7 @@ import java.io.Reader;
  * <code>processChars()</code> methods can be implemented to generate
  * data, for example:
  * </p>
- * 
+ *
  * <pre>
  *  public class TestReader extends NullReader {
  *      public TestReader(int size) {
@@ -63,7 +63,6 @@ import java.io.Reader;
  * </pre>
  *
  * @since 1.3
- *
  */
 public class NullReader extends Reader {
 
@@ -76,6 +75,16 @@ public class NullReader extends Reader {
     private final boolean markSupported;
 
     /**
+     * Creates a {@link Reader} that emulates a size 0 reader
+     * which supports marking and does not throw EOFException.
+     * 
+     * @since 2.7
+     */
+    public NullReader() {
+       this(0, true, false);
+    }
+
+    /**
      * Creates a {@link Reader} that emulates a specified size
      * which supports marking and does not throw EOFException.
      *
diff --git a/src/main/java/org/apache/commons/io/input/TeeReader.java b/src/main/java/org/apache/commons/io/input/TeeReader.java
new file mode 100644
index 0000000..827f062
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/input/TeeReader.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.apache.commons.io.IOUtils.EOF;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.CharBuffer;
+
+/**
+ * Reader proxy that transparently writes a copy of all characters read from the proxied reader to a given Reader. Using
+ * {@link #skip(long)} or {@link #mark(int)}/{@link #reset()} on the reader will result on some characters from the
+ * reader being skipped or duplicated in the writer.
+ * <p>
+ * The proxied reader is closed when the {@link #close()} method is called on this proxy. You may configure whether the
+ * reader closes the writer.
+ * </p>
+ *
+ * @since 2.7
+ */
+public class TeeReader extends ProxyReader {
+
+    /**
+     * The writer that will receive a copy of all characters read from the proxied reader.
+     */
+    private final Writer branch;
+
+    /**
+     * Flag for closing the associated writer when this reader is closed.
+     */
+    private final boolean closeBranch;
+
+    /**
+     * Creates a TeeReader that proxies the given {@link Reader} and copies all read characters to the given
+     * {@link Writer}. The given writer will not be closed when this reader gets closed.
+     *
+     * @param input  reader to be proxied
+     * @param branch writer that will receive a copy of all characters read
+     */
+    public TeeReader(final Reader input, final Writer branch) {
+        this(input, branch, false);
+    }
+
+    /**
+     * Creates a TeeReader that proxies the given {@link Reader} and copies all read characters to the given
+     * {@link Writer}. The given writer will be closed when this reader gets closed if the closeBranch parameter is
+     * {@code true}.
+     *
+     * @param input       reader to be proxied
+     * @param branch      writer that will receive a copy of all characters read
+     * @param closeBranch flag for closing also the writer when this reader is closed
+     */
+    public TeeReader(final Reader input, final Writer branch, final boolean closeBranch) {
+        super(input);
+        this.branch = branch;
+        this.closeBranch = closeBranch;
+    }
+
+    /**
+     * Closes the proxied reader and, if so configured, the associated writer. An exception thrown from the reader will
+     * not prevent closing of the writer.
+     *
+     * @throws IOException if either the reader or writer could not be closed
+     */
+    @Override
+    public void close() throws IOException {
+        try {
+            super.close();
+        } finally {
+            if (closeBranch) {
+                branch.close();
+            }
+        }
+    }
+
+    /**
+     * Reads a single chracter from the proxied reader and writes it to the associated writer.
+     *
+     * @return next character from the reader, or -1 if the reader has ended
+     * @throws IOException if the reader could not be read (or written)
+     */
+    @Override
+    public int read() throws IOException {
+        final int ch = super.read();
+        if (ch != EOF) {
+            branch.write(ch);
+        }
+        return ch;
+    }
+
+    /**
+     * Reads characters from the proxied reader and writes the read characters to the associated writer.
+     *
+     * @param chr character buffer
+     * @return number of characters read, or -1 if the reader has ended
+     * @throws IOException if the reader could not be read (or written)
+     */
+    @Override
+    public int read(final char[] chr) throws IOException {
+        final int n = super.read(chr);
+        if (n != EOF) {
+            branch.write(chr, 0, n);
+        }
+        return n;
+    }
+
+    /**
+     * Reads characters from the proxied reader and writes the read characters to the associated writer.
+     *
+     * @param chr character buffer
+     * @param st  start offset within the buffer
+     * @param end maximum number of characters to read
+     * @return number of characters read, or -1 if the reader has ended
+     * @throws IOException if the reader could not be read (or written)
+     */
+    @Override
+    public int read(final char[] chr, final int st, final int end) throws IOException {
+        final int n = super.read(chr, st, end);
+        if (n != EOF) {
+            branch.write(chr, st, n);
+        }
+        return n;
+    }
+
+    /**
+     * Reads characters from the proxied reader and writes the read characters to the associated writer.
+     *
+     * @param target character buffer
+     * @return number of characters read, or -1 if the reader has ended
+     * @throws IOException if the reader could not be read (or written)
+     */
+    @Override
+    public int read(final CharBuffer target) throws IOException {
+        final int originalPosition = target.position();
+        final int n = super.read(target);
+        if (n != EOF) {
+            // Appending can only be done after resetting the CharBuffer to the
+            // right position and limit.
+            final int newPosition = target.position();
+            final int newLimit = target.limit();
+            try {
+                target.position(originalPosition).limit(newPosition);
+                branch.append(target);
+            } finally {
+                // Reset the CharBuffer as if the appending never happened.
+                target.position(newPosition).limit(newLimit);
+            }
+        }
+        return n;
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java b/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java
index c1df877..bfb4f3f 100644
--- a/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java
@@ -17,11 +17,20 @@
 package org.apache.commons.io.input;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 
+import org.apache.commons.io.testtools.YellOnCloseInputStream;
+import org.apache.commons.io.testtools.YellOnCloseOutputStream;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -106,4 +115,52 @@ public class TeeInputStreamTest  {
         assertEquals("abbc", new String(output.toString(ASCII)));
     }
 
+    /**
+     * Tests that the main {@code InputStream} is closed when closing the branch {@code OutputStream} throws an
+     * exception on {@link TeeInputStream#close()}, if specified to do so.
+     */
+    @Test
+    public void testCloseBranchIOException() throws Exception {
+        final ByteArrayInputStream goodIs = mock(ByteArrayInputStream.class);
+        final OutputStream badOs = new YellOnCloseOutputStream();
+
+        final TeeInputStream nonClosingTis = new TeeInputStream(goodIs, badOs, false);
+        nonClosingTis.close();
+        verify(goodIs).close();
+
+        final TeeInputStream closingTis = new TeeInputStream(goodIs, badOs, true);
+        try {
+            closingTis.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            verify(goodIs, times(2)).close();
+        }
+    }
+
+    /**
+     * Tests that the branch {@code OutputStream} is closed when closing the main {@code InputStream} throws an
+     * exception on {@link TeeInputStream#close()}, if specified to do so.
+     */
+    @Test
+    public void testCloseMainIOException() throws IOException {
+        final InputStream badIs = new YellOnCloseInputStream();
+        final ByteArrayOutputStream goodOs = mock(ByteArrayOutputStream.class);
+
+        final TeeInputStream nonClosingTis = new TeeInputStream(badIs, goodOs, false);
+        try {
+            nonClosingTis.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            verify(goodOs, never()).close();
+        }
+
+        final TeeInputStream closingTis = new TeeInputStream(badIs, goodOs, true);
+        try {
+            closingTis.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            verify(goodOs).close();
+        }
+    }
+
 }
diff --git a/src/test/java/org/apache/commons/io/input/TeeReaderTest.java b/src/test/java/org/apache/commons/io/input/TeeReaderTest.java
new file mode 100644
index 0000000..de175bb
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/input/TeeReaderTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.CharBuffer;
+
+import org.apache.commons.io.output.StringBuilderWriter;
+import org.apache.commons.io.testtools.YellOnCloseReader;
+import org.apache.commons.io.testtools.YellOnCloseWriter;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * JUnit Test Case for {@link TeeReader}.
+ */
+public class TeeReaderTest  {
+
+    private StringBuilderWriter output;
+
+    private Reader tee;
+
+    @Before
+    public void setUp() throws Exception {
+        final Reader input = new CharSequenceReader("abc");
+        output = new StringBuilderWriter();
+        tee = new TeeReader(input, output);
+    }
+
+    /**
+     * Tests that the main {@code Reader} is closed when closing the branch {@code Writer} throws an
+     * exception on {@link TeeReader#close()}, if specified to do so.
+     */
+    @Test
+    public void testCloseBranchIOException() throws Exception {
+        final StringReader goodR = mock(StringReader.class);
+        final Writer badW = new YellOnCloseWriter();
+
+        final TeeReader nonClosingTr = new TeeReader(goodR, badW, false);
+        nonClosingTr.close();
+        verify(goodR).close();
+
+        final TeeReader closingTr = new TeeReader(goodR, badW, true);
+        try {
+            closingTr.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            verify(goodR, times(2)).close();
+        }
+    }
+
+    /**
+     * Tests that the branch {@code Writer} is closed when closing the main {@code Reader} throws an
+     * exception on {@link TeeReader#close()}, if specified to do so.
+     */
+    @Test
+    public void testCloseMainIOException() throws IOException {
+        final Reader badR = new YellOnCloseReader();
+        final StringWriter goodW = mock(StringWriter.class);
+
+        final TeeReader nonClosingTr = new TeeReader(badR, goodW, false);
+        try {
+            nonClosingTr.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            verify(goodW, never()).close();
+        }
+
+        final TeeReader closingTr = new TeeReader(badR, goodW, true);
+        try {
+            closingTr.close();
+            Assert.fail("Expected " + IOException.class.getName());
+        } catch (final IOException e) {
+            //Assert.assertTrue(goodW.closed);
+            verify(goodW).close();
+        }
+    }
+
+    @Test
+    public void testMarkReset() throws Exception {
+        assertEquals('a', tee.read());
+        tee.mark(1);
+        assertEquals('b', tee.read());
+        tee.reset();
+        assertEquals('b', tee.read());
+        assertEquals('c', tee.read());
+        assertEquals(-1, tee.read());
+        assertEquals("abbc", output.toString());
+    }
+
+    @Test
+    public void testReadEverything() throws Exception {
+        assertEquals('a', tee.read());
+        assertEquals('b', tee.read());
+        assertEquals('c', tee.read());
+        assertEquals(-1, tee.read());
+        assertEquals("abc", output.toString());
+    }
+
+    @Test
+    public void testReadNothing() throws Exception {
+        assertEquals("", output.toString());
+    }
+
+    @Test
+    public void testReadOneChar() throws Exception {
+        assertEquals('a', tee.read());
+        assertEquals("a", output.toString());
+    }
+
+    @Test
+    public void testReadToArray() throws Exception {
+        final char[] buffer = new char[8];
+        assertEquals(3, tee.read(buffer));
+        assertEquals('a', buffer[0]);
+        assertEquals('b', buffer[1]);
+        assertEquals('c', buffer[2]);
+        assertEquals(-1, tee.read(buffer));
+        assertEquals("abc", output.toString());
+    }
+
+    @Test
+    public void testReadToArrayWithOffset() throws Exception {
+        final char[] buffer = new char[8];
+        assertEquals(3, tee.read(buffer, 4, 4));
+        assertEquals('a', buffer[4]);
+        assertEquals('b', buffer[5]);
+        assertEquals('c', buffer[6]);
+        assertEquals(-1, tee.read(buffer, 4, 4));
+        assertEquals("abc", output.toString());
+    }
+
+    @Test
+    public void testReadToCharBuffer() throws Exception {
+        final CharBuffer buffer = CharBuffer.allocate(8);
+        buffer.position(1);
+        assertEquals(3, tee.read(buffer));
+        assertEquals(4, buffer.position());
+        buffer.flip();
+        buffer.position(1);
+        assertEquals('a', buffer.charAt(0));
+        assertEquals('b', buffer.charAt(1));
+        assertEquals('c', buffer.charAt(2));
+        assertEquals(-1, tee.read(buffer));
+        assertEquals("abc", output.toString());
+    }
+
+    @Test
+    public void testSkip() throws Exception {
+        assertEquals('a', tee.read());
+        assertEquals(1, tee.skip(1));
+        assertEquals('c', tee.read());
+        assertEquals(-1, tee.read());
+        assertEquals("ac", output.toString());
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
index 0e6073a..6f63879 100644
--- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
+++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
@@ -19,16 +19,23 @@ package org.apache.commons.io.testtools;
 import java.io.IOException;
 import java.io.InputStream;
 
+import org.apache.commons.io.input.NullInputStream;
 import org.apache.commons.io.input.ProxyInputStream;
 
-import junit.framework.AssertionFailedError;
-
 /**
- * Helper class for checking behaviour of IO classes.
+ * Helper class for checking behavior of IO classes.
  */
 public class YellOnCloseInputStream extends ProxyInputStream {
 
     /**
+     * Default ctor.
+     */
+    @SuppressWarnings("resource")
+    public YellOnCloseInputStream() {
+        super(new NullInputStream());
+    }
+
+    /**
      * @param proxy InputStream to delegate to.
      */
     public YellOnCloseInputStream(final InputStream proxy) {
@@ -38,7 +45,7 @@ public class YellOnCloseInputStream extends ProxyInputStream {
     /** @see java.io.InputStream#close() */
     @Override
     public void close() throws IOException {
-        throw new AssertionFailedError("close() was called on OutputStream");
+        throw new IOException("close() was called on OutputStream");
     }
 
 }
diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java
similarity index 61%
copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java
index 0e6073a..4737cb9 100644
--- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
+++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java
@@ -17,28 +17,34 @@
 package org.apache.commons.io.testtools;
 
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.OutputStream;
 
-import org.apache.commons.io.input.ProxyInputStream;
-
-import junit.framework.AssertionFailedError;
+import org.apache.commons.io.output.NullOutputStream;
+import org.apache.commons.io.output.ProxyOutputStream;
 
 /**
- * Helper class for checking behaviour of IO classes.
+ * Helper class for checking behavior of IO classes.
  */
-public class YellOnCloseInputStream extends ProxyInputStream {
+public class YellOnCloseOutputStream extends ProxyOutputStream {
+
+    /**
+     * Default ctor.
+     */
+    public YellOnCloseOutputStream() {
+        super(NullOutputStream.NULL_OUTPUT_STREAM);
+    }
 
     /**
-     * @param proxy InputStream to delegate to.
+     * @param proxy OutputStream to delegate to.
      */
-    public YellOnCloseInputStream(final InputStream proxy) {
+    public YellOnCloseOutputStream(final OutputStream proxy) {
         super(proxy);
     }
 
-    /** @see java.io.InputStream#close() */
+    /** @see java.io.OutputStream#close() */
     @Override
     public void close() throws IOException {
-        throw new AssertionFailedError("close() was called on OutputStream");
+        throw new IOException("close() was called on OutputStream");
     }
 
 }
diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java
similarity index 63%
copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java
index 0e6073a..8ce7075 100644
--- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
+++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java
@@ -17,28 +17,35 @@
 package org.apache.commons.io.testtools;
 
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.Reader;
 
-import org.apache.commons.io.input.ProxyInputStream;
-
-import junit.framework.AssertionFailedError;
+import org.apache.commons.io.input.NullReader;
+import org.apache.commons.io.input.ProxyReader;
 
 /**
- * Helper class for checking behaviour of IO classes.
+ * Helper class for checking behavior of IO classes.
  */
-public class YellOnCloseInputStream extends ProxyInputStream {
+public class YellOnCloseReader extends ProxyReader {
+
+    /**
+     * Default ctor.
+     */
+    @SuppressWarnings("resource")
+    public YellOnCloseReader() {
+        super(new NullReader());
+    }
 
     /**
-     * @param proxy InputStream to delegate to.
+     * @param proxy Reader to delegate to.
      */
-    public YellOnCloseInputStream(final InputStream proxy) {
+    public YellOnCloseReader(final Reader proxy) {
         super(proxy);
     }
 
-    /** @see java.io.InputStream#close() */
+    /** @see java.io.Reader#close() */
     @Override
     public void close() throws IOException {
-        throw new AssertionFailedError("close() was called on OutputStream");
+        throw new IOException("close() was called on OutputStream");
     }
 
 }
diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java
similarity index 68%
copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java
index 0e6073a..026bef2 100644
--- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java
+++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java
@@ -17,28 +17,34 @@
 package org.apache.commons.io.testtools;
 
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.Writer;
 
-import org.apache.commons.io.input.ProxyInputStream;
-
-import junit.framework.AssertionFailedError;
+import org.apache.commons.io.output.NullWriter;
+import org.apache.commons.io.output.ProxyWriter;
 
 /**
  * Helper class for checking behaviour of IO classes.
  */
-public class YellOnCloseInputStream extends ProxyInputStream {
+public class YellOnCloseWriter extends ProxyWriter {
+
+    /**
+     * Default ctor.
+     */
+    public YellOnCloseWriter() {
+        super(NullWriter.NULL_WRITER);
+    }
 
     /**
-     * @param proxy InputStream to delegate to.
+     * @param proxy Writer to delegate to.
      */
-    public YellOnCloseInputStream(final InputStream proxy) {
+    public YellOnCloseWriter(final Writer proxy) {
         super(proxy);
     }
 
-    /** @see java.io.InputStream#close() */
+    /** @see java.io.Writer#close() */
     @Override
     public void close() throws IOException {
-        throw new AssertionFailedError("close() was called on OutputStream");
+        throw new IOException("close() was called on OutputStream");
     }
 
 }