Fixing the bugs demonstrated by WriterTesterTest. Only ObjectOutputStream failures are outstanding; these are suppressed in the test case.

This commit adds SneakyThrow, which is a dangerous class that you should not use! It is used to give Harmony the best behaviour when dealing with failures during stream flush and close. Read the Javadoc for full detalis.

This commit makes behaviour changes to BufferedWriter, FilterOutputStream, and BufferedOutputStream. These streams now throw as required after they are closed, and throw the correct exception when closing fails.

git-svn-id: https://svn.apache.org/repos/asf/harmony/enhanced/classlib/trunk@835972 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/modules/luni/make/exclude.common b/modules/luni/make/exclude.common
index 3d3b327..9ce6e48 100644
--- a/modules/luni/make/exclude.common
+++ b/modules/luni/make/exclude.common
@@ -1,3 +1 @@
 org/apache/harmony/luni/tests/java/net/ExcludedProxyTest.java
-org/apache/harmony/luni/tests/java/io/OutputStreamTesterTest.java
-org/apache/harmony/luni/tests/java/io/WriterTesterTest.java
diff --git a/modules/luni/src/main/java/java/io/BufferedOutputStream.java b/modules/luni/src/main/java/java/io/BufferedOutputStream.java
index a78b155..598298c 100644
--- a/modules/luni/src/main/java/java/io/BufferedOutputStream.java
+++ b/modules/luni/src/main/java/java/io/BufferedOutputStream.java
@@ -88,6 +88,10 @@
      */
     @Override
     public synchronized void flush() throws IOException {
+        if (buf == null) {
+            throw new IOException(Msg.getString("K0059")); //$NON-NLS-1$
+        }
+
         flushInternal();
         out.flush();
     }
@@ -120,12 +124,17 @@
     @Override
     public synchronized void write(byte[] buffer, int offset, int length)
             throws IOException {
+        byte[] internalBuffer = buf;
+        if (internalBuffer == null) {
+            throw new IOException(Msg.getString("K0059")); //$NON-NLS-1$
+        }
+
         if (buffer == null) {
             // K0047=buffer is null
             throw new NullPointerException(Msg.getString("K0047")); //$NON-NLS-1$
         }
         
-        if (length >= buf.length) {
+        if (length >= internalBuffer.length) {
             flushInternal();
             out.write(buffer, offset, length);
             return;
@@ -142,15 +151,27 @@
         }
 
         // flush the internal buffer first if we have not enough space left
-        if (length >= (buf.length - count)) {
+        if (length >= (internalBuffer.length - count)) {
             flushInternal();
         }
 
-        // the length is always less than (buf.length - count) here so arraycopy is safe
-        System.arraycopy(buffer, offset, buf, count, length);
+        // the length is always less than (internalBuffer.length - count) here so arraycopy is safe
+        System.arraycopy(buffer, offset, internalBuffer, count, length);
         count += length;
     }
 
+    @Override public synchronized void close() throws IOException {
+        if (buf == null) {
+            return;
+        }
+        
+        try {
+            super.close();
+        } finally {
+            buf = null;
+        }
+    }
+
     /**
      * Writes one byte to this stream. Only the low order byte of the integer
      * {@code oneByte} is written. If there is room in the buffer, the byte is
@@ -165,11 +186,16 @@
      */
     @Override
     public synchronized void write(int oneByte) throws IOException {
-        if (count == buf.length) {
-            out.write(buf, 0, count);
+        byte[] internalBuffer = buf;
+        if (internalBuffer == null) {
+            throw new IOException(Msg.getString("K0059")); //$NON-NLS-1$
+        }
+
+        if (count == internalBuffer.length) {
+            out.write(internalBuffer, 0, count);
             count = 0;
         }
-        buf[count++] = (byte) oneByte;
+        internalBuffer[count++] = (byte) oneByte;
     }
 
     /**
@@ -178,7 +204,7 @@
     private void flushInternal() throws IOException {
         if (count > 0) {
             out.write(buf, 0, count);
+            count = 0;
         }
-        count = 0;
     }
 }
diff --git a/modules/luni/src/main/java/java/io/BufferedWriter.java b/modules/luni/src/main/java/java/io/BufferedWriter.java
index c9ff83c..72e8b62 100644
--- a/modules/luni/src/main/java/java/io/BufferedWriter.java
+++ b/modules/luni/src/main/java/java/io/BufferedWriter.java
@@ -21,6 +21,7 @@
 
 import org.apache.harmony.luni.util.Msg;
 import org.apache.harmony.luni.util.PriviAction;
+import org.apache.harmony.luni.util.SneakyThrow;
 
 /**
  * Wraps an existing {@link Writer} and <em>buffers</em> the output. Expensive
@@ -95,11 +96,29 @@
     @Override
     public void close() throws IOException {
         synchronized (lock) {
-            if (!isClosed()) {
+            if (isClosed()) {
+                return;
+            }
+
+            Throwable thrown = null;
+            try {
                 flushInternal();
+            } catch (Throwable e) {
+                thrown = e;
+            }
+            buf = null;
+
+            try {
                 out.close();
-                buf = null;
-                out = null;
+            } catch (Throwable e) {
+                if (thrown == null) {
+                    thrown = e;
+                }
+            }
+            out = null;
+
+            if (thrown != null) {
+                SneakyThrow.sneakyThrow(thrown);
             }
         }
     }
diff --git a/modules/luni/src/main/java/java/io/FilterOutputStream.java b/modules/luni/src/main/java/java/io/FilterOutputStream.java
index f7a31bf..8f9db69 100644
--- a/modules/luni/src/main/java/java/io/FilterOutputStream.java
+++ b/modules/luni/src/main/java/java/io/FilterOutputStream.java
@@ -18,6 +18,7 @@
 package java.io;
 
 import org.apache.harmony.luni.util.Msg;
+import org.apache.harmony.luni.util.SneakyThrow;
 
 /**
  * Wraps an existing {@link OutputStream} and performs some transformation on
@@ -55,27 +56,23 @@
      */
     @Override
     public void close() throws IOException {
-        Exception thrown = null;
+        Throwable thrown = null;
         try {
             flush();
-        } catch (Exception e) {
+        } catch (Throwable e) {
             thrown = e;
         }
 
         try {
             out.close();
-        } catch (Exception e) {
-            if (thrown != null) {
+        } catch (Throwable e) {
+            if (thrown == null) {
                 thrown = e;
             }
         }
 
         if (thrown != null) {
-            if (thrown instanceof IOException) {
-                throw (IOException) thrown;
-            } else {
-                throw (RuntimeException) thrown;
-            }
+            SneakyThrow.sneakyThrow(thrown);
         }
     }
 
diff --git a/modules/luni/src/main/java/org/apache/harmony/luni/util/SneakyThrow.java b/modules/luni/src/main/java/org/apache/harmony/luni/util/SneakyThrow.java
new file mode 100644
index 0000000..6e5e7d4
--- /dev/null
+++ b/modules/luni/src/main/java/org/apache/harmony/luni/util/SneakyThrow.java
@@ -0,0 +1,70 @@
+/*
+ *  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.harmony.luni.util;
+
+/**
+ * Exploits a weakness in the runtime to throw an arbitrary throwable without
+ * the traditional declaration. <strong>This is a dangerous API that should be
+ * used with great caution.</strong> Typically this is useful when rethrowing
+ * throwables that are of a known range of types.
+ *
+ * <p>The following code must enumerate several types to rethrow:
+ * <pre>
+ * public void close() throws IOException {
+ *     Throwable thrown = null;
+ *     ...
+ *
+ *     if (thrown != null) {
+ *         if (thrown instanceof IOException) {
+ *             throw (IOException) thrown;
+ *         } else if (thrown instanceof RuntimeException) {
+ *             throw (RuntimeException) thrown;
+ *         } else if (thrown instanceof Error) {
+ *             throw (Error) thrown;
+ *         } else {
+ *             throw new AssertionError();
+ *         }
+ *     }
+ * }</pre>
+ * With SneakyThrow, rethrowing is easier:
+ * <pre>
+ * public void close() throws IOException {
+ *     Throwable thrown = null;
+ *     ...
+ *
+ *     if (thrown != null) {
+ *         SneakyThrow.sneakyThrow(thrown);
+ *     }
+ * }</pre>
+ */
+public final class SneakyThrow {
+    private SneakyThrow() {}
+
+    public static void sneakyThrow(Throwable t) {
+        SneakyThrow.<Error>sneakyThrow2(t);
+    }
+
+    /**
+     * Exploits unsafety to throw an exception that the compiler wouldn't permit
+     * but that the runtime doesn't check. See Java Puzzlers #43.
+     */
+    @SuppressWarnings("unchecked")
+    private static <T extends Throwable> void sneakyThrow2(Throwable t) throws T {
+        throw (T) t;
+    }
+}
diff --git a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/BufferedOutputStreamTest.java b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/BufferedOutputStreamTest.java
index 6c11f72..29a890c 100644
--- a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/BufferedOutputStreamTest.java
+++ b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/BufferedOutputStreamTest.java
@@ -328,9 +328,9 @@
 
         try {
             bos.write(byteArray, -1, -1);
-            fail("should throw ArrayIndexOutOfBoundsException");
-        } catch (ArrayIndexOutOfBoundsException e) {
-            // expected
+            fail();
+        } catch (Exception e) {
+            // expected IOException. RI throws IndexOutOfBoundsException
         }
     }
 
@@ -663,11 +663,6 @@
         buffos.flush();
 
         buffos.close();
-
-        buffos.write(Integer.MIN_VALUE);
-        buffos.write(Integer.MAX_VALUE);
-        buffos.write(buffer, 0, 10);
-        buffos.flush();
     }
 
     public void test_write_Scenario1() throws IOException {
@@ -751,8 +746,6 @@
             assertEquals(buffer2[i], byteArrayis.read());
         }
 
-        buffos.close();
-
         byte[] buffer3 = new byte[] { 'e', 'f', 'g', 'h', 'i' };
         buffos.write(buffer3, 0, 5);
         byteArrayis = new ByteArrayInputStream(byteArrayos.toByteArray());
@@ -778,6 +771,8 @@
         byteArrayis = new ByteArrayInputStream(byteArrayos.toByteArray());
         assertEquals("Bytes not written after flush", 21, byteArrayis
                 .available());
+
+        buffos.close();
     }
 
     public void test_write_Scenario3() throws IOException {
diff --git a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/OutputStreamTesterTest.java b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/OutputStreamTesterTest.java
index b025e04..20362d5 100644
--- a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/OutputStreamTesterTest.java
+++ b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/OutputStreamTesterTest.java
@@ -54,8 +54,8 @@
         // sink tests
         suite.addTest(new FileOutputStreamSinkTester(true).createTests());
         suite.addTest(new FileOutputStreamSinkTester(false).createTests());
-        suite.addTest(new ByteArrayOutputStreamSinkTester(0).createTests());
-        suite.addTest(new ByteArrayOutputStreamSinkTester(4).createTests());
+        suite.addTest(new ByteArrayOutputStreamSinkTester(0).setThrowsExceptions(false).createTests());
+        suite.addTest(new ByteArrayOutputStreamSinkTester(4).setThrowsExceptions(false).createTests());
         suite.addTest(new PipedOutputStreamSinkTester().createTests());
 
         // wrapper tests
@@ -64,7 +64,8 @@
         suite.addTest(new BufferedOutputStreamTester(1024).createTests());
         suite.addTest(new FilterOutputStreamTester().createTests());
         suite.addTest(new DataOutputStreamTester().createTests());
-        suite.addTest(new ObjectOutputStreamTester().createTests());
+        // fails wrapperTestFlushThrowsViaClose() and sinkTestWriteAfterClose():
+        // suite.addTest(new ObjectOutputStreamTester().createTests());
         suite.addTest(new PrintStreamTester().setThrowsExceptions(false).createTests());
 
         return suite;
diff --git a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/WriterTesterTest.java b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/WriterTesterTest.java
index 1d64d1d..910c787 100644
--- a/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/WriterTesterTest.java
+++ b/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/io/WriterTesterTest.java
@@ -52,8 +52,8 @@
         // sink tests
         suite.addTest(new FileWriterCharSinkTester(true).createTests());
         suite.addTest(new FileWriterCharSinkTester(false).createTests());
-        suite.addTest(new CharArrayWriterCharSinkTester().createTests());
-        suite.addTest(new StringWriterCharSinkTester().createTests());
+        suite.addTest(new CharArrayWriterCharSinkTester().setThrowsExceptions(false).createTests());
+        suite.addTest(new StringWriterCharSinkTester().setThrowsExceptions(false).createTests());
         suite.addTest(new PipedWriterCharSinkTester().createTests());
 
         // wrapper tests
diff --git a/support/src/test/java/org/apache/harmony/testframework/CharSinkTester.java b/support/src/test/java/org/apache/harmony/testframework/CharSinkTester.java
index 97ee1bb..8aa9db4 100644
--- a/support/src/test/java/org/apache/harmony/testframework/CharSinkTester.java
+++ b/support/src/test/java/org/apache/harmony/testframework/CharSinkTester.java
@@ -67,6 +67,8 @@
 
         if (throwsExceptions) {
             result.addTest(new SinkTestCase("sinkTestWriteAfterClose"));
+        } else {
+            result.addTest(new SinkTestCase("sinkTestWriteAfterCloseSuppressed"));
         }
 
         return result;
@@ -184,6 +186,13 @@
             Assert.assertArrayEquals(expectedChars, getChars());
         }
 
+        public void sinkTestWriteAfterCloseSuppressed() throws Exception {
+            Writer out = create();
+            out.write("EF".toCharArray());
+            out.close();
+            out.write("GCDE".toCharArray()); // no exception expected!
+        }
+
         // adding a new test? Don't forget to update createTests().
 
         @Override public String getName() {
diff --git a/support/src/test/java/org/apache/harmony/testframework/CharWrapperTester.java b/support/src/test/java/org/apache/harmony/testframework/CharWrapperTester.java
index bbdb56d..2744eed 100644
--- a/support/src/test/java/org/apache/harmony/testframework/CharWrapperTester.java
+++ b/support/src/test/java/org/apache/harmony/testframework/CharWrapperTester.java
@@ -21,7 +21,6 @@
 import junit.framework.TestSuite;
 
 import java.io.IOException;
-import java.io.StringWriter;
 import java.io.Writer;
 
 /**
@@ -63,9 +62,11 @@
         if (throwsExceptions) {
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaFlush"));
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaClose"));
+            result.addTest(new WrapperTestCase("wrapperTestCloseThrows"));
         } else {
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaFlushSuppressed"));
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaCloseSuppressed"));
+            result.addTest(new WrapperTestCase("wrapperTestCloseThrowsSuppressed"));
         }
 
         return result;
@@ -76,15 +77,15 @@
     }
 
     private class WrapperSinkTester extends CharSinkTester {
-        private StringWriter delegate;
+        private ClosableStringWriter delegate;
 
         @Override public Writer create() throws Exception {
-            delegate = new StringWriter();
+            delegate = new ClosableStringWriter();
             return CharWrapperTester.this.create(delegate);
         }
 
         @Override public char[] getChars() throws Exception {
-            return CharWrapperTester.this.decode(delegate.toString().toCharArray());
+            return decode(delegate.buffer.toString().toCharArray());
         }
 
         @Override public String toString() {
@@ -98,6 +99,10 @@
             super(name);
         }
 
+        @Override public String getName() {
+            return CharWrapperTester.this.toString() + ":" + super.getName();
+        }
+
         public void wrapperTestFlushThrowsViaFlushSuppressed() throws Exception {
             FailOnFlushWriter delegate = new FailOnFlushWriter();
             Writer o = create(delegate);
@@ -154,27 +159,83 @@
             }
         }
 
-        // adding a new test? Don't forget to update createTests().
-
-        @Override public String getName() {
-            return CharWrapperTester.this.toString() + ":" + super.getName();
+        public void wrapperTestCloseThrows() throws Exception {
+            FailOnCloseWriter delegate = new FailOnCloseWriter();
+            Writer o = create(delegate);
+            try {
+                o.close();
+                assertTrue(delegate.closed);
+                fail("close exception ignored");
+            } catch (IOException expected) {
+                assertEquals("Close failed" , expected.getMessage());
+            }
         }
 
-        private class FailOnFlushWriter extends Writer {
-            boolean flushed = false;
+        public void wrapperTestCloseThrowsSuppressed() throws Exception {
+            FailOnCloseWriter delegate = new FailOnCloseWriter();
+            Writer o = create(delegate);
+            o.close();
+            assertTrue(delegate.closed);
+        }
 
-            @Override public void write(char[] buf, int offset, int count) throws IOException {}
+        // adding a new test? Don't forget to update createTests().
+    }
 
-            @Override public void close() throws IOException {
-                flush();
+    /**
+     * A custom Writer that respects the closed state. The built-in StringWriter
+     * doesn't respect close(), which makes testing wrapped streams difficult.
+     */
+    private static class ClosableStringWriter extends Writer {
+        private final StringBuilder buffer = new StringBuilder();
+        private boolean closed = false;
+
+        @Override public void close() throws IOException {
+            closed = true;
+        }
+
+        @Override public void flush() throws IOException {}
+
+        @Override public void write(char[] buf, int offset, int count) throws IOException {
+            if (closed) {
+                throw new IOException();
             }
+            buffer.append(buf, offset, count);
+        }
+    }
 
-            @Override public void flush() throws IOException {
-                if (!flushed) {
-                    flushed = true;
-                    throw new IOException("Flush failed");
-                }
+    private static class FailOnFlushWriter extends Writer {
+        boolean flushed = false;
+        boolean closed = false;
+
+        @Override public void write(char[] buf, int offset, int count) throws IOException {
+            if (closed) {
+                throw new IOException("Already closed");
             }
         }
+
+        @Override public void close() throws IOException {
+            closed = true;
+            flush();
+        }
+
+        @Override public void flush() throws IOException {
+            if (!flushed) {
+                flushed = true;
+                throw new IOException("Flush failed");
+            }
+        }
+    }
+
+    private static class FailOnCloseWriter extends Writer {
+        boolean closed = false;
+
+        @Override public void flush() throws IOException {}
+
+        @Override public void write(char[] buf, int offset, int count) throws IOException {}
+
+        @Override public void close() throws IOException {
+            closed = true;
+            throw new IOException("Close failed");
+        }
     }
 }
\ No newline at end of file
diff --git a/support/src/test/java/org/apache/harmony/testframework/SinkTester.java b/support/src/test/java/org/apache/harmony/testframework/SinkTester.java
index f8aaf8e..aceec1c 100644
--- a/support/src/test/java/org/apache/harmony/testframework/SinkTester.java
+++ b/support/src/test/java/org/apache/harmony/testframework/SinkTester.java
@@ -68,6 +68,8 @@
 
         if (throwsExceptions) {
             result.addTest(new SinkTestCase("sinkTestWriteAfterClose"));
+        } else {
+            result.addTest(new SinkTestCase("sinkTestWriteAfterCloseSuppressed"));
         }
 
         return result;
@@ -196,6 +198,13 @@
             Assert.assertArrayEquals(expectedBytes, getBytes());
         }
 
+        public void sinkTestWriteAfterCloseSuppressed() throws Exception {
+            OutputStream out = create();
+            out.write(new byte[] { 5, 6 });
+            out.close();
+            out.write(new byte[] { 7, 3, 4, 5 }); // no exception expected!
+        }
+
         // adding a new test? Don't forget to update createTests().
 
         @Override public String getName() {
diff --git a/support/src/test/java/org/apache/harmony/testframework/WrapperTester.java b/support/src/test/java/org/apache/harmony/testframework/WrapperTester.java
index ee54221..b5594c4 100644
--- a/support/src/test/java/org/apache/harmony/testframework/WrapperTester.java
+++ b/support/src/test/java/org/apache/harmony/testframework/WrapperTester.java
@@ -63,9 +63,11 @@
         if (throwsExceptions) {
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaFlush"));
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaClose"));
+            result.addTest(new WrapperTestCase("wrapperTestCloseThrows"));
         } else {
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaFlushSuppressed"));
             result.addTest(new WrapperTestCase("wrapperTestFlushThrowsViaCloseSuppressed"));
+            result.addTest(new WrapperTestCase("wrapperTestCloseThrowsSuppressed"));
         }
 
         return result;
@@ -76,15 +78,15 @@
     }
 
     private class WrapperSinkTester extends SinkTester {
-        private ByteArrayOutputStream delegate;
+        private ClosableByteArrayOutputStream delegate;
 
         @Override public OutputStream create() throws Exception {
-            delegate = new ByteArrayOutputStream();
+            delegate = new ClosableByteArrayOutputStream();
             return WrapperTester.this.create(delegate);
         }
 
         @Override public byte[] getBytes() throws Exception {
-            return WrapperTester.this.decode(delegate.toByteArray());
+            return WrapperTester.this.decode(delegate.bytesOut.toByteArray());
         }
 
         @Override public String toString() {
@@ -98,6 +100,10 @@
             super(name);
         }
 
+        @Override public String getName() {
+            return WrapperTester.this.toString() + ":" + super.getName();
+        }
+
         public void wrapperTestFlushThrowsViaFlushSuppressed() throws Exception {
             FailOnFlushOutputStream delegate = new FailOnFlushOutputStream();
             OutputStream o = create(delegate);
@@ -154,27 +160,73 @@
             }
         }
 
-        // adding a new test? Don't forget to update createTests().
-
-        @Override public String getName() {
-            return WrapperTester.this.toString() + ":" + super.getName();
+        public void wrapperTestCloseThrows() throws Exception {
+            FailOnCloseOutputStream delegate = new FailOnCloseOutputStream();
+            OutputStream o = create(delegate);
+            try {
+                o.close();
+                assertTrue(delegate.closed);
+                fail("close exception ignored");
+            } catch (IOException expected) {
+                assertEquals("Close failed" , expected.getMessage());
+            }
         }
 
-        private class FailOnFlushOutputStream extends ByteArrayOutputStream {
-            boolean flushed = false;
+        public void wrapperTestCloseThrowsSuppressed() throws Exception {
+            FailOnCloseOutputStream delegate = new FailOnCloseOutputStream();
+            OutputStream o = create(delegate);
+            o.close();
+            assertTrue(delegate.closed);
+        }
 
-            @Override public void close() throws IOException {
-                flush();
-                super.close();
-            }
+        // adding a new test? Don't forget to update createTests().
+    }
 
-            @Override public void flush() throws IOException {
-                if (!flushed) {
-                    flushed = true;
-                    throw new IOException("Flush failed");
-                }
-                super.flush();
+    private static class ClosableByteArrayOutputStream extends OutputStream {
+        private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+        private boolean closed = false;
+
+        @Override public void close() throws IOException {
+            closed = true;
+        }
+
+        @Override public void write(int oneByte) throws IOException {
+            if (closed) {
+                throw new IOException();
             }
+            bytesOut.write(oneByte);
+        }
+    }
+
+    private static class FailOnFlushOutputStream extends OutputStream {
+        boolean flushed = false;
+        boolean closed = false;
+
+        @Override public void write(int oneByte) throws IOException {
+            if (closed) {
+                throw new IOException("Already closed");
+            }
+        }
+
+        @Override public void close() throws IOException {
+            closed = true;
+            flush();
+        }
+
+        @Override public void flush() throws IOException {
+            if (!flushed) {
+                flushed = true;
+                throw new IOException("Flush failed");
+            }
+        }
+    }
+
+    private static class FailOnCloseOutputStream extends ByteArrayOutputStream {
+        boolean closed = false;
+
+        @Override public void close() throws IOException {
+            closed = true;
+            throw new IOException("Close failed");
         }
     }
 }