| // Copyright (c) 2019, 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.retrace; |
| |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.core.StringContains.containsString; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeTrue; |
| |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.ToolHelper.ProcessResult; |
| import com.android.tools.r8.Version; |
| import com.android.tools.r8.retrace.stacktraces.ActualRetraceBotStackTrace; |
| import com.android.tools.r8.retrace.stacktraces.ActualRetraceBotStackTraceWithInfo; |
| import com.android.tools.r8.retrace.stacktraces.FoundMethodVerboseStackTrace; |
| import com.android.tools.r8.retrace.stacktraces.PGStackTrace; |
| import com.android.tools.r8.utils.BooleanUtils; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.ImmutableList; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import org.hamcrest.Matcher; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TemporaryFolder; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| |
| @RunWith(Parameterized.class) |
| public class RetraceCommandLineTests { |
| |
| private static String SMILEY_EMOJI = "\uD83D\uDE00"; |
| |
| private static final String WAITING_MESSAGE = |
| "Waiting for stack-trace input..." + StringUtils.LINE_SEPARATOR; |
| |
| @Rule public TemporaryFolder folder = new TemporaryFolder(); |
| |
| private final boolean testExternal; |
| |
| @Parameters(name = "external: {0}") |
| public static Boolean[] data() { |
| return BooleanUtils.values(); |
| } |
| |
| public RetraceCommandLineTests(boolean testExternal) { |
| this.testExternal = testExternal; |
| } |
| |
| @Test |
| public void testPrintIdentityStackTraceFile() throws IOException { |
| runTest("", nonMappableStackTrace, false, nonMappableStackTrace); |
| } |
| |
| @Test |
| public void testPrintIdentityStackTraceInput() throws IOException { |
| runTest("", nonMappableStackTrace, true, nonMappableStackTrace); |
| } |
| |
| @Test |
| public void testNoMappingFileSpecified() throws IOException { |
| runAbortTest(containsString("Mapping file not specified")); |
| } |
| |
| @Test |
| public void testMissingMappingFile() throws IOException { |
| runAbortTest(containsString("Could not find mapping file 'foo.txt'"), "foo.txt"); |
| } |
| |
| @Test |
| public void testInvalidMappingFile() throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write(mappingFile, "foo.bar.baz <- is invalid mapping".getBytes()); |
| Path stackTraceFile = folder.newFile("stacktrace.txt").toPath(); |
| Files.write(stackTraceFile, new byte[0]); |
| runAbortTest( |
| containsString("Unable to parse mapping file"), |
| mappingFile.toString(), |
| stackTraceFile.toString()); |
| } |
| |
| @Test |
| public void testMissingStackTraceFile() throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write(mappingFile, "foo.bar.baz -> foo:".getBytes()); |
| runAbortTest(containsString("NoSuchFileException"), mappingFile.toString(), "stacktrace.txt"); |
| } |
| |
| @Test |
| public void testVerbose() throws IOException { |
| FoundMethodVerboseStackTrace stackTrace = new FoundMethodVerboseStackTrace(); |
| runTest( |
| stackTrace.mapping(), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retraceVerboseStackTrace()) + StringUtils.LINE_SEPARATOR, |
| "--verbose"); |
| } |
| |
| @Test |
| public void testVerboseSingleHyphen() throws IOException { |
| FoundMethodVerboseStackTrace stackTrace = new FoundMethodVerboseStackTrace(); |
| runTest( |
| stackTrace.mapping(), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retraceVerboseStackTrace()) + StringUtils.LINE_SEPARATOR, |
| "-verbose"); |
| } |
| |
| @Test |
| public void testWindowsLineEndings() throws IOException { |
| ActualRetraceBotStackTrace stackTrace = new ActualRetraceBotStackTrace(); |
| runTest( |
| stackTrace.mapping().replace("\n", "\r\n"), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testRegularExpression() throws IOException { |
| ActualRetraceBotStackTrace stackTrace = new ActualRetraceBotStackTrace(); |
| runTest( |
| stackTrace.mapping(), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testRegularExpressionSingleHyphen() throws IOException { |
| ActualRetraceBotStackTrace stackTrace = new ActualRetraceBotStackTrace(); |
| runTest( |
| stackTrace.mapping(), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testRegularExpressionWithInfo() throws IOException { |
| ActualRetraceBotStackTraceWithInfo stackTrace = new ActualRetraceBotStackTraceWithInfo(); |
| runTest( |
| stackTrace.mapping(), |
| StringUtils.joinLines(stackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(stackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR, |
| "--info"); |
| } |
| |
| @Test |
| public void testPGStackTrace() throws Exception { |
| PGStackTrace pgStackTrace = new PGStackTrace(); |
| runTest( |
| pgStackTrace.mapping(), |
| StringUtils.joinLines(pgStackTrace.obfuscatedStackTrace()), |
| false, |
| StringUtils.joinLines(pgStackTrace.retracedStackTrace()) + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testEmpty() throws IOException { |
| runTest("", "", false, ""); |
| } |
| |
| @Test |
| public void testHelp() throws IOException { |
| ProcessResult processResult = runRetraceCommandLine(null, Arrays.asList("--help")); |
| assertEquals(0, processResult.exitCode); |
| assertThat(processResult.stdout, containsString(Retrace.USAGE_MESSAGE)); |
| } |
| |
| @Test |
| public void testVersion() throws Exception { |
| ProcessResult processResult = runRetraceCommandLine(null, Arrays.asList("--version")); |
| assertEquals(0, processResult.exitCode); |
| assertEquals(StringUtils.lines("Retrace " + Version.getVersionString()), processResult.stdout); |
| } |
| |
| @Test |
| public void testNonAscii() throws IOException { |
| runTest("", SMILEY_EMOJI, false, SMILEY_EMOJI + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testNonAsciiStdIn() throws IOException { |
| runTest("", SMILEY_EMOJI, true, SMILEY_EMOJI + StringUtils.LINE_SEPARATOR); |
| } |
| |
| @Test |
| public void testHelpMessageOnStdIn() throws IOException { |
| ProcessResult processResult = runRetrace("", "", true); |
| assertTrue(processResult.stdout.startsWith(WAITING_MESSAGE)); |
| } |
| |
| @Test |
| public void testHelpMessageWithQuiet() throws IOException { |
| ProcessResult processResult = runRetrace("", "", true, "--quiet"); |
| assertFalse(processResult.stdout.startsWith(WAITING_MESSAGE)); |
| } |
| |
| @Test |
| public void testNoMappingFileHash() throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write(mappingFile, ("# other header\n" + "foo.bar -> a.a\n").getBytes()); |
| ProcessResult result = |
| runRetraceCommandLine( |
| null, ImmutableList.of(mappingFile.toString(), "--verify-mapping-file-hash")); |
| assertEquals(result.toString(), 0, result.exitCode); |
| assertEquals("", result.stdout); |
| assertThat(result.stderr, containsString("Failure to find map hash")); |
| } |
| |
| @Test |
| public void testValidMappingFileHash() throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write( |
| mappingFile, |
| ("# pg_map_hash: SHA-256 aaf7c0230ea6fa768189170543c86ec202c6180d1e0a37b620e5c1fce1bd3ae7\n" |
| + "foo.bar -> a.a\n") |
| .getBytes()); |
| ProcessResult result = |
| runRetraceCommandLine( |
| null, ImmutableList.of(mappingFile.toString(), "--verify-mapping-file-hash")); |
| assertEquals(result.toString(), 0, result.exitCode); |
| assertEquals("", result.stdout); |
| assertEquals("", result.stderr); |
| } |
| |
| @Test |
| public void testInvalidMappingFileHash() throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write(mappingFile, ("# pg_map_hash: SHA-256 abcd1234\n" + "foo.bar -> a.a\n").getBytes()); |
| runAbortTest( |
| containsString("Mismatching map hash"), |
| mappingFile.toString(), |
| "--verify-mapping-file-hash"); |
| } |
| |
| private final String nonMappableStackTrace = |
| StringUtils.lines( |
| "com.android.r8.R8Exception: Problem when compiling program", |
| " at r8.a.a(App:42)", |
| " at r8.a.b(App:10)", |
| " at r8.a.c(App:266)", |
| " at r8.main(App:800)", |
| "Caused by: com.android.r8.R8InnerException: You have to write the program first", |
| " at r8.retrace(App:184)", |
| " ... 7 more"); |
| |
| private void runTest( |
| String mapping, String stackTrace, boolean stacktraceStdIn, String expected, String... args) |
| throws IOException { |
| ProcessResult result = runRetrace(mapping, stackTrace, stacktraceStdIn, args); |
| assertEquals(0, result.exitCode); |
| String stdOut = result.stdout; |
| if (stacktraceStdIn) { |
| assertTrue(result.stdout.startsWith(WAITING_MESSAGE)); |
| stdOut = result.stdout.substring(WAITING_MESSAGE.length()); |
| } |
| assertEquals(expected, stdOut); |
| } |
| |
| private void runAbortTest(Matcher<String> errorMatch, String... args) throws IOException { |
| ProcessResult result = runRetraceCommandLine(null, Arrays.asList(args)); |
| assertEquals(1, result.exitCode); |
| assertThat(result.stderr, errorMatch); |
| } |
| |
| private ProcessResult runRetrace( |
| String mapping, String stackTrace, boolean stacktraceStdIn, String... additionalArgs) |
| throws IOException { |
| Path mappingFile = folder.newFile("mapping.txt").toPath(); |
| Files.write(mappingFile, mapping.getBytes()); |
| File stackTraceFile = folder.newFile("stacktrace.txt"); |
| Files.write(stackTraceFile.toPath(), stackTrace.getBytes(StandardCharsets.UTF_8)); |
| |
| Collection<String> args = new ArrayList<>(); |
| args.add(mappingFile.toString()); |
| if (!stacktraceStdIn) { |
| args.add(stackTraceFile.toPath().toString()); |
| } |
| args.addAll(Arrays.asList(additionalArgs)); |
| return runRetraceCommandLine(stacktraceStdIn ? stackTraceFile : null, args); |
| } |
| |
| private ProcessResult runRetraceCommandLine(File stdInput, Collection<String> args) |
| throws IOException { |
| if (testExternal) { |
| // The external dependency is built on top of R8Lib. If test.py is run with |
| // no r8lib, do not try and run the external R8 Retrace since it has not been built. |
| assumeTrue(ToolHelper.isTestingR8Lib()); |
| assertTrue(Files.exists(ToolHelper.R8LIB_JAR)); |
| List<String> command = new ArrayList<>(); |
| command.add(ToolHelper.getSystemJavaExecutable()); |
| command.add("-ea"); |
| command.add("-cp"); |
| command.add(ToolHelper.R8_RETRACE_JAR.toString()); |
| command.add("com.android.tools.r8.retrace.Retrace"); |
| command.addAll(args); |
| ProcessBuilder builder = new ProcessBuilder(command); |
| if (stdInput != null) { |
| builder.redirectInput(stdInput); |
| } |
| return ToolHelper.runProcess(builder); |
| } else { |
| InputStream originalIn = System.in; |
| PrintStream originalOut = System.out; |
| PrintStream originalErr = System.err; |
| if (stdInput != null) { |
| System.setIn(new FileInputStream(stdInput)); |
| } |
| ByteArrayOutputStream outputByteStream = new ByteArrayOutputStream(); |
| System.setOut(new PrintStream(outputByteStream)); |
| ByteArrayOutputStream errorByteStream = new ByteArrayOutputStream(); |
| System.setErr(new PrintStream(errorByteStream)); |
| int exitCode = 0; |
| try { |
| String[] strArgs = new String[0]; |
| strArgs = args.toArray(strArgs); |
| Retrace.run(strArgs); |
| } catch (Throwable t) { |
| exitCode = 1; |
| } |
| if (originalIn != null) { |
| System.setIn(originalIn); |
| } |
| System.setOut(originalOut); |
| System.setErr(originalErr); |
| return new ProcessResult( |
| exitCode, |
| outputByteStream.toString(Charsets.UTF_8.name()), |
| errorByteStream.toString(Charsets.UTF_8.name()), |
| StringUtils.joinLines(args)); |
| } |
| } |
| } |