Add test of desugared library type in META-INF/services/

Test demonstrates that services file named

  META-INF/services/java.time.chrono.Chronology

is rewritten to

  META-INF/services/j$.time.chrono.Chronology

with R8 and desugared library.

For D8 with desugared library manually rewriting the services file
based on API level also works.

Bug: b/177329741

Change-Id: I39ff53c6ae2f75886c874ee0590142100921a885
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index b5f533d..d6032c7 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1747,6 +1747,20 @@
         && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
   }
 
+  public static AndroidApiLevel apiLevelWithJavaTime() {
+    return AndroidApiLevel.O;
+  }
+
+  public static boolean apiLevelWithJavaTime(TestParameters parameters) {
+    return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithJavaTime());
+  }
+
+  public static boolean runtimeWithJavaTime(TestParameters parameters) {
+    return parameters.isCfRuntime()
+        || parameters.isDexRuntimeVersionNewerThanOrEqual(
+            ToolHelper.getDexVersionForApiLevel(apiLevelWithJavaTime()));
+  }
+
   // TODO(b/131130038): Do not allow accessmodification when kept.
   public boolean isForceAccessModifyingPackagePrivateAndProtectedMethods() {
     return true;
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index ce9a7fc..d93ed8c 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -337,9 +337,9 @@
       V5_1_1("5.1.1"),
       V6_0_1("6.0.1"),
       V7_0_0("7.0.0"),
-      V8_1_0("8.1.0"),
       // TODO(b/204855476): Remove DEFAULT.
       DEFAULT("default"),
+      V8_1_0("8.1.0"),
       V9_0_0("9.0.0"),
       V10_0_0("10.0.0"),
       V12_0_0("12.0.0"),
@@ -1304,6 +1304,43 @@
     }
   }
 
+  public static DexVm.Version getDexVersionForApiLevel(AndroidApiLevel apiLevel) {
+    switch (apiLevel) {
+      case MASTER:
+        return DexVm.Version.MASTER;
+      case U:
+        return DexVm.Version.V14_0_0;
+      case T:
+        return DexVm.Version.V13_0_0;
+      case Sv2:
+      case S:
+        return DexVm.Version.V12_0_0;
+      case R:
+        throw new Unreachable("No Android 11 VM");
+      case Q:
+        return DexVm.Version.V10_0_0;
+      case P:
+        return DexVm.Version.V9_0_0;
+      case O_MR1:
+        return DexVm.Version.V8_1_0;
+      case O:
+        // Somewhere on the way to 8.0.0.
+        return DexVm.Version.DEFAULT;
+      case N:
+        return DexVm.Version.V7_0_0;
+      case M:
+        return DexVm.Version.V6_0_1;
+      case L_MR1:
+        return DexVm.Version.V5_1_1;
+      case K:
+        return DexVm.Version.V4_4_4;
+      case I_MR1:
+        return DexVm.Version.V4_0_4;
+      default:
+        throw new Unreachable("No Android VM for API level " + apiLevel.getLevel());
+    }
+  }
+
   public static DexVersion getDexFileVersionForVm(DexVm vm) {
     return DexVersion.getDexVersion(getMinApiLevelForDexVm(vm));
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
index c871859..cdc0788 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/DesugaredLibraryTestBuilder.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestCompileResult;
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.L8TestBuilder;
@@ -193,6 +194,11 @@
     return this;
   }
 
+  public DesugaredLibraryTestBuilder<T> addRunClasspathFiles(Path... files) {
+    builder.addRunClasspathFiles(files);
+    return this;
+  }
+
   /**
    * By default the compilation uses libraryDesugaringSpecification.getProgramCompilationMode()
    * which maps to the studio set-up: D8-debug, D8-release and R8-release. Use this Api to set a
@@ -203,6 +209,13 @@
     return this;
   }
 
+  private void withD8TestBuilder(Consumer<D8TestBuilder> consumer) {
+    if (!builder.isD8TestBuilder()) {
+      return;
+    }
+    consumer.accept((D8TestBuilder) builder);
+  }
+
   private void withR8TestBuilder(Consumer<R8TestBuilder<?>> consumer) {
     if (!builder.isTestShrinkerBuilder()) {
       return;
@@ -235,6 +248,11 @@
     return this;
   }
 
+  public DesugaredLibraryTestBuilder<T> applyIfD8TestBuilder(Consumer<D8TestBuilder> consumer) {
+    withD8TestBuilder(consumer);
+    return this;
+  }
+
   public DesugaredLibraryTestBuilder<T> applyIfR8TestBuilder(Consumer<R8TestBuilder<?>> consumer) {
     withR8TestBuilder(consumer);
     return this;
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
index 3f70af1..7b2688e 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
@@ -10,6 +10,7 @@
 import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.CustomConversionVersion.LEGACY;
 
 import com.android.tools.r8.L8TestBuilder;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
@@ -34,11 +35,15 @@
 
 public class LibraryDesugaringSpecification {
 
-  public static Descriptor JDK8_DESCRIPTOR = new Descriptor(24, 26, -1, 26, 24);
-  public static Descriptor JDK11_DESCRIPTOR = new Descriptor(24, 26, -1, 10000, -1);
+  public static Descriptor JDK8_DESCRIPTOR =
+      new Descriptor(24, TestBase.apiLevelWithJavaTime().getLevel(), -1, 26, 24);
+  public static Descriptor JDK11_DESCRIPTOR =
+      new Descriptor(24, TestBase.apiLevelWithJavaTime().getLevel(), -1, 10000, -1);
   public static Descriptor EMPTY_DESCRIPTOR_24 = new Descriptor(-1, -1, -1, 24, -1);
-  public static Descriptor JDK11_PATH_DESCRIPTOR = new Descriptor(24, 26, 26, 10000, -1);
-  public static Descriptor JDK11_LEGACY_DESCRIPTOR = new Descriptor(24, 26, -1, 32, 24);
+  public static Descriptor JDK11_PATH_DESCRIPTOR =
+      new Descriptor(24, TestBase.apiLevelWithJavaTime().getLevel(), 26, 10000, -1);
+  public static Descriptor JDK11_LEGACY_DESCRIPTOR =
+      new Descriptor(24, TestBase.apiLevelWithJavaTime().getLevel(), -1, 32, 24);
 
   private static class Descriptor {
 
diff --git a/src/test/java/com/android/tools/r8/shaking/serviceloader/ServiceLoaderDesugaredLibraryTest.java b/src/test/java/com/android/tools/r8/shaking/serviceloader/ServiceLoaderDesugaredLibraryTest.java
new file mode 100644
index 0000000..41aed43
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/serviceloader/ServiceLoaderDesugaredLibraryTest.java
@@ -0,0 +1,407 @@
+// Copyright (c) 2023, 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.shaking.serviceloader;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8SHRINK_TR;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.R8_L8SHRINK_TR;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.LocalTime;
+import java.time.chrono.AbstractChronology;
+import java.time.chrono.ChronoLocalDate;
+import java.time.chrono.ChronoLocalDateTime;
+import java.time.chrono.ChronoPeriod;
+import java.time.chrono.Chronology;
+import java.time.chrono.Era;
+import java.time.format.DateTimeFormatter;
+import java.time.format.TextStyle;
+import java.time.temporal.ChronoField;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalAdjuster;
+import java.time.temporal.TemporalAmount;
+import java.time.temporal.TemporalField;
+import java.time.temporal.TemporalQuery;
+import java.time.temporal.TemporalUnit;
+import java.time.temporal.ValueRange;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderDesugaredLibraryTest extends DesugaredLibraryTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+  @Parameter(2)
+  public CompilationSpecification compilationSpecification;
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        ImmutableList.of(JDK11),
+        ImmutableList.of(D8_L8SHRINK_TR, R8_L8SHRINK_TR));
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("true", "true", "1", "1");
+
+  private static final String servicesPath = "META-INF/services/" + Chronology.class.getTypeName();
+  private static final String servicesPathRewritten = servicesPath.replace("java.", "j$.");
+  private static final String servicesFile =
+      StringUtils.lines(SimpleChronology.class.getTypeName());
+
+  private void configureR8(R8TestBuilder<?> builder) {
+    // When testing R8 add the META-INF/services to the input to apply rewriting.
+    builder
+        .addDataEntryResources(
+            DataEntryResource.fromBytes(servicesFile.getBytes(), servicesPath, Origin.unknown()))
+        .addKeepClassAndMembersRulesWithAllowObfuscation(SimpleChronology.class);
+  }
+
+  private void configureD8(D8TestBuilder builder, boolean useJDollarType) {
+    // When testing D8 add a manually rewritten META-INF/services to the output.
+    try {
+      builder.addRunClasspathFiles(
+          ZipBuilder.builder(temp.newFile("services.jar").toPath())
+              .addBytes(
+                  useJDollarType ? servicesPathRewritten : servicesPath, servicesFile.getBytes())
+              .build());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Test
+  public void testWithDesugaredLibrary() throws Throwable {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .applyIfD8TestBuilder(
+            b -> configureD8(b, libraryDesugaringSpecification.hasTimeDesugaring(parameters)))
+        .applyIfR8TestBuilder(this::configureR8)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8WithoutDesugaredLibrary() throws Throwable {
+    parameters.assumeR8TestParameters();
+    assumeTrue(compilationSpecification == D8_L8SHRINK_TR);
+    testForD8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getAndroidJar(apiLevelWithJavaTime()))
+        .addInnerClasses(getClass())
+        .apply(b -> configureD8(b, false))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            runtimeWithJavaTime(parameters),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            SingleTestRunResult::assertFailure);
+  }
+
+  @Test
+  public void testR8WithoutDesugaredLibrary() throws Throwable {
+    parameters.assumeR8TestParameters();
+    assumeTrue(compilationSpecification == D8_L8SHRINK_TR);
+    testForR8(parameters.getBackend())
+        .addLibraryFiles(ToolHelper.getAndroidJar(apiLevelWithJavaTime()))
+        .addInnerClasses(getClass())
+        .apply(this::configureR8)
+        .setMinApi(parameters)
+        .addKeepMainRule(TestClass.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .applyIf(
+            runtimeWithJavaTime(parameters),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            SingleTestRunResult::assertFailure);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      Chronology chronology = Chronology.of("Simple");
+      System.out.println(chronology instanceof SimpleChronology);
+      ChronoLocalDate simpleDate = chronology.date(1, 1, 1);
+      System.out.println(simpleDate instanceof SimpleDate);
+      System.out.println(chronology.range(ChronoField.DAY_OF_MONTH).getMinimum());
+      System.out.println(chronology.range(ChronoField.DAY_OF_MONTH).getMaximum());
+    }
+  }
+
+  public static final class SimpleChronology extends AbstractChronology {
+
+    public static final SimpleChronology INSTANCE = new SimpleChronology();
+
+    public SimpleChronology() {}
+
+    @Override
+    public String getId() {
+      return "Simple";
+    }
+
+    @Override
+    public String getCalendarType() {
+      return "simple";
+    }
+
+    @Override
+    public SimpleDate date(int prolepticYear, int month, int dayOfMonth) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public SimpleDate dateYearDay(int prolepticYear, int dayOfYear) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public SimpleDate dateEpochDay(long epochDay) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public SimpleDate date(TemporalAccessor dateTime) {
+      if (dateTime instanceof SimpleDate) {
+        return (SimpleDate) dateTime;
+      }
+      return new SimpleDate();
+    }
+
+    @Override
+    public boolean isLeapYear(long prolepticYear) {
+      return false;
+    }
+
+    @Override
+    public int prolepticYear(Era era, int yearOfEra) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Era eraOf(int eraValue) {
+      return SimpleEra.ERA;
+    }
+
+    @Override
+    public List<Era> eras() {
+      return Arrays.asList(SimpleEra.values());
+    }
+
+    @Override
+    public ValueRange range(ChronoField field) {
+      return ValueRange.of(1, 1);
+    }
+  }
+
+  public static class SimpleDate implements ChronoLocalDate, Serializable {
+
+    public SimpleDate() {}
+
+    @Override
+    public Chronology getChronology() {
+      return null;
+    }
+
+    @Override
+    public Era getEra() {
+      return SimpleEra.ERA;
+    }
+
+    @Override
+    public boolean isLeapYear() {
+      return false;
+    }
+
+    @Override
+    public int lengthOfMonth() {
+      return 0;
+    }
+
+    @Override
+    public int lengthOfYear() {
+      return 0;
+    }
+
+    @Override
+    public boolean isSupported(TemporalField field) {
+      return true;
+    }
+
+    @Override
+    public ValueRange range(TemporalField field) {
+      return ValueRange.of(1, 1);
+    }
+
+    @Override
+    public int get(TemporalField field) {
+      return 1;
+    }
+
+    @Override
+    public long getLong(TemporalField field) {
+      return 1;
+    }
+
+    @Override
+    public boolean isSupported(TemporalUnit unit) {
+      return true;
+    }
+
+    @Override
+    public ChronoLocalDate with(TemporalAdjuster adjuster) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public ChronoLocalDate with(TemporalField field, long newValue) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public ChronoLocalDate plus(TemporalAmount amount) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public ChronoLocalDate plus(long amountToAdd, TemporalUnit unit) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public ChronoLocalDate minus(TemporalAmount amount) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public ChronoLocalDate minus(long amountToSubtract, TemporalUnit unit) {
+      return new SimpleDate();
+    }
+
+    @Override
+    public <R> R query(TemporalQuery<R> query) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Temporal adjustInto(Temporal temporal) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long until(Temporal endExclusive, TemporalUnit unit) {
+      return 0;
+    }
+
+    @Override
+    public ChronoPeriod until(ChronoLocalDate endDateExclusive) {
+      return null;
+    }
+
+    @Override
+    public String format(DateTimeFormatter formatter) {
+      return "1";
+    }
+
+    @Override
+    public ChronoLocalDateTime<?> atTime(LocalTime localTime) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long toEpochDay() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int compareTo(ChronoLocalDate other) {
+      return 0;
+    }
+
+    @Override
+    public boolean isAfter(ChronoLocalDate other) {
+      return false;
+    }
+
+    @Override
+    public boolean isBefore(ChronoLocalDate other) {
+      return false;
+    }
+
+    @Override
+    public boolean isEqual(ChronoLocalDate other) {
+      return true;
+    }
+  }
+
+  public enum SimpleEra implements Era {
+    ERA;
+
+    @Override
+    public int getValue() {
+      return 0;
+    }
+
+    @Override
+    public boolean isSupported(TemporalField field) {
+      return Era.super.isSupported(field);
+    }
+
+    @Override
+    public ValueRange range(TemporalField field) {
+      return ValueRange.of(1, 1);
+    }
+
+    @Override
+    public int get(TemporalField field) {
+      return 1;
+    }
+
+    @Override
+    public long getLong(TemporalField field) {
+      return 1;
+    }
+
+    @Override
+    public <R> R query(TemporalQuery<R> query) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Temporal adjustInto(Temporal temporal) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getDisplayName(TextStyle style, Locale locale) {
+      return "1";
+    }
+  }
+}