Add basic Dagger 2 tests

Change-Id: I8e779f512a5a8a662f921e6fa30d7fa345b212b6
diff --git a/src/test/examplesDagger/basic/I1.java b/src/test/examplesDagger/basic/I1.java
new file mode 100644
index 0000000..cd46afa
--- /dev/null
+++ b/src/test/examplesDagger/basic/I1.java
@@ -0,0 +1,8 @@
+// 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 basic;
+
+interface I1 {
+  String getName();
+}
diff --git a/src/test/examplesDagger/basic/I1Impl1.java b/src/test/examplesDagger/basic/I1Impl1.java
new file mode 100644
index 0000000..635a35f
--- /dev/null
+++ b/src/test/examplesDagger/basic/I1Impl1.java
@@ -0,0 +1,17 @@
+// 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 basic;
+
+import javax.inject.Inject;
+
+// @Singleton (added by transformer in some tests)
+public class I1Impl1 implements I1 {
+
+  @Inject
+  public I1Impl1() {}
+
+  public String getName() {
+    return "I1Impl1";
+  }
+}
diff --git a/src/test/examplesDagger/basic/I1Impl2.java b/src/test/examplesDagger/basic/I1Impl2.java
new file mode 100644
index 0000000..ed33281
--- /dev/null
+++ b/src/test/examplesDagger/basic/I1Impl2.java
@@ -0,0 +1,13 @@
+// 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 basic;
+
+public class I1Impl2 implements I1 {
+
+  public I1Impl2() {}
+
+  public String getName() {
+    return "I1Impl2";
+  }
+}
diff --git a/src/test/examplesDagger/basic/I2.java b/src/test/examplesDagger/basic/I2.java
new file mode 100644
index 0000000..51948ee
--- /dev/null
+++ b/src/test/examplesDagger/basic/I2.java
@@ -0,0 +1,8 @@
+// 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 basic;
+
+interface I2 {
+  String getName();
+}
diff --git a/src/test/examplesDagger/basic/I2Impl1.java b/src/test/examplesDagger/basic/I2Impl1.java
new file mode 100644
index 0000000..13f8b9a
--- /dev/null
+++ b/src/test/examplesDagger/basic/I2Impl1.java
@@ -0,0 +1,17 @@
+// 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 basic;
+
+import javax.inject.Inject;
+
+// @Singleton (added by transformer in some tests)
+public class I2Impl1 implements I2 {
+
+  @Inject
+  public I2Impl1() {}
+
+  public String getName() {
+    return "I2Impl1";
+  }
+}
diff --git a/src/test/examplesDagger/basic/I2Impl2.java b/src/test/examplesDagger/basic/I2Impl2.java
new file mode 100644
index 0000000..e1a5da7
--- /dev/null
+++ b/src/test/examplesDagger/basic/I2Impl2.java
@@ -0,0 +1,13 @@
+// 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 basic;
+
+public class I2Impl2 implements I2 {
+
+  public I2Impl2() {}
+
+  public String getName() {
+    return "I2Impl2";
+  }
+}
diff --git a/src/test/examplesDagger/basic/I3.java b/src/test/examplesDagger/basic/I3.java
new file mode 100644
index 0000000..154a47b
--- /dev/null
+++ b/src/test/examplesDagger/basic/I3.java
@@ -0,0 +1,8 @@
+// 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 basic;
+
+interface I3 {
+  String getName();
+}
diff --git a/src/test/examplesDagger/basic/I3Impl1.java b/src/test/examplesDagger/basic/I3Impl1.java
new file mode 100644
index 0000000..560113b
--- /dev/null
+++ b/src/test/examplesDagger/basic/I3Impl1.java
@@ -0,0 +1,17 @@
+// 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 basic;
+
+import javax.inject.Inject;
+
+// @Singleton (added by transformer in some tests)
+public class I3Impl1 implements I3 {
+
+  @Inject
+  public I3Impl1() {}
+
+  public String getName() {
+    return "I3Impl1";
+  }
+}
diff --git a/src/test/examplesDagger/basic/I3Impl2.java b/src/test/examplesDagger/basic/I3Impl2.java
new file mode 100644
index 0000000..99bdede
--- /dev/null
+++ b/src/test/examplesDagger/basic/I3Impl2.java
@@ -0,0 +1,13 @@
+// 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 basic;
+
+public class I3Impl2 implements I3 {
+
+  public I3Impl2() {}
+
+  public String getName() {
+    return "I3Impl2";
+  }
+}
diff --git a/src/test/examplesDagger/basic/MainBase.java b/src/test/examplesDagger/basic/MainBase.java
new file mode 100644
index 0000000..d1ad6f3
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainBase.java
@@ -0,0 +1,18 @@
+// 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 basic;
+
+public class MainBase {
+  public void test(MainComponent mainComponent) {
+    I1 i1 = mainComponent.i1();
+    I2 i2 = mainComponent.i2();
+    I3 i3 = mainComponent.i3();
+    System.out.println(i1 == mainComponent.i1());
+    System.out.println(i2 == mainComponent.i2());
+    System.out.println(i3 == mainComponent.i3());
+    System.out.println(i1.getName());
+    System.out.println(i2.getName());
+    System.out.println(i3.getName());
+  }
+}
diff --git a/src/test/examplesDagger/basic/MainComponent.java b/src/test/examplesDagger/basic/MainComponent.java
new file mode 100644
index 0000000..0a6e1f6
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainComponent.java
@@ -0,0 +1,12 @@
+// 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 basic;
+
+interface MainComponent {
+  I1 i1();
+
+  I2 i2();
+
+  I3 i3();
+}
diff --git a/src/test/examplesDagger/basic/MainComponentUsingBinds.java b/src/test/examplesDagger/basic/MainComponentUsingBinds.java
new file mode 100644
index 0000000..41896b0
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainComponentUsingBinds.java
@@ -0,0 +1,11 @@
+// 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 basic;
+
+import dagger.Component;
+import javax.inject.Singleton;
+
+@Component(modules = ModuleUsingBinds.class)
+@Singleton
+interface MainComponentUsingBinds extends MainComponent {}
diff --git a/src/test/examplesDagger/basic/MainComponentUsingProvides.java b/src/test/examplesDagger/basic/MainComponentUsingProvides.java
new file mode 100644
index 0000000..c83fe30
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainComponentUsingProvides.java
@@ -0,0 +1,11 @@
+// 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 basic;
+
+import dagger.Component;
+import javax.inject.Singleton;
+
+@Component(modules = ModuleUsingProvides.class)
+@Singleton
+interface MainComponentUsingProvides extends MainComponent {}
diff --git a/src/test/examplesDagger/basic/MainUsingBinds.java b/src/test/examplesDagger/basic/MainUsingBinds.java
new file mode 100644
index 0000000..4470917
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainUsingBinds.java
@@ -0,0 +1,10 @@
+// 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 basic;
+
+public class MainUsingBinds extends MainBase {
+  public static void main(String[] args) {
+    new MainUsingBinds().test(DaggerMainComponentUsingBinds.create());
+  }
+}
diff --git a/src/test/examplesDagger/basic/MainUsingProvides.java b/src/test/examplesDagger/basic/MainUsingProvides.java
new file mode 100644
index 0000000..5ebe417
--- /dev/null
+++ b/src/test/examplesDagger/basic/MainUsingProvides.java
@@ -0,0 +1,10 @@
+// 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 basic;
+
+public class MainUsingProvides extends MainBase {
+  public static void main(String[] args) {
+    new MainUsingProvides().test(DaggerMainComponentUsingProvides.create());
+  }
+}
diff --git a/src/test/examplesDagger/basic/ModuleUsingBinds.java b/src/test/examplesDagger/basic/ModuleUsingBinds.java
new file mode 100644
index 0000000..6b8df40
--- /dev/null
+++ b/src/test/examplesDagger/basic/ModuleUsingBinds.java
@@ -0,0 +1,19 @@
+// 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 basic;
+
+import dagger.Binds;
+import dagger.Module;
+
+@Module
+interface ModuleUsingBinds {
+  @Binds
+  I1 i1(I1Impl1 i1);
+
+  @Binds
+  I2 i2(I2Impl1 i2);
+
+  @Binds
+  I3 i3(I3Impl1 i3);
+}
diff --git a/src/test/examplesDagger/basic/ModuleUsingProvides.java b/src/test/examplesDagger/basic/ModuleUsingProvides.java
new file mode 100644
index 0000000..4c3048b
--- /dev/null
+++ b/src/test/examplesDagger/basic/ModuleUsingProvides.java
@@ -0,0 +1,28 @@
+// 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 basic;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+class ModuleUsingProvides {
+  @Provides
+  // @Singleton (added by transformer in some tests)
+  public static I1 i1() {
+    return new I1Impl2();
+  }
+
+  @Provides
+  // @Singleton (added by transformer in some tests)
+  public static I2 i2() {
+    return new I2Impl2();
+  }
+
+  @Provides
+  // @Singleton (added by transformer in some tests)
+  public static I3 i3() {
+    return new I3Impl2();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JavaCompilerTool.java b/src/test/java/com/android/tools/r8/JavaCompilerTool.java
index c7f7652..1021d65 100644
--- a/src/test/java/com/android/tools/r8/JavaCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/JavaCompilerTool.java
@@ -26,8 +26,11 @@
   private final CfRuntime jdk;
   private final TestState state;
   private final List<Path> sources = new ArrayList<>();
+  private final List<String> classNames = new ArrayList<>();
   private final List<Path> classpath = new ArrayList<>();
   private final List<String> options = new ArrayList<>();
+  private final List<Path> annotationProcessorPath = new ArrayList<>();
+  private final List<String> annotationProcessors = new ArrayList<>();
   private Path output = null;
 
   private JavaCompilerTool(CfRuntime jdk, TestState state) {
@@ -63,6 +66,11 @@
     return this;
   }
 
+  public JavaCompilerTool addClassNames(Collection<String> classNames) {
+    this.classNames.addAll(classNames);
+    return this;
+  }
+
   public JavaCompilerTool addClasspathFiles(Path... files) {
     return addClasspathFiles(Arrays.asList(files));
   }
@@ -72,6 +80,24 @@
     return this;
   }
 
+  public JavaCompilerTool addAnnotationProcessorPathFiles(Path... files) {
+    return addAnnotationProcessorPathFiles(Arrays.asList(files));
+  }
+
+  public JavaCompilerTool addAnnotationProcessorPathFiles(Collection<Path> files) {
+    annotationProcessorPath.addAll(files);
+    return this;
+  }
+
+  public JavaCompilerTool addAnnotationProcessors(String... processors) {
+    return addAnnotationProcessor(Arrays.asList(processors));
+  }
+
+  public JavaCompilerTool addAnnotationProcessor(Collection<String> processors) {
+    annotationProcessors.addAll(processors);
+    return this;
+  }
+
   /** Set the output. Must be to an existing directory or to an non-existing jar file. */
   public JavaCompilerTool setOutputPath(Path file) {
     assert (Files.exists(file) && Files.isDirectory(file))
@@ -103,6 +129,8 @@
     return output;
   }
 
+  // For javac command line options see
+  // https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html.
   private ProcessResult compileInternal(Path output) throws IOException {
     Path outdir = Files.isDirectory(output) ? output : state.getNewTempFolder();
     List<String> cmdline = new ArrayList<>();
@@ -115,11 +143,23 @@
               .map(Path::toString)
               .collect(Collectors.joining(isWindows() ? ";" : ":")));
     }
+    if (!annotationProcessorPath.isEmpty()) {
+      cmdline.add("-processorpath");
+      cmdline.add(
+          annotationProcessorPath.stream()
+              .map(Path::toString)
+              .collect(Collectors.joining(isWindows() ? ";" : ":")));
+    }
+    if (!annotationProcessors.isEmpty()) {
+      cmdline.add("-processor");
+      cmdline.add(String.join(",", annotationProcessors));
+    }
     cmdline.add("-d");
     cmdline.add(outdir.toString());
     for (Path source : sources) {
       cmdline.add(source.toString());
     }
+    cmdline.addAll(classNames);
     ProcessBuilder builder = new ProcessBuilder(cmdline);
     ProcessResult javacResult = ToolHelper.runProcess(builder);
     if (FileUtils.isJarFile(output)) {
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java
new file mode 100644
index 0000000..c6ca2b1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingBindsTest.java
@@ -0,0 +1,80 @@
+// 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.dagger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.DaggerUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.BeforeClass;
+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 DaggerBasicNotSingletonUsingBindsTest extends DaggerBasicTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimes().withAllApiLevels().build());
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    compileWithoutSingleton();
+  }
+
+  public static final String MAIN_CLASS = "basic.MainUsingBinds";
+  public static final List<String> EXPECTED_OUTPUT =
+      ImmutableList.of("false", "false", "false", "I1Impl1", "I2Impl1", "I3Impl1");
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    assertEquals(parameters.isCfRuntime() ? 1 : 2, inspector.allClasses().size());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingProvidesTest.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingProvidesTest.java
new file mode 100644
index 0000000..149b665
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicNotSingletonUsingProvidesTest.java
@@ -0,0 +1,80 @@
+// 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.dagger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.DaggerUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.BeforeClass;
+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 DaggerBasicNotSingletonUsingProvidesTest extends DaggerBasicTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimes().withAllApiLevels().build());
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    compileWithoutSingleton();
+  }
+
+  public static final String MAIN_CLASS = "basic.MainUsingProvides";
+  public static final List<String> EXPECTED_OUTPUT =
+      ImmutableList.of("false", "false", "false", "I1Impl2", "I2Impl2", "I3Impl2");
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    assertEquals(1, inspector.allClasses().size());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingBindsTest.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingBindsTest.java
new file mode 100644
index 0000000..50e0493
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingBindsTest.java
@@ -0,0 +1,108 @@
+// 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.dagger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.DaggerUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.BeforeClass;
+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 DaggerBasicSingletonUsingBindsTest extends DaggerBasicTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimes().withAllApiLevels().build());
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    DaggerBasicTestBase.compileWithSingleton();
+  }
+
+  public static final String MAIN_CLASS = "basic.MainUsingBinds";
+  public static final List<String> EXPECTED_OUTPUT =
+      ImmutableList.of("true", "true", "true", "I1Impl1", "I2Impl1", "I3Impl1");
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    assertEquals(
+        ImmutableSet.of(
+            "basic.I1Impl1",
+            "basic.I2Impl1",
+            "basic.I3Impl1",
+            "basic.MainUsingBinds",
+            "basic.DaggerMainComponentUsingBinds",
+            "dagger.internal.DoubleCheck",
+            "javax.inject.Provider"),
+        inspector.allClasses().stream()
+            .map(FoundClassSubject::getOriginalName)
+            .filter(name -> !name.contains("_Factory"))
+            .collect(Collectors.toSet()));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(
+                      "basic.I1Impl1_Factory", "basic.I2Impl1_Factory", "basic.I3Impl1_Factory")
+                  .assertIsCompleteMergeGroup(
+                      "basic.I1Impl1_Factory$InstanceHolder",
+                      "basic.I2Impl1_Factory$InstanceHolder",
+                      "basic.I3Impl1_Factory$InstanceHolder")
+                  .assertNoOtherClassesMerged();
+            })
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype("basic.I1", "basic.I2", "basic.I3"))
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingProvidesTest.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingProvidesTest.java
new file mode 100644
index 0000000..dfe6a1a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicSingletonUsingProvidesTest.java
@@ -0,0 +1,112 @@
+// 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.dagger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.DaggerUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.BeforeClass;
+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 DaggerBasicSingletonUsingProvidesTest extends DaggerBasicTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimes().withAllApiLevels().build());
+  }
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    DaggerBasicTestBase.compileWithSingleton();
+  }
+
+  public static final String MAIN_CLASS = "basic.MainUsingProvides";
+  public static final List<String> EXPECTED_OUTPUT =
+      ImmutableList.of("true", "true", "true", "I1Impl2", "I2Impl2", "I3Impl2");
+
+  private void inspect(CodeInspector inspector) {
+    assertEquals(
+        ImmutableSet.of(
+            "basic.I1Impl2",
+            "basic.I2Impl2",
+            "basic.I3Impl2",
+            "basic.MainUsingProvides",
+            "basic.DaggerMainComponentUsingProvides",
+            "dagger.internal.DoubleCheck",
+            "javax.inject.Provider"),
+        inspector.allClasses().stream()
+            .map(FoundClassSubject::getOriginalName)
+            .filter(name -> !name.contains("Factory"))
+            .collect(Collectors.toSet()));
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramNotDependingOnDagger)
+        .addProgramFiles(DaggerBasicTestBase.compiledProgramDependingOnDagger)
+        .addProgramFiles(DaggerUtils.getDaggerRuntime())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(
+                      "basic.ModuleUsingProvides_I1Factory",
+                      "basic.ModuleUsingProvides_I2Factory",
+                      "basic.ModuleUsingProvides_I3Factory")
+                  .assertIsCompleteMergeGroup(
+                      "basic.ModuleUsingProvides_I1Factory$InstanceHolder",
+                      "basic.ModuleUsingProvides_I2Factory$InstanceHolder",
+                      "basic.ModuleUsingProvides_I3Factory$InstanceHolder")
+                  .assertIsCompleteMergeGroup(
+                      "basic.ModuleUsingProvides", "basic.DaggerMainComponentUsingProvides$1")
+                  .assertNoOtherClassesMerged();
+            })
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype("basic.I1", "basic.I2", "basic.I3"))
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dagger/DaggerBasicTestBase.java b/src/test/java/com/android/tools/r8/dagger/DaggerBasicTestBase.java
new file mode 100644
index 0000000..3f56ab1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dagger/DaggerBasicTestBase.java
@@ -0,0 +1,141 @@
+// 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.dagger;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.transformers.MethodTransformer;
+import com.android.tools.r8.utils.DaggerUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class DaggerBasicTestBase extends TestBase {
+
+  public static Path compiledProgramNotDependingOnDagger;
+  public static Path compiledProgramDependingOnDagger;
+  public static Path daggerGeneratedSourceFiles;
+
+  private static void compile(
+      List<Path> sourceNotDependingOnDagger,
+      List<Path> sourceDependingOnDagger,
+      BiFunction<String, byte[], byte[]> transformer)
+      throws Exception {
+    compiledProgramNotDependingOnDagger =
+        DaggerUtils.compileWithoutAnnotationProcessing(sourceNotDependingOnDagger);
+    // Use transformer on code not depending on Dagger.
+    if (transformer != null) {
+      compiledProgramNotDependingOnDagger =
+          ZipUtils.map(
+              compiledProgramNotDependingOnDagger,
+              getStaticTemp().newFolder().toPath().resolve("transformed.jar"),
+              (entry, bytes) ->
+                  FileUtils.isClassFile(entry.getName())
+                      ? transformer.apply(
+                          entry
+                              .getName()
+                              .substring(
+                                  0, entry.getName().length() - FileUtils.CLASS_EXTENSION.length()),
+                          bytes)
+                      : bytes);
+    }
+    // Compile with Dagger annotation processor. The part of the program not relying on dagger
+    // generated types are passed as class files (potentially after transformation) and on command
+    // line for being included in annotation processing.
+    compiledProgramDependingOnDagger =
+        DaggerUtils.compileWithAnnotationProcessing(
+            sourceDependingOnDagger, ImmutableList.of(compiledProgramNotDependingOnDagger));
+    daggerGeneratedSourceFiles =
+        ZipUtils.filter(
+            compiledProgramDependingOnDagger,
+            getStaticTemp().newFolder().toPath().resolve("source.jar"),
+            entry -> entry.getName().endsWith(".java"));
+    // Check the generated Dagger source files.
+    Set<String> generatedFiles = new HashSet<>();
+    ZipUtils.iter(
+        DaggerBasicTestBase.daggerGeneratedSourceFiles,
+        (entry, unused) -> generatedFiles.add(entry.getName()));
+    assertEquals(
+        ImmutableSet.of(
+            "basic/I1Impl1_Factory.java",
+            "basic/I2Impl1_Factory.java",
+            "basic/I3Impl1_Factory.java",
+            "basic/ModuleUsingProvides_I1Factory.java",
+            "basic/ModuleUsingProvides_I2Factory.java",
+            "basic/ModuleUsingProvides_I3Factory.java",
+            "basic/ModuleUsingProvides_Proxy.java",
+            "basic/DaggerMainComponentUsingBinds.java",
+            "basic/DaggerMainComponentUsingProvides.java"),
+        generatedFiles);
+  }
+
+  private static boolean sourceFileReferingDaggerGeneratedClasses(Path file) {
+    return file.getFileName().toString().startsWith("Main");
+  }
+
+  static void compileWithSingleton() throws Exception {
+    compile(
+        javaSourceFiles("basic", path -> !sourceFileReferingDaggerGeneratedClasses(path)),
+        javaSourceFiles("basic", DaggerBasicTestBase::sourceFileReferingDaggerGeneratedClasses),
+        DaggerBasicTestBase::transformAddSingleton);
+  }
+
+  static void compileWithoutSingleton() throws Exception {
+    compile(
+        javaSourceFiles("basic", path -> !sourceFileReferingDaggerGeneratedClasses(path)),
+        javaSourceFiles("basic", DaggerBasicTestBase::sourceFileReferingDaggerGeneratedClasses),
+        null);
+  }
+
+  private static byte[] transformAddSingleton(String binaryName, byte[] bytes) {
+    // Add @Singleton to the constructors used with @Bind.
+    if (binaryName.endsWith("Impl1")) {
+      return transformer(bytes, Reference.classFromBinaryName(binaryName))
+          .addClassTransformer(
+              new ClassTransformer() {
+                @Override
+                public void visitEnd() {
+                  super.visitAnnotation("Ljavax/inject/Singleton;", true);
+                }
+              })
+          .transform();
+    }
+    // Add @Singleton to the methods annotated with @Provides.
+    if (binaryName.endsWith("ModuleUsingProvides")) {
+      return transformer(bytes, Reference.classFromBinaryName(binaryName))
+          .addMethodTransformer(
+              new MethodTransformer() {
+                @Override
+                public void visitEnd() {
+                  super.visitAnnotation("Ljavax/inject/Singleton;", true);
+                }
+              })
+          .transform();
+    }
+    return bytes;
+  }
+
+  public static List<Path> javaSourceFiles(String testDir, Predicate<Path> filter)
+      throws Exception {
+    try (Stream<Path> walk =
+        Files.walk(Paths.get(ToolHelper.TESTS_DIR, "examplesDagger", testDir))) {
+      return walk.filter(filter).filter(FileUtils::isJavaFile).collect(Collectors.toList());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DaggerUtils.java b/src/test/java/com/android/tools/r8/utils/DaggerUtils.java
new file mode 100644
index 0000000..24eabb5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/DaggerUtils.java
@@ -0,0 +1,111 @@
+// 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.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestRuntime;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class DaggerUtils {
+
+  private static final Path DAGGER_ROOT = Paths.get("third_party", "dagger", "2.41");
+  private static final List<Path> DAGGER_COMPILER =
+      ImmutableList.of(
+              "annotations-13.0.jar",
+              "checker-compat-qual-2.5.5.jar",
+              "checker-qual-3.12.0.jar",
+              "dagger-2.41.jar",
+              "dagger-compiler-2.41.jar",
+              "dagger-producers-2.41.jar",
+              "dagger-spi-2.41.jar",
+              "error_prone_annotations-2.7.1.jar",
+              "failureaccess-1.0.1.jar",
+              "google-java-format-1.5.jar",
+              "guava-31.0.1-jre.jar",
+              "incap-0.2.jar",
+              "j2objc-annotations-1.3.jar",
+              "javac-shaded-9-dev-r4023-3.jar",
+              "javapoet-1.13.0.jar",
+              "javax.inject-1.jar",
+              "jsr305-3.0.2.jar",
+              "kotlin-stdlib-1.5.32.jar",
+              "kotlin-stdlib-common-1.5.32.jar",
+              "kotlin-stdlib-jdk7-1.5.32.jar",
+              "kotlin-stdlib-jdk8-1.5.32.jar",
+              "kotlinx-metadata-jvm-0.3.0.jar",
+              "symbol-processing-api-1.5.30-1.0.0.jar")
+          .stream()
+          .map(DAGGER_ROOT::resolve)
+          .collect(ImmutableList.toImmutableList());
+  private static final List<Path> DAGGER_RUNTIME =
+      ImmutableList.of("dagger-2.41.jar", "javax.inject-1.jar").stream()
+          .map(DAGGER_ROOT::resolve)
+          .collect(ImmutableList.toImmutableList());
+
+  public static List<Path> getDaggerRuntime() {
+    return DAGGER_RUNTIME;
+  }
+
+  public static Path compileWithAnnotationProcessing(
+      Collection<Path> sourceFiles, Collection<Path> classFiles) throws Exception {
+    // Class files are provided in JAR files. Extract all the class names to pass to javac for
+    // annotation processing.
+    List<String> classNames = new ArrayList<>();
+    for (Path path : classFiles) {
+      ZipUtils.iter(
+          path,
+          (entry, inputStream) -> {
+            String entryString = entry.toString();
+            if (FileUtils.isClassFile(entryString)) {
+              byte[] bytes = ByteStreams.toByteArray(inputStream);
+              classNames.add(TestBase.extractClassName(bytes));
+            }
+          });
+    }
+    return TestBase.javac(TestRuntime.getCheckedInJdk8(), TestBase.getStaticTemp())
+        .addSourceFiles(sourceFiles)
+        .addClassNames(classNames)
+        .addClasspathFiles(classFiles)
+        .addClasspathFiles(getDaggerRuntime())
+        .addAnnotationProcessorPathFiles(DAGGER_COMPILER)
+        .addAnnotationProcessors("dagger.internal.codegen.ComponentProcessor")
+        .compile();
+  }
+
+  public static Path compileWithAnnotationProcessing(Collection<Path> files) throws Exception {
+    // Split the files passed into source files and class files. Class files are expected to be in
+    // JARs.
+    List<Path> sourceFiles =
+        files.stream().filter(FileUtils::isJavaFile).collect(Collectors.toList());
+    List<Path> classFiles =
+        files.stream().filter(FileUtils::isJarFile).collect(Collectors.toList());
+    assertEquals(files.size(), sourceFiles.size() + classFiles.size());
+    return compileWithAnnotationProcessing(sourceFiles, classFiles);
+  }
+
+  public static Path compileWithoutAnnotationProcessing(Collection<Path> files) throws Exception {
+    return TestBase.javac(TestRuntime.getCheckedInJdk8(), TestBase.getStaticTemp())
+        .addSourceFiles(files)
+        .addClasspathFiles(getDaggerRuntime())
+        .compile();
+  }
+
+  public static Path compileWithoutAnnotationProcessing(Path... files) throws Exception {
+    return compileWithoutAnnotationProcessing(Arrays.asList(files));
+  }
+
+  public static Path compileWithAnnotationProcessing(Path... files) throws Exception {
+    return compileWithAnnotationProcessing(Arrays.asList(files));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
index 05031f2..d1f04cb 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
@@ -9,6 +9,8 @@
 
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
 
 public class VerticallyMergedClassesInspector {
 
@@ -33,6 +35,20 @@
     return this;
   }
 
+  public VerticallyMergedClassesInspector assertMergedIntoSubtype(ClassReference classReference) {
+    assertTrue(
+        verticallyMergedClasses.hasBeenMergedIntoSubtype(
+            toDexType(classReference, dexItemFactory)));
+    return this;
+  }
+
+  public VerticallyMergedClassesInspector assertMergedIntoSubtype(String... typeNames) {
+    for (String typeName : typeNames) {
+      assertMergedIntoSubtype(Reference.classFromTypeName(typeName));
+    }
+    return this;
+  }
+
   public VerticallyMergedClassesInspector assertNoClassesMerged() {
     assertTrue(verticallyMergedClasses.isEmpty());
     return this;