blob: 9bbc75117c84318af8f3ef18a575ba348e4dd958 [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 java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl;
import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.IntBox;
import com.google.common.collect.ImmutableList;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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_VERSIONS_XML =
Paths.get(ToolHelper.THIRD_PARTY_DIR, "android_jar", "api-versions", "api-versions.xml");
private static final Path API_DATABASE_HASH_LOOKUP =
Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_hash_lookup.ser");
private static final Path API_DATABASE_API_LEVEL =
Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_api_level.ser");
private static final Path API_DATABASE_AMBIGUOUS =
Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_ambiguous.txt");
private static final AndroidApiLevel API_LEVEL = AndroidApiLevel.S;
@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 indices;
private final Path apiLevels;
private final Path ambiguous;
public GenerateDatabaseResourceFilesResult(Path indices, Path apiLevels, Path ambiguous) {
this.indices = indices;
this.apiLevels = apiLevels;
this.ambiguous = ambiguous;
}
}
private static GenerateDatabaseResourceFilesResult generateResourcesFiles() throws Exception {
return generateResourcesFiles(
AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL));
}
private static GenerateDatabaseResourceFilesResult generateResourcesFiles(
List<ParsedApiClass> apiClasses) throws Exception {
TemporaryFolder temp = new TemporaryFolder();
temp.create();
Path indices = temp.newFile("indices.ser").toPath();
Path apiLevels = temp.newFile("apiLevels.ser").toPath();
Path ambiguous = temp.newFile("ambiguous.ser").toPath();
AndroidApiHashingDatabaseBuilderGenerator.generate(apiClasses, indices, apiLevels, ambiguous);
return new GenerateDatabaseResourceFilesResult(indices, apiLevels, ambiguous);
}
@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.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL);
IntBox numberOfFields = new IntBox(0);
IntBox numberOfMethods = new IntBox(0);
parsedApiClasses.forEach(
apiClass -> {
apiClass.visitFieldReferences(
((apiLevel, fieldReferences) -> {
fieldReferences.forEach(field -> numberOfFields.increment());
}));
apiClass.visitMethodReferences(
((apiLevel, methodReferences) -> {
methodReferences.forEach(field -> numberOfMethods.increment());
}));
});
// These numbers will change when updating api-versions.xml
assertEquals(5037, parsedApiClasses.size());
assertEquals(26362, numberOfFields.get());
assertEquals(40416, numberOfMethods.get());
}
@Test
public void testDatabaseGenerationUpToDate() throws Exception {
GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
TestBase.filesAreEqual(result.indices, API_DATABASE_HASH_LOOKUP);
TestBase.filesAreEqual(result.apiLevels, API_DATABASE_API_LEVEL);
TestBase.filesAreEqual(result.ambiguous, API_DATABASE_AMBIGUOUS);
}
@Test
public void testCanLookUpAllParsedApiClassesAndMembers() throws Exception {
List<ParsedApiClass> parsedApiClasses =
AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL);
DexItemFactory factory = new DexItemFactory();
AndroidApiLevelHashingDatabaseImpl androidApiLevelDatabase =
new AndroidApiLevelHashingDatabaseImpl(ImmutableList.of());
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);
}));
});
}
/**
* 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 to third_party/android_jar/api-database/
* and override the current file in there.
*/
public static void main(String[] args) throws Exception {
List<ParsedApiClass> parsedApiClasses =
AndroidApiVersionsXmlParser.getParsedApiClasses(API_VERSIONS_XML.toFile(), API_LEVEL);
GenerateDatabaseResourceFilesResult result = generateResourcesFiles(parsedApiClasses);
verifyNoDuplicateHashes(result.indices);
Files.move(result.indices, API_DATABASE_HASH_LOOKUP, REPLACE_EXISTING);
Files.move(result.apiLevels, API_DATABASE_API_LEVEL, REPLACE_EXISTING);
Files.move(result.ambiguous, API_DATABASE_AMBIGUOUS, REPLACE_EXISTING);
}
private static void verifyNoDuplicateHashes(Path indicesPath) throws Exception {
Set<Integer> elements = new HashSet<>();
int[] indices;
try (FileInputStream fileInputStream = new FileInputStream(indicesPath.toFile());
ObjectInputStream indicesObjectStream = new ObjectInputStream(fileInputStream)) {
indices = (int[]) indicesObjectStream.readObject();
for (int index : indices) {
assertTrue(elements.add(index));
}
}
assertEquals(elements.size(), indices.length);
}
}