Merge "Add service loader test"
diff --git a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
index e235e67..8731d2a 100644
--- a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.utils.OutputBuilder;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.IOException;
@@ -181,6 +182,12 @@
 
     public static void writeResources(Path archive, List<ProgramResource> resources)
         throws IOException, ResourceException {
+      writeResources(archive, resources, ImmutableList.of());
+    }
+
+    public static void writeResources(
+        Path archive, List<ProgramResource> resources, List<DataEntryResource> dataResources)
+        throws IOException, ResourceException {
       OpenOption[] options =
           new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
       try (Closer closer = Closer.create()) {
@@ -191,6 +198,11 @@
             byte[] bytes = ByteStreams.toByteArray(closer.register(resource.getByteStream()));
             ZipUtils.writeToZipStream(out, entryName, bytes, ZipEntry.STORED);
           }
+          for (DataEntryResource dataResource : dataResources) {
+            String entryName = dataResource.getName();
+            byte[] bytes = ByteStreams.toByteArray(closer.register(dataResource.getByteStream()));
+            ZipUtils.writeToZipStream(out, entryName, bytes, ZipEntry.STORED);
+          }
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 28d1628..369367e 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.DataEntryResource;
 import com.android.tools.r8.DataResource;
 import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DataResourceProvider.Visitor;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DirectoryClassFileProvider;
@@ -213,6 +214,29 @@
     }
   }
 
+  public List<DataEntryResource> getDataEntryResourcesForTesting() throws ResourceException {
+    List<DataEntryResource> out = new ArrayList<>();
+    for (ProgramResourceProvider programResourceProvider : getProgramResourceProviders()) {
+      DataResourceProvider dataResourceProvider = programResourceProvider.getDataResourceProvider();
+      if (dataResourceProvider != null) {
+        dataResourceProvider.accept(
+            new Visitor() {
+
+              @Override
+              public void visit(DataDirectoryResource directory) {
+                // Ignore.
+              }
+
+              @Override
+              public void visit(DataEntryResource file) {
+                out.add(file);
+              }
+            });
+      }
+    }
+    return out;
+  }
+
   /** Get program resource providers. */
   public List<ProgramResourceProvider> getProgramResourceProviders() {
     return programResourceProviders;
@@ -320,14 +344,12 @@
     }
   }
 
-  /**
-   * Write the dex program resources to @code{archive} and the proguard resource as its sibling.
-   */
+  /** Write the dex program resources to @code{archive}. */
   public void writeToZip(Path archive, OutputMode outputMode) throws IOException {
     try {
       if (outputMode == OutputMode.DexIndexed) {
-        List<ProgramResource> resources = getDexProgramResourcesForTesting();
-        DexIndexedConsumer.ArchiveConsumer.writeResources(archive, resources);
+        DexIndexedConsumer.ArchiveConsumer.writeResources(
+            archive, getDexProgramResourcesForTesting(), getDataEntryResourcesForTesting());
       } else if (outputMode == OutputMode.DexFilePerClassFile) {
         List<ProgramResource> resources = getDexProgramResourcesForTesting();
         DexFilePerClassFileConsumer.ArchiveConsumer.writeResources(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
index 4371edd..c7078ec 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppConsumers.java
@@ -6,6 +6,9 @@
 import com.android.tools.r8.BaseCompilerCommand;
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DataDirectoryResource;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.DataResourceConsumer;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DexIndexedConsumer.ForwardingConsumer;
@@ -105,6 +108,29 @@
             }
           }
 
+          @Override
+          public DataResourceConsumer getDataResourceConsumer() {
+            assert consumer.getDataResourceConsumer() == null;
+            return new DataResourceConsumer() {
+
+              @Override
+              public void accept(
+                  DataDirectoryResource directory, DiagnosticsHandler diagnosticsHandler) {
+                // Ignore.
+              }
+
+              @Override
+              public void accept(DataEntryResource file, DiagnosticsHandler diagnosticsHandler) {
+                builder.addDataResource(file);
+              }
+
+              @Override
+              public void finished(DiagnosticsHandler handler) {
+                // Ignore.
+              }
+            };
+          }
+
           synchronized void addDexFile(int fileIndex, byte[] data, Set<String> descriptors) {
             files.put(fileIndex, new DescriptorsWithContents(descriptors, data));
           }
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index 244639e..c91c288 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -141,7 +141,7 @@
     return builder.toString();
   }
 
-  public static String lines(String... lines) {
+  public static String lines(List<String> lines) {
     StringBuilder builder = new StringBuilder();
     for (String line : lines) {
       builder.append(line).append(LINE_SEPARATOR);
@@ -149,6 +149,10 @@
     return builder.toString();
   }
 
+  public static String lines(String... lines) {
+    return lines(Arrays.asList(lines));
+  }
+
   public static String withNativeLineSeparator(String s) {
     s = s.replace("\r\n", "\n");
     if (LINE_SEPARATOR.equals("\r\n")) {
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 9dfb30d..245a35e 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -131,6 +131,11 @@
   }
 
   @Override
+  public ExternalR8TestBuilder addDataEntryResources(DataEntryResource... resources) {
+    throw new Unimplemented("No support for adding data entry resources");
+  }
+
+  @Override
   public ExternalR8TestBuilder addProgramClasses(Collection<Class<?>> classes) {
     // Adding a collection of classes will build a jar of exactly those classes so that no other
     // classes are made available via a too broad classpath directory.
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 0fd0354..3b4647f 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -181,6 +181,11 @@
   }
 
   @Override
+  public ProguardTestBuilder addDataEntryResources(DataEntryResource... resources) {
+    throw new Unimplemented("No support for adding data entry resources");
+  }
+
+  @Override
   public ProguardTestBuilder addProgramClasses(Collection<Class<?>> classes) {
     // Adding a collection of classes will build a jar of exactly those classes so that no other
     // classes are made available via a too broad classpath directory.
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index daf5568..e8555d7 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -97,6 +97,11 @@
   }
 
   @Override
+  public R8TestBuilder addDataEntryResources(DataEntryResource... resources) {
+    return addDataResources(Arrays.asList(resources));
+  }
+
+  @Override
   public R8TestBuilder addKeepRuleFiles(List<Path> files) {
     builder.addProguardConfigurationFiles(files);
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 9d8e03c..a17ab09 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -46,6 +46,8 @@
     return minification(false);
   }
 
+  public abstract T addDataEntryResources(DataEntryResource... resources);
+
   public abstract T addKeepRuleFiles(List<Path> files);
 
   public T addKeepRuleFiles(Path... files) throws IOException {
@@ -62,6 +64,10 @@
     return addKeepRules("-keep class ** { *; }");
   }
 
+  public T addKeepAllInterfacesRule() {
+    return addKeepRules("-keep interface ** { *; }");
+  }
+
   public T addKeepClassRules(Class<?>... classes) {
     for (Class<?> clazz : classes) {
       addKeepRules("-keep class " + clazz.getTypeName());
diff --git a/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java
new file mode 100644
index 0000000..e005a2c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ServiceLoaderTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2019, 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;
+
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.ServiceLoader;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderTest extends TestBase {
+
+  private final boolean includeWorldGreeter;
+
+  @Parameters(name = "Include WorldGreeter: {0}")
+  public static Boolean[] data() {
+    return BooleanUtils.values();
+  }
+
+  public ServiceLoaderTest(boolean includeWorldGreeter) {
+    this.includeWorldGreeter = includeWorldGreeter;
+  }
+
+  @Test
+  public void test() throws Exception {
+    String expectedOutput = includeWorldGreeter ? "Hello world!" : "Hello";
+
+    List<String> serviceImplementations = Lists.newArrayList(HelloGreeter.class.getTypeName());
+    if (includeWorldGreeter) {
+      serviceImplementations.add(WorldGreeter.class.getTypeName());
+    }
+
+    CodeInspector inspector =
+        testForR8(Backend.DEX)
+            .addInnerClasses(ServiceLoaderTest.class)
+            .addKeepMainRule(TestClass.class)
+            // TODO(b/124181030): Test should work without the following keep-all-rules.
+            .addKeepAllClassesRule()
+            .addKeepAllInterfacesRule()
+            .addDataEntryResources(
+                DataEntryResource.fromBytes(
+                    StringUtils.lines(serviceImplementations).getBytes(),
+                    "META-INF/services/" + Greeter.class.getTypeName(),
+                    Origin.unknown()))
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expectedOutput)
+            .inspector();
+
+    // TODO(b/124181030): Verify that Greeter is merged into HelloGreeter when `includeWorldGreeter`
+    //  is false.
+
+    // TODO(b/124181030): Verify that META-INF/services/...WorldGreeter is removed when
+    //  `includeWorldGreeter` is false.
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      for (Greeter greeter : ServiceLoader.load(Greeter.class)) {
+        System.out.print(greeter.greeting());
+      }
+    }
+  }
+
+  public interface Greeter {
+
+    String greeting();
+  }
+
+  public static class HelloGreeter implements Greeter {
+
+    @Override
+    public String greeting() {
+      return "Hello";
+    }
+  }
+
+  public static class WorldGreeter implements Greeter {
+
+    @Override
+    public String greeting() {
+      return " world!";
+    }
+  }
+}