1 /*
2  * Copyright (C) 2012 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.io;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import com.google.common.base.MoreObjects;
22 import com.google.common.base.Objects;
23 import com.google.common.base.Throwables;
24 import com.google.common.collect.ImmutableList;
25 import com.google.common.collect.ImmutableSet;
26 import com.google.common.collect.Lists;
27 import com.google.common.testing.TestLogHandler;
28 import java.io.Closeable;
29 import java.io.IOException;
30 import java.lang.reflect.Method;
31 import java.util.List;
32 import java.util.logging.LogRecord;
33 import junit.framework.TestCase;
34 import org.checkerframework.checker.nullness.compatqual.NullableDecl;
35 
36 /**
37  * Tests for {@link Closer}.
38  *
39  * @author Colin Decker
40  */
41 public class CloserTest extends TestCase {
42 
43   private TestSuppressor suppressor;
44 
45   @Override
setUp()46   protected void setUp() throws Exception {
47     suppressor = new TestSuppressor();
48   }
49 
50   @AndroidIncompatible // TODO(cpovirk): Look up Build.VERSION.SDK_INT reflectively.
testCreate()51   public void testCreate() {
52     assertThat(Closer.create().suppressor).isInstanceOf(Closer.SuppressingSuppressor.class);
53   }
54 
testNoExceptionsThrown()55   public void testNoExceptionsThrown() throws IOException {
56     Closer closer = new Closer(suppressor);
57 
58     TestCloseable c1 = closer.register(TestCloseable.normal());
59     TestCloseable c2 = closer.register(TestCloseable.normal());
60     TestCloseable c3 = closer.register(TestCloseable.normal());
61 
62     assertFalse(c1.isClosed());
63     assertFalse(c2.isClosed());
64     assertFalse(c3.isClosed());
65 
66     closer.close();
67 
68     assertTrue(c1.isClosed());
69     assertTrue(c2.isClosed());
70     assertTrue(c3.isClosed());
71 
72     assertTrue(suppressor.suppressions.isEmpty());
73   }
74 
testExceptionThrown_fromTryBlock()75   public void testExceptionThrown_fromTryBlock() throws IOException {
76     Closer closer = new Closer(suppressor);
77 
78     TestCloseable c1 = closer.register(TestCloseable.normal());
79     TestCloseable c2 = closer.register(TestCloseable.normal());
80 
81     IOException exception = new IOException();
82 
83     try {
84       try {
85         throw exception;
86       } catch (Throwable e) {
87         throw closer.rethrow(e);
88       } finally {
89         closer.close();
90       }
91     } catch (Throwable expected) {
92       assertSame(exception, expected);
93     }
94 
95     assertTrue(c1.isClosed());
96     assertTrue(c2.isClosed());
97 
98     assertTrue(suppressor.suppressions.isEmpty());
99   }
100 
testExceptionThrown_whenCreatingCloseables()101   public void testExceptionThrown_whenCreatingCloseables() throws IOException {
102     Closer closer = new Closer(suppressor);
103 
104     TestCloseable c1 = null;
105     TestCloseable c2 = null;
106     TestCloseable c3 = null;
107     try {
108       try {
109         c1 = closer.register(TestCloseable.normal());
110         c2 = closer.register(TestCloseable.normal());
111         c3 = closer.register(TestCloseable.throwsOnCreate());
112       } catch (Throwable e) {
113         throw closer.rethrow(e);
114       } finally {
115         closer.close();
116       }
117     } catch (Throwable expected) {
118       assertThat(expected).isInstanceOf(IOException.class);
119     }
120 
121     assertTrue(c1.isClosed());
122     assertTrue(c2.isClosed());
123     assertNull(c3);
124 
125     assertTrue(suppressor.suppressions.isEmpty());
126   }
127 
testExceptionThrown_whileClosingLastCloseable()128   public void testExceptionThrown_whileClosingLastCloseable() throws IOException {
129     Closer closer = new Closer(suppressor);
130 
131     IOException exception = new IOException();
132 
133     // c1 is added first, closed last
134     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(exception));
135     TestCloseable c2 = closer.register(TestCloseable.normal());
136 
137     try {
138       closer.close();
139     } catch (Throwable expected) {
140       assertSame(exception, expected);
141     }
142 
143     assertTrue(c1.isClosed());
144     assertTrue(c2.isClosed());
145 
146     assertTrue(suppressor.suppressions.isEmpty());
147   }
148 
testExceptionThrown_whileClosingFirstCloseable()149   public void testExceptionThrown_whileClosingFirstCloseable() throws IOException {
150     Closer closer = new Closer(suppressor);
151 
152     IOException exception = new IOException();
153 
154     // c2 is added last, closed first
155     TestCloseable c1 = closer.register(TestCloseable.normal());
156     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(exception));
157 
158     try {
159       closer.close();
160     } catch (Throwable expected) {
161       assertSame(exception, expected);
162     }
163 
164     assertTrue(c1.isClosed());
165     assertTrue(c2.isClosed());
166 
167     assertTrue(suppressor.suppressions.isEmpty());
168   }
169 
testCloseExceptionsSuppressed_whenExceptionThrownFromTryBlock()170   public void testCloseExceptionsSuppressed_whenExceptionThrownFromTryBlock() throws IOException {
171     Closer closer = new Closer(suppressor);
172 
173     IOException tryException = new IOException();
174     IOException c1Exception = new IOException();
175     IOException c2Exception = new IOException();
176 
177     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(c1Exception));
178     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(c2Exception));
179 
180     try {
181       try {
182         throw tryException;
183       } catch (Throwable e) {
184         throw closer.rethrow(e);
185       } finally {
186         closer.close();
187       }
188     } catch (Throwable expected) {
189       assertSame(tryException, expected);
190     }
191 
192     assertTrue(c1.isClosed());
193     assertTrue(c2.isClosed());
194 
195     assertSuppressed(
196         new Suppression(c2, tryException, c2Exception),
197         new Suppression(c1, tryException, c1Exception));
198   }
199 
testCloseExceptionsSuppressed_whenExceptionThrownClosingFirstCloseable()200   public void testCloseExceptionsSuppressed_whenExceptionThrownClosingFirstCloseable()
201       throws IOException {
202     Closer closer = new Closer(suppressor);
203 
204     IOException c1Exception = new IOException();
205     IOException c2Exception = new IOException();
206     IOException c3Exception = new IOException();
207 
208     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(c1Exception));
209     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(c2Exception));
210     TestCloseable c3 = closer.register(TestCloseable.throwsOnClose(c3Exception));
211 
212     try {
213       closer.close();
214     } catch (Throwable expected) {
215       assertSame(c3Exception, expected);
216     }
217 
218     assertTrue(c1.isClosed());
219     assertTrue(c2.isClosed());
220     assertTrue(c3.isClosed());
221 
222     assertSuppressed(
223         new Suppression(c2, c3Exception, c2Exception),
224         new Suppression(c1, c3Exception, c1Exception));
225   }
226 
testRuntimeExceptions()227   public void testRuntimeExceptions() throws IOException {
228     Closer closer = new Closer(suppressor);
229 
230     RuntimeException tryException = new RuntimeException();
231     RuntimeException c1Exception = new RuntimeException();
232     RuntimeException c2Exception = new RuntimeException();
233 
234     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(c1Exception));
235     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(c2Exception));
236 
237     try {
238       try {
239         throw tryException;
240       } catch (Throwable e) {
241         throw closer.rethrow(e);
242       } finally {
243         closer.close();
244       }
245     } catch (Throwable expected) {
246       assertSame(tryException, expected);
247     }
248 
249     assertTrue(c1.isClosed());
250     assertTrue(c2.isClosed());
251 
252     assertSuppressed(
253         new Suppression(c2, tryException, c2Exception),
254         new Suppression(c1, tryException, c1Exception));
255   }
256 
testErrors()257   public void testErrors() throws IOException {
258     Closer closer = new Closer(suppressor);
259 
260     Error c1Exception = new Error();
261     Error c2Exception = new Error();
262     Error c3Exception = new Error();
263 
264     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(c1Exception));
265     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(c2Exception));
266     TestCloseable c3 = closer.register(TestCloseable.throwsOnClose(c3Exception));
267 
268     try {
269       closer.close();
270     } catch (Throwable expected) {
271       assertSame(c3Exception, expected);
272     }
273 
274     assertTrue(c1.isClosed());
275     assertTrue(c2.isClosed());
276     assertTrue(c3.isClosed());
277 
278     assertSuppressed(
279         new Suppression(c2, c3Exception, c2Exception),
280         new Suppression(c1, c3Exception, c1Exception));
281   }
282 
testLoggingSuppressor()283   public static void testLoggingSuppressor() throws IOException {
284     TestLogHandler logHandler = new TestLogHandler();
285 
286     Closeables.logger.addHandler(logHandler);
287     try {
288       Closer closer = new Closer(new Closer.LoggingSuppressor());
289 
290       TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(new IOException()));
291       TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(new RuntimeException()));
292       try {
293         throw closer.rethrow(new IOException("thrown"), IOException.class);
294       } catch (IOException expected) {
295       }
296 
297       assertTrue(logHandler.getStoredLogRecords().isEmpty());
298 
299       closer.close();
300 
301       assertEquals(2, logHandler.getStoredLogRecords().size());
302 
303       LogRecord record = logHandler.getStoredLogRecords().get(0);
304       assertEquals("Suppressing exception thrown when closing " + c2, record.getMessage());
305 
306       record = logHandler.getStoredLogRecords().get(1);
307       assertEquals("Suppressing exception thrown when closing " + c1, record.getMessage());
308     } finally {
309       Closeables.logger.removeHandler(logHandler);
310     }
311   }
312 
testSuppressingSuppressorIfPossible()313   public static void testSuppressingSuppressorIfPossible() throws IOException {
314     // can't test the JDK7 suppressor when not running on JDK7
315     if (!Closer.SuppressingSuppressor.isAvailable()) {
316       return;
317     }
318 
319     Closer closer = new Closer(new Closer.SuppressingSuppressor());
320 
321     IOException thrownException = new IOException();
322     IOException c1Exception = new IOException();
323     RuntimeException c2Exception = new RuntimeException();
324 
325     TestCloseable c1 = closer.register(TestCloseable.throwsOnClose(c1Exception));
326     TestCloseable c2 = closer.register(TestCloseable.throwsOnClose(c2Exception));
327     try {
328       try {
329         throw thrownException;
330       } catch (Throwable e) {
331         throw closer.rethrow(thrownException, IOException.class);
332       } finally {
333         assertThat(getSuppressed(thrownException)).isEmpty();
334         closer.close();
335       }
336     } catch (IOException expected) {
337       assertSame(thrownException, expected);
338     }
339 
340     assertTrue(c1.isClosed());
341     assertTrue(c2.isClosed());
342 
343     ImmutableSet<Throwable> suppressed = ImmutableSet.copyOf(getSuppressed(thrownException));
344     assertEquals(2, suppressed.size());
345 
346     assertEquals(ImmutableSet.of(c1Exception, c2Exception), suppressed);
347   }
348 
testNullCloseable()349   public void testNullCloseable() throws IOException {
350     Closer closer = Closer.create();
351     closer.register(null);
352     closer.close();
353   }
354 
getSuppressed(Throwable throwable)355   static Throwable[] getSuppressed(Throwable throwable) {
356     try {
357       Method getSuppressed = Throwable.class.getDeclaredMethod("getSuppressed");
358       return (Throwable[]) getSuppressed.invoke(throwable);
359     } catch (Exception e) {
360       throw new AssertionError(e); // only called if running on JDK7
361     }
362   }
363 
364   /**
365    * Asserts that an exception was thrown when trying to close each of the given throwables and that
366    * each such exception was suppressed because of the given thrown exception.
367    */
assertSuppressed(Suppression... expected)368   private void assertSuppressed(Suppression... expected) {
369     assertEquals(ImmutableList.copyOf(expected), suppressor.suppressions);
370   }
371 
372   /** Suppressor that records suppressions. */
373   private static class TestSuppressor implements Closer.Suppressor {
374 
375     private final List<Suppression> suppressions = Lists.newArrayList();
376 
377     @Override
suppress(Closeable closeable, Throwable thrown, Throwable suppressed)378     public void suppress(Closeable closeable, Throwable thrown, Throwable suppressed) {
379       suppressions.add(new Suppression(closeable, thrown, suppressed));
380     }
381   }
382 
383   /** Record of a call to suppress. */
384   private static class Suppression {
385     private final Closeable closeable;
386     private final Throwable thrown;
387     private final Throwable suppressed;
388 
Suppression(Closeable closeable, Throwable thrown, Throwable suppressed)389     private Suppression(Closeable closeable, Throwable thrown, Throwable suppressed) {
390       this.closeable = closeable;
391       this.thrown = thrown;
392       this.suppressed = suppressed;
393     }
394 
395     @Override
equals(Object obj)396     public boolean equals(Object obj) {
397       if (obj instanceof Suppression) {
398         Suppression other = (Suppression) obj;
399         return closeable.equals(other.closeable)
400             && thrown.equals(other.thrown)
401             && suppressed.equals(other.suppressed);
402       }
403       return false;
404     }
405 
406     @Override
hashCode()407     public int hashCode() {
408       return Objects.hashCode(closeable, thrown, suppressed);
409     }
410 
411     @Override
toString()412     public String toString() {
413       return MoreObjects.toStringHelper(this)
414           .add("closeable", closeable)
415           .add("thrown", thrown)
416           .add("suppressed", suppressed)
417           .toString();
418     }
419   }
420 
421   private static class TestCloseable implements Closeable {
422 
423     private final Throwable throwOnClose;
424     private boolean closed;
425 
normal()426     static TestCloseable normal() throws IOException {
427       return new TestCloseable(null);
428     }
429 
throwsOnClose(Throwable throwOnClose)430     static TestCloseable throwsOnClose(Throwable throwOnClose) throws IOException {
431       return new TestCloseable(throwOnClose);
432     }
433 
throwsOnCreate()434     static TestCloseable throwsOnCreate() throws IOException {
435       throw new IOException();
436     }
437 
TestCloseable(@ullableDecl Throwable throwOnClose)438     private TestCloseable(@NullableDecl Throwable throwOnClose) {
439       this.throwOnClose = throwOnClose;
440     }
441 
isClosed()442     public boolean isClosed() {
443       return closed;
444     }
445 
446     @Override
close()447     public void close() throws IOException {
448       closed = true;
449       if (throwOnClose != null) {
450         Throwables.propagateIfPossible(throwOnClose, IOException.class);
451         throw new AssertionError(throwOnClose);
452       }
453     }
454   }
455 }
456