| // Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8.diagnostics; |
| |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException; |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage; |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin; |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType; |
| import static org.hamcrest.CoreMatchers.allOf; |
| import static org.hamcrest.CoreMatchers.anyOf; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.fail; |
| |
| import com.android.tools.r8.CompilationFailedException; |
| import com.android.tools.r8.D8TestBuilder; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.TestParametersCollection; |
| import com.android.tools.r8.ThrowableConsumer; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.utils.Reporter; |
| import com.android.tools.r8.utils.StringDiagnostic; |
| import com.android.tools.r8.utils.StringUtils; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import org.hamcrest.Matcher; |
| import org.hamcrest.core.IsAnything; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class ErrorDuringIrConversionTest extends TestBase { |
| |
| static final Origin ORIGIN = |
| new Origin(Origin.root()) { |
| @Override |
| public String part() { |
| return "<test-origin>"; |
| } |
| }; |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static TestParametersCollection data() { |
| return getTestParameters().withNoneRuntime().build(); |
| } |
| |
| public ErrorDuringIrConversionTest(TestParameters parameters) { |
| parameters.assertNoneRuntime(); |
| } |
| |
| private ThrowableConsumer<D8TestBuilder> addTestClassWithOrigin() { |
| return b -> |
| b.getBuilder().addClassProgramData(ToolHelper.getClassAsBytes(TestClass.class), ORIGIN); |
| } |
| |
| private void checkCompilationFailedException( |
| CompilationFailedException e, Matcher<String> messageMatcher, Matcher<String> stackMatcher) { |
| // Check that the failure exception exiting the compiler contains origin info in the message. |
| assertThat(e.getMessage(), messageMatcher); |
| StringWriter writer = new StringWriter(); |
| e.printStackTrace(new PrintWriter(writer)); |
| String fullStackTrace = writer.toString(); |
| // Extract the top cause stack. |
| int topStackTraceEnd = fullStackTrace.indexOf("Caused by:"); |
| String topStackTrace = fullStackTrace.substring(0, topStackTraceEnd); |
| String restStackTrace = fullStackTrace.substring(topStackTraceEnd); |
| // Check that top stack trace always has the version marker. |
| assertThat(topStackTrace, containsString("fakeStackEntry")); |
| // Check that top stack has the D8 entry (from tests the non-renamed entry is ToolHelper.runX). |
| assertThat(topStackTrace, containsString("com.android.tools.r8.ToolHelper.run")); |
| // Check that the stack trace always has the suppressed info. |
| assertThat(restStackTrace, containsString(StringUtils.LINE_SEPARATOR + "\tSuppressed:")); |
| // Custom test checks. |
| assertThat(restStackTrace, stackMatcher); |
| } |
| |
| private static void throwNPE() { |
| throw new NullPointerException("A test NPE"); |
| } |
| |
| @Test |
| public void testNPE() throws Exception { |
| try { |
| testForD8() |
| .apply(addTestClassWithOrigin()) |
| .addOptionsModification( |
| options -> options.testing.hookInIrConversion = ErrorDuringIrConversionTest::throwNPE) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| // Check that the error is reported as an error to the diagnostics handler. |
| diagnostics |
| .assertOnlyErrors() |
| .assertErrorsMatch( |
| allOf( |
| diagnosticOrigin(ORIGIN), |
| diagnosticException(NullPointerException.class), |
| diagnosticMessage(containsString("A test NPE")))); |
| }); |
| } catch (CompilationFailedException e) { |
| checkCompilationFailedException( |
| e, containsString(ORIGIN.toString()), containsString("throwNPE")); |
| return; |
| } |
| fail("Expected compilation to fail"); |
| } |
| |
| @Test |
| public void testFatalError() throws Exception { |
| try { |
| testForD8() |
| .apply(addTestClassWithOrigin()) |
| .addOptionsModification( |
| options -> |
| options.testing.hookInIrConversion = |
| () -> options.reporter.fatalError("My Fatal Error!")) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| // Check that the error is reported as an error to the diagnostics handler. |
| diagnostics.assertOnlyErrors(); |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticType(StringDiagnostic.class), |
| diagnosticMessage(containsString("My Fatal Error")), |
| // The fatal error is not given an origin, so it can't provide it. |
| // Note: This could be fixed by delaying reporting and associate the info |
| // at the top-level handler. It would require mangling of the diagnostic, |
| // so maybe not that elegant. |
| diagnosticOrigin(Origin.unknown()))); |
| }); |
| } catch (CompilationFailedException e) { |
| checkCompilationFailedException(e, containsString(ORIGIN.toString()), new IsAnything<>()); |
| return; |
| } |
| fail("Expected compilation to fail"); |
| } |
| |
| private static void reportErrors(Reporter reporter) { |
| reporter.error("FOO!"); |
| reporter.error("BAR!"); |
| reporter.error("BAZ!"); |
| } |
| |
| @Test |
| public void testThreeErrors() throws Exception { |
| AtomicBoolean doError = new AtomicBoolean(true); |
| try { |
| testForD8() |
| .apply(addTestClassWithOrigin()) |
| .addOptionsModification( |
| options -> |
| options.testing.hookInIrConversion = |
| () -> { |
| // Ensure that the errors are reported just once as IR conversion is |
| // threaded. |
| if (doError.getAndSet(false)) { |
| reportErrors(options.reporter); |
| } |
| }) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| // Check that the error is reported as an error to the diagnostics handler. |
| diagnostics |
| .assertOnlyErrors() |
| .assertErrorsCount(3) |
| .assertAllErrorsMatch( |
| allOf( |
| diagnosticOrigin(Origin.unknown()), |
| diagnosticType(StringDiagnostic.class), |
| diagnosticMessage( |
| anyOf( |
| containsString("FOO!"), |
| containsString("BAR!"), |
| containsString("BAZ!"))))); |
| }); |
| } catch (CompilationFailedException e) { |
| checkCompilationFailedException( |
| e, |
| // There may be no fail-if-error barrier inside any origin association, thus only the |
| // top level message can be expected here. |
| containsString("Compilation failed to complete"), |
| // The stack trace must contain the reportErrors frame for the hook above, and one |
| // of the error messages. |
| allOf( |
| containsString("reportErrors"), |
| anyOf(containsString("FOO!"), containsString("BAR!"), containsString("BAZ!")))); |
| return; |
| } |
| fail("Expected compilation to fail"); |
| } |
| |
| static class TestClass { |
| |
| public static void main(String[] args) { |
| System.out.println("Hello, world"); |
| } |
| } |
| } |