Skip to content

Commit

Permalink
Add excludedModules and includedModules to config (#25)
Browse files Browse the repository at this point in the history
* Add excludePackages which does case insensitive substring search on moduleVersion

* Add test for excludedPackages

* use emptySet instead of setOf()

* Rename to excludedModules

* Add includedModules and extensive DependencyTraversal tests

* Switch to imperative language

* Consider group & name + improve LibYearPluginTest

* Remove log, accident

* Flesh out test

* Lint

* Rename props yet again after thinking more

* Introduce include/exclude logic and test
  • Loading branch information
Breefield authored Nov 17, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent a89e1a7 commit ad2703c
Showing 10 changed files with 292 additions and 10 deletions.
1 change: 1 addition & 0 deletions libyear-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@ dependencies {

testImplementation("com.squareup.okhttp3:mockwebserver:4.8.1")
testImplementation("org.mockito:mockito-core:3.7.7")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.7.0")
testImplementation("org.assertj:assertj-core:3.18.1")
Original file line number Diff line number Diff line change
@@ -36,6 +36,22 @@ internal class LibYearPluginTest {
.extracting { it?.outcome }.isEqualTo(TaskOutcome.FAILED)
}

@Test
fun testExcludedModules() {
setUpProject("excludedModules.gradle.kts")

val result = withGradleRunner("reportLibyears").build()
val output = result.output

assertThat(output)
.contains("from 3 dependencies")
.contains("org.apache.commons:commons-text")
.contains("org.apache.commons:commons-collections")
.contains("slf4j-simple")
.doesNotContain("slf4j-api")
assertThat(output).doesNotContain("unknown.package")
}

@Test
fun testReportLibyear() {
setUpProject("valid.gradle.kts")
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id("com.libyear.libyear-gradle-plugin")
java
}

repositories {
mavenCentral()
}

dependencies {
implementation("org.apache.commons:commons-text:1.9")
implementation("org.apache.commons:commons-collections4:4.4")
implementation("org.slf4j", "slf4j-api", "2.0.9")
implementation("org.slf4j", "slf4j-simple", "2.0.9")
}

libyear {
failOnError = false
validator = singleArtifactMustNotBeOlderThan(100.years)
excludedModules = setOf("org.slf4j*")
includedModules = setOf("*slf4j-simple")
}
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@ open class LibYearExtension {

var configurations: List<String> = defaultConfigurations

var excludedModules: Set<String> = emptySet()
var includedModules: Set<String> = emptySet()

// DSL for build script authors

val defaultConfigurations: List<String> get() = listOf("compileClasspath")
Original file line number Diff line number Diff line change
@@ -59,7 +59,14 @@ class LibYearPlugin : Plugin<Project> {
val ageOracle = createOracle(project, extension)
val validator = createValidator(project, extension)
val visitor = ValidatingVisitor(project.logger, ageOracle, validator, ValidationConfig(failOnError = extension.failOnError))
DependencyTraversal.visit(resolvableDependencies.resolutionResult.root, visitor, extension.maxTransitiveDepth)
DependencyTraversal.visit(
project.logger,
resolvableDependencies.resolutionResult.root,
visitor,
extension.maxTransitiveDepth,
extension.excludedModules,
extension.includedModules
)
maybeReportFailure(project.logger, validator)
}

Original file line number Diff line number Diff line change
@@ -15,7 +15,14 @@ open class LibYearReportTask : DefaultTask() {
.forEach {
val ageOracle = createOracle(project, extension)
val visitor = ReportingVisitor(project.logger, ageOracle)
DependencyTraversal.visit(it.incoming.resolutionResult.root, visitor, extension.maxTransitiveDepth)
DependencyTraversal.visit(
project.logger,
it.incoming.resolutionResult.root,
visitor,
extension.maxTransitiveDepth,
extension.excludedModules,
extension.includedModules
)
visitor.print()
visitor.saveReportToJson(project)
}
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory
* Adapter selection: the algorithm prefers to select an adapter by name of the repository Gradle has
* sourced the artifact from, see [adapters]. If none is present, [defaultAdapter] is queried.
*/
class DefaultVersionOracle(
open class DefaultVersionOracle(
private val defaultAdapter: VersionInfoAdapter,
private val adapters: Map<String, VersionInfoAdapter>,
private val repositories: Map<String, ArtifactRepository>
Original file line number Diff line number Diff line change
@@ -4,11 +4,18 @@ import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.result.ComponentResult
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.logging.Logger
import org.gradle.internal.impldep.com.google.common.annotations.VisibleForTesting

class DependencyTraversal private constructor(
private val logger: Logger,
private val visitor: DependencyVisitor,
private val maxTransitiveDepth: Int?
private val maxTransitiveDepth: Int?,
excludedModules: Set<String>,
includedModules: Set<String>
) {
private val excludedPatterns = excludedModules.map { it to it.wildcardToRegex() }
private val includedPatterns = includedModules.map { it to it.wildcardToRegex() }

private val seen = mutableSetOf<ComponentIdentifier>()

@@ -24,10 +31,11 @@ class DependencyTraversal private constructor(
if (!visitor.canContinue()) return

if (dependency is ResolvedDependencyResult) {
if (maxTransitiveDepth != null && depth > maxTransitiveDepth) {
continue
val group = dependency.selected.moduleVersion?.group.toString()
val name = dependency.selected.moduleVersion?.name.toString()
if (shouldIncludeModule("$group:$name", depth)) {
nextComponents.add(dependency.selected)
}
nextComponents.add(dependency.selected)
}
}

@@ -37,12 +45,55 @@ class DependencyTraversal private constructor(
}
}

@VisibleForTesting
fun shouldIncludeModule(
module: String,
depth: Int
): Boolean {
if (maxTransitiveDepth != null && depth > maxTransitiveDepth) {
return false
}

// Inclusions supersede exclusions
val matchedInclusion = includedPatterns.firstOrNull { pattern -> pattern.second.matches(module) }
val matchedExclusion = excludedPatterns.firstOrNull { pattern -> pattern.second.matches(module) }

val matchesInclusions = includedPatterns.isEmpty() || matchedInclusion != null
val matchesExclusions = matchedExclusion != null
val shouldIncludeModule = matchesInclusions && !matchesExclusions

if (!shouldIncludeModule) {
if (matchesExclusions) {
logger.info("Excluding $module because it matches ${matchedExclusion!!.first}")
} else {
logger.info("Excluding $module because it does not match inclusion list")
}
}

return shouldIncludeModule
}

companion object {

fun visit(
logger: Logger,
root: ResolvedComponentResult,
visitor: DependencyVisitor,
maxTransitiveDepth: Int? = null
): Unit = DependencyTraversal(visitor, maxTransitiveDepth).visit(root, depth = 0)
maxTransitiveDepth: Int? = null,
excludedModules: Set<String> = emptySet(),
includedModules: Set<String> = emptySet()
): Unit = DependencyTraversal(
logger,
visitor,
maxTransitiveDepth,
excludedModules,
includedModules
).visit(root, depth = 0)

@VisibleForTesting
fun String.wildcardToRegex(): Regex {
val globsToRegex = this.replace(".", "\\.").replace("*", ".*")
return "^$globsToRegex$".toRegex(RegexOption.IGNORE_CASE)
}
}
}
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ private data class ReportingInfo(
val latestVersion: String
)

class ReportingVisitor(
open class ReportingVisitor(
logger: Logger,
private val ageOracle: VersionOracle
) : DependencyVisitor(logger) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.libyear.traversal

import com.libyear.sourcing.DefaultVersionOracle
import com.libyear.traversal.DependencyTraversal.Companion.wildcardToRegex
import org.gradle.api.artifacts.ModuleVersionIdentifier
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class DependencyTraversalTest {
@Test
fun testVisitsDependencies() {
val project = ProjectBuilder.builder().build()
val visitorSpy = spy(ReportingVisitor(project.logger, mock<DefaultVersionOracle>()))

val rootComponent = mockResolvedComponentResult("root", "component")
val slf4jComponent = rootComponent.addDependency("org.slf4j", "slf4j")
val slf4jCoreComponent = rootComponent.addDependency("org.slf4j", "slf4j-core")
val slf4jCoreSubComponentA = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-a")
val slf4jCoreSubComponentB = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-b")

DependencyTraversal.visit(
project.logger,
rootComponent.selected,
visitorSpy,
10
)

verify(visitorSpy).visitComponentResult(eq(rootComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreSubComponentA.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreSubComponentB.selected))
}

@Test
fun testIncludedAndExcludedDependencies() {
val project = ProjectBuilder.builder().build()
val visitorSpy = spy(ReportingVisitor(project.logger, mock<DefaultVersionOracle>()))

val includedModules = setOf(
"org.slf4j*" // Will include all org.slf4j modules
)
val excludedModules = setOf(
"*core-b" // Will NOT include slf4j-core-b
)

val rootComponent = mockResolvedComponentResult("root", "component")
val slf4jComponent = rootComponent.addDependency("org.slf4j", "slf4j")
val slf4jCoreComponent = rootComponent.addDependency("org.slf4j", "slf4j-core")
val slf4jCoreSubComponentA = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-a")
val slf4jCoreSubComponentB = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-b")

DependencyTraversal.visit(
project.logger,
rootComponent.selected,
visitorSpy,
10,
excludedModules,
includedModules
)

verify(visitorSpy).visitComponentResult(eq(rootComponent.selected))
// Excluded:
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreSubComponentB.selected))
// Included
verify(visitorSpy).visitComponentResult(eq(slf4jComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreSubComponentA.selected))
}

@Test
fun testExcludeDependencies() {
val project = ProjectBuilder.builder().build()
val visitorSpy = spy(ReportingVisitor(project.logger, mock<DefaultVersionOracle>()))

val includedModules = emptySet<String>() // empty set means all are included
val excludedModules = setOf(
"*slf4j-core*" // Will NOT include core modules
)

val rootComponent = mockResolvedComponentResult("root", "component")
val slf4jComponent = rootComponent.addDependency("org.slf4j", "slf4j")
val slf4jCoreComponent = rootComponent.addDependency("org.slf4j", "slf4j-core")
val slf4jCoreSubComponentA = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-a")
val slf4jCoreSubComponentB = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-b")

DependencyTraversal.visit(
project.logger,
rootComponent.selected,
visitorSpy,
10,
excludedModules,
includedModules
)

verify(visitorSpy).visitComponentResult(eq(rootComponent.selected))
// Excluded:
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreComponent.selected))
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreSubComponentA.selected))
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreSubComponentB.selected))
// Included
verify(visitorSpy).visitComponentResult(eq(slf4jComponent.selected))
}

@Test
fun testMaxTransitiveDepth() {
val project = ProjectBuilder.builder().build()
val visitorSpy = spy(ReportingVisitor(project.logger, mock<DefaultVersionOracle>()))

val rootComponent = mockResolvedComponentResult("root", "component")
val slf4jComponent = rootComponent.addDependency("org.slf4j", "slf4j")
val slf4jCoreComponent = rootComponent.addDependency("org.slf4j", "slf4j-core")
val slf4jCoreSubComponentA = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-a")
val slf4jCoreSubComponentB = slf4jCoreComponent.addDependency("org.slf4j", "slf4j-core-b")

DependencyTraversal.visit(
project.logger,
rootComponent.selected,
visitorSpy,
0
)

// Root component doesn't count towards depth, it's just the starting point
verify(visitorSpy).visitComponentResult(eq(rootComponent.selected))
// These two nodes should be visited
verify(visitorSpy).visitComponentResult(eq(slf4jComponent.selected))
verify(visitorSpy).visitComponentResult(eq(slf4jCoreComponent.selected))
// We stop traversing after root + 1 depth, these nodes are not visited:
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreSubComponentA.selected))
verify(visitorSpy, never()).visitComponentResult(eq(slf4jCoreSubComponentB.selected))
}

@Test
fun testWildcardToRegex() {
assertEquals("com.libyear.*".wildcardToRegex().toString(), "^com\\.libyear\\..*$")
assertEquals("com.libyear".wildcardToRegex().toString(), "^com\\.libyear$")
assertEquals("*.libyear".wildcardToRegex().toString(), "^.*\\.libyear$")
assertEquals("*.libyear.*".wildcardToRegex().toString(), "^.*\\.libyear\\..*$")
assertEquals("*.libyear.*-core".wildcardToRegex().toString(), "^.*\\.libyear\\..*-core$")
assertEquals("*.libyear.*-core*".wildcardToRegex().toString(), "^.*\\.libyear\\..*-core.*$")
assertEquals("**".wildcardToRegex().toString(), "^.*.*$")
}

private fun mockResolvedComponentResult(group: String, name: String): ResolvedDependencyResult {
val componentResult = mock<ResolvedComponentResult>().apply {
val version = mock<ModuleVersionIdentifier>().apply {
whenever(this.group).thenReturn(group)
whenever(this.name).thenReturn(name)
}
whenever(this.moduleVersion).thenReturn(version)
whenever(this.id).thenReturn(ComponentIdentifier { "$group:$name" })
}
return mock<ResolvedDependencyResult>().apply {
whenever(this.selected).thenReturn(componentResult)
}
}

private fun ResolvedDependencyResult.addDependency(group: String, name: String): ResolvedDependencyResult {
val parent = this@addDependency.selected
return mockResolvedComponentResult(group, name).apply {
val newDependencies = (parent.dependencies + this).toMutableSet()
whenever(parent.dependencies).thenReturn(newDependencies)
}
}
}

0 comments on commit ad2703c

Please sign in to comment.