|  | // 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.rewrite; | 
|  |  | 
|  | import static junit.framework.Assert.assertNotNull; | 
|  | import static junit.framework.TestCase.assertEquals; | 
|  | import static junit.framework.TestCase.assertNull; | 
|  | import static junit.framework.TestCase.assertTrue; | 
|  | import static org.junit.Assume.assumeTrue; | 
|  |  | 
|  | import com.android.tools.r8.CompilationFailedException; | 
|  | import com.android.tools.r8.DataEntryResource; | 
|  | import com.android.tools.r8.NeverInline; | 
|  | import com.android.tools.r8.TestBase; | 
|  | import com.android.tools.r8.TestParameters; | 
|  | import com.android.tools.r8.TestParametersCollection; | 
|  | import com.android.tools.r8.ToolHelper.DexVm.Version; | 
|  | import com.android.tools.r8.origin.Origin; | 
|  | import com.android.tools.r8.utils.StringUtils; | 
|  | import com.android.tools.r8.utils.codeinspector.ClassSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.CodeInspector; | 
|  | import com.android.tools.r8.utils.codeinspector.InstructionSubject; | 
|  | import com.android.tools.r8.utils.codeinspector.MethodSubject; | 
|  | import java.io.IOException; | 
|  | import java.nio.file.Path; | 
|  | import java.util.ServiceLoader; | 
|  | import java.util.concurrent.ExecutionException; | 
|  | import java.util.zip.ZipFile; | 
|  | import org.junit.Test; | 
|  | import org.junit.runner.RunWith; | 
|  | import org.junit.runners.Parameterized; | 
|  |  | 
|  | @RunWith(Parameterized.class) | 
|  | public class ServiceLoaderRewritingTest extends TestBase { | 
|  |  | 
|  | private final TestParameters parameters; | 
|  | private final String EXPECTED_OUTPUT = | 
|  | StringUtils.lines("Hello World!", "Hello World!", "Hello World!"); | 
|  |  | 
|  | public interface Service { | 
|  |  | 
|  | void print(); | 
|  | } | 
|  |  | 
|  | public static class ServiceImpl implements Service { | 
|  |  | 
|  | @Override | 
|  | public void print() { | 
|  | System.out.println("Hello World!"); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class MainRunner { | 
|  |  | 
|  | public static void main(String[] args) { | 
|  | ServiceLoader.load(Service.class, Service.class.getClassLoader()).iterator().next().print(); | 
|  | ServiceLoader.load(Service.class, null).iterator().next().print(); | 
|  | for (Service x : ServiceLoader.load(Service.class, Service.class.getClassLoader())) { | 
|  | x.print(); | 
|  | } | 
|  | // TODO(b/120436373) The stream API for ServiceLoader was added in Java 9. When we can model | 
|  | //   streams correctly, uncomment the lines below and adjust EXPECTED_OUTPUT. | 
|  | // ServiceLoader.load(Service.class, Service.class.getClassLoader()) | 
|  | //   .stream().forEach(x -> x.get().print()); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class OtherRunner { | 
|  |  | 
|  | public static void main(String[] args) { | 
|  | ServiceLoader.load(Service.class).iterator().next().print(); | 
|  | ServiceLoader.load(Service.class, Thread.currentThread().getContextClassLoader()) | 
|  | .iterator() | 
|  | .next() | 
|  | .print(); | 
|  | ServiceLoader.load(Service.class, OtherRunner.class.getClassLoader()) | 
|  | .iterator() | 
|  | .next() | 
|  | .print(); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class EscapingRunner { | 
|  |  | 
|  | public ServiceLoader<Service> serviceImplementations; | 
|  |  | 
|  | @NeverInline | 
|  | public ServiceLoader<Service> getServices() { | 
|  | return ServiceLoader.load(Service.class, Thread.currentThread().getContextClassLoader()); | 
|  | } | 
|  |  | 
|  | @NeverInline | 
|  | public void printServices() { | 
|  | print(ServiceLoader.load(Service.class, Thread.currentThread().getContextClassLoader())); | 
|  | } | 
|  |  | 
|  | @NeverInline | 
|  | public void print(ServiceLoader<Service> loader) { | 
|  | loader.iterator().next().print(); | 
|  | } | 
|  |  | 
|  | @NeverInline | 
|  | public void assignServicesField() { | 
|  | serviceImplementations = | 
|  | ServiceLoader.load(Service.class, Thread.currentThread().getContextClassLoader()); | 
|  | } | 
|  |  | 
|  | public static void main(String[] args) { | 
|  | EscapingRunner escapingRunner = new EscapingRunner(); | 
|  | escapingRunner.getServices().iterator().next().print(); | 
|  | escapingRunner.printServices(); | 
|  | escapingRunner.assignServicesField(); | 
|  | escapingRunner.print(escapingRunner.serviceImplementations); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Parameterized.Parameters(name = "{0}") | 
|  | public static TestParametersCollection data() { | 
|  | return getTestParameters().withAllRuntimes().build(); | 
|  | } | 
|  |  | 
|  | public ServiceLoaderRewritingTest(TestParameters parameters) { | 
|  | this.parameters = parameters; | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testRewritings() throws IOException, CompilationFailedException, ExecutionException { | 
|  | Path path = temp.newFile("out.zip").toPath(); | 
|  | testForR8(parameters.getBackend()) | 
|  | .addInnerClasses(ServiceLoaderRewritingTest.class) | 
|  | .addKeepMainRule(MainRunner.class) | 
|  | .setMinApi(parameters.getRuntime()) | 
|  | .addDataEntryResources( | 
|  | DataEntryResource.fromBytes( | 
|  | StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(), | 
|  | "META-INF/services/" + Service.class.getTypeName(), | 
|  | Origin.unknown())) | 
|  | .compile() | 
|  | .writeToZip(path) | 
|  | .run(parameters.getRuntime(), MainRunner.class) | 
|  | .assertSuccessWithOutput(EXPECTED_OUTPUT) | 
|  | .inspect( | 
|  | inspector -> { | 
|  | // Check that we have actually rewritten the calls to ServiceLoader.load. | 
|  | ClassSubject clazz = inspector.clazz(MainRunner.class); | 
|  | assertTrue(clazz.isPresent()); | 
|  | MethodSubject main = clazz.uniqueMethodWithName("main"); | 
|  | assertTrue(main.isPresent()); | 
|  | assertTrue( | 
|  | main.streamInstructions() | 
|  | .noneMatch(ServiceLoaderRewritingTest::isServiceLoaderLoad)); | 
|  | }); | 
|  |  | 
|  | // Check that we have removed the service configuration from META-INF/services. | 
|  | ZipFile zip = new ZipFile(path.toFile()); | 
|  | assertNull(zip.getEntry("META-INF/services")); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testDoNoRewrite() throws IOException, CompilationFailedException, ExecutionException { | 
|  | Path path = temp.newFile("out.zip").toPath(); | 
|  | CodeInspector inspector = | 
|  | testForR8(parameters.getBackend()) | 
|  | .addInnerClasses(ServiceLoaderRewritingTest.class) | 
|  | .addKeepMainRule(OtherRunner.class) | 
|  | .setMinApi(parameters.getRuntime()) | 
|  | .addDataEntryResources( | 
|  | DataEntryResource.fromBytes( | 
|  | StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(), | 
|  | "META-INF/services/" + Service.class.getTypeName(), | 
|  | Origin.unknown())) | 
|  | .compile() | 
|  | .writeToZip(path) | 
|  | .run(parameters.getRuntime(), OtherRunner.class) | 
|  | .assertSuccessWithOutput(EXPECTED_OUTPUT) | 
|  | .inspector(); | 
|  |  | 
|  | // Check that we have not rewritten the calls to ServiceLoader.load. | 
|  | ClassSubject clazz = inspector.clazz(OtherRunner.class); | 
|  | assertTrue(clazz.isPresent()); | 
|  | MethodSubject main = clazz.uniqueMethodWithName("main"); | 
|  | assertTrue(main.isPresent()); | 
|  | assertEquals( | 
|  | 3, | 
|  | main.streamInstructions().filter(ServiceLoaderRewritingTest::isServiceLoaderLoad).count()); | 
|  |  | 
|  | // Check that we have not removed the service configuration from META-INF/services. | 
|  | ZipFile zip = new ZipFile(path.toFile()); | 
|  | ClassSubject serviceImpl = inspector.clazz(ServiceImpl.class); | 
|  | assertTrue(serviceImpl.isPresent()); | 
|  | assertNotNull(zip.getEntry("META-INF/services/" + serviceImpl.getFinalName())); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testDoNoRewriteWhenEscaping() | 
|  | throws IOException, CompilationFailedException, ExecutionException { | 
|  | Path path = temp.newFile("out.zip").toPath(); | 
|  | CodeInspector inspector = | 
|  | testForR8(parameters.getBackend()) | 
|  | .addInnerClasses(ServiceLoaderRewritingTest.class) | 
|  | .addKeepMainRule(EscapingRunner.class) | 
|  | .enableInliningAnnotations() | 
|  | .setMinApi(parameters.getRuntime()) | 
|  | .noMinification() | 
|  | .addDataEntryResources( | 
|  | DataEntryResource.fromBytes( | 
|  | StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(), | 
|  | "META-INF/services/" + Service.class.getTypeName(), | 
|  | Origin.unknown())) | 
|  | .compile() | 
|  | .writeToZip(path) | 
|  | .run(parameters.getRuntime(), EscapingRunner.class) | 
|  | .assertSuccessWithOutput(EXPECTED_OUTPUT) | 
|  | .inspector(); | 
|  |  | 
|  | // Check that we have not rewritten the calls to ServiceLoader.load. | 
|  | ClassSubject clazz = inspector.clazz(EscapingRunner.class); | 
|  | assertTrue(clazz.isPresent()); | 
|  | int allServiceLoaders = | 
|  | clazz.allMethods().stream() | 
|  | .mapToInt( | 
|  | method -> | 
|  | (int) | 
|  | method | 
|  | .streamInstructions() | 
|  | .filter(ServiceLoaderRewritingTest::isServiceLoaderLoad) | 
|  | .count()) | 
|  | .sum(); | 
|  | assertEquals(3, allServiceLoaders); | 
|  | // Check that we have not removed the service configuration from META-INF/services. | 
|  | ZipFile zip = new ZipFile(path.toFile()); | 
|  | ClassSubject serviceImpl = inspector.clazz(ServiceImpl.class); | 
|  | assertTrue(serviceImpl.isPresent()); | 
|  | assertNotNull(zip.getEntry("META-INF/services/" + serviceImpl.getFinalName())); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testKeepAsOriginal() | 
|  | throws IOException, CompilationFailedException, ExecutionException { | 
|  | // The CL that changed behaviour after Nougat is: | 
|  | // https://android-review.googlesource.com/c/platform/libcore/+/273135 | 
|  | assumeTrue( | 
|  | parameters.getRuntime().isCf() | 
|  | || !parameters.getRuntime().asDex().getVm().getVersion().equals(Version.V7_0_0)); | 
|  | Path path = temp.newFile("out.zip").toPath(); | 
|  | CodeInspector inspector = | 
|  | testForR8(parameters.getBackend()) | 
|  | .addInnerClasses(ServiceLoaderRewritingTest.class) | 
|  | .addKeepMainRule(MainRunner.class) | 
|  | .addKeepClassRules(Service.class) | 
|  | .setMinApi(parameters.getRuntime()) | 
|  | .addDataEntryResources( | 
|  | DataEntryResource.fromBytes( | 
|  | StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(), | 
|  | "META-INF/services/" + Service.class.getTypeName(), | 
|  | Origin.unknown())) | 
|  | .compile() | 
|  | .writeToZip(path) | 
|  | .run(parameters.getRuntime(), MainRunner.class) | 
|  | .assertSuccessWithOutput(EXPECTED_OUTPUT) | 
|  | .inspector(); | 
|  |  | 
|  | // Check that we have not rewritten the calls to ServiceLoader.load. | 
|  | ClassSubject clazz = inspector.clazz(MainRunner.class); | 
|  | assertTrue(clazz.isPresent()); | 
|  | MethodSubject main = clazz.uniqueMethodWithName("main"); | 
|  | assertTrue(main.isPresent()); | 
|  | assertEquals( | 
|  | 3, | 
|  | main.streamInstructions().filter(ServiceLoaderRewritingTest::isServiceLoaderLoad).count()); | 
|  |  | 
|  | // Check that we have not removed the service configuration from META-INF/services. | 
|  | ZipFile zip = new ZipFile(path.toFile()); | 
|  | ClassSubject service = inspector.clazz(Service.class); | 
|  | assertTrue(service.isPresent()); | 
|  | assertNotNull(zip.getEntry("META-INF/services/" + service.getFinalName())); | 
|  | } | 
|  |  | 
|  | private static boolean isServiceLoaderLoad(InstructionSubject instruction) { | 
|  | return instruction.isInvokeStatic() | 
|  | && instruction.getMethod().qualifiedName().contains("ServiceLoader.load"); | 
|  | } | 
|  | } |