blob: 6cbb86eb6848674b3a2e29e70eaf18c11c86c2af [file] [log] [blame]
// Copyright (c) 2021, 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.apimodel;
import static com.android.tools.r8.androidapi.AndroidApiLevelDatabaseHelper.notModeledFields;
import static com.android.tools.r8.androidapi.AndroidApiLevelDatabaseHelper.notModeledMethods;
import static com.android.tools.r8.androidapi.AndroidApiLevelDatabaseHelper.notModeledTypes;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestDiagnosticMessagesImpl;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute.DefaultAndroidApiLevelCompute;
import com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl;
import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexLibraryClass;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.IntBox;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
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 AndroidApiHashingDatabaseBuilderGeneratorTest extends TestBase {
protected final TestParameters parameters;
private static final Path API_DATABASE_FOLDER =
Paths.get(ToolHelper.THIRD_PARTY_DIR, "api_database");
private static final Path API_DATABASE =
API_DATABASE_FOLDER
.resolve("api_database")
.resolve("resources")
.resolve("new_api_database.ser");
// Update the API_LEVEL below to have the database generated for a new api level.
private static final AndroidApiLevel API_LEVEL = AndroidApiLevel.API_DATABASE_LEVEL;
@Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withNoneRuntime().build();
}
public AndroidApiHashingDatabaseBuilderGeneratorTest(TestParameters parameters) {
this.parameters = parameters;
}
private static class GenerateDatabaseResourceFilesResult {
private final Path apiLevels;
public GenerateDatabaseResourceFilesResult(Path apiLevels) {
this.apiLevels = apiLevels;
}
}
private static GenerateDatabaseResourceFilesResult generateResourcesFiles() throws Exception {
return generateResourcesFiles(
AndroidApiVersionsXmlParser.builder()
.setApiVersionsXml(ToolHelper.getApiVersionsXmlFile(API_LEVEL))
.setAndroidJar(ToolHelper.getAndroidJar(API_LEVEL))
.setApiLevel(API_LEVEL)
.build()
.run(),
API_LEVEL);
}
private static GenerateDatabaseResourceFilesResult generateResourcesFiles(
List<ParsedApiClass> apiClasses, AndroidApiLevel androidJarApiLevel) throws Exception {
TemporaryFolder temp = new TemporaryFolder();
temp.create();
Path apiLevels = temp.newFile("new_api_levels.ser").toPath();
AndroidApiHashingDatabaseBuilderGenerator.generate(apiClasses, apiLevels, androidJarApiLevel);
return new GenerateDatabaseResourceFilesResult(apiLevels);
}
@Test
public void testCanParseApiVersionsXml() throws Exception {
// This tests makes a rudimentary check on the number of classes, fields and methods in
// api-versions.xml to ensure that the runtime tests do not vacuously succeed.
List<ParsedApiClass> parsedApiClasses =
AndroidApiVersionsXmlParser.builder()
.setApiVersionsXml(ToolHelper.getApiVersionsXmlFile(API_LEVEL))
.setAndroidJar(ToolHelper.getAndroidJar(API_LEVEL))
.setApiLevel(API_LEVEL)
.build()
.run();
IntBox numberOfFields = new IntBox(0);
IntBox numberOfMethods = new IntBox(0);
parsedApiClasses.forEach(
apiClass -> {
apiClass.visitFieldReferences(
((apiLevel, fieldReferences) ->
fieldReferences.forEach(field -> numberOfFields.increment())));
apiClass.visitMethodReferences(
((AndroidApiLevel apiLevel, List<MethodReference> methodReferences) ->
methodReferences.forEach(field -> numberOfMethods.increment())));
});
// These numbers will change when updating api-versions.xml
assertEquals(6222, parsedApiClasses.size());
assertEquals(31430, numberOfFields.get());
assertEquals(48025, numberOfMethods.get());
}
private static String sampleVersion4ApiVersionsXml =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<api version=\"4\">\n"
+ " <sdk id=\"36\" shortname=\"B-ext\" name=\"Baklava Extensions\"\n"
+ " reference=\"android/os/Build$VERSION_CODES$BAKLAVA\"/>\n"
+ "\n"
+ " <!-- This class was introduced in Android R -->\n"
+ " <class name=\"android/os/ext/SdkExtensions\" since=\"30.0\">\n"
+ " <extends name=\"java/lang/Object\"/>\n"
+ " <!-- This method was introduced in Android S. It was \"backported\""
+ " to Android R via the R extension,\n"
+ " version 2. It also exists in later extensions, including the"
+ " Baklava extension (id 36). -->\n"
+ " <method name=\"getAllExtensionVersions()Ljava/util/Map;\""
+ " since=\"31.0\"\n"
+ " sdks=\"30:2,31:2,33:4,34:7,35:12,36:16,0:31.0\"/>\n"
+ " <method name=\"getExtensionVersion(I)I\"/>\n"
+ " <!-- This field was introduced in Android U. It was \"backported\""
+ " to Android R via the R extension,\n"
+ " version 4. It also exists in later extensions, including the"
+ " Baklava extension (id 36). -->\n"
+ " <field name=\"AD_SERVICES\" since=\"34.0\""
+ " sdks=\"30:4,31:4,33:4,34:7,35:12,36:16,0:34.0\"/>\n"
+ " </class>\n"
+ "\n"
+ " <!-- This class was introduced in Baklava. It does not exist in any SDK"
+ " extension. -->\n"
+ " <class name=\"android/os/PlatformOnly\" since=\"36.0\">\n"
+ " <extends name=\"java/lang/Object\"/>\n"
+ " <method name=\"foo(I)V\" />\n"
+ " </class>\n"
+ "</api>\n";
static class SdkExtensions {
int AD_SERVICES;
}
static class PlatformOnly {}
private static void mockAndroidJarForSampleVersion4ApiVersionsXml(Path outputPath)
throws Exception {
try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outputPath))) {
ZipUtils.writeToZipStream(
out,
"android/os/ext/SdkExtensions.class",
transformer(SdkExtensions.class)
.setClassDescriptor("Landroid/os/ext/SdkExtensions;")
.transform(),
ZipEntry.STORED);
ZipUtils.writeToZipStream(
out,
"android/os/PlatformOnly.class",
transformer(PlatformOnly.class)
.setClassDescriptor("Landroid/os/PlatformOnly;")
.transform(),
ZipEntry.STORED);
}
}
@Test
public void testApiVersionsXmlVersion4() throws Exception {
Path apiVersionsXml = temp.newFile("api-versions.xml").toPath();
FileUtils.writeTextFile(apiVersionsXml, sampleVersion4ApiVersionsXml);
Path apiLibrary = temp.newFile("android.jar").toPath();
mockAndroidJarForSampleVersion4ApiVersionsXml(apiLibrary);
List<ParsedApiClass> parsedApiClasses =
AndroidApiVersionsXmlParser.builder()
.setApiVersionsXml(apiVersionsXml)
.setAndroidJar(apiLibrary)
.setApiLevel(API_LEVEL)
.setIgnoreExemptionList(true)
.build()
.run();
assertEquals(2, parsedApiClasses.size());
assertEquals(
parsedApiClasses.get(0).getClassReference(),
Reference.classFromDescriptor("Landroid/os/ext/SdkExtensions;"));
assertEquals(parsedApiClasses.get(0).getApiLevel(), AndroidApiLevel.R);
parsedApiClasses
.get(0)
.visitMethodReferences(
(apiLevel, methods) -> {
if (apiLevel.equals(AndroidApiLevel.R)) {
assertEquals(
methods,
ImmutableList.of(
Reference.methodFromDescriptor(
"Landroid/os/ext/SdkExtensions;", "getExtensionVersion", "(I)I")));
} else if (apiLevel.equals(AndroidApiLevel.S)) {
assertEquals(
methods,
ImmutableList.of(
Reference.methodFromDescriptor(
"Landroid/os/ext/SdkExtensions;",
"getAllExtensionVersions",
"()Ljava/util/Map;")));
} else {
fail();
}
});
parsedApiClasses
.get(0)
.visitFieldReferences(
(apiLevel, fields) -> {
if (apiLevel.equals(AndroidApiLevel.U)) {
assertEquals(
fields,
ImmutableList.of(
Reference.field(
Reference.classFromDescriptor("Landroid/os/ext/SdkExtensions;"),
"AD_SERVICES",
Reference.typeFromDescriptor("I"))));
} else {
fail();
}
});
assertEquals(
parsedApiClasses.get(1).getClassReference(),
Reference.classFromDescriptor("Landroid/os/PlatformOnly;"));
assertEquals(parsedApiClasses.get(1).getApiLevel(), AndroidApiLevel.BAKLAVA);
parsedApiClasses
.get(1)
.visitMethodReferences(
(apiLevel, methods) -> {
if (apiLevel.equals(AndroidApiLevel.BAKLAVA)) {
assertEquals(
methods,
ImmutableList.of(
Reference.methodFromDescriptor(
"Landroid/os/PlatformOnly;", "foo", "(I)V")));
} else {
fail();
}
});
assertEquals(1, parsedApiClasses.get(1).getTotalMemberCount());
}
@Test
public void testDatabaseGenerationUpToDate() throws Exception {
GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
assertTrue(TestBase.filesAreEqual(result.apiLevels, API_DATABASE));
}
@Test
public void testAmendedClassesToApiDatabase() throws Exception {
Path androidJar = ToolHelper.getAndroidJar(API_LEVEL);
AppView<AppInfoWithClassHierarchy> appView =
computeAppViewWithClassHierarchy(AndroidApp.builder().addLibraryFile(androidJar).build());
AndroidApiLevelCompute androidApiLevelCompute = DefaultAndroidApiLevelCompute.create(appView);
assertTrue(androidApiLevelCompute.isEnabled());
ensureAllPublicMethodsAreMapped(appView, androidApiLevelCompute);
}
private static void ensureAllPublicMethodsAreMapped(
AppView<AppInfoWithClassHierarchy> appView, AndroidApiLevelCompute apiLevelCompute) {
Set<String> notModeledTypes = notModeledTypes();
Set<String> notModeledFields = notModeledFields();
Set<String> notModeledMethods = notModeledMethods();
for (DexLibraryClass clazz : appView.app().asDirect().libraryClasses()) {
if (notModeledTypes.contains(clazz.getClassReference().getTypeName())) {
continue;
}
assertTrue(
apiLevelCompute
.computeApiLevelForLibraryReference(clazz.getReference())
.isKnownApiLevel());
clazz.forEachClassField(
field -> {
if (field.getAccessFlags().isPublic()
&& !field.toSourceString().contains("this$0")
&& !notModeledFields.contains(field.toSourceString())) {
assertTrue(
apiLevelCompute
.computeApiLevelForLibraryReference(field.getReference())
.isKnownApiLevel());
}
});
clazz.forEachClassMethod(
method -> {
if (method.getAccessFlags().isPublic()
&& !notModeledMethods.contains(method.toSourceString())) {
assertTrue(
method.toSourceString()
+ " not found in API database. Did you forget to run main method in this"
+ " class to regenerate it?",
apiLevelCompute
.computeApiLevelForLibraryReference(method.getReference())
.isKnownApiLevel());
}
});
}
}
@Test
public void testCanLookUpAllParsedApiClassesAndMembers() throws Exception {
List<ParsedApiClass> parsedApiClasses =
AndroidApiVersionsXmlParser.builder()
.setApiVersionsXml(ToolHelper.getApiVersionsXmlFile(API_LEVEL))
.setAndroidJar(ToolHelper.getAndroidJar(API_LEVEL))
.setApiLevel(API_LEVEL)
.build()
.run();
DexItemFactory factory = new DexItemFactory();
TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
AndroidApiLevelHashingDatabaseImpl androidApiLevelDatabase =
new AndroidApiLevelHashingDatabaseImpl(
ImmutableList.of(), new InternalOptions(), diagnosticsHandler);
parsedApiClasses.forEach(
parsedApiClass -> {
DexType type = factory.createType(parsedApiClass.getClassReference().getDescriptor());
AndroidApiLevel apiLevel = androidApiLevelDatabase.getTypeApiLevel(type);
assertEquals(parsedApiClass.getApiLevel(), apiLevel);
parsedApiClass.visitMethodReferences(
(methodApiLevel, methodReferences) ->
methodReferences.forEach(
methodReference -> {
DexMethod method = factory.createMethod(methodReference);
AndroidApiLevel androidApiLevel;
if (factory.objectMembers.isObjectMember(method)) {
androidApiLevel = AndroidApiLevel.B;
} else {
androidApiLevel = androidApiLevelDatabase.getMethodApiLevel(method);
}
androidApiLevel.isLessThanOrEqualTo(methodApiLevel);
}));
parsedApiClass.visitFieldReferences(
(fieldApiLevel, fieldReferences) ->
fieldReferences.forEach(
fieldReference -> {
DexField field = factory.createField(fieldReference);
androidApiLevelDatabase
.getFieldApiLevel(field)
.isLessThanOrEqualTo(fieldApiLevel);
}));
});
diagnosticsHandler.assertNoMessages();
}
/**
* Main entry point for building a database over references in framework to the api level they
* were introduced. Running main will generate a new jar and run tests on it to ensure it is
* compatible with R8 sources and works as expected.
*
* <p>The generated jar depends on r8NoManifestWithoutDeps.
*
* <p>If the generated jar passes tests it will be moved and overwrite
* third_party/api_database/new_api_database.ser.
*/
public static void main(String[] args) throws Exception {
GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
API_DATABASE.toFile().mkdirs();
Files.move(result.apiLevels, API_DATABASE, REPLACE_EXISTING);
System.out.println(
"Updated file in: "
+ API_DATABASE
+ "\nRemember to upload to cloud storage:"
+ "\n(cd "
+ API_DATABASE_FOLDER
+ " && upload_to_google_storage.py -a --bucket r8-deps "
+ API_DATABASE_FOLDER.getFileName()
+ ")");
}
}