| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // Licensed 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 com.google.devtools.build.android.desugar.runtime; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.devtools.build.android.desugar.runtime.ThrowableExtension.MimicDesugaringStrategy.SUPPRESSED_PREFIX; |
| import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty; |
| import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy; |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.junit.Assert.fail; |
| |
| import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.MimicDesugaringStrategy; |
| import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.NullDesugaringStrategy; |
| import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.ReuseDesugaringStrategy; |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintStream; |
| import java.io.PrintWriter; |
| import java.util.function.Consumer; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Test case for {@link ThrowableExtension} */ |
| @RunWith(JUnit4.class) |
| public class ThrowableExtensionTest { |
| |
| /** |
| * This test tests the behavior of closing resources via reflection. This is only enabled below |
| * API 19. So, if the API level is 19 or above, this test will simply skip. |
| */ |
| @Test |
| public void testCloseResourceViaReflection() throws Throwable { |
| class Resource extends AbstractResource { |
| protected Resource(boolean exceptionOnClose) { |
| super(exceptionOnClose); |
| } |
| |
| public void close() throws Exception { |
| super.internalClose(); |
| } |
| } |
| if (ThrowableExtension.API_LEVEL >= 19) { |
| return; |
| } |
| { |
| Resource r = new Resource(false); |
| assertThat(r.isClosed()).isFalse(); |
| ThrowableExtension.closeResource(null, r); |
| assertThat(r.isClosed()).isTrue(); |
| } |
| { |
| Resource r = new Resource(true); |
| assertThat(r.isClosed()).isFalse(); |
| assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r)); |
| } |
| { |
| Resource r = new Resource(false); |
| assertThat(r.isClosed()).isFalse(); |
| ThrowableExtension.closeResource(new Exception(), r); |
| assertThat(r.isClosed()).isTrue(); |
| } |
| { |
| Resource r = new Resource(true); |
| assertThat(r.isClosed()).isFalse(); |
| assertThrows(Exception.class, () -> ThrowableExtension.closeResource(new Exception(), r)); |
| } |
| } |
| |
| /** |
| * Test the new method closeResources() in the runtime library. |
| * |
| * <p>The method is introduced to fix b/37167433. |
| */ |
| @Test |
| public void testCloseResource() throws Throwable { |
| |
| /** |
| * A resource implementing the interface AutoCloseable. This interface is only available since |
| * API 19. |
| */ |
| class AutoCloseableResource extends AbstractResource implements AutoCloseable { |
| |
| protected AutoCloseableResource(boolean exceptionOnClose) { |
| super(exceptionOnClose); |
| } |
| |
| @Override |
| public void close() throws Exception { |
| internalClose(); |
| } |
| } |
| |
| /** A resource implementing the interface Closeable. */ |
| class CloseableResource extends AbstractResource implements Closeable { |
| |
| protected CloseableResource(boolean exceptionOnClose) { |
| super(exceptionOnClose); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| internalClose(); |
| } |
| } |
| |
| { |
| CloseableResource r = new CloseableResource(false); |
| assertThat(r.isClosed()).isFalse(); |
| ThrowableExtension.closeResource(null, r); |
| assertThat(r.isClosed()).isTrue(); |
| } |
| { |
| CloseableResource r = new CloseableResource(false); |
| assertThat(r.isClosed()).isFalse(); |
| Exception suppressor = new Exception(); |
| ThrowableExtension.closeResource(suppressor, r); |
| assertThat(r.isClosed()).isTrue(); |
| assertThat(ThrowableExtension.getSuppressed(suppressor)).isEmpty(); |
| } |
| { |
| CloseableResource r = new CloseableResource(true); |
| assertThat(r.isClosed()).isFalse(); |
| assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r)); |
| assertThat(r.isClosed()).isFalse(); |
| } |
| { |
| CloseableResource r = new CloseableResource(true); |
| assertThat(r.isClosed()).isFalse(); |
| Exception suppressor = new Exception(); |
| assertThrows(Exception.class, () -> ThrowableExtension.closeResource(suppressor, r)); |
| assertThat(r.isClosed()).isFalse(); // Failed to close. |
| if (!isNullStrategy()) { |
| assertThat(ThrowableExtension.getSuppressed(suppressor)).hasLength(1); |
| assertThat(ThrowableExtension.getSuppressed(suppressor)[0].getClass()) |
| .isEqualTo(IOException.class); |
| } |
| } |
| { |
| AutoCloseableResource r = new AutoCloseableResource(false); |
| assertThat(r.isClosed()).isFalse(); |
| ThrowableExtension.closeResource(null, r); |
| assertThat(r.isClosed()).isTrue(); |
| } |
| { |
| AutoCloseableResource r = new AutoCloseableResource(false); |
| assertThat(r.isClosed()).isFalse(); |
| Exception suppressor = new Exception(); |
| ThrowableExtension.closeResource(suppressor, r); |
| assertThat(r.isClosed()).isTrue(); |
| assertThat(ThrowableExtension.getSuppressed(suppressor)).isEmpty(); |
| } |
| { |
| AutoCloseableResource r = new AutoCloseableResource(true); |
| assertThat(r.isClosed()).isFalse(); |
| assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r)); |
| assertThat(r.isClosed()).isFalse(); |
| } |
| { |
| AutoCloseableResource r = new AutoCloseableResource(true); |
| assertThat(r.isClosed()).isFalse(); |
| Exception suppressor = new Exception(); |
| assertThrows(Exception.class, () -> ThrowableExtension.closeResource(suppressor, r)); |
| assertThat(r.isClosed()).isFalse(); // Failed to close. |
| if (!isNullStrategy()) { |
| assertThat(ThrowableExtension.getSuppressed(suppressor)).hasLength(1); |
| assertThat(ThrowableExtension.getSuppressed(suppressor)[0].getClass()) |
| .isEqualTo(IOException.class); |
| } |
| assertThat(r.isClosed()).isFalse(); |
| } |
| } |
| |
| /** |
| * LightweightStackTraceRecorder tracks the calls of various printStackTrace(*), and ensures that |
| * |
| * <p>suppressed exceptions are printed only once. |
| */ |
| @Test |
| public void testLightweightStackTraceRecorder() throws IOException { |
| MimicDesugaringStrategy strategy = new MimicDesugaringStrategy(); |
| ExceptionForTest receiver = new ExceptionForTest(strategy); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| strategy.addSuppressed(receiver, suppressed); |
| |
| String trace = printStackTraceStderrToString(() -> strategy.printStackTrace(receiver)); |
| assertThat(trace).contains(SUPPRESSED_PREFIX); |
| assertThat(countOccurrences(trace, SUPPRESSED_PREFIX)).isEqualTo(1); |
| } |
| |
| @Test |
| public void testMimicDesugaringStrategy() throws IOException { |
| MimicDesugaringStrategy strategy = new MimicDesugaringStrategy(); |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| strategy.addSuppressed(receiver, suppressed); |
| |
| assertThat( |
| printStackTracePrintStreamToString( |
| stream -> strategy.printStackTrace(receiver, stream))) |
| .contains(SUPPRESSED_PREFIX); |
| |
| assertThat( |
| printStackTracePrintWriterToString( |
| writer -> strategy.printStackTrace(receiver, writer))) |
| .contains(SUPPRESSED_PREFIX); |
| |
| assertThat(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver))) |
| .contains(SUPPRESSED_PREFIX); |
| } |
| |
| private void testThrowableExtensionWithMimicDesugaringStrategy() throws IOException { |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| ThrowableExtension.addSuppressed(receiver, suppressed); |
| |
| assertThat( |
| printStackTracePrintStreamToString( |
| stream -> ThrowableExtension.printStackTrace(receiver, stream))) |
| .contains(SUPPRESSED_PREFIX); |
| assertThat( |
| printStackTracePrintWriterToString( |
| writer -> ThrowableExtension.printStackTrace(receiver, writer))) |
| .contains(SUPPRESSED_PREFIX); |
| assertThat(printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver))) |
| .contains(SUPPRESSED_PREFIX); |
| } |
| |
| private interface PrintStackTraceCaller { |
| void printStackTrace(); |
| } |
| |
| private static String printStackTraceStderrToString(PrintStackTraceCaller caller) |
| throws IOException { |
| PrintStream err = System.err; |
| try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { |
| PrintStream newErr = new PrintStream(stream); |
| System.setErr(newErr); |
| caller.printStackTrace(); |
| newErr.flush(); |
| return stream.toString(); |
| } finally { |
| System.setErr(err); |
| } |
| } |
| |
| private static String printStackTracePrintStreamToString(Consumer<PrintStream> caller) |
| throws IOException { |
| try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { |
| PrintStream printStream = new PrintStream(stream); |
| caller.accept(printStream); |
| printStream.flush(); |
| return stream.toString(); |
| } |
| } |
| |
| private static String printStackTracePrintWriterToString(Consumer<PrintWriter> caller) |
| throws IOException { |
| try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { |
| PrintWriter printWriter = |
| new PrintWriter(new BufferedWriter(new OutputStreamWriter(stream, UTF_8))); |
| caller.accept(printWriter); |
| printWriter.flush(); |
| return stream.toString(); |
| } |
| } |
| |
| @Test |
| public void testNullDesugaringStrategy() throws IOException { |
| NullDesugaringStrategy strategy = new NullDesugaringStrategy(); |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| strategy.addSuppressed(receiver, suppressed); |
| assertThat(strategy.getSuppressed(receiver)).isEmpty(); |
| |
| strategy.addSuppressed(receiver, suppressed); |
| assertThat(strategy.getSuppressed(receiver)).isEmpty(); |
| |
| assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream))) |
| .isEqualTo( |
| printStackTracePrintStreamToString( |
| stream -> strategy.printStackTrace(receiver, stream))); |
| |
| assertThat(printStackTracePrintWriterToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTracePrintWriterToString( |
| writer -> strategy.printStackTrace(receiver, writer))); |
| |
| assertThat(printStackTraceStderrToString(receiver::printStackTrace)) |
| .isEqualTo(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver))); |
| } |
| |
| private void testThrowableExtensionWithNullDesugaringStrategy() throws IOException { |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| ThrowableExtension.addSuppressed(receiver, suppressed); |
| assertThat(ThrowableExtension.getSuppressed(receiver)).isEmpty(); |
| |
| ThrowableExtension.addSuppressed(receiver, suppressed); |
| assertThat(ThrowableExtension.getSuppressed(receiver)).isEmpty(); |
| |
| assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream))) |
| .isEqualTo( |
| printStackTracePrintStreamToString( |
| stream -> ThrowableExtension.printStackTrace(receiver, stream))); |
| assertThat(printStackTracePrintWriterToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTracePrintWriterToString( |
| writer -> ThrowableExtension.printStackTrace(receiver, writer))); |
| |
| assertThat(printStackTraceStderrToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver))); |
| } |
| |
| @Test |
| public void testReuseDesugaringStrategy() throws IOException { |
| ReuseDesugaringStrategy strategy = new ReuseDesugaringStrategy(); |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| strategy.addSuppressed(receiver, suppressed); |
| assertThat(strategy.getSuppressed(receiver)) |
| .asList() |
| .containsExactly((Object[]) receiver.getSuppressed()); |
| |
| assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream))) |
| .isEqualTo( |
| printStackTracePrintStreamToString( |
| stream -> strategy.printStackTrace(receiver, stream))); |
| |
| assertThat(printStackTracePrintWriterToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTracePrintWriterToString( |
| writer -> strategy.printStackTrace(receiver, writer))); |
| assertThat(printStackTraceStderrToString(receiver::printStackTrace)) |
| .isEqualTo(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver))); |
| } |
| |
| private void testThrowableExtensionWithReuseDesugaringStrategy() throws IOException { |
| IOException receiver = new IOException(); |
| FileNotFoundException suppressed = new FileNotFoundException(); |
| ThrowableExtension.addSuppressed(receiver, suppressed); |
| assertThat(ThrowableExtension.getSuppressed(receiver)) |
| .asList() |
| .containsExactly((Object[]) receiver.getSuppressed()); |
| |
| assertThat(printStackTracePrintStreamToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTracePrintStreamToString( |
| stream -> ThrowableExtension.printStackTrace(receiver, stream))); |
| |
| assertThat(printStackTracePrintWriterToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTracePrintWriterToString( |
| writer -> ThrowableExtension.printStackTrace(receiver, writer))); |
| |
| assertThat(printStackTraceStderrToString(receiver::printStackTrace)) |
| .isEqualTo( |
| printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver))); |
| } |
| |
| /** This class */ |
| private static class ExceptionForTest extends Exception { |
| |
| private final MimicDesugaringStrategy strategy; |
| |
| public ExceptionForTest(MimicDesugaringStrategy strategy) { |
| this.strategy = strategy; |
| } |
| |
| @Override |
| public void printStackTrace() { |
| this.printStackTrace(System.err); |
| } |
| |
| /** |
| * This method should call this.printStackTrace(PrintWriter) directly. I deliberately change it |
| * to strategy.printStackTrace(Throwable, PrintWriter) to simulate the behavior of Desguar, that |
| * is, the direct call is intercepted and redirected to ThrowableExtension. |
| */ |
| @Override |
| public void printStackTrace(PrintStream s) { |
| this.strategy.printStackTrace( |
| this, new PrintWriter(new BufferedWriter(new OutputStreamWriter(s, UTF_8)))); |
| } |
| } |
| |
| @Test |
| public void testStrategySelection() throws ClassNotFoundException, IOException { |
| String expectedStrategyClassName = getTwrStrategyClassNameSpecifiedInSystemProperty(); |
| assertThat(expectedStrategyClassName).isNotEmpty(); |
| assertThat(expectedStrategyClassName) |
| .isEqualTo(ThrowableExtension.STRATEGY.getClass().getName()); |
| |
| Class<?> expectedStrategyClass = Class.forName(expectedStrategyClassName); |
| if (expectedStrategyClass.equals(ReuseDesugaringStrategy.class)) { |
| testThrowableExtensionWithReuseDesugaringStrategy(); |
| } else if (expectedStrategyClass.equals(MimicDesugaringStrategy.class)) { |
| testThrowableExtensionWithMimicDesugaringStrategy(); |
| } else if (expectedStrategyClass.equals(NullDesugaringStrategy.class)) { |
| testThrowableExtensionWithNullDesugaringStrategy(); |
| } else { |
| fail("unrecognized expected strategy class " + expectedStrategyClassName); |
| } |
| } |
| |
| private static int countOccurrences(String string, String substring) { |
| int i = 0; |
| int count = 0; |
| while ((i = string.indexOf(substring, i)) >= 0) { |
| ++count; |
| i = i + string.length(); |
| } |
| return count; |
| } |
| |
| /** A mocked closeable class, which we can query the closedness. */ |
| private abstract static class AbstractResource { |
| private final boolean exceptionOnClose; |
| private boolean closed; |
| |
| protected AbstractResource(boolean exceptionOnClose) { |
| this.exceptionOnClose = exceptionOnClose; |
| } |
| |
| boolean isClosed() { |
| return closed; |
| } |
| |
| void internalClose() throws IOException { |
| if (exceptionOnClose) { |
| throw new IOException("intended exception"); |
| } |
| closed = true; |
| } |
| } |
| } |