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