diff --git a/README.md b/README.md index 5d245fd..46eba2c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ affectedModuleDetector { "Run static analysis tool without auto-correction by Impact analysis" ) ] + configurationPredicate = new Predicate() { + boolean test(Configuration configuration) { + return !configuration.name.contains("somethingToExclude") + } + } } ``` @@ -128,6 +133,7 @@ affectedModuleDetector { - `includeUncommitted`: If uncommitted files should be considered affected - `top`: The top of the git log to use. Must be used in combination with configuration `includeUncommitted = false` - `customTasks`: set of [CustomTask](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt) + - `configurationPredicate`: A predicate to filter configurations that should be considered for the dependency graph. By default, all configurations are considered. By default, the Detector will look for `assembleAndroidDebugTest`, `connectedAndroidDebugTest`, and `testDebug`. Modules can specify a configuration block to specify which variant tests to run: ```groovy diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt index 7150166..ae0a765 100644 --- a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt @@ -1,8 +1,10 @@ package com.dropbox.affectedmoduledetector import com.dropbox.affectedmoduledetector.util.toOsSpecificPath +import org.gradle.api.artifacts.Configuration import java.io.File import java.io.Serializable +import java.util.function.Predicate class AffectedModuleConfiguration : Serializable { @@ -45,6 +47,13 @@ class AffectedModuleConfiguration : Serializable { */ var customTasks = emptySet() + /** + * Predicate to determine if a configuration should be considered or ignored. This predicate + * will be called for every configuration defined by each project module. By default, + * all configurations are considered. + */ + var configurationPredicate: Predicate = AlwaysConfigurationPredicate() + /** * Folder to place the log in */ diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt new file mode 100644 index 0000000..a80d206 --- /dev/null +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AlwaysConfigurationPredicate.kt @@ -0,0 +1,14 @@ +package com.dropbox.affectedmoduledetector + +import org.gradle.api.artifacts.Configuration +import java.util.function.Predicate + +/** + * Default implementation of a [Configuration] [Predicate] that always returns true, indicating + * that all configurations should be considered. + */ +internal class AlwaysConfigurationPredicate : Predicate { + override fun test(t: Configuration): Boolean { + return true + } +} diff --git a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt index 924b3e0..23a5678 100644 --- a/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt +++ b/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt @@ -30,24 +30,30 @@ import java.io.Serializable */ class DependencyTracker(rootProject: Project, logger: Logger?) : Serializable { + private val configuration: AffectedModuleConfiguration by lazy { + rootProject.extensions.getByType(AffectedModuleConfiguration::class.java) + } + private val dependentList: Map> by lazy { val result = mutableMapOf>() rootProject.subprojects.forEach { project -> logger?.info("checking ${project.path} for dependencies") - project.configurations.forEach { config -> - logger?.info("checking config ${project.path}/$config for dependencies") - config - .dependencies - .filterIsInstance() - .forEach { - logger?.info( - "there is a dependency from ${project.projectPath} to " + - it.path, - ) - result.getOrPut(ProjectPath(it.path)) { mutableSetOf() } - .add(project.projectPath) - } - } + project.configurations + .filter(configuration.configurationPredicate::test) + .forEach { config -> + logger?.info("checking config ${project.path}/$config for dependencies") + config + .dependencies + .filterIsInstance() + .forEach { + logger?.info( + "there is a dependency from ${project.projectPath} to " + + it.path, + ) + result.getOrPut(ProjectPath(it.path)) { mutableSetOf() } + .add(project.projectPath) + } + } } result } diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt index ad006a7..22f82ee 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt @@ -1,6 +1,8 @@ package com.dropbox.affectedmoduledetector +import com.dropbox.affectedmoduledetector.mocks.MockConfiguration import com.google.common.truth.Truth.assertThat +import org.gradle.api.artifacts.Configuration import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Assert.fail @@ -11,6 +13,7 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 import java.io.File +import java.util.function.Predicate @RunWith(JUnit4::class) class AffectedModuleConfigurationTest { @@ -376,4 +379,27 @@ class AffectedModuleConfigurationTest { // THEN assertFalse(actual) } + + @Test + fun `GIVEN AffectedModuleConfiguration WHEN configuration predicate is set THEN is configuration predicate`() { + // GIVEN + config.configurationPredicate = Predicate { false } + + // WHEN + val actual = config.configurationPredicate.test(MockConfiguration()) + + // THEN + assertFalse(actual) + } + + @Test + fun `GIVEN AffectedModuleConfiguration WHEN configuration predicate is not set THEN is default`() { + // GIVEN default configuration + + // WHEN + val actual = config.configurationPredicate.test(MockConfiguration()) + + // THEN + assertTrue(actual) + } } diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt index 2c8a4be..2e39f83 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorImplTest.kt @@ -15,6 +15,7 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 import java.io.File +import java.util.function.Predicate @RunWith(JUnit4::class) class AffectedModuleDetectorImplTest { @@ -238,6 +239,9 @@ class AffectedModuleDetectorImplTest { it.baseDir = tmpDir.absolutePath it.pathsAffectingAllModules = pathsAffectingAllModules } + listOf(root, root2, root3).forEach { rootProject -> + rootProject.extensions.add(AffectedModuleConfiguration.name, affectedModuleConfiguration) + } } @Test @@ -1773,6 +1777,59 @@ class AffectedModuleDetectorImplTest { ) } + @Test + fun `GIVEN upward configuration reference from p2 to p6 WHEN no predicate is supplied THEN p2 is affected`() { + p2.configurations.create("p2-upward-p6") { config -> + config.dependencies.add(p2.dependencies.project(mapOf("path" to p6.path))) + } + val detector = AffectedModuleDetectorImpl( + projectGraph = rootProjectGraph, + dependencyTracker = rootDependencyTracker, + logger = logger.toLogger(), + ignoreUnknownProjects = false, + projectSubset = ProjectSubset.ALL_AFFECTED_PROJECTS, + modules = null, + changedFilesProvider = MockGitClient( + changedFiles = listOf( + convertToFilePath("d1/d3/d6", "foo.java") + ), + tmpFolder = tmpFolder.root + ).findChangedFiles(root), + gitRoot = tmpFolder.root, + config = affectedModuleConfiguration + ) + Truth.assertThat(detector.shouldInclude(p2.projectPath)).isTrue() + Truth.assertThat(detector.shouldInclude(p6.projectPath)).isTrue() + } + + @Test + fun `GIVEN upward configuration reference from p2 to p6 WHEN predicate filtered THEN p2 is unaffected`() { + p2.configurations.create("p2-upward-p6") { config -> + config.dependencies.add(p2.dependencies.project(mapOf("path" to p6.path))) + } + affectedModuleConfiguration.configurationPredicate = Predicate { configuration -> + !configuration.name.contains("-upward-") + } + val detector = AffectedModuleDetectorImpl( + projectGraph = rootProjectGraph, + dependencyTracker = rootDependencyTracker, + logger = logger.toLogger(), + ignoreUnknownProjects = false, + projectSubset = ProjectSubset.ALL_AFFECTED_PROJECTS, + modules = null, + changedFilesProvider = MockGitClient( + changedFiles = listOf( + convertToFilePath("d1/d3/d6", "foo.java") + ), + tmpFolder = tmpFolder.root + ).findChangedFiles(root), + gitRoot = tmpFolder.root, + config = affectedModuleConfiguration + ) + Truth.assertThat(detector.shouldInclude(p2.projectPath)).isFalse() + Truth.assertThat(detector.shouldInclude(p6.projectPath)).isTrue() + } + // For both Linux/Windows fun convertToFilePath(vararg list: String): String { return list.toList().joinToString(File.separator) diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt index 3ac25fd..e77350c 100644 --- a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt @@ -10,7 +10,6 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import java.lang.IllegalStateException @RunWith(JUnit4::class) class AffectedModuleDetectorPluginTest { diff --git a/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockConfiguration.kt b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockConfiguration.kt new file mode 100644 index 0000000..0dc0b6c --- /dev/null +++ b/affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockConfiguration.kt @@ -0,0 +1,279 @@ +package com.dropbox.affectedmoduledetector.mocks + +import groovy.lang.Closure +import org.gradle.api.Action +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationPublications +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.DependencyConstraintSet +import org.gradle.api.artifacts.DependencySet +import org.gradle.api.artifacts.ExcludeRule +import org.gradle.api.artifacts.PublishArtifactSet +import org.gradle.api.artifacts.ResolutionStrategy +import org.gradle.api.artifacts.ResolvableDependencies +import org.gradle.api.artifacts.ResolvedConfiguration +import org.gradle.api.attributes.AttributeContainer +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.FileTree +import org.gradle.api.provider.Provider +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.TaskDependency +import java.io.File + +class MockConfiguration : Configuration { + override fun getResolutionStrategy(): ResolutionStrategy { + TODO("Not yet implemented") + } + + override fun resolutionStrategy(closure: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun resolutionStrategy(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getState(): Configuration.State { + TODO("Not yet implemented") + } + + override fun isVisible(): Boolean { + TODO("Not yet implemented") + } + + override fun setVisible(visible: Boolean): Configuration { + TODO("Not yet implemented") + } + + override fun getExtendsFrom(): Set { + TODO("Not yet implemented") + } + + override fun setExtendsFrom(superConfigs: Iterable): Configuration { + TODO("Not yet implemented") + } + + override fun extendsFrom(vararg superConfigs: Configuration): Configuration { + TODO("Not yet implemented") + } + + override fun isTransitive(): Boolean { + TODO("Not yet implemented") + } + + override fun setTransitive(t: Boolean): Configuration { + TODO("Not yet implemented") + } + + override fun getDescription(): String? { + TODO("Not yet implemented") + } + + override fun setDescription(description: String?): Configuration { + TODO("Not yet implemented") + } + + override fun getHierarchy(): Set { + TODO("Not yet implemented") + } + + override fun resolve(): Set { + TODO("Not yet implemented") + } + + override fun getResolvedConfiguration(): ResolvedConfiguration { + TODO("Not yet implemented") + } + + override fun getBuildDependencies(): TaskDependency { + TODO("Not yet implemented") + } + + override fun getTaskDependencyFromProjectDependency( + useDependedOn: Boolean, + taskName: String + ): TaskDependency { + TODO("Not yet implemented") + } + + override fun getDependencies(): DependencySet { + TODO("Not yet implemented") + } + + override fun getAllDependencies(): DependencySet { + TODO("Not yet implemented") + } + + override fun getDependencyConstraints(): DependencyConstraintSet { + TODO("Not yet implemented") + } + + override fun getAllDependencyConstraints(): DependencyConstraintSet { + TODO("Not yet implemented") + } + + override fun getArtifacts(): PublishArtifactSet { + TODO("Not yet implemented") + } + + override fun getAllArtifacts(): PublishArtifactSet { + TODO("Not yet implemented") + } + + override fun getExcludeRules(): Set { + TODO("Not yet implemented") + } + + override fun exclude(excludeProperties: Map): Configuration { + TODO("Not yet implemented") + } + + override fun defaultDependencies(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun withDependencies(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getIncoming(): ResolvableDependencies { + TODO("Not yet implemented") + } + + override fun getOutgoing(): ConfigurationPublications { + TODO("Not yet implemented") + } + + override fun outgoing(action: Action) { + TODO("Not yet implemented") + } + + override fun copy(): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(): Configuration { + TODO("Not yet implemented") + } + + override fun copy(dependencySpec: Spec): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(dependencySpec: Spec): Configuration { + TODO("Not yet implemented") + } + + override fun copy(dependencySpec: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(dependencySpec: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun setCanBeConsumed(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun isCanBeConsumed(): Boolean { + TODO("Not yet implemented") + } + + override fun setCanBeResolved(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun isCanBeResolved(): Boolean { + TODO("Not yet implemented") + } + + override fun setCanBeDeclared(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun isCanBeDeclared(): Boolean { + TODO("Not yet implemented") + } + + override fun shouldResolveConsistentlyWith(versionsSource: Configuration): Configuration { + TODO("Not yet implemented") + } + + override fun disableConsistentResolution(): Configuration { + TODO("Not yet implemented") + } + + override fun getSingleFile(): File { + TODO("Not yet implemented") + } + + override fun getFiles(): Set { + TODO("Not yet implemented") + } + + override fun contains(file: File): Boolean { + TODO("Not yet implemented") + } + + override fun getAsPath(): String { + TODO("Not yet implemented") + } + + override fun plus(collection: FileCollection): FileCollection { + TODO("Not yet implemented") + } + + override fun minus(collection: FileCollection): FileCollection { + TODO("Not yet implemented") + } + + override fun filter(filterClosure: Closure<*>): FileCollection { + TODO("Not yet implemented") + } + + override fun filter(filterSpec: Spec): FileCollection { + TODO("Not yet implemented") + } + + override fun isEmpty(): Boolean { + TODO("Not yet implemented") + } + + override fun getAsFileTree(): FileTree { + TODO("Not yet implemented") + } + + override fun getElements(): Provider> { + TODO("Not yet implemented") + } + + override fun addToAntBuilder( + builder: Any, + nodeName: String, + type: FileCollection.AntType + ) { + TODO("Not yet implemented") + } + + override fun addToAntBuilder(builder: Any, nodeName: String): Any { + TODO("Not yet implemented") + } + + override fun iterator(): MutableIterator { + TODO("Not yet implemented") + } + + override fun attributes(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getAttributes(): AttributeContainer { + TODO("Not yet implemented") + } + + override fun getName(): String { + TODO("Not yet implemented") + } +} \ No newline at end of file