Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions projectguard/api/projectguard.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ public final class com/rubensousa/projectguard/plugin/GuardRule {

public abstract interface class com/rubensousa/projectguard/plugin/GuardScope {
public abstract fun applyRule (Lcom/rubensousa/projectguard/plugin/GuardRule;)V
public fun deny (Ljava/lang/String;)V
public fun deny (Ljava/lang/String;Lgroovy/lang/Closure;)V
public abstract fun deny (Ljava/lang/String;Lorg/gradle/api/Action;)V
public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;)V
public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lgroovy/lang/Closure;)V
public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lorg/gradle/api/Action;)V
public fun deny (Lorg/gradle/api/provider/Provider;)V
public fun deny (Lorg/gradle/api/provider/Provider;Lgroovy/lang/Closure;)V
public abstract fun deny (Lorg/gradle/api/provider/Provider;Lorg/gradle/api/Action;)V
public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V
public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lorg/gradle/api/Action;ILjava/lang/Object;)V
public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/Action;ILjava/lang/Object;)V
}

public abstract interface class com/rubensousa/projectguard/plugin/ModuleRestrictionScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,63 @@ import org.gradle.api.internal.catalog.DelegatingProjectDependency
import org.gradle.api.provider.Provider
import org.gradle.util.internal.ConfigureUtil

private val emptyDenyScope = Action<DenyScope> { }

interface GuardScope {

fun deny(
dependencyPath: String,
action: Action<DenyScope> = Action<DenyScope> { },
action: Action<DenyScope>,
)

fun deny(
provider: Provider<MinimalExternalModuleDependency>,
action: Action<DenyScope>,
)

fun applyRule(rule: GuardRule)

fun deny(
dependencyDelegation: DelegatingProjectDependency,
action: Action<DenyScope> = Action<DenyScope> { },
) = deny(dependencyPath = dependencyDelegation.path, action = action)
action: Action<DenyScope>,
) {
deny(dependencyPath = dependencyDelegation.path, action = action)
}

// Required for groovy compatibility
fun deny(dependencyPath: String) {
deny(dependencyPath, emptyDenyScope)
}

fun deny(dependencyDelegation: DelegatingProjectDependency) {
deny(dependencyDelegation.path, emptyDenyScope)
}

fun deny(
provider: Provider<MinimalExternalModuleDependency>,
action: Action<DenyScope> = Action<DenyScope> { },
)
) {
deny(provider, emptyDenyScope)
}

fun deny(
dependencyDelegation: DelegatingProjectDependency,
closure: Closure<DenyScope>,
) {
deny(dependencyDelegation.path, ConfigureUtil.configureUsing(closure))
}

// Required for groovy compatibility
fun deny(
dependencyPath: String,
closure: Closure<DenyScope>
closure: Closure<DenyScope>,
) {
deny(dependencyPath, ConfigureUtil.configureUsing(closure))
}

// Required for groovy compatibility
fun deny(
provider: Provider<MinimalExternalModuleDependency>,
closure: Closure<DenyScope>
closure: Closure<DenyScope>,
) {
deny(provider, ConfigureUtil.configureUsing(closure))
}

fun applyRule(rule: GuardRule)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2026 Rúben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.rubensousa.projectguard.plugin

import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File

class GroovyIntegrationTest {

@get:Rule
val temporaryFolder = TemporaryFolder()

private val pluginRunner = PluginRunner(temporaryFolder)
private lateinit var rootBuildFile: File

@Before
fun setup() {
rootBuildFile = temporaryFolder.newFile("build.gradle")
rootBuildFile.writeText(
"""
plugins {
id 'com.rubensousa.projectguard'
}
subprojects {
apply plugin: 'java-library'
apply plugin: 'com.rubensousa.projectguard'
}

""".trimIndent()
)
}

@Test
fun `check fails for guard restriction`() {
// given
val module = "consumer"
val dependency = "libraryA"
val reason = "Bla bla"
pluginRunner.createModule(module)
pluginRunner.createModule(dependency)
rootBuildFile.appendText(
"""
projectGuard {
guard(":$module") {
deny(":$dependency") {
reason("$reason")
}
}
}
""".trimIndent()
)

// when
pluginRunner.addDependency(from = module, to = dependency)

// then
pluginRunner.assertCheckFails(module)
pluginRunner.assertTaskOutputContains(reason)
}

@Test
fun `check succeeds for guard restriction that does not match`() {
// given
val module = "consumer"
val dependency = "libraryA"
pluginRunner.createModule(module)
pluginRunner.createModule(dependency)
rootBuildFile.appendText(
"""
projectGuard {
guard(":$module") {
deny(":another")
}
}
""".trimIndent()
)

// when
pluginRunner.addDependency(from = module, to = dependency)

// then
pluginRunner.assertCheckSucceeds(module)
pluginRunner.assertTaskOutputContains("No fatal matches found")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@

package com.rubensousa.projectguard.plugin

import com.google.common.truth.Truth.assertThat
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand All @@ -31,19 +27,12 @@ class PluginIntegrationTest {
@get:Rule
val temporaryFolder = TemporaryFolder()

private val pluginRunner = PluginRunner(temporaryFolder)
private lateinit var rootBuildFile: File
private lateinit var settingsFile: File
private lateinit var gradleRunner: GradleRunner
private val checkTask = ":consumer:projectGuardCheck"

@Before
fun setup() {
settingsFile = temporaryFolder.newFile("settings.gradle.kts")
rootBuildFile = temporaryFolder.newFile("build.gradle.kts")
gradleRunner = GradleRunner.create()
.withProjectDir(temporaryFolder.root)
.withPluginClasspath()
.withGradleVersion("8.13")
rootBuildFile.writeText(
"""
plugins {
Expand All @@ -61,9 +50,9 @@ class PluginIntegrationTest {
@Test
fun `transitive dependencies are found and cause check to fail`() {
// given
createModule("consumer")
createModule("libraryA")
createModule("libraryB")
pluginRunner.createModule("consumer")
pluginRunner.createModule("libraryA")
pluginRunner.createModule("libraryB")

rootBuildFile.appendText(
"""
Expand All @@ -75,21 +64,19 @@ class PluginIntegrationTest {
""".trimIndent()
)

addDependency(from = "consumer", to = "libraryA")
addDependency(from = "libraryA", to = "libraryB")

// when
val result = runCheckTask(expectSuccess = false)
pluginRunner.addDependency(from = "consumer", to = "libraryA")
pluginRunner.addDependency(from = "libraryA", to = "libraryB")

// then
assertThat(result.task(checkTask)?.outcome!!).isEqualTo(TaskOutcome.FAILED)
pluginRunner.assertCheckFails("consumer")
}

@Test
fun `direct dependencies are found and cause check to fail`() {
// given
createModule("consumer")
createModule("library")
pluginRunner.createModule("consumer")
pluginRunner.createModule("library")

rootBuildFile.appendText(
"""
Expand All @@ -99,20 +86,18 @@ class PluginIntegrationTest {
""".trimIndent()
)

addDependency(from = "consumer", to = "library")

// when
val result = runCheckTask(expectSuccess = false)
pluginRunner.addDependency(from = "consumer", to = "library")

// then
assertThat(result.task(checkTask)?.outcome!!).isEqualTo(TaskOutcome.FAILED)
pluginRunner.assertCheckFails("consumer")
}

@Test
fun `check task succeeds when no matches are found`() {
// given
createModule("consumer")
createModule("library")
pluginRunner.createModule("consumer")
pluginRunner.createModule("library")

rootBuildFile.appendText(
"""
Expand All @@ -122,38 +107,10 @@ class PluginIntegrationTest {
""".trimIndent()
)

addDependency(from = "consumer", to = "library")

// when
val result = runCheckTask(expectSuccess = true)
pluginRunner.addDependency(from = "consumer", to = "library")

// then
assertThat(result.task(checkTask)!!.outcome).isEqualTo(TaskOutcome.SUCCESS)
}

private fun runCheckTask(expectSuccess: Boolean): BuildResult {
val runner = gradleRunner.withArguments(checkTask)
return if (expectSuccess) {
runner.build()
} else {
runner.buildAndFail()
}
}

private fun createModule(name: String) {
temporaryFolder.newFolder(name)
temporaryFolder.newFile("$name/build.gradle.kts")
settingsFile.appendText("\ninclude(\":$name\")")
pluginRunner.assertCheckSucceeds("consumer")
}

private fun addDependency(from: String, to: String, configuration: String = "implementation") {
temporaryFolder.getRoot().resolve("$from/build.gradle.kts").appendText(
"""
dependencies {
$configuration(project(":$to"))
}
""".trimIndent()
)
}

}
Loading