1 /*
2  * Copyright 2018 The Android Open Source Project
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 androidx.build.dependencyTracker
18 
19 import java.io.File
20 import java.util.function.BiFunction
21 import org.gradle.api.Project
22 import org.gradle.api.Transformer
23 import org.gradle.api.plugins.ExtraPropertiesExtension
24 import org.gradle.api.provider.Provider
25 import org.gradle.api.specs.Spec
26 import org.gradle.testfixtures.ProjectBuilder
27 import org.hamcrest.CoreMatchers
28 import org.hamcrest.MatcherAssert
29 import org.junit.Before
30 import org.junit.Rule
31 import org.junit.Test
32 import org.junit.rules.TemporaryFolder
33 import org.junit.runner.RunWith
34 import org.junit.runners.JUnit4
35 
36 @RunWith(JUnit4::class)
37 class AffectedModuleDetectorImplTest {
38 
39     @Rule
40     @JvmField
41     val attachLogsRule = AttachLogsTestRule()
42     private val logger = attachLogsRule.logger
43 
44     @Rule
45     @JvmField
46     val tmpFolder = TemporaryFolder()
47 
48     @Rule
49     @JvmField
50     val tmpFolder2 = TemporaryFolder()
51 
52     private lateinit var root: Project
53     private lateinit var p1: Project
54     private lateinit var p2: Project
55     private lateinit var p3: Project
56     private lateinit var p4: Project
57     private lateinit var p5: Project
58     private lateinit var p6: Project
59     private lateinit var p7: Project
60     private lateinit var p8: Project
61     private lateinit var p9: Project
62     private lateinit var p10: Project
63     private lateinit var p11: Project
<lambda>null64     private val projectGraph by lazy {
65         ProjectGraph(root)
66     }
<lambda>null67     private val dependencyTracker by lazy {
68         DependencyTracker(root, logger)
69     }
70     private val cobuiltTestPaths = setOf(setOf(":cobuilt1", ":cobuilt2"))
71     private val ignoredPaths = setOf("ignored/")
72 
73     @Before
initnull74     fun init() {
75         val tmpDir = tmpFolder.root
76 
77         /*
78 
79         Dummy project file tree:
80 
81                root -----------------
82               / |  \     |   |   |   |
83             p1  p7  p2  p8   p9 p10  p11
84            /         \
85           p3          p5
86          /  \
87        p4   p6
88 
89         Dependency forest:
90 
91             p1    p2    p7 p8  p9 p10 p11
92            /  \  /  \
93           p3   p5   p6
94          /
95         p4
96 
97          */
98 
99         root = ProjectBuilder.builder()
100             .withProjectDir(tmpDir)
101             .withName("root")
102             .build()
103         // Project Graph expects supportRootFolder.
104         (root.properties.get("ext") as ExtraPropertiesExtension).set("supportRootFolder", tmpDir)
105         p1 = ProjectBuilder.builder()
106             .withProjectDir(tmpDir.resolve("p1"))
107             .withName("p1")
108             .withParent(root)
109             .build()
110         p2 = ProjectBuilder.builder()
111             .withProjectDir(tmpDir.resolve("p2"))
112             .withName("p2")
113             .withParent(root)
114             .build()
115         p3 = ProjectBuilder.builder()
116             .withProjectDir(tmpDir.resolve("p1:p3"))
117             .withName("p3")
118             .withParent(p1)
119             .build()
120         val p3config = p3.configurations.create("p3config")
121         p3config.dependencies.add(p3.dependencies.project(mutableMapOf("path" to ":p1")))
122         p4 = ProjectBuilder.builder()
123             .withProjectDir(tmpDir.resolve("p1:p3:p4"))
124             .withName("p4")
125             .withParent(p3)
126             .build()
127         val p4config = p4.configurations.create("p4config")
128         p4config.dependencies.add(p4.dependencies.project(mutableMapOf("path" to ":p1:p3")))
129         p5 = ProjectBuilder.builder()
130             .withProjectDir(tmpDir.resolve("p2:p5"))
131             .withName("p5")
132             .withParent(p2)
133             .build()
134         val p5config = p5.configurations.create("p5config")
135         p5config.dependencies.add(p5.dependencies.project(mutableMapOf("path" to ":p2")))
136         p5config.dependencies.add(p5.dependencies.project(mutableMapOf("path" to ":p1:p3")))
137         p6 = ProjectBuilder.builder()
138             .withProjectDir(tmpDir.resolve("p1:p3:p6"))
139             .withName("p6")
140             .withParent(p3)
141             .build()
142         val p6config = p6.configurations.create("p6config")
143         p6config.dependencies.add(p6.dependencies.project(mutableMapOf("path" to ":p2")))
144         p7 = ProjectBuilder.builder()
145             .withProjectDir(tmpDir.resolve("p7"))
146             .withName("p7")
147             .withParent(root)
148             .build()
149         p8 = ProjectBuilder.builder()
150             .withProjectDir(tmpDir.resolve("p8"))
151             .withName("cobuilt1")
152             .withParent(root)
153             .build()
154         p9 = ProjectBuilder.builder()
155             .withProjectDir(tmpDir.resolve("p9"))
156             .withName("cobuilt2")
157             .withParent(root)
158             .build()
159         p10 = ProjectBuilder.builder()
160             .withProjectDir(tmpDir.resolve("p10"))
161             .withName("benchmark")
162             .withParent(root)
163             .build()
164         p11 = ProjectBuilder.builder()
165             .withProjectDir(tmpDir.resolve("p11"))
166             .withName("placeholder-tests")
167             .withParent(root)
168             .build()
169     }
170 
171     class TestProvider(private val list: List<String>) : Provider<List<String>> {
getnull172         override fun get(): List<String> = list
173         override fun getOrNull(): List<String> = list
174         override fun isPresent(): Boolean = TODO("unused")
175         @Deprecated("Super method is deprecated")
176         override fun forUseAtConfigurationTime(): Provider<List<String>> = TODO("unused")
177         override fun <U : Any?, R : Any?> zip(
178             right: Provider<U>,
179             combiner: BiFunction<in List<String>, in U, out R?>
180         ): Provider<R> = TODO("unused")
181         override fun orElse(provider: Provider<out List<String>>): Provider<List<String>> {
182             TODO("unused")
183         }
orElsenull184         override fun orElse(value: List<String>): Provider<List<String>> = TODO("unused")
185         override fun <S : Any?> flatMap(
186             transformer: Transformer<out Provider<out S>?, in List<String>>
187         ): Provider<S> = TODO("unused")
188         override fun filter(spec: Spec<in List<String>>): Provider<List<String>> = TODO("unused")
189         override fun <S : Any?> map(
190             transformer: Transformer<out S?, in List<String>>
191         ): Provider<S> = TODO("unused")
192         override fun getOrElse(defaultValue: List<String>): List<String> = TODO("unused")
193     }
194 
195     @Test
196     fun noChangeCLs() {
197         val detector = AffectedModuleDetectorImpl(
198             projectGraph = projectGraph,
199             dependencyTracker = dependencyTracker,
200             logger = logger,
201             ignoreUnknownProjects = false,
202             cobuiltTestPaths = cobuiltTestPaths,
203             changedFilesProvider = TestProvider(emptyList())
204         )
205         MatcherAssert.assertThat(
206             detector.changedProjects,
207             CoreMatchers.`is`(
208                 setOf(p11.path)
209             )
210         )
211         MatcherAssert.assertThat(
212             detector.dependentProjects,
213             CoreMatchers.`is`(
214                 setOf(p11.path)
215             )
216         )
217         MatcherAssert.assertThat(
218             detector.buildAll,
219             CoreMatchers.`is`(
220                 true
221             )
222         )
223     }
224 
225     @Test
changeInOnenull226     fun changeInOne() {
227         val detector = AffectedModuleDetectorImpl(
228             projectGraph = projectGraph,
229             dependencyTracker = dependencyTracker,
230             logger = logger,
231             ignoreUnknownProjects = false,
232             cobuiltTestPaths = cobuiltTestPaths,
233             changedFilesProvider = TestProvider(
234                 listOf(convertToFilePath("p1", "foo.java"))
235             )
236         )
237         MatcherAssert.assertThat(
238             detector.changedProjects,
239             CoreMatchers.`is`(
240                 setOf(p1.path, p11.path)
241             )
242         )
243         MatcherAssert.assertThat(
244             detector.dependentProjects,
245             CoreMatchers.`is`(
246                 setOf(p3.path, p4.path, p5.path, p11.path)
247             )
248         )
249     }
250 
251     @Test
changeInTwonull252     fun changeInTwo() {
253         val detector = AffectedModuleDetectorImpl(
254             projectGraph = projectGraph,
255             dependencyTracker = dependencyTracker,
256             logger = logger,
257             ignoreUnknownProjects = false,
258             cobuiltTestPaths = cobuiltTestPaths,
259             changedFilesProvider = TestProvider(
260                 listOf(
261                     convertToFilePath("p1", "foo.java"),
262                     convertToFilePath("p2", "bar.java")
263                 )
264             )
265         )
266         MatcherAssert.assertThat(
267             detector.changedProjects,
268             CoreMatchers.`is`(
269                 setOf(p1.path, p2.path, p11.path)
270             )
271         )
272         MatcherAssert.assertThat(
273             detector.dependentProjects,
274             CoreMatchers.`is`(
275                 setOf(p3.path, p4.path, p5.path, p6.path, p11.path)
276             )
277         )
278     }
279 
280     @Test
changeInRootnull281     fun changeInRoot() {
282         val detector = AffectedModuleDetectorImpl(
283             projectGraph = projectGraph,
284             dependencyTracker = dependencyTracker,
285             logger = logger,
286             ignoreUnknownProjects = false,
287             cobuiltTestPaths = cobuiltTestPaths,
288             changedFilesProvider = TestProvider(
289                 listOf("foo.java")
290             )
291         )
292         MatcherAssert.assertThat(
293             detector.changedProjects,
294             CoreMatchers.`is`(
295                 setOf(p11.path)
296             )
297         )
298         MatcherAssert.assertThat(
299             detector.dependentProjects,
300             CoreMatchers.`is`(
301                 setOf(p11.path)
302             )
303         )
304         MatcherAssert.assertThat(
305             detector.buildAll,
306             CoreMatchers.`is`(
307                 true
308             )
309         )
310     }
311 
312     @Test
changeInRootAndSubprojectnull313     fun changeInRootAndSubproject() {
314         val detector = AffectedModuleDetectorImpl(
315             projectGraph = projectGraph,
316             dependencyTracker = dependencyTracker,
317             logger = logger,
318             ignoreUnknownProjects = false,
319             cobuiltTestPaths = cobuiltTestPaths,
320             changedFilesProvider = TestProvider(
321                 listOf("foo.java", convertToFilePath("p7", "bar.java"))
322             )
323         )
324         MatcherAssert.assertThat(
325             detector.changedProjects,
326             CoreMatchers.`is`(
327                 setOf(p7.path, p11.path)
328             )
329         )
330         MatcherAssert.assertThat(
331             detector.dependentProjects,
332             CoreMatchers.`is`(
333                 setOf(p11.path)
334             )
335         )
336         MatcherAssert.assertThat(
337             detector.buildAll,
338             CoreMatchers.`is`(
339                 true
340             )
341         )
342     }
343 
344     @Test
changeInCobuiltnull345     fun changeInCobuilt() {
346         val detector = AffectedModuleDetectorImpl(
347             projectGraph = projectGraph,
348             dependencyTracker = dependencyTracker,
349             logger = logger,
350             ignoreUnknownProjects = false,
351             cobuiltTestPaths = cobuiltTestPaths,
352             changedFilesProvider = TestProvider(
353                 listOf(
354                     convertToFilePath(
355                         "p8", "foo.java"
356                     )
357                 )
358             )
359         )
360         MatcherAssert.assertThat(
361             detector.changedProjects,
362             CoreMatchers.`is`(
363                 setOf(p8.path, p9.path, p11.path)
364             )
365         )
366         MatcherAssert.assertThat(
367             detector.dependentProjects,
368             CoreMatchers.`is`(
369                 setOf(p11.path)
370             )
371         )
372     }
373 
374     @Test(expected = IllegalStateException::class)
changeInCobuiltMissingCobuiltnull375     fun changeInCobuiltMissingCobuilt() {
376         val detector = AffectedModuleDetectorImpl(
377             projectGraph = projectGraph,
378             dependencyTracker = dependencyTracker,
379             logger = logger,
380             ignoreUnknownProjects = false,
381             cobuiltTestPaths = setOf(setOf(":cobuilt1", ":cobuilt2", ":cobuilt3")),
382             changedFilesProvider = TestProvider(
383                 listOf(
384                     convertToFilePath(
385                         "p8", "foo.java"
386                     )
387                 )
388             )
389         )
390         // This should trigger IllegalStateException due to missing cobuilt3
391         detector.changedProjects
392     }
393 
394     @Test
changeInCobuiltAllCobuiltsMissingnull395     fun changeInCobuiltAllCobuiltsMissing() {
396         val detector = AffectedModuleDetectorImpl(
397             projectGraph = projectGraph,
398             dependencyTracker = dependencyTracker,
399             logger = logger,
400             ignoreUnknownProjects = false,
401             cobuiltTestPaths = setOf(setOf("cobuilt3", "cobuilt4", "cobuilt5")),
402             changedFilesProvider = TestProvider(
403                 listOf(
404                     convertToFilePath(
405                         "p8", "foo.java"
406                     )
407                 )
408             )
409         )
410         // There should be no exception thrown here because *all* cobuilts are missing.
411         detector.changedProjects
412     }
413 
414     @Test
projectSubsetnull415     fun projectSubset() {
416         val detector = AffectedModuleDetectorImpl(
417             projectGraph = projectGraph,
418             dependencyTracker = dependencyTracker,
419             logger = logger,
420             ignoreUnknownProjects = false,
421             changedFilesProvider = TestProvider(
422                 listOf(convertToFilePath("p1", "foo.java"))
423             )
424         )
425         // Verify expectations on affected projects
426         MatcherAssert.assertThat(
427             detector.changedProjects,
428             CoreMatchers.`is`(
429                 setOf(p1.path, p11.path)
430             )
431         )
432         MatcherAssert.assertThat(
433             detector.dependentProjects,
434             CoreMatchers.`is`(
435                 setOf(p3.path, p4.path, p5.path, p11.path)
436             )
437         )
438         // Test changed
439         MatcherAssert.assertThat(
440             detector.getSubset(p1.path),
441             CoreMatchers.`is`(
442                 ProjectSubset.CHANGED_PROJECTS
443             )
444         )
445         // Test dependent
446         MatcherAssert.assertThat(
447             detector.getSubset(p3.path),
448             CoreMatchers.`is`(
449                 ProjectSubset.DEPENDENT_PROJECTS
450             )
451         )
452         // Random unrelated project should return none
453         MatcherAssert.assertThat(
454             detector.getSubset(p7.path),
455             CoreMatchers.`is`(
456                 ProjectSubset.NONE
457             )
458         )
459     }
460 
461     @Test
projectSubset_noChangedFilesnull462     fun projectSubset_noChangedFiles() {
463         val detector = AffectedModuleDetectorImpl(
464             projectGraph = projectGraph,
465             dependencyTracker = dependencyTracker,
466             logger = logger,
467             ignoreUnknownProjects = false,
468             changedFilesProvider = TestProvider(emptyList())
469         )
470         // Verify expectations on affected projects
471         MatcherAssert.assertThat(
472             detector.affectedProjects,
473             CoreMatchers.`is`(
474                 setOf(p11.path)
475             )
476         )
477         // No changed files in postsubmit -> return all
478         MatcherAssert.assertThat(
479             detector.getSubset(p1.path),
480             CoreMatchers.`is`(
481                 ProjectSubset.NONE
482             )
483         )
484         MatcherAssert.assertThat(
485             detector.getSubset(p3.path),
486             CoreMatchers.`is`(
487                 ProjectSubset.NONE
488             )
489         )
490         // Only the placeholder test should return CHANGED_PROJECTS
491         MatcherAssert.assertThat(
492             detector.getSubset(p11.path),
493             CoreMatchers.`is`(
494                 ProjectSubset.CHANGED_PROJECTS
495             )
496         )
497         MatcherAssert.assertThat(
498             detector.buildAll,
499             CoreMatchers.`is`(
500                 true
501             )
502         )
503     }
504 
505     @Test
projectSubset_unknownChangedFilesnull506     fun projectSubset_unknownChangedFiles() {
507         val detector = AffectedModuleDetectorImpl(
508             projectGraph = projectGraph,
509             dependencyTracker = dependencyTracker,
510             logger = logger,
511             ignoreUnknownProjects = false,
512             changedFilesProvider = TestProvider(
513                 listOf(convertToFilePath("unknown", "file.java"))
514             )
515         )
516         // Verify expectations on affected projects
517         MatcherAssert.assertThat(
518             detector.changedProjects,
519             CoreMatchers.`is`(
520                 setOf(p11.path)
521             )
522         )
523         MatcherAssert.assertThat(
524             detector.dependentProjects,
525             CoreMatchers.`is`(
526                 setOf(p11.path)
527             )
528         )
529         // Everything should return NONE in buildall case
530         MatcherAssert.assertThat(
531             detector.getSubset(p1.path),
532             CoreMatchers.`is`(
533                 ProjectSubset.NONE
534             )
535         )
536         MatcherAssert.assertThat(
537             detector.getSubset(p3.path),
538             CoreMatchers.`is`(
539                 ProjectSubset.NONE
540             )
541         )
542         // Only the placeholder test should return CHANGED_PROJECTS
543         MatcherAssert.assertThat(
544             detector.getSubset(p11.path),
545             CoreMatchers.`is`(
546                 ProjectSubset.CHANGED_PROJECTS
547             )
548         )
549     }
550 
551     @Test
changeInPlaygroundCommonnull552     fun changeInPlaygroundCommon() {
553         val detector = AffectedModuleDetectorImpl(
554             projectGraph = projectGraph,
555             dependencyTracker = dependencyTracker,
556             logger = logger,
557             ignoreUnknownProjects = false,
558             cobuiltTestPaths = cobuiltTestPaths,
559             changedFilesProvider = TestProvider(
560                 listOf("playground-common/tmp.kt")
561             )
562         )
563         MatcherAssert.assertThat(
564             detector.changedProjects,
565             CoreMatchers.`is`(
566                 setOf(p11.path)
567             )
568         )
569         MatcherAssert.assertThat(
570             detector.buildAll,
571             CoreMatchers.`is`(
572                 false
573             )
574         )
575     }
576 
577     @Test
changeInGithubConfignull578     fun changeInGithubConfig() {
579         val detector = AffectedModuleDetectorImpl(
580             projectGraph = projectGraph,
581             dependencyTracker = dependencyTracker,
582             logger = logger,
583             ignoreUnknownProjects = false,
584             cobuiltTestPaths = cobuiltTestPaths,
585             changedFilesProvider = TestProvider(
586                 listOf(".github/workflows/bar.txt")
587             )
588         )
589         MatcherAssert.assertThat(
590             detector.changedProjects,
591             CoreMatchers.`is`(
592                 setOf(p11.path)
593             )
594         )
595         MatcherAssert.assertThat(
596             detector.buildAll,
597             CoreMatchers.`is`(
598                 false
599             )
600         )
601     }
602 
603     @Test
changeInPlaygroundCommonAndRootnull604     fun changeInPlaygroundCommonAndRoot() {
605         val detector = AffectedModuleDetectorImpl(
606             projectGraph = projectGraph,
607             dependencyTracker = dependencyTracker,
608             logger = logger,
609             ignoreUnknownProjects = false,
610             cobuiltTestPaths = cobuiltTestPaths,
611             changedFilesProvider = TestProvider(
612                 listOf("playground-common/tmp.kt", "root.txt")
613             )
614         )
615         MatcherAssert.assertThat(
616             detector.buildAll,
617             CoreMatchers.`is`(
618                 true
619             )
620         )
621     }
622 
623     @Test
Only ignored filenull624     fun `Only ignored file`() {
625         val detector = AffectedModuleDetectorImpl(
626             projectGraph = projectGraph,
627             dependencyTracker = dependencyTracker,
628             logger = logger,
629             ignoreUnknownProjects = false,
630             cobuiltTestPaths = cobuiltTestPaths,
631             ignoredPaths = ignoredPaths,
632             changedFilesProvider = TestProvider(
633                 listOf(convertToFilePath("ignored", "example.txt"))
634             )
635         )
636         MatcherAssert.assertThat(
637             detector.buildAll,
638             CoreMatchers.`is`(
639                 false
640             )
641         )
642     }
643 
644     @Test
Ignored file and changed filenull645     fun `Ignored file and changed file`() {
646         val detector = AffectedModuleDetectorImpl(
647             projectGraph = projectGraph,
648             dependencyTracker = dependencyTracker,
649             logger = logger,
650             ignoreUnknownProjects = false,
651             cobuiltTestPaths = cobuiltTestPaths,
652             ignoredPaths = ignoredPaths,
653             changedFilesProvider = TestProvider(
654                 listOf(
655                     convertToFilePath("ignored", "example.txt"),
656                     convertToFilePath("p1", "foo.kt")
657                 )
658             )
659         )
660         MatcherAssert.assertThat(
661             detector.changedProjects,
662             CoreMatchers.`is`(
663                 setOf(p1.path, p11.path)
664             )
665         )
666         MatcherAssert.assertThat(
667             detector.buildAll,
668             CoreMatchers.`is`(
669                 false
670             )
671         )
672     }
673 
674     @Test
Ignored file and unknown filenull675     fun `Ignored file and unknown file`() {
676         val detector = AffectedModuleDetectorImpl(
677             projectGraph = projectGraph,
678             dependencyTracker = dependencyTracker,
679             logger = logger,
680             ignoreUnknownProjects = false,
681             cobuiltTestPaths = cobuiltTestPaths,
682             ignoredPaths = ignoredPaths,
683             changedFilesProvider = TestProvider(
684                 listOf(
685                     convertToFilePath("ignored", "example.txt"),
686                     convertToFilePath("unknown", "foo.kt")
687                 )
688             )
689         )
690         MatcherAssert.assertThat(
691             detector.buildAll,
692             CoreMatchers.`is`(
693                 true
694             )
695         )
696     }
697 
698     @Test
Ignored file, unknown file, and changed filenull699     fun `Ignored file, unknown file, and changed file`() {
700         val detector = AffectedModuleDetectorImpl(
701             projectGraph = projectGraph,
702             dependencyTracker = dependencyTracker,
703             logger = logger,
704             ignoreUnknownProjects = false,
705             cobuiltTestPaths = cobuiltTestPaths,
706             ignoredPaths = ignoredPaths,
707             changedFilesProvider = TestProvider(
708                 listOf(
709                     "ignored/eg.txt",
710                     convertToFilePath("unknown", "foo.kt"),
711                     convertToFilePath("p1", "bar.kt")
712                 )
713             )
714         )
715         MatcherAssert.assertThat(
716             detector.buildAll,
717             CoreMatchers.`is`(
718                 true
719             )
720         )
721     }
722 
723     // For both Linux/Windows
convertToFilePathnull724     fun convertToFilePath(vararg list: String): String {
725         return list.toList().joinToString(File.separator)
726     }
727 }
728