| // Copyright (c) 2022, 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.desugar.desugaredlibrary; |
| |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage; |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin; |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType; |
| import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.RELEASED_1_1_5; |
| import static org.hamcrest.CoreMatchers.allOf; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.fail; |
| |
| import com.android.tools.r8.DiagnosticsHandler; |
| import com.android.tools.r8.StringResource; |
| import com.android.tools.r8.TestDiagnosticMessages; |
| import com.android.tools.r8.TestDiagnosticMessagesImpl; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification; |
| import com.android.tools.r8.errors.UnsupportedDesugaredLibraryConfigurationVersionDiagnostic; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.LegacyDesugaredLibrarySpecification; |
| import com.android.tools.r8.ir.desugar.desugaredlibrary.legacyspecification.LegacyDesugaredLibrarySpecificationParser; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.utils.AbortException; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.ListUtils; |
| import com.android.tools.r8.utils.Reporter; |
| import com.android.tools.r8.utils.SemanticVersion; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.android.tools.r8.utils.StringUtils.BraceType; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class LegacyDesugaredLibraryConfigurationParsingTest extends DesugaredLibraryTestBase { |
| |
| private final LibraryDesugaringSpecification libraryDesugaringSpecification; |
| |
| @Parameterized.Parameters(name = "{0}, spec: {1}") |
| public static List<Object[]> data() { |
| return buildParameters( |
| getTestParameters().withNoneRuntime().build(), ImmutableList.of(RELEASED_1_1_5)); |
| } |
| |
| public LegacyDesugaredLibraryConfigurationParsingTest( |
| TestParameters parameters, LibraryDesugaringSpecification libraryDesugaringSpecification) { |
| parameters.assertNoneRuntime(); |
| this.libraryDesugaringSpecification = libraryDesugaringSpecification; |
| } |
| |
| final AndroidApiLevel minApi = AndroidApiLevel.B; |
| final boolean libraryCompilation = true; |
| |
| final DexItemFactory factory = new DexItemFactory(); |
| final Origin origin = |
| new Origin(Origin.root()) { |
| @Override |
| public String part() { |
| return "Test Origin"; |
| } |
| }; |
| |
| final Map<String, Object> TEMPLATE = |
| ImmutableMap.<String, Object>builder() |
| .put( |
| "configuration_format_version", |
| LegacyDesugaredLibrarySpecificationParser.MAX_SUPPORTED_VERSION) |
| .put("group_id", "com.tools.android") |
| .put("artifact_id", "desugar_jdk_libs") |
| .put( |
| "version", LegacyDesugaredLibrarySpecificationParser.MIN_SUPPORTED_VERSION.toString()) |
| .put("required_compilation_api_level", 1) |
| .put("synthesized_library_classes_package_prefix", "j$.") |
| .put("common_flags", Collections.emptyList()) |
| .put("program_flags", Collections.emptyList()) |
| .put("library_flags", Collections.emptyList()) |
| .build(); |
| |
| private LinkedHashMap<String, Object> template() { |
| return new LinkedHashMap<>(TEMPLATE); |
| } |
| |
| private LegacyDesugaredLibrarySpecificationParser parser(DiagnosticsHandler handler) { |
| return new LegacyDesugaredLibrarySpecificationParser( |
| factory, new Reporter(handler), libraryCompilation, minApi.getLevel()); |
| } |
| |
| private LegacyDesugaredLibrarySpecification runPassing(String resource) { |
| return runPassing(StringResource.fromString(resource, origin)); |
| } |
| |
| private LegacyDesugaredLibrarySpecification runPassing(StringResource resource) { |
| TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl(); |
| LegacyDesugaredLibrarySpecification spec = parser(handler).parse(resource); |
| handler.assertNoMessages(); |
| return spec; |
| } |
| |
| private void runFailing(String json, Consumer<TestDiagnosticMessages> checker) { |
| TestDiagnosticMessagesImpl handler = new TestDiagnosticMessagesImpl(); |
| try { |
| parser(handler).parse(StringResource.fromString(json, origin)); |
| fail("Expected failure"); |
| } catch (AbortException e) { |
| checker.accept(handler); |
| } |
| } |
| |
| @Test |
| public void testReference() throws Exception { |
| // Just test that the reference file parses without issues. |
| LegacyDesugaredLibrarySpecification spec = |
| runPassing(StringResource.fromFile(libraryDesugaringSpecification.getSpecification())); |
| assertEquals(libraryCompilation, spec.isLibraryCompilation()); |
| } |
| |
| @Test |
| public void testEmpty() { |
| runFailing( |
| "", |
| diagnostics -> { |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticMessage(containsString("Not a JSON Object")), |
| diagnosticOrigin(origin))); |
| }); |
| } |
| |
| @Test |
| public void testRequiredKeys() { |
| ImmutableList<String> requiredKeys = |
| ImmutableList.of( |
| "configuration_format_version", |
| "group_id", |
| "artifact_id", |
| "version", |
| "required_compilation_api_level", |
| "synthesized_library_classes_package_prefix", |
| "common_flags", |
| "program_flags", |
| "library_flags"); |
| for (String key : requiredKeys) { |
| Map<String, Object> data = template(); |
| data.remove(key); |
| runFailing( |
| toJson(data), |
| diagnostics -> |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticMessage(containsString("Invalid desugared library configuration")), |
| diagnosticMessage(containsString("Expected required key '" + key + "'")), |
| diagnosticOrigin(origin)))); |
| } |
| } |
| |
| @Test |
| public void testUnsupportedVersion() { |
| LinkedHashMap<String, Object> data = template(); |
| SemanticVersion minVersion = LegacyDesugaredLibrarySpecificationParser.MIN_SUPPORTED_VERSION; |
| data.put( |
| "version", |
| SemanticVersion.create( |
| minVersion.getMajor(), minVersion.getMinor(), minVersion.getPatch() - 1) |
| .toString()); |
| runFailing( |
| toJson(data), |
| diagnostics -> |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticMessage(containsString("upgrade the desugared library")), |
| diagnosticOrigin(origin)))); |
| } |
| |
| @Test |
| public void testUnsupportedAbove() { |
| LinkedHashMap<String, Object> data = template(); |
| data.put("configuration_format_version", 99); |
| runFailing( |
| toJson(data), |
| diagnostics -> |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticType(UnsupportedDesugaredLibraryConfigurationVersionDiagnostic.class), |
| diagnosticMessage(containsString("upgrade the D8/R8 compiler")), |
| diagnosticMessage( |
| containsString( |
| "https://developer.android.com/studio/build/library-desugaring-versions")), |
| diagnosticMessage( |
| containsString( |
| "https://developer.android.com/studio/build/library-desugaring")), |
| diagnosticOrigin(origin)))); |
| } |
| |
| @Test |
| public void testCustomAndWrapperOverlap() { |
| LinkedHashMap<String, Object> data = template(); |
| data.put( |
| "common_flags", |
| ImmutableList.of( |
| ImmutableMap.of( |
| "api_level_below_or_equal", |
| 100000, |
| "custom_conversion", |
| ImmutableMap.of("java.util.Foo", "j$.util.FooConv"), |
| "wrapper_conversion", |
| ImmutableList.of("java.util.Foo")))); |
| runFailing( |
| toJson(data), |
| diagnostics -> |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticMessage(containsString("Duplicate types")), |
| diagnosticMessage(containsString("java.util.Foo")), |
| diagnosticOrigin(origin)))); |
| } |
| |
| @Test |
| public void testRedefinition() { |
| LinkedHashMap<String, Object> data = template(); |
| data.put( |
| "common_flags", |
| ImmutableList.of( |
| ImmutableMap.of( |
| "api_level_below_or_equal", |
| 100000, |
| "custom_conversion", |
| ImmutableMap.of("java.util.Foo", "j$.util.FooConv1")))); |
| data.put( |
| "library_flags", |
| ImmutableList.of( |
| ImmutableMap.of( |
| "api_level_below_or_equal", |
| 100000, |
| "custom_conversion", |
| ImmutableMap.of("java.util.Foo", "j$.util.FooConv2")))); |
| runFailing( |
| toJson(data), |
| diagnostics -> |
| diagnostics.assertErrorsMatch( |
| allOf( |
| diagnosticMessage(containsString("Duplicate assignment of key")), |
| diagnosticMessage(containsString("java.util.Foo")), |
| diagnosticMessage(containsString("custom_conversion")), |
| diagnosticOrigin(origin)))); |
| } |
| |
| @Test |
| public void testDuplicate() { |
| LinkedHashMap<String, Object> data = template(); |
| data.put( |
| "common_flags", |
| ImmutableList.of( |
| ImmutableMap.of( |
| "api_level_below_or_equal", |
| 100000, |
| "custom_conversion", |
| ImmutableMap.of( |
| "java.util.Foo", "j$.util.FooConv1", |
| "java.util.Foo2", "j$.util.FooConv2")))); |
| // The gson parser will overwrite the key in order during parsing, thus hiding potential issues. |
| LegacyDesugaredLibrarySpecification spec = runPassing(toJson(data).replace("Foo2", "Foo")); |
| assertEquals( |
| Collections.singletonList("java.util.Foo"), |
| spec.getCustomConversions().keySet().stream() |
| .map(DexType::toString) |
| .collect(Collectors.toList())); |
| assertEquals( |
| Collections.singletonList("j$.util.FooConv2"), |
| spec.getCustomConversions().values().stream() |
| .map(DexType::toString) |
| .collect(Collectors.toList())); |
| } |
| |
| // JSON building helpers. |
| // This does not use gson to make the text input construction independent of gson. |
| |
| private static String toJson(Map<String, Object> data) { |
| StringBuilder builder = new StringBuilder(); |
| toJsonObject(data, builder); |
| return builder.toString(); |
| } |
| |
| private static void toJsonObject(Map<String, Object> data, StringBuilder builder) { |
| StringUtils.append( |
| builder, |
| ListUtils.map( |
| data.entrySet(), |
| entry -> "\n " + quote(entry.getKey()) + ": " + toJsonElement(entry.getValue())), |
| ", ", |
| BraceType.TUBORG); |
| } |
| |
| private static String toJsonElement(Object element) { |
| StringBuilder builder = new StringBuilder(); |
| toJsonElement(element, builder); |
| return builder.toString(); |
| } |
| |
| private static void toJsonElement(Object element, StringBuilder builder) { |
| if (element instanceof String) { |
| builder.append(quote((String) element)); |
| } else if (element instanceof Integer) { |
| builder.append(element); |
| } else if (element instanceof List) { |
| @SuppressWarnings("unchecked") |
| List<Object> elements = (List<Object>) element; |
| toJsonList(elements, builder); |
| } else if (element instanceof Map) { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> elements = (Map<String, Object>) element; |
| toJsonObject(elements, builder); |
| } else { |
| throw new IllegalStateException("Unexpected object type: " + element.getClass()); |
| } |
| } |
| |
| private static void toJsonList(List<Object> element, StringBuilder builder) { |
| StringUtils.append( |
| builder, ListUtils.map(element, o -> "\n " + toJsonElement(o)), ", ", BraceType.SQUARE); |
| } |
| |
| private static String quote(String str) { |
| return "\"" + str + "\""; |
| } |
| } |