1 /* 2 * Copyright (C) 2022 The Dagger Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package buildtests; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; 21 22 import java.io.File; 23 import java.io.IOException; 24 import java.util.Arrays; 25 import java.util.Collection; 26 import org.gradle.testkit.runner.BuildResult; 27 import org.gradle.testkit.runner.GradleRunner; 28 import org.junit.Rule; 29 import org.junit.Test; 30 import org.junit.rules.TemporaryFolder; 31 import org.junit.runner.RunWith; 32 import org.junit.runners.Parameterized; 33 import org.junit.runners.Parameterized.Parameters; 34 35 // This is a regression test for https://github.com/google/dagger/issues/3136 36 @RunWith(Parameterized.class) 37 public class TransitiveQualifierTest { 38 @Parameters(name = "transitiveDependencyType = {0}, strictSuperficialValidationMode = {1}") parameters()39 public static Collection<Object[]> parameters() { 40 return Arrays.asList( 41 new Object[][] { 42 { "implementation", "ENABLED" }, 43 { "implementation", "DISABLED" }, 44 { "api", "ENABLED" }, 45 { "api", "DISABLED" } 46 }); 47 } 48 49 @Rule public TemporaryFolder folder = new TemporaryFolder(); 50 51 private final String transitiveDependencyType; 52 private final String strictSuperficialValidationMode; 53 TransitiveQualifierTest( String transitiveDependencyType, String strictSuperficialValidationMode)54 public TransitiveQualifierTest( 55 String transitiveDependencyType, String strictSuperficialValidationMode) { 56 this.transitiveDependencyType = transitiveDependencyType; 57 this.strictSuperficialValidationMode = strictSuperficialValidationMode; 58 } 59 60 @Test testQualifierOnInjectConstructorParameter()61 public void testQualifierOnInjectConstructorParameter() throws IOException { 62 GradleRunner runner = 63 setupRunnerWith( 64 GradleFile.create( 65 "QualifierUsage.java", 66 "package library1;", 67 "", 68 "import javax.inject.Inject;", 69 "import library2.MyQualifier;", 70 "", 71 "public class QualifierUsage {", 72 " @Inject QualifierUsage(@MyQualifier int i) {}", 73 "}")); 74 BuildResult result; 75 switch (transitiveDependencyType) { 76 case "implementation": 77 switch (strictSuperficialValidationMode) { 78 case "ENABLED": 79 result = runner.buildAndFail(); 80 assertThat(result.getOutput()).contains("Task :app:compileJava FAILED"); 81 assertThat(result.getOutput()) 82 .contains( 83 "ComponentProcessingStep was unable to process 'app.MyComponent' because " 84 + "'library2.MyQualifier' could not be resolved." 85 + "\n " 86 + "\n Dependency trace:" 87 + "\n => element (INTERFACE): library1.MyModule" 88 + "\n => element (METHOD): provideInt()" 89 + "\n => annotation: @MyQualifier" 90 + "\n => type (ERROR annotation type): library2.MyQualifier"); 91 break; 92 case "DISABLED": 93 // When strict mode is disabled we fall back to the old behavior where the qualifier is 94 // missing and we do not throw an exception. 95 result = runner.build(); 96 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 97 assertThat(result.getOutput()).contains("REQUEST: java.lang.Integer"); 98 break; 99 default: throw new AssertionError("Unexpected mode: " + strictSuperficialValidationMode); 100 } 101 break; 102 case "api": 103 result = runner.build(); 104 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 105 assertThat(result.getOutput()).contains("REQUEST: @library2.MyQualifier java.lang.Integer"); 106 break; 107 } 108 } 109 110 @Test testQualifierOnInjectField()111 public void testQualifierOnInjectField() throws IOException { 112 GradleRunner runner = 113 setupRunnerWith( 114 GradleFile.create( 115 "QualifierUsage.java", 116 "package library1;", 117 "", 118 "import javax.inject.Inject;", 119 "import library2.MyQualifier;", 120 "", 121 "public class QualifierUsage {", 122 " @Inject @MyQualifier int i;", 123 "", 124 " @Inject QualifierUsage() {}", 125 "}")); 126 BuildResult result; 127 switch (transitiveDependencyType) { 128 case "implementation": 129 switch (strictSuperficialValidationMode) { 130 case "ENABLED": 131 result = runner.buildAndFail(); 132 assertThat(result.getOutput()).contains("Task :app:compileJava FAILED"); 133 assertThat(result.getOutput()) 134 .contains( 135 "ComponentProcessingStep was unable to process 'app.MyComponent' because " 136 + "'library2.MyQualifier' could not be resolved." 137 + "\n " 138 + "\n Dependency trace:" 139 + "\n => element (INTERFACE): library1.MyModule" 140 + "\n => element (METHOD): provideInt()" 141 + "\n => annotation: @MyQualifier" 142 + "\n => type (ERROR annotation type): library2.MyQualifier"); 143 break; 144 case "DISABLED": 145 // When strict mode is disabled we fall back to the old behavior where the qualifier is 146 // missing and we do not throw an exception. 147 result = runner.build(); 148 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 149 assertThat(result.getOutput()).contains("REQUEST: java.lang.Integer"); 150 break; 151 default: throw new AssertionError("Unexpected mode: " + strictSuperficialValidationMode); 152 } 153 break; 154 case "api": 155 result = runner.build(); 156 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 157 assertThat(result.getOutput()).contains("REQUEST: @library2.MyQualifier java.lang.Integer"); 158 break; 159 } 160 } 161 162 @Test testQualifierOnInjectMethodParameter()163 public void testQualifierOnInjectMethodParameter() throws IOException { 164 GradleRunner runner = 165 setupRunnerWith( 166 GradleFile.create( 167 "QualifierUsage.java", 168 "package library1;", 169 "", 170 "import javax.inject.Inject;", 171 "import library2.MyQualifier;", 172 "", 173 "public class QualifierUsage {", 174 " @Inject QualifierUsage() {}", 175 "", 176 " @Inject void injectMethod(@MyQualifier int i) {}", 177 "}")); 178 BuildResult result; 179 switch (transitiveDependencyType) { 180 case "implementation": 181 switch (strictSuperficialValidationMode) { 182 case "ENABLED": 183 result = runner.buildAndFail(); 184 assertThat(result.getOutput()).contains("Task :app:compileJava FAILED"); 185 assertThat(result.getOutput()) 186 .contains( 187 "ComponentProcessingStep was unable to process 'app.MyComponent' because " 188 + "'library2.MyQualifier' could not be resolved." 189 + "\n " 190 + "\n Dependency trace:" 191 + "\n => element (INTERFACE): library1.MyModule" 192 + "\n => element (METHOD): provideInt()" 193 + "\n => annotation: @MyQualifier" 194 + "\n => type (ERROR annotation type): library2.MyQualifier"); 195 break; 196 case "DISABLED": 197 // When strict mode is disabled we fall back to the old behavior where the qualifier is 198 // missing and we do not throw an exception. 199 result = runner.build(); 200 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 201 assertThat(result.getOutput()).contains("REQUEST: java.lang.Integer"); 202 break; 203 default: throw new AssertionError("Unexpected mode: " + strictSuperficialValidationMode); 204 } 205 break; 206 case "api": 207 result = runner.build(); 208 assertThat(result.task(":app:assemble").getOutcome()).isEqualTo(SUCCESS); 209 assertThat(result.getOutput()).contains("REQUEST: @library2.MyQualifier java.lang.Integer"); 210 break; 211 } 212 } 213 setupRunnerWith(GradleFile qualifierUsage)214 private GradleRunner setupRunnerWith(GradleFile qualifierUsage) throws IOException { 215 File projectDir = folder.getRoot(); 216 GradleModule.create(projectDir) 217 .addSettingsFile( 218 "include 'app'", 219 "include 'library1'", 220 "include 'library2'", 221 "include 'spi-plugin'") 222 .addBuildFile( 223 "buildscript {", 224 " ext {", 225 String.format("dagger_version = \"%s\"", System.getProperty("dagger_version")), 226 " }", 227 "}", 228 "", 229 "allprojects {", 230 " repositories {", 231 " mavenCentral()", 232 " mavenLocal()", 233 " }", 234 "}"); 235 236 GradleModule.create(projectDir, "app") 237 .addBuildFile( 238 "plugins {", 239 " id 'java'", 240 " id 'application'", 241 "}", 242 "tasks.withType(JavaCompile) {", 243 String.format( 244 " options.compilerArgs += '-Adagger.strictSuperficialValidation=%s'", 245 strictSuperficialValidationMode), 246 "}", 247 "dependencies {", 248 " implementation project(':library1')", 249 " annotationProcessor project(':spi-plugin')", 250 " implementation \"com.google.dagger:dagger:$dagger_version\"", 251 " annotationProcessor \"com.google.dagger:dagger-compiler:$dagger_version\"", 252 "}") 253 .addSrcFile( 254 "MyComponent.java", 255 "package app;", 256 "", 257 "import dagger.Component;", 258 "import library1.MyModule;", 259 "import library1.QualifierUsage;", 260 "", 261 "@Component(modules = MyModule.class)", 262 "public interface MyComponent {", 263 " QualifierUsage qualifierUsage();", 264 "}"); 265 266 GradleModule.create(projectDir, "library1") 267 .addBuildFile( 268 "plugins {", 269 " id 'java'", 270 " id 'java-library'", 271 "}", 272 "dependencies {", 273 transitiveDependencyType + " project(':library2')", 274 " implementation \"com.google.dagger:dagger:$dagger_version\"", 275 " annotationProcessor \"com.google.dagger:dagger-compiler:$dagger_version\"", 276 "}") 277 .addSrcFile( 278 "MyModule.java", 279 "package library1;", 280 "", 281 "import dagger.Module;", 282 "import dagger.Provides;", 283 "import library2.MyQualifier;", 284 "", 285 "@Module", 286 "public interface MyModule {", 287 " @Provides", 288 " @MyQualifier", 289 " static int provideInt() {", 290 " return 0;", 291 " }", 292 "}") 293 .addSrcFile(qualifierUsage); 294 295 GradleModule.create(projectDir, "library2") 296 .addBuildFile( 297 "plugins {", 298 " id 'java'", 299 " id 'java-library'", 300 "}", 301 "dependencies {", 302 " implementation 'javax.inject:javax.inject:1'", 303 "}") 304 .addSrcFile( 305 "MyQualifier.java", 306 "package library2;", 307 "", 308 "import javax.inject.Qualifier;", 309 "", 310 "@Qualifier", 311 "public @interface MyQualifier {}"); 312 313 // This plugin is used to print output about bindings that we can assert on in tests. 314 GradleModule.create(projectDir, "spi-plugin") 315 .addBuildFile( 316 "plugins {", 317 " id 'java'", 318 "}", 319 "dependencies {", 320 " implementation \"com.google.dagger:dagger-spi:$dagger_version\"", 321 " implementation 'com.google.auto.service:auto-service-annotations:1.0.1'", 322 " annotationProcessor 'com.google.auto.service:auto-service:1.0.1'", 323 "}") 324 .addSrcFile( 325 "TestBindingGraphPlugin.java", 326 "package spiplugin;", 327 "", 328 "import com.google.auto.service.AutoService;", 329 "import dagger.model.BindingGraph;", 330 "import dagger.model.BindingGraph.DependencyEdge;", 331 "import dagger.model.DependencyRequest;", 332 "import dagger.spi.BindingGraphPlugin;", 333 "import dagger.spi.DiagnosticReporter;", 334 "", 335 "@AutoService(BindingGraphPlugin.class)", 336 "public class TestBindingGraphPlugin implements BindingGraphPlugin {", 337 " @Override", 338 " public void visitGraph(", 339 " BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) {", 340 " bindingGraph.dependencyEdges().stream()", 341 " .map(DependencyEdge::dependencyRequest)", 342 " .map(DependencyRequest::key)", 343 " .forEach(key -> System.out.println(\"REQUEST: \" + key));", 344 " }", 345 "}"); 346 347 return GradleRunner.create() 348 .withArguments("--stacktrace", "build") 349 .withProjectDir(projectDir); 350 } 351 } 352