| // Copyright (c) 2016, 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; | |
| import static org.junit.Assert.assertEquals; | |
| import com.android.tools.r8.ToolHelper.DexVm; | |
| import com.android.tools.r8.ToolHelper.DexVm.Version; | |
| import com.android.tools.r8.utils.TestDescriptionWatcher; | |
| import com.android.tools.r8.utils.internal.StringUtils; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import org.junit.Rule; | |
| import org.junit.Test; | |
| import org.junit.rules.ExpectedException; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.Parameterized; | |
| import org.junit.runners.Parameterized.Parameters; | |
| @RunWith(Parameterized.class) | |
| public class R8RunSmaliTestsTest extends TestBase { | |
| @Rule | |
| public ExpectedException thrown = ExpectedException.none(); | |
| @Parameters(name = "{0}: {1}") | |
| public static Collection<Object[]> data() { | |
| return buildParameters( | |
| getTestParameters().withDexRuntimesAndAllApiLevels().build(), tests.keySet()); | |
| } | |
| private static final String SMALI_DIR = ToolHelper.SMALI_BUILD_DIR; | |
| private static Map<String, String> tests; | |
| static { | |
| ImmutableMap.Builder<String, String> testsBuilder = ImmutableMap.builder(); | |
| testsBuilder | |
| .put( | |
| "arithmetic", | |
| StringUtils.lines( | |
| "-1", "3", "2", "3", "3.0", "1", "0", "-131580", "-131580", "2", "4", "-2")) | |
| .put( | |
| "controlflow", | |
| StringUtils.lines("2", "1", "2", "1", "2", "1", "2", "1", "2", "1", "2", "1", "2")) | |
| .put("fibonacci", StringUtils.lines("55", "55", "55", "55")) | |
| .put("fill-array-data", "[1, 2, 3][4, 5, 6]") | |
| .put("filled-new-array", "[1, 2, 3][4, 5, 6][1, 2, 3, 4, 5, 6][6, 5, 4, 3, 2, 1]") | |
| .put("packed-switch", "12345") | |
| .put("sparse-switch", "12345") | |
| .put("unreachable-code-1", "777") | |
| .put( | |
| "multiple-returns", | |
| StringUtils.lines("TFtf", "1", "4611686018427387904", "true", "false")) | |
| .put("try-catch", "") | |
| .put("phi-removal-regression", StringUtils.lines("returnBoolean")) | |
| .put( | |
| "overlapping-long-registers", | |
| StringUtils.lines("-9151314442816847872", "-9151314442816319488")) | |
| .put( | |
| "type-confusion-regression", | |
| StringUtils.lines("java.lang.RuntimeException: Test.<init>()")) | |
| .put( | |
| "type-confusion-regression2", | |
| StringUtils.lines("java.lang.NullPointerException: Attempt to read from null array")) | |
| .put( | |
| "type-confusion-regression3", | |
| StringUtils.lines( | |
| "java.lang.NullPointerException: Attempt to read from field 'byte[] Test.a'" | |
| + " on a null object reference")) | |
| .put("type-confusion-regression4", "") | |
| .put( | |
| "type-confusion-regression5", StringUtils.lines("java.lang.RuntimeException: getId()I")) | |
| .put("chain-of-loops", StringUtils.lines("java.lang.RuntimeException: f(II)")) | |
| .put("new-instance-and-init", StringUtils.lines("Test(0)", "Test(0)", "Test(0)")) | |
| .put( | |
| "bad-codegen", | |
| StringUtils.lines( | |
| "java.lang.NullPointerException: Attempt to read from field " | |
| + "'Test Test.a' on a null object reference")) | |
| .put( | |
| "merge-blocks-regression", | |
| StringUtils.lines( | |
| "java.lang.NullPointerException: Attempt to invoke virtual" | |
| + " method 'Test Test.bW_()' on a null object reference")) | |
| .put("self-is-catch-block", StringUtils.lines("100", "-1")) | |
| .put("infinite-loop", "") | |
| .put( | |
| "regression/33336471", | |
| StringUtils.lines( | |
| "START", "0", "2", "LOOP", "1", "2", "LOOP", "2", "2", "DONE", "START", "0", "2", | |
| "LOOP", "1", "2", "LOOP", "2", "2", "DONE")) | |
| .put("regression/33846227", "") | |
| .put("illegal-invokes", StringUtils.lines("ICCE", "ICCE")) | |
| .build(); | |
| tests = testsBuilder.build(); | |
| } | |
| private static Set<String> invokeErrors = ImmutableSet.of("bad-codegen", "illegal-invokes"); | |
| private static Map<String, Set<String>> missingClasses = | |
| ImmutableMap.of( | |
| "try-catch", ImmutableSet.of("test.X"), | |
| "type-confusion-regression5", ImmutableSet.of("jok", "jol"), | |
| "bad-codegen", ImmutableSet.of("java.util.LTest")); | |
| // Tests where the original smali code fails on Art, but runs after R8 processing. | |
| private static final Map<DexVm.Version, List<String>> originalFailingOnArtVersions = | |
| ImmutableMap.of( | |
| Version.V5_1_1, | |
| ImmutableList.of( | |
| // Smali code contains an empty switch payload. | |
| "sparse-switch", "regression/33846227"), | |
| Version.V4_4_4, | |
| ImmutableList.of( | |
| // Smali code contains an empty switch payload. | |
| "sparse-switch", "regression/33846227"), | |
| Version.V4_0_4, | |
| ImmutableList.of( | |
| // Smali code contains an empty switch payload. | |
| "sparse-switch", "regression/33846227")); | |
| // Tests where the output has a different output than the original on certain VMs. | |
| private static final Map<String, String> dalvikExpectations = | |
| ImmutableMap.of( | |
| "bad-codegen", "java.lang.NullPointerException\n", | |
| "type-confusion-regression2", "java.lang.NullPointerException\n", | |
| "type-confusion-regression3", "java.lang.NullPointerException\n", | |
| "merge-blocks-regression", "java.lang.NullPointerException\n"); | |
| private static final Map<String, String> art13PlusExpectations = | |
| ImmutableMap.of( | |
| "bad-codegen", | |
| StringUtils.lines( | |
| "java.lang.NullPointerException: Attempt to read from field 'Test Test.a'" | |
| + " on a null object reference in method 'Test TestObject.a(Test," | |
| + " Test, Test, Test, boolean)'"), | |
| "type-confusion-regression3", | |
| StringUtils.lines( | |
| "java.lang.NullPointerException: Attempt to read from field 'byte[]" | |
| + " Test.a' on a null object reference in method 'int" | |
| + " TestObject.a(Test, Test)'")); | |
| private static final Map<DexVm.Version, Map<String, String>> customProcessedOutputExpectation = | |
| ImmutableMap.of( | |
| Version.V4_4_4, | |
| dalvikExpectations, | |
| Version.V4_0_4, | |
| dalvikExpectations, | |
| Version.V13_0_0, | |
| art13PlusExpectations, | |
| Version.V14_0_0, | |
| art13PlusExpectations, | |
| Version.V15_0_0, | |
| art13PlusExpectations, | |
| Version.V16_0_0, | |
| art13PlusExpectations, | |
| Version.V17_0_0, | |
| art13PlusExpectations); | |
| // Tests where the input fails with a verification error on Dalvik instead of the | |
| // expected runtime exception. | |
| private static final Map<DexVm.Version, List<String>> dalvikVerificationErrors = | |
| ImmutableMap.of( | |
| Version.V4_4_4, | |
| ImmutableList.of( | |
| // The invokes are in fact invalid, but the test expects the current Art behavior | |
| // of throwing an IncompatibleClassChange exception. Dalvik fails to verify. | |
| "illegal-invokes"), | |
| Version.V4_0_4, | |
| ImmutableList.of( | |
| // The invokes are in fact invalid, but the test expects the current Art behavior | |
| // of throwing an IncompatibleClassChange exception. Dalvik fails to verify. | |
| "illegal-invokes")); | |
| private Set<String> failingOnX8 = ImmutableSet.of( | |
| // Contains use of register as both an int and a float. | |
| "regression/33336471" | |
| ); | |
| @Rule | |
| public TestDescriptionWatcher watcher = new TestDescriptionWatcher(); | |
| private final TestParameters parameters; | |
| private final String directoryName; | |
| private final String dexFileName; | |
| private final String expectedOutput; | |
| public R8RunSmaliTestsTest(TestParameters parameters, String name) { | |
| this.parameters = parameters; | |
| String expectedOutput = tests.get(name); | |
| if (customProcessedOutputExpectation.containsKey(parameters.asDexRuntime().getVersion()) | |
| && customProcessedOutputExpectation | |
| .get(parameters.asDexRuntime().getVersion()) | |
| .containsKey(name)) { | |
| // If the original and the processed code have different expected output, only run | |
| // the code produced by R8. | |
| expectedOutput = | |
| customProcessedOutputExpectation.get(parameters.asDexRuntime().getVersion()).get(name); | |
| } | |
| this.directoryName = name; | |
| this.dexFileName = name.substring(name.lastIndexOf('/') + 1) + ".dex"; | |
| this.expectedOutput = expectedOutput; | |
| } | |
| @Test | |
| public void SmaliTest() throws Exception { | |
| Path originalDexFile = Paths.get(SMALI_DIR, directoryName, dexFileName); | |
| if (failingOnX8.contains(directoryName)) { | |
| thrown.expect(CompilationFailedException.class); | |
| } | |
| Version version = parameters.asDexRuntime().getVersion(); | |
| boolean dalvikVerificationError = | |
| dalvikVerificationErrors.containsKey(version) | |
| && dalvikVerificationErrors.get(version).contains(directoryName); | |
| boolean originalFailing = | |
| (originalFailingOnArtVersions.containsKey(version) | |
| && originalFailingOnArtVersions.get(version).contains(directoryName)); | |
| Path testJar = Paths.get(SMALI_DIR, directoryName, "Test.jar"); | |
| boolean testJarExists = testJar.toFile().exists(); | |
| Path testJarDex = null; | |
| if (testJarExists) { | |
| testJarDex = | |
| testForD8(Backend.DEX) | |
| .setMinApi(parameters) | |
| .addProgramFiles(testJar) | |
| .compile() | |
| .writeToZip(); | |
| } | |
| testForR8(parameters.getBackend()) | |
| .addKeepAllClassesRule() | |
| .addProgramDexFileData(Files.readAllBytes(originalDexFile)) | |
| .applyIf(testJarExists, p -> p.addProgramFiles(testJar)) | |
| .addDontWarn(missingClasses.getOrDefault(directoryName, Collections.emptySet())) | |
| .addOptionsModification( | |
| options -> | |
| options.getTestingOptions().allowInvokeErrors = | |
| invokeErrors.contains(directoryName)) | |
| .setMinApi(parameters) | |
| .compile() | |
| .run(parameters.getRuntime(), "Test") | |
| .applyIf( | |
| dalvikVerificationError, | |
| r -> r.assertFailureWithErrorThatThrows(VerifyError.class), | |
| r -> r.assertSuccessWithOutput(expectedOutput)); | |
| // Also run the original DEX if possible. | |
| if (!dalvikVerificationError && !originalFailing) { | |
| ImmutableList.Builder<String> dexListBuilder = ImmutableList.builder(); | |
| dexListBuilder.add(originalDexFile.toString()); | |
| if (testJarExists) { | |
| dexListBuilder.add(testJarDex.toString()); | |
| } | |
| String originalOutput = | |
| ToolHelper.runArtNoVerificationErrors( | |
| dexListBuilder.build(), "Test", null, parameters.getRuntime().asDex().getVm()); | |
| assertEquals(expectedOutput, originalOutput); | |
| } | |
| } | |
| } |