blob: dc077051b544f906285230bad26fb8c3d325c899 [file] [log] [blame]
// Copyright (c) 2023, 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.dex.whitespaceinidentifiers;
import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.ProguardVersion;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestCompilerBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.naming.ProguardMapReader.ParseException;
import com.android.tools.r8.naming.retrace.StackTrace;
import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.DexVersion;
import com.android.tools.r8.utils.StringUtils;
import java.nio.file.Path;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class WhiteSpaceInIdentifiersTest extends TestBase {
@Parameter() public TestParameters parameters;
@Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withAllRuntimesAndApiLevels().build();
}
private static final String EXPECTED_OUTPUT =
StringUtils.lines(
"0x20", "0xa0", "0x1680", "0x2000", "0x2001", "0x2002", "0x2003", "0x2004", "0x2005",
"0x2006", "0x2007", "0x2008", "0x2009", "0x200a", "0x202f", "0x205f", "0x3000");
private void assumeParametersWithSupportForWhitespaceInIdentifiers() {
assumeTrue(
parameters.isCfRuntime()
|| (parameters.isDexRuntime()
&& parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.R)));
}
private void assumeDexRuntimeSupportingDexVersion039() {
assumeTrue(
parameters.isDexRuntime()
&& parameters.getApiLevel().getDexVersion().isGreaterThanOrEqualTo(DexVersion.V39));
}
public void configure(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
testBuilder
.addProgramClassFileData(getTransformed())
.applyIf(parameters.isDexRuntime(), b -> b.setMinApi(parameters))
.applyIf(
parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
b -> {
try {
b.compileWithExpectedDiagnostics(
diagnostics ->
diagnostics.assertErrorsMatch(
diagnosticMessage(
containsString("are not allowed prior to DEX version 040"))));
fail("Unexpected success");
} catch (CompilationFailedException e) {
// Expected.
}
},
b ->
b.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED_OUTPUT));
}
@Test
public void testD8() throws Exception {
parameters.assumeDexRuntime();
configure(testForD8(parameters.getBackend()));
}
@Test
public void testR8() {
assumeParametersWithSupportForWhitespaceInIdentifiers();
Exception e =
assertThrows(
RuntimeException.class,
() -> configure(testForR8(parameters.getBackend()).addKeepMainRule(TestClass.class)));
// TODO(b/141287396): Proguard maps with spaces in identifiers are not supported. Running
// on R8 always creates an inspector to find the name of the potentially renamed main class.
assertTrue(e.getCause() instanceof ExecutionException);
assertTrue(e.getCause().getCause() instanceof ParseException);
}
@Test
public void testR8Mapping() throws Exception {
assumeParametersWithSupportForWhitespaceInIdentifiers();
String map =
testForR8(parameters.getBackend())
.addProgramClassFileData(getTransformed())
.applyIf(parameters.isDexRuntime(), b -> b.setMinApi(parameters))
.addKeepMainRule(TestClass.class)
.compile()
.getProguardMap();
// R8 renames methods with white space, so they appear in the mapping file.
assertTrue(StringUtils.splitLines(map).size() > 40);
}
@Test
public void testProguard() throws Exception {
parameters.assumeCfRuntime();
configure(
testForProguard(ProguardVersion.V7_0_0)
.addKeepMainRule(TestClass.class)
.addDontWarn(TestClass.class.getTypeName()));
}
@Test
public void testProguardMapping() throws Exception {
parameters.assumeCfRuntime();
String map =
testForProguard(ProguardVersion.V7_0_0)
.addProgramClassFileData(getTransformed())
.addKeepMainRule(TestClass.class)
.addDontWarn(TestClass.class.getTypeName())
.compile()
.getProguardMap();
// Proguard leaves the methods with white space alone, so they don't need to appear in the
// mapping file.
assertEquals(
StringUtils.lines(
TestClass.class.getTypeName() + " -> " + TestClass.class.getTypeName() + ":",
" void <init>() -> <init>",
" void main(java.lang.String[]) -> main"),
map);
}
@Test
public void testD8MergeWithSpaces() throws Exception {
assumeDexRuntimeSupportingDexVersion039();
// Compile with API level 30 to allow white space in identifiers. This generates DEX
// version 039.
Path dex =
testForD8(parameters.getBackend())
.addProgramClassFileData(getTransformed())
.setMinApi(AndroidApiLevel.R)
.compile()
.writeToZip();
// Run merge step with DEX with white space in input (not forcing min API level of R).
testForD8(parameters.getBackend())
.addProgramFiles(dex)
.setMinApi(parameters)
.applyIf(
parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
b -> {
try {
// TODO(b/269089718): This should not be an AssertionError but a compilation error.
b.compileWithExpectedDiagnostics(
diagnostics ->
diagnostics.assertErrorsMatch(diagnosticException(AssertionError.class)));
fail("Unexpected success");
} catch (CompilationFailedException e) {
// Expected.
}
},
b ->
b.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED_OUTPUT));
}
@Test
public void testD8RunWithSpaces() throws Exception {
assumeDexRuntimeSupportingDexVersion039();
testForD8(parameters.getBackend())
.addProgramClassFileData(getTransformed())
.setMinApi(AndroidApiLevel.R)
.run(parameters.getRuntime(), TestClass.class)
.applyIf(
parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
b -> b.assertFailureWithErrorThatMatches(containsString("Failure to verify dex file")),
b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
}
@Test
public void testD8RunWithSpacesUsingDexV40() throws Exception {
assumeDexRuntimeSupportingDexVersion039();
testForD8(parameters.getBackend())
.addOptionsModification(options -> options.testing.dexVersion40FromApiLevel30 = true)
.addProgramClassFileData(getTransformed())
.setMinApi(AndroidApiLevel.R)
.run(parameters.getRuntime(), TestClass.class)
.applyIf(
parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
b ->
b.assertFailureWithErrorThatMatches(
allOf(containsString("Unrecognized version"), containsString("0 4 0"))),
b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
}
@Test
public void testJvmStackTrace() throws Exception {
parameters.assumeCfRuntime();
testForJvm(parameters)
.addProgramClassFileData(getTransformed())
.run(parameters.asCfRuntime(), TestClass.class, "some-argument")
.assertFailureWithErrorThatThrows(RuntimeException.class)
.inspectOriginalStackTrace(
stackTrace ->
assertThat(
stackTrace,
StackTrace.isSameExceptForLineNumbers(
StackTrace.builder()
.add(
StackTraceLine.builder()
.setMethodName(" ")
.setClassName(TestClass.class.getTypeName())
.setFileName("WhiteSpaceInIdentifiersTest.java")
.build())
.add(
StackTraceLine.builder()
.setMethodName("main")
.setClassName(TestClass.class.getTypeName())
.setFileName("WhiteSpaceInIdentifiersTest.java")
.build())
.build())));
}
private String rename(String name) {
return new String(Character.toChars(Integer.parseInt(name.substring(1), 16)));
}
private byte[] getTransformed() throws Exception {
return transformer(TestClass.class)
.renameMethod(MethodPredicate.onName(name -> name.startsWith("t")), this::rename)
.transformMethodInsnInMethod(
"main",
((opcode, owner, name, descriptor, isInterface, continuation) -> {
continuation.visitMethodInsn(
opcode,
owner,
name.startsWith("t") ? rename(name) : name,
descriptor,
isInterface);
}))
.transform();
}
// Test with white space characters added in https://r8-review.git.corp.google.com/c/r8/+/42269.
// Supported on Art in https://android-review.git.corp.google.com/c/platform/art/+/1106719.
static class TestClass {
private static void t20(boolean throwException) {
System.out.println("0x20");
if (throwException) {
throw new RuntimeException();
}
}
private static void ta0() {
System.out.println("0xa0");
}
private static void t1680() {
System.out.println("0x1680");
}
private static void t2000() {
System.out.println("0x2000");
}
private static void t2001() {
System.out.println("0x2001");
}
private static void t2002() {
System.out.println("0x2002");
}
private static void t2003() {
System.out.println("0x2003");
}
private static void t2004() {
System.out.println("0x2004");
}
private static void t2005() {
System.out.println("0x2005");
}
private static void t2006() {
System.out.println("0x2006");
}
private static void t2007() {
System.out.println("0x2007");
}
private static void t2008() {
System.out.println("0x2008");
}
private static void t2009() {
System.out.println("0x2009");
}
private static void t200a() {
System.out.println("0x200a");
}
private static void t202f() {
System.out.println("0x202f");
}
private static void t205f() {
System.out.println("0x205f");
}
private static void t3000() {
System.out.println("0x3000");
}
public static void main(String[] args) {
t20(args.length > 0);
ta0();
t1680();
t2000();
t2001();
t2002();
t2003();
t2004();
t2005();
t2006();
t2007();
t2008();
t2009();
t200a();
t202f();
t205f();
t3000();
}
}
}