blob: 17c01e3629b08184dc1211110195762a8e56fb0d [file]
// Copyright (c) 2026, 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 com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.apimodel.ApiJarInfo;
import com.android.tools.r8.apimodel.ApiJarReader;
import com.android.tools.r8.apimodel.FieldTypelessReference;
import com.android.tools.r8.origin.CommandLineOrigin;
import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.references.TypeReference;
import com.android.tools.r8.utils.internal.FileUtils;
import com.android.tools.r8.utils.internal.StringUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class ApiDatabaseGeneratorTest extends TestBase {
@Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withNoneRuntime().build();
}
public ApiDatabaseGeneratorTest(TestParameters parameters) {
parameters.assertNoneRuntime();
}
@Test
public void testGenerator() throws Exception {
Path apiVersionsXml =
writeApiXml(
"api-versions.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path dummyJar = temp.newFile("dummy.jar").toPath();
Path outputDb = temp.newFile("api_database.ser").toPath();
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.builder()
.addInputPath(apiVersionsXml)
.addInputPath(dummyJar)
.setOutputPath(outputDb)
.build();
ApiDatabaseGenerator.run(command);
assertTrue(Files.exists(outputDb));
assertTrue(Files.size(outputDb) > 0);
}
@Test
public void testGeneratorWithMergeAndErrors() throws Exception {
Path apiVersionsXml1 =
writeApiXml(
"api-versions-1.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path apiVersionsXml2 =
writeApiXml(
"api-versions-2.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" <field name=\"baz\" since=\"33\"/>",
" </class>");
Path dummyJar = temp.newFile("dummy.jar").toPath();
Path outputDb = temp.newFile("api_database.ser").toPath();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.builder(diagnosticsHandler)
.addInputPath(apiVersionsXml1)
.addInputPath(apiVersionsXml2)
.addInputPath(dummyJar)
.setOutputPath(outputDb)
.build();
try {
ApiDatabaseGenerator.run(command);
fail("Expected API database generation to fail due to duplicate entries");
} catch (ApiDatabaseGeneratorException e) {
// Expected.
}
diagnosticsHandler.assertErrorsMatch(
diagnosticMessage(containsString("Duplicate class android.Foo")));
}
@Test
public void testGeneratorWithThreeInputsAndErrors() throws Exception {
Path apiVersionsXml1 =
writeApiXml(
"api-versions-1.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path apiVersionsXml2 =
writeApiXml(
"api-versions-2.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"baz()V\" since=\"32\"/>",
" </class>");
Path apiVersionsXml3 =
writeApiXml(
"api-versions-3.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path dummyJar = temp.newFile("dummy.jar").toPath();
Path outputDb = temp.newFile("api_database.ser").toPath();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.builder(diagnosticsHandler)
.addInputPath(apiVersionsXml1)
.addInputPath(apiVersionsXml2)
.addInputPath(apiVersionsXml3)
.addInputPath(dummyJar)
.setOutputPath(outputDb)
.build();
try {
ApiDatabaseGenerator.run(command);
fail("Expected API database generation to fail due to duplicate entries");
} catch (ApiDatabaseGeneratorException e) {
// Expected.
}
diagnosticsHandler.assertErrorsMatch(
diagnosticMessage(equalTo("Duplicate class android.Foo found when merging .xml files.")),
diagnosticMessage(equalTo("Duplicate class android.Foo found when merging .xml files.")));
}
@Test
public void testGeneratorWithMergeAndSuppressedWarnings() throws Exception {
Path apiVersionsXml1 =
writeApiXml(
"api-versions-1.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path apiVersionsXml2 =
writeApiXml(
"api-versions-2.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" <field name=\"baz\" since=\"33\"/>",
" </class>");
Path dummyJar = temp.newFile("dummy.jar").toPath();
Path outputDb = temp.newFile("api_database.ser").toPath();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
String[] args = {
"--output",
outputDb.toString(),
"--map-diagnostics:DuplicateApiDatabaseEntryDiagnostic",
"error",
"info",
apiVersionsXml1.toString(),
apiVersionsXml2.toString(),
dummyJar.toString()
};
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.parse(args, CommandLineOrigin.INSTANCE, diagnosticsHandler)
.build();
ApiDatabaseGenerator.run(command);
assertTrue(Files.exists(outputDb));
assertTrue(Files.size(outputDb) > 0);
// Errors and Warnings should be empty because they were mapped to info.
diagnosticsHandler
.assertNoErrors()
.assertNoWarnings()
// Instead, they should be in the info list.
.assertInfosMatch(diagnosticMessage(containsString("Duplicate class android.Foo")));
}
@Test
public void testGeneratorWithMergeAndNoneWarnings() throws Exception {
Path apiVersionsXml1 =
writeApiXml(
"api-versions-1.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" </class>");
Path apiVersionsXml2 =
writeApiXml(
"api-versions-2.xml",
" <class name=\"android/Foo\" since=\"30\">",
" <extends name=\"java/lang/Object\"/>",
" <method name=\"bar()V\" since=\"31\"/>",
" <field name=\"baz\" since=\"33\"/>",
" </class>");
Path dummyJar = temp.newFile("dummy.jar").toPath();
Path outputDb = temp.newFile("api_database.ser").toPath();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
String[] args = {
"--output",
outputDb.toString(),
"--map-diagnostics:DuplicateApiDatabaseEntryDiagnostic",
"error",
"none",
apiVersionsXml1.toString(),
apiVersionsXml2.toString(),
dummyJar.toString()
};
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.parse(args, CommandLineOrigin.INSTANCE, diagnosticsHandler)
.build();
ApiDatabaseGenerator.run(command);
assertTrue(Files.exists(outputDb));
assertTrue(Files.size(outputDb) > 0);
// Errors, Warnings, and Infos should all be empty because they were mapped to none.
diagnosticsHandler.assertNoMessages();
}
@Test
public void testJarAndXmlInputs() throws Exception {
Path xmlFile = temp.newFile("api-versions.xml").toPath();
Path jarFile = temp.newFile("android.jar").toPath();
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.builder().addInputPath(xmlFile).addInputPath(jarFile).build();
assertEquals(1, command.getXmlPaths().size());
assertEquals(xmlFile, command.getXmlPaths().get(0));
assertEquals(1, command.getJarPaths().size());
assertEquals(jarFile, command.getJarPaths().get(0));
}
@Test
public void testInvalidInputExtension() throws Exception {
Path txtFile = temp.newFile("invalid.txt").toPath();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
try {
ApiDatabaseGeneratorCommand.builder(diagnosticsHandler).addInputPath(txtFile).build();
fail("Expected Command to fail building due to invalid input extension");
} catch (ApiDatabaseGeneratorException e) {
// Expected.
}
diagnosticsHandler.assertErrorsMatch(
diagnosticMessage(containsString("Unsupported input file extension")),
diagnosticMessage(containsString("At least one SDK JAR")),
diagnosticMessage(containsString("At least one API XML")));
}
private Path writeApiXml(String filename, String... contentLines) throws Exception {
Path file = temp.newFile(filename).toPath();
String xml =
StringUtils.lines(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<api version=\"3\">",
StringUtils.joinLines(contentLines),
"</api>");
FileUtils.writeTextFile(file, xml);
return file;
}
interface TestInterface {
@SuppressWarnings("unused")
void instanceMethod();
static void staticMethod() {}
}
static class TestClass {
@SuppressWarnings("unused")
int field = 0;
@SuppressWarnings("unused")
void instanceMethod() {}
@SuppressWarnings("unused")
static void staticMethod() {}
}
@Test
public void testExtractJarInfo() throws Exception {
Path jar = temp.newFile("test.jar").toPath();
testForR8(Backend.CF)
.addProgramClasses(TestInterface.class, TestClass.class)
.addKeepAllClassesRule()
.compile()
.writeToZip(jar);
ApiDatabaseGeneratorCommand command =
ApiDatabaseGeneratorCommand.builder()
.addInputPath(jar)
.addInputPath(writeApiXml("dummy.xml")) // Need at least one XML
.build();
ApiJarInfo jarInfo = ApiJarReader.extractJarInfo(command);
ClassReference interfaceRef = Reference.classFromClass(TestInterface.class);
assertTrue(jarInfo.isClassDefined(interfaceRef));
assertTrue(jarInfo.isInterface(interfaceRef));
ClassReference classRef = Reference.classFromClass(TestClass.class);
assertTrue(jarInfo.isClassDefined(classRef));
assertFalse(jarInfo.isInterface(classRef));
TypeReference voidType = Reference.returnTypeFromDescriptor("V");
List<TypeReference> noArgs = Collections.emptyList();
MethodReference interfaceInstanceMethod =
Reference.method(interfaceRef, "instanceMethod", noArgs, voidType);
assertTrue(jarInfo.isMethodDefined(interfaceInstanceMethod));
assertFalse(jarInfo.isStatic(interfaceInstanceMethod));
MethodReference interfaceStaticMethod =
Reference.method(interfaceRef, "staticMethod", noArgs, voidType);
assertTrue(jarInfo.isMethodDefined(interfaceStaticMethod));
assertTrue(jarInfo.isStatic(interfaceStaticMethod));
MethodReference classInstanceMethod =
Reference.method(classRef, "instanceMethod", noArgs, voidType);
assertTrue(jarInfo.isMethodDefined(classInstanceMethod));
assertFalse(jarInfo.isStatic(classInstanceMethod));
MethodReference classStaticMethod =
Reference.method(classRef, "staticMethod", noArgs, voidType);
assertTrue(jarInfo.isMethodDefined(classStaticMethod));
assertTrue(jarInfo.isStatic(classStaticMethod));
FieldTypelessReference field = new FieldTypelessReference(classRef, "field");
assertTrue(jarInfo.isFieldDefined(field));
}
}