| // 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.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.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.references.MethodReference; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.IntBox; |
| import com.android.tools.r8.utils.InternalOptions; |
| 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 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.getParsedApiClasses( |
| ToolHelper.getApiVersionsXmlFile(API_LEVEL).toFile(), API_LEVEL), |
| 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.getParsedApiClasses( |
| ToolHelper.getApiVersionsXmlFile(API_LEVEL).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( |
| ((AndroidApiLevel apiLevel, List<MethodReference> methodReferences) -> |
| methodReferences.forEach(field -> numberOfMethods.increment()))); |
| }); |
| // These numbers will change when updating api-versions.xml |
| assertEquals(5635, parsedApiClasses.size()); |
| assertEquals(29017, numberOfFields.get()); |
| assertEquals(44107, numberOfMethods.get()); |
| } |
| |
| @Test |
| public void testDatabaseGenerationUpToDate() throws Exception { |
| GenerateDatabaseResourceFilesResult result = generateResourcesFiles(); |
| assertTrue(TestBase.filesAreEqual(result.apiLevels, API_DATABASE)); |
| } |
| |
| @Test |
| public void testCanLookUpAllParsedApiClassesAndMembers() throws Exception { |
| List<ParsedApiClass> parsedApiClasses = |
| AndroidApiVersionsXmlParser.getParsedApiClasses( |
| ToolHelper.getApiVersionsXmlFile(API_LEVEL).toFile(), API_LEVEL); |
| 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() |
| + ")"); |
| } |
| } |