diff --git a/build.gradle b/build.gradle index 1b210f3bd..b92cfbd87 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { classpath 'com.palantir.jakartapackagealignment:jakarta-package-alignment:0.6.0' classpath 'com.palantir.gradle.jdks:gradle-jdks:0.69.0' classpath 'com.palantir.gradle.jdkslatest:gradle-jdks-latest:0.23.0' - classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.19.0' + classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.23.0' classpath 'com.palantir.gradle.externalpublish:gradle-external-publish-plugin:1.26.0' classpath 'com.palantir.gradle.failure-reports:gradle-failure-reports:1.17.0' classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.81.0' @@ -74,6 +74,8 @@ dependencies { annotationProcessor "org.immutables:value" compileOnly "org.immutables:value::annotations" + testImplementation 'com.palantir.gradle.plugintesting:gradle-plugin-testing-junit' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.withType(Test) { diff --git a/src/test/groovy/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.groovy deleted file mode 100644 index 6f8ab4d09..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.groovy +++ /dev/null @@ -1,163 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.TaskOutcome - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS - -class CheckOverbroadConstraintsIntegrationSpec extends IntegrationSpec { - - def setup() { - buildFile << """ - plugins { - id 'com.palantir.versions-lock' - id 'com.palantir.versions-props' - } - """.stripIndent(true) - file('versions.props') - file('versions.lock') - } - - def buildSucceed() { - BuildResult result = runTasks('checkOverbroadConstraints') - result.task(':checkOverbroadConstraints').outcome == TaskOutcome.SUCCESS - result - } - - void buildAndFailWith(String error) { - BuildResult result = runTasksAndFail('checkOverbroadConstraints') - assert result.output.contains(error) - } - - def buildWithFixWorks() { - def currentVersionsProps = file('versions.props').readLines() - // Check that running with --fix modifies the file - runTasks('checkOverbroadConstraints', '--fix') - assert file('versions.props').readLines() != currentVersionsProps - - // Check that the task now succeeds - runTasks('checkOverbroadConstraints') - } - - def '#gradleVersionNumber: Task should run as part of :check'() { - setup: - gradleVersion = gradleVersionNumber - - expect: - def result = runTasks('check', '-m') - result.output.contains(':checkOverbroadConstraints') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: All versions are pinned'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - com.fasterxml.jackson.*:* = 2.9.3 - com.fasterxml.jackson.core:jackson-annotations = 2.9.5 - """.stripIndent(true) - file('versions.lock').text = """ - com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) - com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) - """.stripIndent(true).trim() - - then: - buildSucceed() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Not all versions are pinned throws error and fix works and no new line is added'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - com.fasterxml.jackson.*:* = 2.9.3 - """.stripIndent(true) - file('versions.lock').text = """ - com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) - com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) - """.stripIndent(true).trim() - - then: - buildAndFailWith(String.join( - "\n", - "Over-broad version constraints found in versions.props.", - "Over-broad constraints often arise due to wildcards in versions.props", - "which apply to more dependencies than they should, this can lead to slow builds.", - "The following additional pins are recommended:", - "com.fasterxml.jackson.core:jackson-annotations = 2.9.5", - "com.fasterxml.jackson.core:jackson-core = 2.9.3", - "", - "Run ./gradlew checkOverbroadConstraints --fix to add them.", - "See https://github.com/palantir/gradle-consistent-versions?tab=readme-ov-file#gradlew-checkoverbroadconstraints for details")) - buildWithFixWorks() - file('versions.props').text == """ - com.fasterxml.jackson.core:jackson-annotations = 2.9.5 - com.fasterxml.jackson.core:jackson-core = 2.9.3 - """.stripIndent(true) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Fixes are inserted in the correct locations new lines are maintained'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - - com.random:* = 1.0.0 - com.fasterxml.jackson.*:* = 2.9.3 - - # A random comment - org.different:artifact = 2.0.0 - - """.stripIndent(true) - file('versions.lock').text = """ - com.random:random:1.0.0 (2 constraints: abcdef0) - org.different:artifact:2.0.0 (2 constraints: abcdef0) - com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) - com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) - """.stripIndent(true).trim() - - then: - buildWithFixWorks() - file('versions.props').text == """ - - com.random:* = 1.0.0 - com.fasterxml.jackson.core:jackson-annotations = 2.9.5 - com.fasterxml.jackson.core:jackson-core = 2.9.3 - - # A random comment - org.different:artifact = 2.0.0 - - """.stripIndent(true) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.groovy deleted file mode 100644 index ecbea00f3..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.groovy +++ /dev/null @@ -1,226 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.TaskOutcome - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS - -class CheckUnusedConstraintIntegrationSpec extends IntegrationSpec { - - def setup() { - buildFile << """ - plugins { - id 'java' - id 'com.palantir.versions-lock' - id 'com.palantir.versions-props' - } - - repositories { - mavenCentral() - maven { url "${projectDir.toURI()}/maven" } - } - - // Get rid of deprecation warnings for Gradle 7+ - versionRecommendations { - excludeConfigurations 'compile', 'runtime', 'testCompile', 'testRuntime' - } - """.stripIndent(true) - file('gradle.properties').text = """ - ignoreLockFile=true - """.stripIndent(true) - createFile('versions.props') - } - - def buildSucceed() { - BuildResult result = runTasks('checkUnusedConstraints') - result.task(':checkUnusedConstraints').outcome == TaskOutcome.SUCCESS - result - } - - void buildAndFailWith(String error) { - BuildResult result = runTasksAndFail('checkUnusedConstraints') - assert result.output.contains(error) - } - - def buildWithFixWorks() { - def currentVersionsProps = file('versions.props').readLines() - // Check that running with --fix modifies the file - runTasks('checkUnusedConstraints', '--fix') - assert file('versions.props').readLines() != currentVersionsProps - - // Check that the task now succeeds - runTasks('checkUnusedConstraints') - } - - def '#gradleVersionNumber: checkVersionsProps does not resolve artifacts'() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - dependencies { - implementation 'com.palantir.product:foo:1.0.0' - } - """.stripIndent(true) - file("versions.props").text = "" - - // We're not producing a jar for this dependency, so artifact resolution would fail - file('maven/com/palantir/product/foo/1.0.0/foo-1.0.0.pom') << - pomWithJarPackaging("com.palantir.product", "foo", "1.0.0") - - expect: - buildSucceed() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Task should run as part of :check'() { - setup: - gradleVersion = gradleVersionNumber - - expect: - def result = runTasks('check', '-m') - result.output.contains(':checkUnusedConstraints') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Version props conflict should succeed'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - com.fasterxml.jackson.*:* = 2.9.3 - com.fasterxml.jackson.core:jackson-annotations = 2.9.5 - """.stripIndent(true) - buildFile << """ - dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind' - }""".stripIndent(true) - - then: - buildSucceed() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Most specific matching version should win'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - org.slf4j:slf4j-api = 1.7.25 - org.slf4j:* = 1.7.20 - """.stripIndent(true) - - buildFile << """ - dependencies { - implementation 'org.slf4j:slf4j-api' - }""".stripIndent(true) - - then: - buildAndFailWith('There are unused pins in your versions.props: \n[org.slf4j:*]') - buildWithFixWorks() - file('versions.props').text.trim() == "org.slf4j:slf4j-api = 1.7.25" - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Most specific glob should win'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - org.slf4j:slf4j-* = 1.7.25 - org.slf4j:* = 1.7.20 - """.stripIndent(true) - - buildFile << """ - dependencies { - implementation 'org.slf4j:slf4j-api' - implementation 'org.slf4j:slf4j-jdk14' - }""".stripIndent(true) - - then: - buildAndFailWith('There are unused pins in your versions.props: \n[org.slf4j:*]') - buildWithFixWorks() - file('versions.props').text.trim() == "org.slf4j:slf4j-* = 1.7.25" - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Unused version should fail'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = "notused:atall = 42.42" - - then: - buildAndFailWith("There are unused pins in your versions.props") - buildWithFixWorks() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: Unused check should use exact matching'() { - setup: - gradleVersion = gradleVersionNumber - - when: - file('versions.props').text = """ - com.google.guava:guava-testlib = 23.0 - com.google.guava:guava = 22.0 - """.stripIndent(true) - buildFile << """ - dependencies { - implementation 'com.google.guava:guava' - implementation 'com.google.guava:guava-testlib' - }""".stripIndent(true) - - then: - buildSucceed() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - static String pomWithJarPackaging(String group, String artifact, String version) { - return """ - - - 4.0.0 - $group - $artifact - jar - $version - - - - """.stripIndent(true) - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/ConfigurationOnDemandSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/ConfigurationOnDemandSpec.groovy deleted file mode 100644 index 9a048860b..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/ConfigurationOnDemandSpec.groovy +++ /dev/null @@ -1,428 +0,0 @@ -/* - * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.TaskOutcome -import spock.lang.Unroll - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS -import static com.palantir.gradle.versions.PomUtils.makePlatformPom - -/** - * This tests the interaction of this plugin with Gradle's configuration-on-demand feature: - * https://docs.gradle.org/current/userguide/multi_project_configuration_and_execution.html#sec:configuration_on_demand - */ -@Unroll -class ConfigurationOnDemandSpec extends IntegrationSpec { - - static def PLUGIN_NAME = "com.palantir.consistent-versions" - private File mavenRepo - - /* - * Project structure (arrows indicate dependencies): - * - * upstream unrelated - * ^ ^ - * | | - * downstream1 downstream2 - */ - void setup() { - mavenRepo = generateMavenRepo( - 'com.example:dependency-of-upstream:1.2.3', - 'com.example:dependency-of-upstream:100.1.1', - 'com.example:dependency-of-downstream1:1.2.3', - 'com.example:dependency-of-downstream2:1.2.3', - 'com.example:dependency-of-unrelated:1.2.3', - 'com.example:dep-with-version-bumped-by-unrelated:1.0.0', - 'com.example:dep-with-version-bumped-by-unrelated:1.1.0', - 'com.example:transitive-test-dep:1.0.0', - 'com.example:transitive-test-dep:1.1.0', - 'com.example:transitive-test-dep:1.2.0', - ) - - makePlatformPom(mavenRepo, "org", "platform", "1.0") - - buildFile.text = """ - plugins { - id '${PLUGIN_NAME}' - } - allprojects { - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - } - subprojects { - tasks.register('writeClasspath') { - doLast { - println(configurations.runtimeClasspath.getFiles()) - } - } - } - - // Get rid of deprecation warnings for Gradle 7+ - versionRecommendations { - excludeConfigurations 'compile', 'runtime', 'testCompile', 'testRuntime' - } - """.stripIndent(true) - - file('versions.props').text = """ - com.example:dependency-of-upstream = 1.2.3 - com.example:dependency-of-downstream1 = 1.2.3 - com.example:dependency-of-downstream2 = 1.2.3 - com.example:dependency-of-unrelated = 1.2.3 - # 1.0.0 is a minimum, we expect this to be locked to 1.1.0 - com.example:dep-with-version-bumped-by-unrelated = 1.0.0 - """.stripIndent(true) - - addSubproject("upstream", """ - plugins { - id 'java' - } - println 'configuring upstream' - dependencies { - implementation 'com.example:dependency-of-upstream' - } - """.stripIndent(true)) - - addSubproject("downstream1", """ - plugins { - id 'java' - } - println 'configuring downstream1' - dependencies { - implementation project(':upstream') - implementation 'com.example:dependency-of-downstream1' - } - """.stripIndent(true)) - - addSubproject("downstream2", """ - plugins { - id 'java' - } - println 'configuring downstream2' - dependencies { - implementation project(':upstream') - implementation 'com.example:dependency-of-downstream2' - implementation 'com.example:dep-with-version-bumped-by-unrelated' - } - """.stripIndent(true)) - - addSubproject("unrelated", """ - plugins { - id 'java' - } - println 'configuring unrelated' - dependencies { - implementation 'com.example:dependency-of-unrelated' - implementation 'com.example:dep-with-version-bumped-by-unrelated:1.1.0' - } - """.stripIndent(true)) - - file("gradle.properties").text = """ - org.gradle.configureondemand=true - """.stripIndent(true) - } - - def '#gradleVersionNumber: can write locks'() { - setup: - gradleVersion = gradleVersionNumber - - when: - BuildResult result = runTasks('--write-locks') - - then: - result.output.contains('configuring upstream') - result.output.contains('configuring downstream1') - result.output.contains('configuring downstream2') - result.output.contains('configuring unrelated') - - new File(projectDir, "versions.lock").exists() - file("versions.lock").text.contains("com.example:dependency-of-upstream:1.2.3") - file("versions.lock").text.contains("com.example:dep-with-version-bumped-by-unrelated:1.1.0") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: can write locks when a task in one project is specified'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks(':downstream1:build', '--write-locks') - - then: - new File(projectDir, "versions.lock").exists() - file("versions.lock").text.contains("com.example:dependency-of-unrelated:1.2.3") - file("versions.lock").text.contains("com.example:dep-with-version-bumped-by-unrelated:1.1.0") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: applying the plugin does not force all projects to be configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - // Both absolute and relative formats work, as long as Gradle is run from the root project directory - BuildResult result1 = runTasks(':downstream1:build') - BuildResult result2 = runTasks('downstream1:build') - - then: - result1.output.contains('configuring upstream') - result1.output.contains('configuring downstream1') - !result1.output.contains('configuring downstream2') - !result1.output.contains('configuring unrelated') - - result2.output.contains('configuring upstream') - result2.output.contains('configuring downstream1') - !result2.output.contains('configuring downstream2') - !result2.output.contains('configuring unrelated') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: resolving a classpath does not force all projects to be configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasks(':downstream1:writeClasspath') - - then: - result.output.contains('configuring upstream') - result.output.contains('configuring downstream1') - !result.output.contains('configuring downstream2') - !result.output.contains('configuring unrelated') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: after lockfile is written, versions constraints due to non-configured projects are still respected'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasks(':downstream2:writeClasspath') - - then: - // Version used is 1.1.0 due to the unrelated project - result.output.contains('dep-with-version-bumped-by-unrelated-1.1.0.jar') - !result.output.contains('configured unrelated') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: transitive dependencies cause upstream projects to be configured sufficiently early'() { - setup: - gradleVersion = gradleVersionNumber - addSubproject("a", """ - plugins { id 'java' } - dependencies { - implementation 'com.example:transitive-test-dep:1.0.0' - } - """.stripIndent(true)) - addSubproject("b", """ - plugins { id 'java' } - dependencies { - implementation project(':a') - } - """.stripIndent(true)) - addSubproject("c", """ - plugins { id 'java' } - dependencies { - implementation project(':b') - } - tasks.register('writeClasspathOfA') { - doLast { - println project(':a').configurations.runtimeClasspath.files - } - } - """.stripIndent(true)) - addSubproject("u", """ - plugins { id 'java' } - dependencies { - implementation 'com.example:transitive-test-dep:1.1.0' - } - """.stripIndent(true)) - - when: - runTasks('--write-locks') - BuildResult result = runTasks(':c:writeClasspathOfA') - - then: - // Version used should be 1.1.0, indicating that the version.lock constraint was applied - result.output.contains('transitive-test-dep-1.1.0.jar') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: task dependencies cause upstream projects to be configured sufficiently early'() { - setup: - gradleVersion = gradleVersionNumber - addSubproject("a", """ - plugins { id 'java' } - dependencies { - implementation 'com.example:transitive-test-dep:1.0.0' - } - """.stripIndent(true)) - addSubproject("b", """ - tasks.register('foo') { - dependsOn ':a:writeClasspath' - } - """.stripIndent(true)) - addSubproject("c", """ - tasks.register('bar') { - dependsOn ':b:foo' - } - """.stripIndent(true)) - addSubproject("u", """ - plugins { id 'java' } - dependencies { - implementation 'com.example:transitive-test-dep:1.1.0' - } - """.stripIndent(true)) - - when: - runTasks('--write-locks') - BuildResult result = runTasks(':c:bar') - - then: - // Version used should be 1.1.0, indicating that the version.lock constraint was applied - result.output.contains('transitive-test-dep-1.1.0.jar') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: verification tasks pass when all projects are configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - // Note: Not specifying the project causes all projects to be configured regardless of CoD - BuildResult result = runTasks('checkUnusedConstraints', 'verifyLocks') - - then: - result.output.contains('configuring upstream') - result.task(':checkUnusedConstraints').outcome == TaskOutcome.SUCCESS - result.task(':verifyLocks').outcome == TaskOutcome.SUCCESS - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: checkUnusedConstraints fails and warns when not all projects are configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasksAndFail(':checkUnusedConstraints') - - then: - !result.output.contains('configuring upstream') - result.task(':checkUnusedConstraints').outcome == TaskOutcome.FAILED - result.output.contains("The gradle-consistent-versions checkUnusedConstraints task " + - "must have all projects configured to work accurately, but due to Gradle " + - "configuration-on-demand, not all projects were configured.") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: verifyLocks fails and warns when not all projects are configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasksAndFail(':verifyLocks') - - then: - !result.output.contains('configuring upstream') - result.task(':verifyLocks').outcome == TaskOutcome.FAILED - result.output.contains("All projects must have been configured for this task to work correctly, but due to " + - "Gradle configuration-on-demand, not all projects were configured.") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - // As failing tasks can't be considered UP-TO-DATE, we only need to check the case where the task passing - // is followed by the task running with incomplete configuration. - def '#gradleVersionNumber: verification tasks are not UP-TO-DATE when the set of configured projects differs'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - runTasks('build') - - then: - runTasksAndFail(':checkUnusedConstraints') - runTasksAndFail(':verifyLocks') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: the why task works when all projects are configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasks('why', '--hash=0805f935') - - then: - result.task(':why').outcome == TaskOutcome.SUCCESS - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: the why task somehow forces all projects to be configured'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - BuildResult result = runTasks(':why', '--hash=0805f935') - - then: - result.output.contains('configuring upstream') - result.output.contains('configuring downstream1') - result.output.contains('configuring downstream2') - result.output.contains('configuring unrelated') - result.task(':why').outcome == TaskOutcome.SUCCESS - result.output.contains('com.example:dependency-of-unrelated:1.2.3\n\tprojects -> 1.2.3') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/ConsistentVersionsPluginIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/ConsistentVersionsPluginIntegrationSpec.groovy deleted file mode 100644 index 9d8353684..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/ConsistentVersionsPluginIntegrationSpec.groovy +++ /dev/null @@ -1,619 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import com.fasterxml.jackson.databind.ObjectMapper -import org.gradle.util.GradleVersion -import spock.lang.Unroll - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS -import static com.palantir.gradle.versions.PomUtils.makePlatformPom - -@Unroll -class ConsistentVersionsPluginIntegrationSpec extends IntegrationSpec { - - static def PLUGIN_NAME = "com.palantir.consistent-versions" - private File mavenRepo - - void setup() { - mavenRepo = generateMavenRepo( - "ch.qos.logback:logback-classic:1.1.11 -> org.slf4j:slf4j-api:1.7.22", - "org.slf4j:slf4j-api:1.7.22", - "org.slf4j:slf4j-api:1.7.25", - "test-alignment:module-that-should-be-aligned-up:1.0", - "test-alignment:module-that-should-be-aligned-up:1.1", - "test-alignment:module-with-higher-version:1.1") - - makePlatformPom(mavenRepo, "org", "platform", "1.0") - - buildFile << """ - apply plugin: '${PLUGIN_NAME}' - allprojects { - tasks.register("resolveConfigurations", { - project.configurations.all { configuration -> - // Resolving these throws deprecation warnings for Gradle 8 - def deprecatedResolve = configuration.name == "default" || configuration.name == "archives" - if (deprecatedResolve || - (configuration.metaClass.respondsTo(configuration, "isCanBeResolved") && - !configuration.isCanBeResolved())) { - return - } - configuration.resolve() - } - }) - - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - } - - subprojects { - // Parallel 'resolveConfigurations' sometimes breaks unless we force the root one to run first. - tasks.named("resolveConfigurations", { it.mustRunAfter ":resolveConfigurations" }) - } - """.stripIndent(true) - - keepFiles = true - definePluginOutsideOfPluginBlock = true - } - - def '#gradleVersionNumber: can write locks using --write-locks'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasks('--write-locks') - - then: - new File(projectDir, "versions.lock").exists() - runTasks('resolveConfigurations') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: can write locks using writeVersionsLocks'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasksWithConfigurationCache('writeVersionsLocks') - - then: - new File(projectDir, "versions.lock").exists() - runTasks('resolveConfigurations') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: can write locks using abbreviated writeVersionsLocks'() { - setup: - gradleVersion = gradleVersionNumber - - when: - runTasksWithConfigurationCache('wVL') - - then: - new File(projectDir, "versions.lock").exists() - runTasks('resolveConfigurations') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: locks are consistent whether or not we do --write-locks for glob-forced direct dependency"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api' - runtimeOnly 'ch.qos.logback:logback-classic:1.1.11' // brings in slf4j-api 1.7.22 - } - - task resolve { doLast { configurations.runtimeClasspath.resolve() } } - '''.stripIndent(true) - - file('versions.props') << 'org.slf4j:* = 1.7.25' - - expect: - runTasks('resolve', '--write-locks') - runTasks('resolve') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: getVersion function works"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api' - } - - task demo { - doLast { println "demo=" + getVersion('org.slf4j:slf4j-api', configurations.compileClasspath) } - } - '''.stripIndent(true) - - // Pretend we have a lock file - file('versions.lock') << '' - - file('versions.props') << 'org.slf4j:* = 1.7.25' - - expect: - runTasks('demo').output.contains("demo=1.7.25") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: getVersion function works even when writing locks"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api' - } - - task demo { - doLast { println "demo=" + getVersion('org.slf4j:slf4j-api') } - } - '''.stripIndent(true) - - file('versions.props') << 'org.slf4j:* = 1.7.25' - - expect: - runTasks('demo', '--write-locks').output.contains("demo=1.7.25") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: virtual platform is respected across projects"() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject('foo', """ - apply plugin: 'java' - dependencies { - implementation 'test-alignment:module-that-should-be-aligned-up:1.0' - } - """.stripIndent(true)) - - addSubproject('bar', """ - apply plugin: 'java' - dependencies { - implementation 'test-alignment:module-with-higher-version:1.1' - } - """.stripIndent(true)) - - file('versions.props') << """ - # Just to create a platform around test-alignment:* - test-alignment:* = 1.0 - """.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - def expectedLock = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - test-alignment:module-that-should-be-aligned-up:1.1 (1 constraints: a5041a2c) - - test-alignment:module-with-higher-version:1.1 (1 constraints: a6041b2c) - """.stripIndent(true) - file('versions.lock').text == expectedLock - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: star dependencies in the absence of dependency versions"() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject('foo', """ - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api' - } - """.stripIndent(true)) - - file('versions.props') << """ - org.slf4j:* = 1.7.25 - """.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - def expectedLock = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - org.slf4j:slf4j-api:1.7.25 (1 constraints: 4105483b) - """.stripIndent(true) - file('versions.lock').text == expectedLock - - // Ensure that this is a required constraint - runTasks('why', '--hash', '4105483b').output.contains "projects -> 1.7.25" - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: writeLocks and verifyLocks work in the presence of versions props constraints"() { - setup: - gradleVersion = gradleVersionNumber - - makePlatformPom(mavenRepo, "org1", "platform", "1.0") - makePlatformPom(mavenRepo, "org2", "platform", "1.0") - - addSubproject('foo', """ - apply plugin: 'java' - - configurations { - other - } - - dependencies { - implementation 'org.slf4j:slf4j-api' - - rootConfiguration platform('org1:platform') - rootConfiguration platform('org2:platform') - } - - task resolveLockedConfigurations { - doLast { - configurations.compileClasspath.resolve() - configurations.runtimeClasspath.resolve() - } - } - - // This is to ensure that the platform deps are successfully resolvable on a non-locked configuration - task resolveNonLockedConfiguration { - doLast { - configurations.other.resolve() - } - } - """.stripIndent(true)) - - file('versions.props') << """ - org1:platform = 1.0 - org2:* = 1.0 - org.slf4j:slf4j-api = 1.7.25 - """.stripIndent(true) - - expect: - runTasks('--write-locks') - - file('versions.lock').text == """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - org.slf4j:slf4j-api:1.7.25 (1 constraints: 4105483b) - - org1:platform:1.0 (1 constraints: a5041a2c) - - org2:platform:1.0 (1 constraints: a5041a2c) - """.stripIndent(true) - - and: 'Ensure you can verify locks and resolve the actual locked configurations' - runTasks('verifyLocks', 'resolveLockedConfigurations', 'resolveNonLockedConfiguration') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: versions props contents do not get published as constraints"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - allprojects { - apply plugin: 'java' - } - """.stripIndent(true) - - String publish = """ - apply plugin: 'maven-publish' - publishing.publications { - maven(MavenPublication) { - from components.java - } - } - """.stripIndent(true) - - addSubproject('foo', """ - apply plugin: 'java' - $publish - dependencies { - implementation 'ch.qos.logback:logback-classic' - } - """.stripIndent(true)) - - file('versions.props') << """ - org.slf4j:* = 1.7.25 - ch.qos.logback:* = 1.1.11 - should:not-publish = 1.0 - """.stripIndent(true) - - if (GradleVersion.version(gradleVersionNumber) < GradleVersion.version("6.0")) { - settingsFile << """ - enableFeaturePreview('GRADLE_METADATA') - """.stripIndent(true) - } - - when: - runTasks('--write-locks', 'generateMetadataFileForMavenPublication') - - def logbackDep = new MetadataFile.Dependency( - group: 'ch.qos.logback', - module: 'logback-classic', - version: [requires: '1.1.11']) - def slf4jDep = new MetadataFile.Dependency( - group: 'org.slf4j', - module: 'slf4j-api', - version: [requires: '1.7.25']) - - then: "foo's metadata file has the right dependency constraints" - def fooMetadataFilename = new File(projectDir, "foo/build/publications/maven/module.json") - def fooMetadata = new ObjectMapper().readValue(fooMetadataFilename, MetadataFile) - - fooMetadata.variants == [ - new MetadataFile.Variant( - name: 'apiElements', - dependencies: null, - dependencyConstraints: [logbackDep, slf4jDep]), - new MetadataFile.Variant( - name: 'runtimeElements', - dependencies: [logbackDep], - dependencyConstraints: [logbackDep, slf4jDep]), - ] as Set - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: intransitive dependency on published configuration should not break realizing it later"() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject('source', """ - configurations { - transitive - intransitive - } - dependencies { - // This wrecks us - intransitive project(':target'), { transitive = false } - - transitive project(':target') - } - - task resolveIntransitively { - doLast { - configurations.intransitive.resolvedConfiguration - } - } - task resolveTransitively { - mustRunAfter resolveIntransitively - doLast { - configurations.transitive.resolvedConfiguration - } - } - """.stripIndent(true)) - - addSubproject('target', """ - apply plugin: 'java' - - dependencies { - // Test the lazy action on published configurations like apiElements, runtimeElements - // that copies over platform dependencies from rootConfiguration. - rootConfiguration platform("org:platform") - } - """.stripIndent(true)) - - file('versions.props') << """ - org:platform = 1.0 - """.stripIndent(true) - - // This is just for debugging - buildFile << """ - allprojects { - configurations.all { - incoming.beforeResolve { - println "Resolving: \$it" - } - } - } - """.stripIndent(true) - - runTasks('--write-locks') - - expect: - runTasks('resolveIntransitively', 'resolveTransitively') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - - def "#gradleVersionNumber: works with included builds"() { - setup: - gradleVersion = gradleVersionNumber - - // add included build - def includedBuild = directory("included-build") - settingsFile << """ - includeBuild '${includedBuild.name}' - """.stripIndent(true) - - // configure the included build - file("settings.gradle", includedBuild) << """ - rootProject.name = 'included-build' - - include 'innerA' - include 'innerB' - """.stripIndent(true) - - file ("build.gradle", includedBuild) << """ - buildscript { - repositories { - mavenCentral() - } - } - plugins { - id '${PLUGIN_NAME}' - } - allprojects { - group 'com.palantir.included-build' - version '1.0.0' - - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - } - - subprojects { - apply plugin: 'java' - apply plugin: 'maven-publish' - publishing { - repositories { - maven { - url = "${mavenRepo.absolutePath}" - } - } - - publications { - maven(MavenPublication) { - from components.java - } - } - } - } - - """.stripIndent(true) - def innerA = directory("innerA", includedBuild) - file("build.gradle", innerA) << """ - apply plugin: 'java' - - dependencies { - implementation 'org.slf4j:slf4j-api' - runtimeOnly 'ch.qos.logback:logback-classic:1.1.11' // brings in slf4j-api 1.7.22 - } - """.stripIndent(true) - - def innerB = directory("innerB", includedBuild) - file("build.gradle", innerB) << """ - apply plugin: 'java' - - dependencies { - implementation 'test-alignment:module-with-higher-version' - } - """.stripIndent(true) - - file('versions.props', includedBuild) << """ - org.slf4j:slf4j-api = 1.7.25 - test-alignment:* = 1.1 - """.stripIndent(true) - - // configure main build - buildFile << """ - apply plugin: 'java' - - group 'com.palantir.main-build' - version '1.2.3' - - apply plugin: 'maven-publish' - publishing { - repositories { - maven { - url = "${mavenRepo.absolutePath}" - } - } - - publications { - maven(MavenPublication) { - from components.java - } - } - } - - dependencies { - implementation 'test-alignment:module-that-should-be-aligned-up' - } - """.stripIndent(true) - - file('versions.props') << """ - test-alignment:* = 1.0 - """.stripIndent(true) - - expect: - runTasks('--write-locks') - - and: 'inner versions lock is expected' - file("versions.lock", includedBuild).text == """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - ch.qos.logback:logback-classic:1.1.11 (1 constraints: 36052a3b) - - org.slf4j:slf4j-api:1.7.25 (2 constraints: 7d12a137) - - test-alignment:module-with-higher-version:1.1 (1 constraints: a6041b2c) - """.stripIndent(true) - - and: 'root build: versions lock is expected' - file("versions.lock").text == """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - test-alignment:module-that-should-be-aligned-up:1.0 (1 constraints: a5041a2c) - """.stripIndent(true) - - when: 'we add a dependencies on the inner build' - buildFile << """ - dependencies { - implementation 'com.palantir.included-build:innerA' - implementation 'com.palantir.included-build:innerB' - } - """.stripIndent(true) - - then: 'build succeeds' - runTasks('--write-locks') - - println runTasks( - ':publishMavenPublicationToMavenRepository', - ':included-build:innerA:publishMavenPublicationToMavenRepository', - ':included-build:innerB:publishMavenPublicationToMavenRepository').output - - and: 'root build: dependency is bumped - there is a difference in resolution between Gradle versions hence why we do not compare contents directly' - String rootVersionsLock = file("versions.lock").text - rootVersionsLock.contains "ch.qos.logback:logback-classic:1.1.11 (1 constraints: 36052a3b)" - rootVersionsLock.contains "test-alignment:module-that-should-be-aligned-up:1.1 (1 constraints: a5041a2c)" - rootVersionsLock.contains "test-alignment:module-with-higher-version:1.1 (1 constraints: a6041b2c)" - - where: - gradleVersionNumber << GRADLE_VERSIONS - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/IntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/IntegrationSpec.groovy deleted file mode 100644 index 29815fd33..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/IntegrationSpec.groovy +++ /dev/null @@ -1,61 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import groovy.transform.CompileStatic -import nebula.test.IntegrationTestKitSpec -import nebula.test.dependencies.DependencyGraph -import nebula.test.dependencies.GradleDependencyGenerator - -class IntegrationSpec extends IntegrationTestKitSpec { - void setup() { - keepFiles = true - debug = true - settingsFile.createNewFile() - } - - @CompileStatic - protected File generateMavenRepo(String... graph) { - DependencyGraph dependencyGraph = new DependencyGraph(graph) - GradleDependencyGenerator generator = new GradleDependencyGenerator( - dependencyGraph, new File(projectDir, "build/testrepogen").toString()) - return generator.generateTestMavenRepo() - } - - - /** - * Runs the specified tasks twice with configuration cache and verifies cache behavior. - * Returns true if the configuration cache was properly used on the second run. - */ - boolean runTasksWithConfigurationCache(String... tasks) { - def firstRun = createRunner(tasks + ['--configuration-cache'] as String[]).build() - assert firstRun.output.contains('Configuration cache entry stored.'), - "Expected first run to store configuration cache, but output was: ${firstRun.output}" - - def secondRun = createRunner(tasks + ['--configuration-cache'] as String[]).build() - assert secondRun.output.contains('Configuration cache entry reused.'), - "Expected second run to reuse configuration cache, but output was: ${secondRun.output}" - - File configCacheDir = new File(projectDir, ".gradle/configuration-cache") - if (configCacheDir.exists()) { - configCacheDir.deleteDir() - } - assert !configCacheDir.exists(), "Configuration cache directory was not deleted" - - return true - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.groovy deleted file mode 100644 index b8d92efa4..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.groovy +++ /dev/null @@ -1,124 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import org.gradle.testkit.runner.TaskOutcome -import spock.lang.IgnoreIf - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS - -/** - * https://github.com/palantir/sls-packaging does some funky stuff when resolving inter-project dependencies for the - * purposes of detecting published recommended product dependencies, so we want to make double sure that GCV doesn't - * accidentally break it. - */ -class SlsPackagingCompatibilityIntegrationSpec extends IntegrationSpec { - - static def PLUGIN_NAME = "com.palantir.consistent-versions" - - void setup() { - File mavenRepo = generateMavenRepo( - "org.slf4j:slf4j-api:1.7.24", - ) - buildFile << """ - buildscript { - repositories { - mavenCentral() - } - } - plugins { - id '${PLUGIN_NAME}' - id 'com.palantir.sls-java-service-distribution' version '7.31.0' apply false - } - allprojects { - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - } - """.stripIndent(true) - } - - @IgnoreIf( - reason = """ - sls-packaging is creating a configuration as part of a task input, which is happening far too late. \ - Once gradle has done a resolution, it will not look at any new Configurations that have popped up \ - since then. See https://github.com/palantir/gradle-consistent-versions/pull/1443 for more details.""", - value = { data.gradleVersionNumber.startsWith("9") }) - def '#gradleVersionNumber can consume recommended product dependencies project'() { - setup: - gradleVersion = gradleVersionNumber - - file("versions.props") << """ - org.slf4j:* = 1.7.24 - """.stripIndent(true) - - buildFile << """ - allprojects { - version = '1.0.0' - } - """.stripIndent(true) - - addSubproject('api', """ - apply plugin: 'java' - apply plugin: 'com.palantir.sls-recommended-dependencies' - - dependencies { - implementation 'org.slf4j:slf4j-api' - } - - recommendedProductDependencies { - productDependency { - productGroup = 'org' - productName = 'product' - minimumVersion = '1.1.0' - maximumVersion = '1.x.x' - } - } - """.stripIndent(true)) - - addSubproject('service', """ - apply plugin: 'java' - apply plugin: 'com.palantir.sls-java-service-distribution' - - dependencies { - // Gets picked up by the productDependenciesConfig which is runtimeClasspath - implementation project(':api') - } - """.stripIndent(true)) - - expect: - def wroteLocks = runTasks('--write-locks') - // Maybe this is a bit too much but for a fixed version of sls-packaging, we expect this to not change - wroteLocks.tasks(TaskOutcome.SUCCESS).collect { it.path } as Set == [ - ':api:compileRecommendedProductDependencies', - ':api:processResources', - ':service:mergeDiagnosticsJson', - ':service:resolveProductDependencies', - ':service:createManifest', - ':api:classes', - ':api:configureProductDependencies', - ':api:jar', - ':service:jar' - ] as Set - - runTasks('createManifest', 'verifyLocks') - - where: - gradleVersionNumber << GRADLE_VERSIONS - - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.groovy deleted file mode 100644 index 15eabba38..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.groovy +++ /dev/null @@ -1,84 +0,0 @@ -/* - * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import groovy.xml.XmlNodePrinter -import nebula.test.IntegrationSpec - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS - -class VersionPropsIdeaPluginIntegrationSpec extends IntegrationSpec { - - def setup() { - //language=gradle - buildFile << """ - repositories { - maven { - url 'https://test' - } - maven { - url 'https://demo/' - } - mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } } - } - - apply plugin: 'com.palantir.version-props-idea' - apply plugin: 'idea' - """.stripIndent(true) - - def ideaDir = new File(projectDir, '.idea') - ideaDir.mkdirs() - } - - def "#gradleVersionNumber: plugin creates gcv-maven-repositories.xml file in .idea folder"() { - setup: - gradleVersion = gradleVersionNumber - when: 'we run the first time' - runTasksSuccessfully('-Didea.active=true') - - then: 'we generate the correct config' - def repoFile = new File(projectDir, '.idea/gcv-maven-repositories.xml') - repoFile.exists() - - // language=xml - def expectedXml = ''' - - - - - - '''.stripIndent(true).trim() - - def projectNode = new XmlParser().parse(repoFile) - nodeToXmlString(projectNode) == expectedXml - - when: 'we run the second time' - def secondRun = runTasksSuccessfully('-Didea.active=true') - - then: "if nothing has changed, the task is then up-to-date" - secondRun.wasUpToDate(":writeMavenRepositories") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - private static String nodeToXmlString(debugRunConf) { - ByteArrayOutputStream baos = new ByteArrayOutputStream() - new XmlNodePrinter(new PrintWriter(baos)).print(debugRunConf) - return baos.toString().trim() - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy deleted file mode 100644 index 8744d10c4..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy +++ /dev/null @@ -1,1128 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import com.fasterxml.jackson.databind.ObjectMapper -import nebula.test.dependencies.DependencyGraph -import nebula.test.dependencies.GradleDependencyGenerator -import one.util.streamex.StreamEx -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.TaskOutcome -import org.gradle.util.GradleVersion -import spock.lang.Unroll - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS -import static com.palantir.gradle.versions.PomUtils.makePlatformPom - -@Unroll -class VersionsLockPluginIntegrationSpec extends IntegrationSpec { - - static def PLUGIN_NAME = "com.palantir.versions-lock" - - void setup() { - File mavenRepo = generateMavenRepo( - "ch.qos.logback:logback-classic:1.2.3 -> org.slf4j:slf4j-api:1.7.25", - "org.slf4j:slf4j-api:1.7.11", - "org.slf4j:slf4j-api:1.7.20", - "org.slf4j:slf4j-api:1.7.24", - "org.slf4j:slf4j-api:1.7.25", - "junit:junit:4.10", - "org:test-dep-that-logs:1.0 -> org.slf4j:slf4j-api:1.7.11", - "org:another-transitive-dependency:3.2.1", - "org:another-direct-dependency:1.2.3 -> org:another-transitive-dependency:3.2.1", - ) - makePlatformPom(mavenRepo, "org", "platform", "1.0") - - buildFile << """ - buildscript { - repositories { - mavenCentral() - } - } - plugins { - id '${PLUGIN_NAME}' - } - allprojects { - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - - task resolveConfigurations { - doLast { - if (pluginManager.hasPlugin('java')) { - configurations.compileClasspath.resolve() - configurations.runtimeClasspath.resolve() - } - } - } - } - """ - } - - def '#gradleVersionNumber: can write locks'() { - setup: - gradleVersion = gradleVersionNumber - - expect: - runTasks('--write-locks') - new File(projectDir, "versions.lock").exists() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - private def standardSetup() { - addSubproject('foo', ''' - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api:1.7.24' - } - '''.stripIndent(true)) - - addSubproject('bar', ''' - apply plugin: 'java' - dependencies { - implementation "org.slf4j:slf4j-api:${project.bar_version}" - } - '''.stripIndent(true)) - file("gradle.properties") << "bar_version=1.7.11" - - addSubproject('forced', ''' - apply plugin: 'java' - dependencies { - implementation "org.slf4j:slf4j-api" - } - configurations.all { - resolutionStrategy { - force "org.slf4j:slf4j-api:1.7.20" - } - } - '''.stripIndent(true)) - } - - def '#gradleVersionNumber: cannot resolve without a root lock file'() { - setup: - gradleVersion = gradleVersionNumber - standardSetup() - - expect: - def result = runTasksAndFail('resolveConfigurations') - result.output.readLines().any { - it.matches ".*Root lock file '([^']+)' doesn't exist, please run.*" - } - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: can resolve without a root lock file if lock file is ignored'() { - setup: - gradleVersion = gradleVersionNumber - standardSetup() - - expect: - runTasks('resolveConfigurations', '-PignoreLockFile') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: consolidates subproject dependencies'() { - def expectedError = "Locked by versions.lock" - setup: - gradleVersion = gradleVersionNumber - standardSetup() - buildFile << ''' - subprojects { - configurations.matching { it.name == 'runtimeClasspath' }.all { - resolutionStrategy.activateDependencyLocking() - } - } - '''.stripIndent(true) - - when: "I write locks" - runTasks('resolveConfigurations', '--write-locks') - - then: "Lock files are consistent with version resolved at root" - file("versions.lock").text.readLines().any { it.startsWith('org.slf4j:slf4j-api:1.7.24') } - - ["foo", "bar"].each { verifyLockfile(file(it), "org.slf4j:slf4j-api:1.7.24") } - - then: "Manually forced version overrides unified dependency" - verifyLockfile(file("forced"), "org.slf4j:slf4j-api:1.7.20") - - then: "I can resolve configurations" - runTasks('resolveConfigurations') - - when: "I make bar's version constraint incompatible with the force" - BuildResult incompatible = runTasksAndFail("-Pbar_version=1.7.25", 'resolveConfigurations') - - then: "Resolution fails" - incompatible.output.contains(expectedError) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: works on just root project'() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' // brings in slf4j-api 1.7.25 - } - '''.stripIndent(true) - - expect: - runTasks('--write-locks') - def lines = file('versions.lock').readLines() - lines.contains('ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935)') - lines.contains('org.slf4j:slf4j-api:1.7.25 (1 constraints: 400d4d2a)') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: get a conflict even if no lock files applied'() { - def expectedError = "Locked by versions.lock" - setup: - gradleVersion = gradleVersionNumber - standardSetup() - - when: "I write locks" - runTasks('--write-locks') - - then: "Root lock file has expected resolution result" - file("versions.lock").text.readLines().any { it.contains('org.slf4j:slf4j-api:1.7.24') } - - then: "I can resolve configurations" - runTasks('resolveConfigurations') - - when: "I make bar's version constraint incompatible with the force" - def incompatible = runTasksAndFail("-Pbar_version=1.7.25", 'resolveConfigurations') - - then: "Resolution fails" - incompatible.output.contains(expectedError) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: fails fast when subproject that is depended on has same name as root project'() { - setup: - gradleVersion = gradleVersionNumber - - def expectedError = "This plugin doesn't work if the root project shares both group and name with a subproject" - buildFile << """ - allprojects { - group 'same' - } - """.stripIndent(true) - settingsFile << "rootProject.name = 'foobar'\n" - addSubproject("foobar", ''' - apply plugin: 'java-library' - '''.stripIndent(true)) - addSubproject("other", ''' - apply plugin: 'java-library' - dependencies { - implementation project(':foobar') - } - '''.stripIndent(true)) - // Otherwise the lack of a lock file will throw first - file('versions.lock') << "" - - expect: - def error = runTasksAndFail() - error.output.contains(expectedError) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: fails fast when multiple subprojects share the same coordinate'() { - setup: - gradleVersion = gradleVersionNumber - - def expectedError = "All subprojects must have unique \$group:\$name" - buildFile << """ - allprojects { - group 'same' - } - """.stripIndent(true) - // both projects will have name = 'a' - addSubproject("foo:a") - addSubproject("bar:a") - // Otherwise the lack of a lock file will throw first - file('versions.lock') << "" - - expect: - def error = runTasksAndFail() - error.output.contains(expectedError) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - @Override - File addSubproject(String name) { - File subprojectDir = new File(projectDir, name.replace(":", "/")) - subprojectDir.mkdirs() - StreamEx.split(name, ":") - .scanLeft { left, right -> "$left:$right" } - .each { subproject -> settingsFile << "include \"${subproject}\"${LINE_END}" } - - return subprojectDir - } - - def "#gradleVersionNumber: detects failOnVersionConflict on locked configuration"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - configurations.compileClasspath.resolutionStrategy.failOnVersionConflict() - """.stripIndent(true) - file('versions.lock').text = '' - - expect: - def failure = runTasksAndFail() - failure.output.contains('Must not use failOnVersionConflict') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: ignores failOnVersionConflict on non-locked configuration"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - configurations { - foo { - resolutionStrategy.failOnVersionConflict() - } - } - """.stripIndent(true) - file('versions.lock').text = '' - - expect: - runTasks() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: fails if new dependency added that was not in the lock file'() { - setup: - gradleVersion = gradleVersionNumber - - def expectedError = "Found dependencies that were not in the lock state" - DependencyGraph dependencyGraph = new DependencyGraph("org:a:1.0", "org:b:1.0") - GradleDependencyGenerator generator = new GradleDependencyGenerator(dependencyGraph) - def mavenRepo = generator.generateTestMavenRepo() - - buildFile << """ - repositories { - maven { url "file:///${mavenRepo.absolutePath}" } - } - - subprojects { - apply plugin: 'java' - } - """.stripIndent(true) - - addSubproject('foo', ''' - dependencies { - implementation 'org:a:1.0' - } - '''.stripIndent(true)) - - runTasks('--write-locks') - - when: - file('foo/build.gradle') << """ - dependencies { - implementation 'org:b:1.0' - } - """.stripIndent(true) - - then: 'Check fails because locks are not up to date' - def failure = runTasksAndFail(':check') - failure.task(':verifyLocks').outcome == TaskOutcome.FAILED - failure.output.contains(expectedError) - - and: 'Can finally write locks once again' - runTasks('--write-locks') - runTasks('verifyLocks') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: does not fail if unifiedClasspath is unresolvable'() { - setup: - gradleVersion = gradleVersionNumber - - file('versions.lock') << """\ - org.slf4j:slf4j-api:1.7.11 (0 constraints: 0000000) - """.stripIndent(true) - - addSubproject('foo', ''' - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api:1.7.20' - } - '''.stripIndent(true)) - - expect: - runTasks('dependencies', '--configuration', 'unifiedClasspath') - runTasks() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: fails if dependency was removed but still in the lock file'() { - setup: - gradleVersion = gradleVersionNumber - - def expectedError = "Locked dependencies missing from the resolution result" - DependencyGraph dependencyGraph = new DependencyGraph("org:a:1.0", "org:b:1.0") - GradleDependencyGenerator generator = new GradleDependencyGenerator(dependencyGraph) - def mavenRepo = generator.generateTestMavenRepo() - - buildFile << """ - repositories { - maven { url "file:///${mavenRepo.absolutePath}" } - } - - subprojects { - apply plugin: 'java' - } - """.stripIndent(true) - - addSubproject('foo', ''' - dependencies { - implementation 'org:a:1.0' - implementation 'org:b:1.0' - } - '''.stripIndent(true)) - - runTasks('--write-locks') - - when: - file('foo/build.gradle').text = """ - dependencies { - implementation 'org:a:1.0' - } - """.stripIndent(true) - - then: 'Check fails because locks are not up to date' - def failure = runTasksAndFail(':check') - failure.task(':verifyLocks').outcome == TaskOutcome.FAILED - failure.output.contains(expectedError) - - and: 'Can finally write locks once again' - runTasks('--write-locks') - runTasks('verifyLocks') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: why works"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' // brings in slf4j-api 1.7.25 - } - '''.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - def result = runTasks('why', '--dependency', 'slf4j-api') - result.output.contains('org.slf4j:slf4j-api:1.7.25') - result.output.contains('ch.qos.logback:logback-classic -> 1.7.25') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: why with hash works"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' // brings in slf4j-api 1.7.25 - } - '''.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - def result = runTasks('why', '--hash', '400d4d2a') // slf4j-api - result.output.contains('org.slf4j:slf4j-api:1.7.25') - result.output.contains('ch.qos.logback:logback-classic -> 1.7.25') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: why with comma-delimited multiple hashes works"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' // brings in slf4j-api 1.7.25 - implementation 'org:another-direct-dependency:1.2.3' // brings in org:another-transitive-dependency:3.2.1 - } - '''.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - def result = runTasks('why', '--hash', '400d4d2a,050d6518') // both transitive dependencies - result.output.contains('org.slf4j:slf4j-api:1.7.25') - result.output.contains('ch.qos.logback:logback-classic -> 1.7.25') - result.output.contains('org:another-transitive-dependency:3.2.1') - result.output.contains('org:another-direct-dependency -> 3.2.1') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: does not fail if subproject evaluated later applies base plugin in own build file'() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject('foo', """ - apply plugin: 'java-library' - dependencies { - implementation project(':foo:bar') - } - """.stripIndent(true)) - - // Need to make sure bar is evaluated after foo, so we're nesting it! - def subdir = new File(projectDir, 'foo/bar') - subdir.mkdirs() - settingsFile << "include ':foo:bar'" - new File(subdir, 'build.gradle') << """ - apply plugin: 'java-library' - """.stripIndent(true) - - expect: - runTasks('--write-locks') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: locks platform"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation platform('org:platform:1.0') - } - """.stripIndent(true) - - when: - runTasks('--write-locks') - - then: - file('versions.lock').readLines() == [ - '# Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts.', - '', - 'org:platform:1.0 (1 constraints: a5041a2c)', - ] - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: verifyLocks is cacheable"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation "org.slf4j:slf4j-api:\$depVersion" - } - """ - - file('gradle.properties') << 'depVersion = 1.7.20' - - when: - runTasks('--write-locks') - - then: 'verifyLocks is up to date the second time' - runTasks('verifyLocks').task(':verifyLocks').outcome == TaskOutcome.SUCCESS - runTasks('verifyLocks').task(':verifyLocks').outcome == TaskOutcome.UP_TO_DATE - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - - def "#gradleVersionNumber: verifyLocks current lock state does not get poisoned by existing lock file"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation "org.slf4j:slf4j-api:\$depVersion" - } - """ - - file('gradle.properties') << 'depVersion = 1.7.20' - - when: - runTasks('--write-locks') - - then: 'verifyLocks fails if we lower the dep version' - def fail = runTasksAndFail('verifyLocks', '-PdepVersion=1.7.11') - - and: 'it expects the correct version to be 1.7.11' - fail.output.contains """\ - > Found dependencies whose dependents changed: - -org.slf4j:slf4j-api:1.7.20 (1 constraints: 3c05433b) - +org.slf4j:slf4j-api:1.7.11 (1 constraints: 3c05423b) - """.stripIndent(true) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: excludes from compileOnly do not obscure real dependency"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - } - configurations.compileOnly { - // convoluted, but the idea is to exclude a transitive - exclude group: 'org.slf4j', module: 'slf4j-api' - } - """.stripIndent(true) - - when: - runTasks('--write-locks') - - then: 'slf4j-api still appears in the lock file' - file('versions.lock').readLines() == [ - '# Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts.', - '', - 'ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935)', - '', - 'org.slf4j:slf4j-api:1.7.25 (1 constraints: 400d4d2a)', - ] - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: can resolve configuration dependency"() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject("foo", """ - apply plugin: 'java' - dependencies { - implementation project(path: ":bar", configuration: "fun") - } - """.stripIndent(true)) - - addSubproject("bar", """ - configurations { - fun - } - - dependencies { - fun 'ch.qos.logback:logback-classic:1.2.3' - } - """.stripIndent(true)) - - // Make sure that we can still add dependencies to the original 'fun' configuration after resolving lock state. - // - // Adding a constraint to 'fun' calls Configuration.preventIllegalMutation() which fails if observedState is - // GRAPH_RESOLVED or ARTIFACTS_RESOLVED. That would happen if a configuration that extends from it has been - // resolved. - buildFile << """ - configurations.unifiedClasspath.incoming.afterResolve { - project(':bar').dependencies.constraints { - fun 'some:other-dep' - } - } - """.stripIndent(true) - - expect: - runTasks('--write-locks', 'classes') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: inter-project normal dependency works"() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject("foo", """ - apply plugin: 'java' - dependencies { - implementation project(":bar") - } - """.stripIndent(true)) - - addSubproject("bar", """ - apply plugin: 'java' - """.stripIndent(true)) - - expect: - runTasks('--write-locks', 'classes') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: test dependencies appear in a separate block"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - testImplementation 'org:test-dep-that-logs:1.0' - } - """.stripIndent(true) - - expect: - runTasks('--write-locks') - def expected = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935) - - org.slf4j:slf4j-api:1.7.25 (2 constraints: 7917e690) - - - - [Test dependencies] - - org:test-dep-that-logs:1.0 (1 constraints: a5041a2c) - """.stripIndent(true) - file('versions.lock').text == expected - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: locks dependencies from extra source sets that end in test"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - sourceSets { - eteTest - } - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - testImplementation 'junit:junit:4.10' - eteTestImplementation 'org:test-dep-that-logs:1.0' - } - """.stripIndent(true) - - expect: - runTasks('--write-locks') - def expected = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935) - - org.slf4j:slf4j-api:1.7.25 (2 constraints: 7917e690) - - - - [Test dependencies] - - junit:junit:4.10 (1 constraints: d904fd30) - - org:test-dep-that-logs:1.0 (1 constraints: a5041a2c) - """.stripIndent(true) - file('versions.lock').text == expected - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: versionsLock.testProject() works"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation 'junit:junit:4.10' - } - - versionsLock.testProject() - """.stripIndent(true) - - expect: - runTasks('--write-locks') - def expected = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - - - [Test dependencies] - - junit:junit:4.10 (1 constraints: d904fd30) - """.stripIndent(true) - file('versions.lock').text == expected - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: constraints on production do not affect scope of test only dependencies"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - constraints { - implementation 'ch.qos.logback:logback-classic:1.2.3' - } - dependencies { - testImplementation 'ch.qos.logback:logback-classic' - } - } - """.stripIndent(true) - - expect: - runTasks('--write-locks') - def expected = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - - - [Test dependencies] - - ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935) - - org.slf4j:slf4j-api:1.7.25 (1 constraints: 400d4d2a) - """.stripIndent(true) - file('versions.lock').text == expected - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: published constraints are derived from lock file (with local constraints)"() { - setup: - // Test with local constraints enabled - file('gradle.properties') << 'com.palantir.gradle.versions.publishLocalConstraints = true' - gradleVersion = gradleVersionNumber - - buildFile << """ - allprojects { - apply plugin: 'java' - } - """.stripIndent(true) - - String publish = """ - apply plugin: 'maven-publish' - group = 'com.palantir.published-constraints' - version = '1.2.3' - publishing.publications { - maven(MavenPublication) { - from components.java - } - } - """.stripIndent(true) - - addSubproject('foo', """ - $publish - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - } - """.stripIndent(true)) - - addSubproject('bar', """ - $publish - dependencies { - implementation 'junit:junit:4.10' - } - """.stripIndent(true)) - - if (GradleVersion.version(gradleVersionNumber) < GradleVersion.version("6.0")) { - settingsFile << """ - enableFeaturePreview('GRADLE_METADATA') - """.stripIndent(true) - } - - runTasks('--write-locks') - - when: - runTasks('generatePomFileForMavenPublication', 'generateMetadataFileForMavenPublication') - - def junitDep = new MetadataFile.Dependency( - group: 'junit', - module: 'junit', - version: [requires: '4.10']) - def logbackDep = new MetadataFile.Dependency( - group: 'ch.qos.logback', - module: 'logback-classic', - version: [requires: '1.2.3']) - def slf4jDep = new MetadataFile.Dependency( - group: 'org.slf4j', - module: 'slf4j-api', - version: [requires: '1.7.25']) - def fooDep = new MetadataFile.Dependency( - group: 'com.palantir.published-constraints', - module: 'foo', - version: [requires: '1.2.3']) - def barDep = new MetadataFile.Dependency( - group: 'com.palantir.published-constraints', - module: 'bar', - version: [requires: '1.2.3']) - - then: "foo's metadata file has the right dependency constraints" - def fooMetadataFilename = new File(projectDir, "foo/build/publications/maven/module.json") - def fooMetadata = new ObjectMapper().readValue(fooMetadataFilename, MetadataFile) - - fooMetadata.variants == [ - new MetadataFile.Variant( - name: 'runtimeElements', - dependencies: [logbackDep], - dependencyConstraints: [barDep, junitDep, logbackDep, slf4jDep]), - new MetadataFile.Variant( - name: 'apiElements', - dependencies: null, - dependencyConstraints: [barDep, junitDep, logbackDep, slf4jDep]) - ] as Set - - and: "bar's metadata file has the right dependency constraints" - def barMetadataFilename = new File(projectDir, "bar/build/publications/maven/module.json") - def barMetadata = new ObjectMapper().readValue(barMetadataFilename, MetadataFile) - - barMetadata.variants == [ - new MetadataFile.Variant( - name: 'runtimeElements', - dependencies: [junitDep], - dependencyConstraints: [fooDep, junitDep, logbackDep, slf4jDep]), - new MetadataFile.Variant( - name: 'apiElements', - dependencies: null, - dependencyConstraints: [fooDep, junitDep, logbackDep, slf4jDep]), - ] as Set - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: published constraints are derived from lock file (without local constraints)"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - allprojects { - apply plugin: 'java' - apply plugin: 'maven-publish' - publishing.publications { - maven(MavenPublication) { - from components.java - } - } - } - """.stripIndent(true) - - addSubproject('foo', """ - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - } - """.stripIndent(true)) - - addSubproject('bar', """ - dependencies { - implementation 'junit:junit:4.10' - } - """.stripIndent(true)) - - if (GradleVersion.version(gradleVersionNumber) < GradleVersion.version("6.0")) { - settingsFile << """ - enableFeaturePreview('GRADLE_METADATA') - """.stripIndent(true) - } - - runTasks('--write-locks') - - when: - runTasks('generatePomFileForMavenPublication', 'generateMetadataFileForMavenPublication') - - def junitDep = new MetadataFile.Dependency( - group: 'junit', - module: 'junit', - version: [requires: '4.10']) - def logbackDep = new MetadataFile.Dependency( - group: 'ch.qos.logback', - module: 'logback-classic', - version: [requires: '1.2.3']) - def slf4jDep = new MetadataFile.Dependency( - group: 'org.slf4j', - module: 'slf4j-api', - version: [requires: '1.7.25']) - - then: "foo's metadata file has the right dependency constraints" - def fooMetadataFilename = new File(projectDir, "foo/build/publications/maven/module.json") - def fooMetadata = new ObjectMapper().readValue(fooMetadataFilename, MetadataFile) - - fooMetadata.variants == [ - new MetadataFile.Variant( - name: 'apiElements', - dependencies: null, - dependencyConstraints: [junitDep, logbackDep, slf4jDep]), - new MetadataFile.Variant( - name: 'runtimeElements', - dependencies: [logbackDep], - dependencyConstraints: [junitDep, logbackDep, slf4jDep]), - ] as Set - - and: "bar's metadata file has the right dependency constraints" - def barMetadataFilename = new File(projectDir, "bar/build/publications/maven/module.json") - def barMetadata = new ObjectMapper().readValue(barMetadataFilename, MetadataFile) - - barMetadata.variants == [ - new MetadataFile.Variant( - name: 'apiElements', - dependencies: null, - dependencyConstraints: [junitDep, logbackDep, slf4jDep]), - new MetadataFile.Variant( - name: 'runtimeElements', - dependencies: [junitDep], - dependencyConstraints: [junitDep, logbackDep, slf4jDep]), - ] as Set - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: can depend on artifact"() { - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation "junit:junit:4.10@zip" - } - """.stripIndent(true) - - expect: - runTasks("--write-locks") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: direct test dependency that is also a production transitive ends up in production"() { - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic:1.2.3' - testImplementation 'org.slf4j:slf4j-api:1.7.25' - } - """.stripIndent(true) - - expect: - runTasks("--write-locks") - file('versions.lock').text == """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - - ch.qos.logback:logback-classic:1.2.3 (1 constraints: 0805f935) - - org.slf4j:slf4j-api:1.7.25 (2 constraints: 8012a437) - """.stripIndent(true) - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: does not write lock file when property 'gcvSkipWriteLocks' is set"() { - gradleVersion = gradleVersionNumber - - buildFile << """ - apply plugin: 'java' - dependencies { - testImplementation 'org.slf4j:slf4j-api:1.7.25' - } - """.stripIndent(true) - - def lockFileContent = """\ - # Run ./gradlew writeVersionsLocks to regenerate this file. Blank lines are to minimize merge conflicts. - """.stripIndent(true) - - file('versions.lock').text = lockFileContent - - expect: - def result = runTasks("--write-locks", "-PgcvSkipWriteLocks") - - file('versions.lock').text == lockFileContent - assert result.getOutput().contains("Skipped writing lock state") - assert !result.getOutput().contains("Finished writing lock state") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - boolean verifyLockfile(File projectDir, String... lines) { - // Gradle 7+ only uses a single lockfile per project: - // https://docs.gradle.org/current/userguide/upgrading_version_6.html#locking_single - if (GradleVersion.version(gradleVersion) >= GradleVersion.version("7.0.0")) { - def lockfile = file("gradle.lockfile", projectDir).text - lines.each{ assert lockfile.contains(it + "=runtimeClasspath") } - } else { - def lockfile = file("gradle/dependency-locks/runtimeClasspath.lockfile", projectDir).text - lines.each{ assert lockfile.contains(it) } - } - } -} diff --git a/src/test/groovy/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.groovy deleted file mode 100644 index a0caafa1b..000000000 --- a/src/test/groovy/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.groovy +++ /dev/null @@ -1,326 +0,0 @@ -/* - * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. - * - * 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.palantir.gradle.versions - -import groovy.util.slurpersupport.GPathResult -import groovy.util.slurpersupport.NodeChildren -import org.gradle.util.GradleVersion -import spock.lang.Unroll - -import static com.palantir.gradle.versions.GradleTestVersions.GRADLE_VERSIONS -import static com.palantir.gradle.versions.PomUtils.makePlatformPom - -@Unroll -class VersionsPropsPluginIntegrationSpec extends IntegrationSpec { - static def PLUGIN_NAME = "com.palantir.versions-props" - - void setup() { - File mavenRepo = generateMavenRepo( - "ch.qos.logback:logback-classic:1.2.3 -> org.slf4j:slf4j-api:1.7.25", - "ch.qos.logback:logback-classic:1.1.11 -> org.slf4j:slf4j-api:1.7.22", - "org.slf4j:slf4j-api:1.7.21", - "org.slf4j:slf4j-api:1.7.22", - "org.slf4j:slf4j-api:1.7.24", - "org.slf4j:slf4j-api:1.7.25", - "com.fasterxml.jackson.core:jackson-databind:2.9.0 -> com.fasterxml.jackson.core:jackson-annotations:2.9.0", - "com.fasterxml.jackson.core:jackson-annotations:2.9.0", - "com.fasterxml.jackson.core:jackson-annotations:2.9.7", - "com.fasterxml.jackson.core:jackson-databind:2.9.7" - ) - makePlatformPom(mavenRepo, "org", "platform", "1.0") - - buildFile << """ - buildscript { - repositories { - mavenCentral() - } - } - plugins { - id '${PLUGIN_NAME}' - } - allprojects { - repositories { - maven { url "file:///${mavenRepo.getAbsolutePath()}" } - } - } - - // Make it easy to verify what versions of dependencies you got. - allprojects { - configurations.matching { it.name == 'runtimeClasspath' }.all { - resolutionStrategy.activateDependencyLocking() - } - task resolveConfigurations { - doLast { - if (pluginManager.hasPlugin('java')) { - configurations.compileClasspath.resolve() - configurations.runtimeClasspath.resolve() - } - } - } - } - """.stripIndent(true) - } - - def '#gradleVersionNumber: star dependency constraint is injected for direct dependency'() { - setup: - gradleVersion = gradleVersionNumber - - file("versions.props") << """ - org.slf4j:* = 1.7.24 - """.stripIndent(true) - - def fooProject = addSubproject('foo', """ - apply plugin: 'java' - dependencies { - implementation 'org.slf4j:slf4j-api' - } - """.stripIndent(true)) - - expect: - runTasks('resolveConfigurations', '--write-locks') - - verifyLockfile(fooProject, "org.slf4j:slf4j-api:1.7.24") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: star dependency constraint is not forcefully downgraded for transitive dependency'() { - setup: - gradleVersion = gradleVersionNumber - - file("versions.props") << """ - org.slf4j:* = 1.7.21 - ch.qos.logback:logback-classic = 1.1.11 # brings in slf4j-api 1.7.22 - """.stripIndent(true) - - def fooProject = addSubproject('foo', """ - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic' - } - """.stripIndent(true)) - - expect: - runTasks('resolveConfigurations', '--write-locks') - - verifyLockfile(fooProject, "org.slf4j:slf4j-api:1.7.22") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: star dependency constraint upgrades transitive dependency'() { - setup: - gradleVersion = gradleVersionNumber - - file("versions.props") << """ - org.slf4j:* = 1.7.25 - ch.qos.logback:logback-classic = 1.1.11 # brings in slf4j-api 1.7.22 - """.stripIndent(true) - - def fooProject = addSubproject('foo', """ - apply plugin: 'java' - dependencies { - implementation 'ch.qos.logback:logback-classic' - } - """.stripIndent(true)) - - expect: - runTasks('resolveConfigurations', '--write-locks') - - verifyLockfile(fooProject, "org.slf4j:slf4j-api:1.7.25") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: imported platform generated correctly in pom'() { - setup: - gradleVersion = gradleVersionNumber - - file("versions.props") << """ - org:platform = 1.0 - # This shouldn't end up in the POM - other:constraint = 1.0.0 - """.stripIndent(true) - - addSubproject('foo', """ - apply plugin: 'java-library' - apply plugin: 'maven-publish' - dependencies { - rootConfiguration platform('org:platform') - } - publishing { - publications { - main(MavenPublication) { - from components.java - } - } - } - """.stripIndent(true)) - - expect: - runTasks('foo:generatePomFile') - - file('foo/build/publications/main/pom-default.xml').exists() - - def slurper = new XmlSlurper() - def pom = slurper.parse(file('foo/build/publications/main/pom-default.xml')) - NodeChildren dependencies = pom.dependencyManagement.dependencies.dependency - dependencies.collect { convertToMap(it) } as Set == [ - [ - groupId: 'org', - artifactId: 'platform', - version: '1.0', - scope: 'import', - type: 'pom' - ], - ] as Set - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: non-glob module forces do not get added to a matching platform too'() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - apply plugin: 'java' - dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind' - } - '''.stripIndent(true) - file('versions.props') << ''' - com.fasterxml.jackson.core:jackson-databind = 2.9.0 - com.fasterxml.jackson.*:* = 2.9.7 - '''.stripIndent(true) - - when: - runTasks('resolveConfigurations', '--write-locks') - - then: - verifyLockfile(projectDir, "com.fasterxml.jackson.core:jackson-databind:2.9.0", "com.fasterxml.jackson.core:jackson-annotations:2.9.7") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: throws if resolving configuration in afterEvaluate"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - configurations { foo } - - afterEvaluate { - configurations.foo.resolve() - } - ''' - file('versions.props') << '' - - expect: - def e = runTasksAndFail() - e.output.contains("Not allowed to resolve") - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: does not throw if excluded configuration is resolved early"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << ''' - configurations { foo } - - versionRecommendations { - excludeConfigurations 'foo' - } - - afterEvaluate { - configurations.foo.resolve() - } - ''' - file('versions.props') << '' - - expect: - runTasks() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def "#gradleVersionNumber: creates rootConfiguration even if versions props file missing"() { - setup: - gradleVersion = gradleVersionNumber - - buildFile << """ - dependencies { - constraints { - rootConfiguration 'org.slf4j:slf4j-api:1.7.25' - } - } - """.stripIndent(true) - file('versions.props').delete() - - expect: - runTasks() - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - def '#gradleVersionNumber: build succeeds without versions.props or versions.lock'() { - setup: - gradleVersion = gradleVersionNumber - - addSubproject('foo', """ - apply plugin: 'java' - """.stripIndent(true)) - - expect: - runTasks('build') - - where: - gradleVersionNumber << GRADLE_VERSIONS - } - - boolean verifyLockfile(File projectDir, String... lines) { - // Gradle 7+ only uses a single lockfile per project: - // https://docs.gradle.org/current/userguide/upgrading_version_6.html#locking_single - if (GradleVersion.version(gradleVersion) >= GradleVersion.version("7.0.0")) { - def lockfile = file("gradle.lockfile", projectDir).text - lines.each{ assert lockfile.contains(it + "=runtimeClasspath") } - } else { - def lockfile = file("gradle/dependency-locks/runtimeClasspath.lockfile", projectDir).text - lines.each{ assert lockfile.contains(it) } - } - } - - /** - * Recursively converts a node's children to a map of (tag name): (value inside tag). - *

- * See: https://stackoverflow.com/a/26889997/274699 - */ - def convertToMap(GPathResult node) { - node.children().collectEntries { - [ it.name(), it.childNodes() ? convertToMap(it) : it.text() ] - } - } -} diff --git a/src/test/java/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.java b/src/test/java/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.java new file mode 100644 index 000000000..f0c6de603 --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/CheckOverbroadConstraintsIntegrationSpec.java @@ -0,0 +1,139 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.execution.TaskOutcome; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@GradlePluginTests +class CheckOverbroadConstraintsIntegrationSpec { + + @BeforeEach + void setup(RootProject rootProject) { + rootProject.buildGradle().append(""" + plugins { + id 'com.palantir.versions-lock' + id 'com.palantir.versions-props' + } + """); + rootProject.file("versions.props").createEmpty(); + rootProject.file("versions.lock").createEmpty(); + } + + @Test + void task_should_run_as_part_of_check(GradleInvoker gradle) { + InvocationResult result = gradle.withArgs("check", "-m").buildsSuccessfully(); + assertThat(result.output()).contains(":checkOverbroadConstraints"); + } + + @Test + void all_versions_are_pinned(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + com.fasterxml.jackson.*:* = 2.9.3 + com.fasterxml.jackson.core:jackson-annotations = 2.9.5 + """); + rootProject.file("versions.lock").overwrite(""" + com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) + com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) + """); + + InvocationResult result = gradle.withArgs("checkOverbroadConstraints").buildsSuccessfully(); + assertThat(result.task(":checkOverbroadConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } + + @Test + void not_all_versions_are_pinned_throws_error_and_fix_works_and_no_new_line_is_added( + GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + com.fasterxml.jackson.*:* = 2.9.3 + """); + rootProject.file("versions.lock").overwrite(""" + com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) + com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) + """); + + InvocationResult failure = gradle.withArgs("checkOverbroadConstraints").buildsWithFailure(); + assertThat(failure.output()).contains(""" + Over-broad version constraints found in versions.props. + Over-broad constraints often arise due to wildcards in versions.props + which apply to more dependencies than they should, this can lead to slow builds. + The following additional pins are recommended: + com.fasterxml.jackson.core:jackson-annotations = 2.9.5 + com.fasterxml.jackson.core:jackson-core = 2.9.3 + + Run ./gradlew checkOverbroadConstraints --fix to add them. + See https://github.com/palantir/gradle-consistent-versions?tab=readme-ov-file#gradlew-checkoverbroadconstraints for details + """); + + List currentVersionsProps = + rootProject.file("versions.props").text().lines().toList(); + gradle.withArgs("checkOverbroadConstraints", "--fix").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().lines().toList()).isNotEqualTo(currentVersionsProps); + + gradle.withArgs("checkOverbroadConstraints").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text()).isEqualTo(""" + com.fasterxml.jackson.core:jackson-annotations = 2.9.5 + com.fasterxml.jackson.core:jackson-core = 2.9.3 + """); + } + + @Test + void fixes_are_inserted_in_the_correct_locations_new_lines_are_maintained( + GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + + com.random:* = 1.0.0 + com.fasterxml.jackson.*:* = 2.9.3 + \s\s\s\s + # A random comment + org.different:artifact = 2.0.0 + \s\s\s\s + """); + rootProject.file("versions.lock").overwrite(""" + com.random:random:1.0.0 (2 constraints: abcdef0) + org.different:artifact:2.0.0 (2 constraints: abcdef0) + com.fasterxml.jackson.core:jackson-annotations:2.9.5 (2 constraints: abcdef0) + com.fasterxml.jackson.core:jackson-core:2.9.3 (2 constraints: abcdef1) + """); + + List currentVersionsProps = + rootProject.file("versions.props").text().lines().toList(); + gradle.withArgs("checkOverbroadConstraints", "--fix").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().lines().toList()).isNotEqualTo(currentVersionsProps); + + gradle.withArgs("checkOverbroadConstraints").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text()).isEqualTo(""" + + com.random:* = 1.0.0 + com.fasterxml.jackson.core:jackson-annotations = 2.9.5 + com.fasterxml.jackson.core:jackson-core = 2.9.3 + \s\s\s\s + # A random comment + org.different:artifact = 2.0.0 + \s\s\s\s + """); + } +} diff --git a/src/test/java/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.java b/src/test/java/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.java new file mode 100644 index 000000000..4203fa2ff --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/CheckUnusedConstraintIntegrationSpec.java @@ -0,0 +1,192 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.execution.TaskOutcome; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@GradlePluginTests +class CheckUnusedConstraintIntegrationSpec { + + @BeforeEach + void setup(RootProject rootProject) { + rootProject.buildGradle().append(""" + plugins { + id 'java' + id 'com.palantir.versions-lock' + id 'com.palantir.versions-props' + } + + repositories { + mavenCentral() + maven { url "%s/maven" } + } + \s\s\s\s + // Get rid of deprecation warnings for Gradle 7+ + versionRecommendations { + excludeConfigurations 'compile', 'runtime', 'testCompile', 'testRuntime' + } + """, rootProject.path().toUri()); + rootProject.gradlePropertiesFile().append(""" + ignoreLockFile=true + """); + rootProject.file("versions.props").createEmpty(); + } + + @Test + void check_versions_props_does_not_resolve_artifacts(GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().append(""" + dependencies { + implementation 'com.palantir.product:foo:1.0.0' + } + """); + rootProject.file("versions.props").overwrite(""); + + // We're not producing a jar for this dependency, so artifact resolution would fail + rootProject.file("maven/com/palantir/product/foo/1.0.0/foo-1.0.0.pom").overwrite(""" + + + 4.0.0 + com.palantir.product + foo + jar + 1.0.0 + + + + """); + + InvocationResult result = gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + assertThat(result.task(":checkUnusedConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } + + @Test + void task_should_run_as_part_of_check(GradleInvoker gradle) { + InvocationResult result = gradle.withArgs("check", "-m").buildsSuccessfully(); + assertThat(result.output()).contains(":checkUnusedConstraints"); + } + + @Test + void version_props_conflict_should_succeed(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + com.fasterxml.jackson.*:* = 2.9.3 + com.fasterxml.jackson.core:jackson-annotations = 2.9.5 + """); + rootProject.buildGradle().append(""" + dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind' + }\ + """); + + InvocationResult result = gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + assertThat(result.task(":checkUnusedConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } + + @Test + void most_specific_matching_version_should_win(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:slf4j-api = 1.7.25 + org.slf4j:* = 1.7.20 + """); + + rootProject.buildGradle().append(""" + dependencies { + implementation 'org.slf4j:slf4j-api' + }\ + """); + + InvocationResult failure = gradle.withArgs("checkUnusedConstraints").buildsWithFailure(); + assertThat(failure.output()).contains("There are unused pins in your versions.props: \n[org.slf4j:*]"); + + List currentVersionsProps = + rootProject.file("versions.props").text().lines().toList(); + gradle.withArgs("checkUnusedConstraints", "--fix").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().lines().toList()).isNotEqualTo(currentVersionsProps); + + gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().trim()).isEqualTo("org.slf4j:slf4j-api = 1.7.25"); + } + + @Test + void most_specific_glob_should_win(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:slf4j-* = 1.7.25 + org.slf4j:* = 1.7.20 + """); + + rootProject.buildGradle().append(""" + dependencies { + implementation 'org.slf4j:slf4j-api' + implementation 'org.slf4j:slf4j-jdk14' + }\ + """); + + InvocationResult failure = gradle.withArgs("checkUnusedConstraints").buildsWithFailure(); + assertThat(failure.output()).contains("There are unused pins in your versions.props: \n[org.slf4j:*]"); + + List currentVersionsProps = + rootProject.file("versions.props").text().lines().toList(); + gradle.withArgs("checkUnusedConstraints", "--fix").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().lines().toList()).isNotEqualTo(currentVersionsProps); + + gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().trim()).isEqualTo("org.slf4j:slf4j-* = 1.7.25"); + } + + @Test + void unused_version_should_fail(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite("notused:atall = 42.42"); + + InvocationResult failure = gradle.withArgs("checkUnusedConstraints").buildsWithFailure(); + assertThat(failure.output()).contains("There are unused pins in your versions.props"); + + List currentVersionsProps = + rootProject.file("versions.props").text().lines().toList(); + gradle.withArgs("checkUnusedConstraints", "--fix").buildsSuccessfully(); + assertThat(rootProject.file("versions.props").text().lines().toList()).isNotEqualTo(currentVersionsProps); + + gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + } + + @Test + void unused_check_should_use_exact_matching(GradleInvoker gradle, RootProject rootProject) { + rootProject.file("versions.props").overwrite(""" + com.google.guava:guava-testlib = 23.0 + com.google.guava:guava = 22.0 + """); + rootProject.buildGradle().append(""" + dependencies { + implementation 'com.google.guava:guava' + implementation 'com.google.guava:guava-testlib' + }\ + """); + + InvocationResult result = gradle.withArgs("checkUnusedConstraints").buildsSuccessfully(); + assertThat(result.task(":checkUnusedConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } +} diff --git a/src/test/java/com/palantir/gradle/versions/ConfigurationOnDemandSpec.java b/src/test/java/com/palantir/gradle/versions/ConfigurationOnDemandSpec.java new file mode 100644 index 000000000..9c5eee062 --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/ConfigurationOnDemandSpec.java @@ -0,0 +1,401 @@ +/* + * (c) Copyright 2021 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.execution.TaskOutcome; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import com.palantir.gradle.testing.project.SubProject; +import java.io.File; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * This tests the interaction of this plugin with Gradle's configuration-on-demand feature: + * https://docs.gradle.org/current/userguide/multi_project_configuration_and_execution.html#sec:configuration_on_demand + */ +@GradlePluginTests +class ConfigurationOnDemandSpec { + + private static final String PLUGIN_NAME = "com.palantir.consistent-versions"; + private File mavenRepo; + + /* + * Project structure (arrows indicate dependencies): + * + * upstream unrelated + * ^ ^ + * | | + * downstream1 downstream2 + */ + @BeforeEach + void setup( + RootProject rootProject, + SubProject upstreamProject, + SubProject downstream1Project, + SubProject downstream2Project, + SubProject unrelatedProject) { + mavenRepo = MavenRepoUtils.generateMavenRepo( + rootProject.path().resolve("build"), + "com.example:dependency-of-upstream:1.2.3", + "com.example:dependency-of-upstream:100.1.1", + "com.example:dependency-of-downstream1:1.2.3", + "com.example:dependency-of-downstream2:1.2.3", + "com.example:dependency-of-unrelated:1.2.3", + "com.example:dep-with-version-bumped-by-unrelated:1.0.0", + "com.example:dep-with-version-bumped-by-unrelated:1.1.0", + "com.example:transitive-test-dep:1.0.0", + "com.example:transitive-test-dep:1.1.0", + "com.example:transitive-test-dep:1.2.0"); + + PomUtils.makePlatformPom(mavenRepo, "org", "platform", "1.0"); + + rootProject.buildGradle().overwrite(""" + plugins { + id '%s' + } + allprojects { + repositories { + maven { url "file:///%s" } + } + } + subprojects { + tasks.register('writeClasspath') { + doLast { + println(configurations.runtimeClasspath.getFiles()) + } + } + } + \s\s\s\s + // Get rid of deprecation warnings for Gradle 7+ + versionRecommendations { + excludeConfigurations 'compile', 'runtime', 'testCompile', 'testRuntime' + } + """, PLUGIN_NAME, mavenRepo.getAbsolutePath()); + + rootProject.file("versions.props").overwrite(""" + com.example:dependency-of-upstream = 1.2.3 + com.example:dependency-of-downstream1 = 1.2.3 + com.example:dependency-of-downstream2 = 1.2.3 + com.example:dependency-of-unrelated = 1.2.3 + # 1.0.0 is a minimum, we expect this to be locked to 1.1.0 + com.example:dep-with-version-bumped-by-unrelated = 1.0.0 + """); + + rootProject.settingsGradle().rootProjectName("test"); + rootProject.settingsGradle().include("upstream"); + rootProject.settingsGradle().include("downstream1"); + rootProject.settingsGradle().include("downstream2"); + rootProject.settingsGradle().include("unrelated"); + + upstreamProject.buildGradle().append(""" + plugins { + id 'java' + } + println 'configuring upstream' + dependencies { + implementation 'com.example:dependency-of-upstream' + } + """); + + downstream1Project.buildGradle().append(""" + plugins { + id 'java' + } + println 'configuring downstream1' + dependencies { + implementation project(':upstream') + implementation 'com.example:dependency-of-downstream1' + } + """); + + downstream2Project.buildGradle().append(""" + plugins { + id 'java' + } + println 'configuring downstream2' + dependencies { + implementation project(':upstream') + implementation 'com.example:dependency-of-downstream2' + implementation 'com.example:dep-with-version-bumped-by-unrelated' + } + """); + + unrelatedProject.buildGradle().append(""" + plugins { + id 'java' + } + println 'configuring unrelated' + dependencies { + implementation 'com.example:dependency-of-unrelated' + implementation 'com.example:dep-with-version-bumped-by-unrelated:1.1.0' + } + """); + + rootProject.gradlePropertiesFile().append(""" + org.gradle.configureondemand=true + """); + } + + @Test + void can_write_locks(GradleInvoker gradle, RootProject rootProject) { + InvocationResult result = gradle.withArgs("--write-locks").buildsSuccessfully(); + + assertThat(result.output()).contains("configuring upstream"); + assertThat(result.output()).contains("configuring downstream1"); + assertThat(result.output()).contains("configuring downstream2"); + assertThat(result.output()).contains("configuring unrelated"); + + assertThat(rootProject.path().resolve("versions.lock")).exists(); + assertThat(rootProject.file("versions.lock").text()).contains("com.example:dependency-of-upstream:1.2.3"); + assertThat(rootProject.file("versions.lock").text()) + .contains("com.example:dep-with-version-bumped-by-unrelated:1.1.0"); + } + + @Test + void can_write_locks_when_a_task_in_one_project_is_specified(GradleInvoker gradle, RootProject rootProject) { + gradle.withArgs(":downstream1:build", "--write-locks").buildsSuccessfully(); + + assertThat(rootProject.path().resolve("versions.lock")).exists(); + assertThat(rootProject.file("versions.lock").text()).contains("com.example:dependency-of-unrelated:1.2.3"); + assertThat(rootProject.file("versions.lock").text()) + .contains("com.example:dep-with-version-bumped-by-unrelated:1.1.0"); + } + + @Test + void applying_the_plugin_does_not_force_all_projects_to_be_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + // Both absolute and relative formats work, as long as Gradle is run from the root project directory + InvocationResult result1 = gradle.withArgs(":downstream1:build").buildsSuccessfully(); + InvocationResult result2 = gradle.withArgs("downstream1:build").buildsSuccessfully(); + + assertThat(result1.output()).contains("configuring upstream"); + assertThat(result1.output()).contains("configuring downstream1"); + assertThat(result1.output()).doesNotContain("configuring downstream2"); + assertThat(result1.output()).doesNotContain("configuring unrelated"); + + assertThat(result2.output()).contains("configuring upstream"); + assertThat(result2.output()).contains("configuring downstream1"); + assertThat(result2.output()).doesNotContain("configuring downstream2"); + assertThat(result2.output()).doesNotContain("configuring unrelated"); + } + + @Test + void resolving_a_classpath_does_not_force_all_projects_to_be_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs(":downstream1:writeClasspath").buildsSuccessfully(); + + assertThat(result.output()).contains("configuring upstream"); + assertThat(result.output()).contains("configuring downstream1"); + assertThat(result.output()).doesNotContain("configuring downstream2"); + assertThat(result.output()).doesNotContain("configuring unrelated"); + } + + @Test + void after_lockfile_is_written_versions_constraints_due_to_non_configured_projects_are_still_respected( + GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs(":downstream2:writeClasspath").buildsSuccessfully(); + + // Version used is 1.1.0 due to the unrelated project + assertThat(result.output()).contains("dep-with-version-bumped-by-unrelated-1.1.0.jar"); + assertThat(result.output()).doesNotContain("configured unrelated"); + } + + @Test + void transitive_dependencies_cause_upstream_projects_to_be_configured_sufficiently_early( + GradleInvoker gradle, + RootProject rootProject, + SubProject aProject, + SubProject bProject, + SubProject cProject, + SubProject uProject) { + rootProject.settingsGradle().include("a"); + rootProject.settingsGradle().include("b"); + rootProject.settingsGradle().include("c"); + rootProject.settingsGradle().include("u"); + + aProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation 'com.example:transitive-test-dep:1.0.0' + } + """); + + bProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation project(':a') + } + """); + + cProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation project(':b') + } + tasks.register('writeClasspathOfA') { + doLast { + println project(':a').configurations.runtimeClasspath.files + } + } + """); + + uProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation 'com.example:transitive-test-dep:1.1.0' + } + """); + + gradle.withArgs("--write-locks").buildsSuccessfully(); + InvocationResult result = gradle.withArgs(":c:writeClasspathOfA").buildsSuccessfully(); + + // Version used should be 1.1.0, indicating that the version.lock constraint was applied + assertThat(result.output()).contains("transitive-test-dep-1.1.0.jar"); + } + + @Test + void task_dependencies_cause_upstream_projects_to_be_configured_sufficiently_early( + GradleInvoker gradle, + RootProject rootProject, + SubProject aProject, + SubProject bProject, + SubProject cProject, + SubProject uProject) { + rootProject.settingsGradle().include("a"); + rootProject.settingsGradle().include("b"); + rootProject.settingsGradle().include("c"); + rootProject.settingsGradle().include("u"); + + aProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation 'com.example:transitive-test-dep:1.0.0' + } + """); + + bProject.buildGradle().append(""" + tasks.register('foo') { + dependsOn ':a:writeClasspath' + } + """); + + cProject.buildGradle().append(""" + tasks.register('bar') { + dependsOn ':b:foo' + } + """); + + uProject.buildGradle().append(""" + plugins { id 'java' } + dependencies { + implementation 'com.example:transitive-test-dep:1.1.0' + } + """); + + gradle.withArgs("--write-locks").buildsSuccessfully(); + InvocationResult result = gradle.withArgs(":c:bar").buildsSuccessfully(); + + // Version used should be 1.1.0, indicating that the version.lock constraint was applied + assertThat(result.output()).contains("transitive-test-dep-1.1.0.jar"); + } + + @Test + void verification_tasks_pass_when_all_projects_are_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + // Note: Not specifying the project causes all projects to be configured regardless of CoD + InvocationResult result = + gradle.withArgs("checkUnusedConstraints", "verifyLocks").buildsSuccessfully(); + + assertThat(result.output()).contains("configuring upstream"); + assertThat(result.task(":checkUnusedConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + assertThat(result.task(":verifyLocks")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } + + @Test + void check_unused_constraints_fails_and_warns_when_not_all_projects_are_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs(":checkUnusedConstraints").buildsWithFailure(); + + assertThat(result.output()).doesNotContain("configuring upstream"); + assertThat(result.task(":checkUnusedConstraints")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.FAILED)); + assertThat(result.output()) + .contains("The gradle-consistent-versions checkUnusedConstraints task " + + "must have all projects configured to work accurately, but due to Gradle " + + "configuration-on-demand, not all projects were configured."); + } + + @Test + void verify_locks_fails_and_warns_when_not_all_projects_are_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs(":verifyLocks").buildsWithFailure(); + + assertThat(result.output()).doesNotContain("configuring upstream"); + assertThat(result.task(":verifyLocks")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.FAILED)); + assertThat(result.output()) + .contains("All projects must have been configured for this task to work correctly, but due to " + + "Gradle configuration-on-demand, not all projects were configured."); + } + + @Test + void verification_tasks_are_not_up_to_date_when_the_set_of_configured_projects_differs(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + gradle.withArgs("build").buildsSuccessfully(); + + gradle.withArgs(":checkUnusedConstraints").buildsWithFailure(); + gradle.withArgs(":verifyLocks").buildsWithFailure(); + } + + @Test + void the_why_task_works_when_all_projects_are_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs("why", "--hash=0805f935").buildsSuccessfully(); + assertThat(result.task(":why")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + } + + @Test + void the_why_task_somehow_forces_all_projects_to_be_configured(GradleInvoker gradle) { + gradle.withArgs("--write-locks").buildsSuccessfully(); + + InvocationResult result = gradle.withArgs(":why", "--hash=0805f935").buildsSuccessfully(); + + assertThat(result.output()).contains("configuring upstream"); + assertThat(result.output()).contains("configuring downstream1"); + assertThat(result.output()).contains("configuring downstream2"); + assertThat(result.output()).contains("configuring unrelated"); + assertThat(result.task(":why")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.SUCCESS)); + assertThat(result.output()).contains("com.example:dependency-of-unrelated:1.2.3\n\tprojects -> 1.2.3"); + } +} diff --git a/src/test/java/com/palantir/gradle/versions/MavenRepoUtils.java b/src/test/java/com/palantir/gradle/versions/MavenRepoUtils.java new file mode 100644 index 000000000..2ea1a40ef --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/MavenRepoUtils.java @@ -0,0 +1,40 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import nebula.test.dependencies.DependencyGraph; +import nebula.test.dependencies.GradleDependencyGenerator; + +public final class MavenRepoUtils { + private MavenRepoUtils() {} + + public static File generateMavenRepo(Path tempDir, String... graph) { + DependencyGraph dependencyGraph = new DependencyGraph(graph); + try { + Path repoPath = Files.createDirectories(tempDir.resolve("testrepogen")); + GradleDependencyGenerator generator = new GradleDependencyGenerator(dependencyGraph, repoPath.toString()); + return generator.generateTestMavenRepo(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create Maven repo", e); + } + } +} diff --git a/src/test/java/com/palantir/gradle/versions/PomUtils.java b/src/test/java/com/palantir/gradle/versions/PomUtils.java new file mode 100644 index 000000000..9a50eb9d8 --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/PomUtils.java @@ -0,0 +1,52 @@ +/* + * (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class PomUtils { + private PomUtils() {} + + public static void makePlatformPom(File repo, String group, String name, String version) { + Path dir = repo.toPath().resolve(group).resolve(name).resolve(version); + try { + Files.createDirectories(dir); + String pomContent = """ + + + 4.0.0 + pom + %s + %s + %s + + + + + + """.formatted(group, name, version); + Files.writeString(dir.resolve("platform-1.0.pom"), pomContent); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create platform POM", e); + } + } +} diff --git a/src/test/java/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.java b/src/test/java/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.java new file mode 100644 index 000000000..9e8e89497 --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/SlsPackagingCompatibilityIntegrationSpec.java @@ -0,0 +1,138 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import com.palantir.gradle.testing.project.SubProject; +import java.io.File; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +/** + * https://github.com/palantir/sls-packaging does some funky stuff when resolving inter-project dependencies for the + * purposes of detecting published recommended product dependencies, so we want to make double sure that GCV doesn't + * accidentally break it. + */ +@GradlePluginTests +class SlsPackagingCompatibilityIntegrationSpec { + + private static final String PLUGIN_NAME = "com.palantir.consistent-versions"; + private File mavenRepo; + + @BeforeEach + void setup(RootProject rootProject) { + mavenRepo = MavenRepoUtils.generateMavenRepo(rootProject.path().resolve("build"), "org.slf4j:slf4j-api:1.7.24"); + rootProject.buildGradle().append(""" + buildscript { + repositories { + mavenCentral() + } + } + plugins { + id '%s' + id 'com.palantir.sls-java-service-distribution' version '7.31.0' apply false + } + allprojects { + repositories { + maven { url "file:///%s" } + } + } + """, PLUGIN_NAME, mavenRepo.getAbsolutePath()); + } + + @DisabledIf("isGradle9OrLater") + @Test + void can_consume_recommended_product_dependencies_project( + GradleInvoker gradle, RootProject rootProject, SubProject apiProject, SubProject serviceProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:* = 1.7.24 + """); + + rootProject.buildGradle().append(""" + allprojects { + version = '1.0.0' + } + """); + + rootProject.settingsGradle().include("api"); + rootProject.settingsGradle().include("service"); + apiProject.buildGradle().append(""" + apply plugin: 'java' + apply plugin: 'com.palantir.sls-recommended-dependencies' + \s\s\s\s + dependencies { + implementation 'org.slf4j:slf4j-api' + } + \s\s\s\s + recommendedProductDependencies { + productDependency { + productGroup = 'org' + productName = 'product' + minimumVersion = '1.1.0' + maximumVersion = '1.x.x' + } + } + """); + + serviceProject.buildGradle().append(""" + apply plugin: 'java' + apply plugin: 'com.palantir.sls-java-service-distribution' + \s\s\s\s + dependencies { + // Gets picked up by the productDependenciesConfig which is runtimeClasspath + implementation project(':api') + } + """); + + InvocationResult wroteLocks = gradle.withArgs("--write-locks").buildsSuccessfully(); + // Maybe this is a bit too much but for a fixed version of sls-packaging, we expect this to not change + Set successfulTasks = wroteLocks + .output() + .lines() + .filter(line -> line.contains("> Task :")) + .map(line -> line.substring(line.indexOf(":"), line.length()).trim()) + .collect(Collectors.toSet()); + + assertThat(successfulTasks) + .containsExactlyInAnyOrder( + ":api:compileRecommendedProductDependencies", + ":api:processResources", + ":service:mergeDiagnosticsJson", + ":service:resolveProductDependencies", + ":service:createManifest", + ":api:classes", + ":api:configureProductDependencies", + ":api:jar", + ":service:jar"); + + gradle.withArgs("createManifest", "verifyLocks").buildsSuccessfully(); + } + + @SuppressWarnings("unused") + private static boolean isGradle9OrLater() { + String gradleVersion = org.gradle.util.GradleVersion.current().getVersion(); + return gradleVersion.startsWith("9"); + } +} diff --git a/src/test/java/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.java b/src/test/java/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.java new file mode 100644 index 000000000..4077ce7b5 --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/VersionPropsIdeaPluginIntegrationSpec.java @@ -0,0 +1,73 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.execution.TaskOutcome; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@GradlePluginTests +class VersionPropsIdeaPluginIntegrationSpec { + + @BeforeEach + void setup(RootProject rootProject) { + rootProject.buildGradle().append(""" + repositories { + maven { + url 'https://test' + } + maven { + url 'https://demo/' + } + mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } } + } + + apply plugin: 'com.palantir.version-props-idea' + apply plugin: 'idea' + """); + + Path ideaDir = rootProject.path().resolve(".idea"); + ideaDir.toFile().mkdirs(); + } + + @Test + void plugin_creates_gcv_maven_repositories_xml_file_in_idea_folder(GradleInvoker gradle, RootProject rootProject) + throws IOException { + gradle.withArgs("-Didea.active=true").buildsSuccessfully(); + + Path repoFile = rootProject.path().resolve(".idea/gcv-maven-repositories.xml"); + assertThat(repoFile).exists(); + + String content = Files.readString(repoFile); + assertThat(content).contains(""); + assertThat(content).contains(""); + assertThat(content).contains(""); + + InvocationResult secondRun = gradle.withArgs("-Didea.active=true").buildsSuccessfully(); + assertThat(secondRun.task(":writeMavenRepositories")) + .hasValueSatisfying(task -> assertThat(task.outcome()).isEqualTo(TaskOutcome.UP_TO_DATE)); + } +} diff --git a/src/test/java/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.java b/src/test/java/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.java new file mode 100644 index 000000000..ae79fbe6a --- /dev/null +++ b/src/test/java/com/palantir/gradle/versions/VersionsPropsPluginIntegrationSpec.java @@ -0,0 +1,321 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.gradle.versions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.gradle.testing.execution.GradleInvoker; +import com.palantir.gradle.testing.execution.InvocationResult; +import com.palantir.gradle.testing.junit.GradlePluginTests; +import com.palantir.gradle.testing.project.RootProject; +import com.palantir.gradle.testing.project.SubProject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +@GradlePluginTests +class VersionsPropsPluginIntegrationSpec { + private static final String PLUGIN_NAME = "com.palantir.versions-props"; + private File mavenRepo; + + @BeforeEach + void setup(RootProject rootProject) { + mavenRepo = MavenRepoUtils.generateMavenRepo( + rootProject.path().resolve("build"), + "ch.qos.logback:logback-classic:1.2.3 -> org.slf4j:slf4j-api:1.7.25", + "ch.qos.logback:logback-classic:1.1.11 -> org.slf4j:slf4j-api:1.7.22", + "org.slf4j:slf4j-api:1.7.21", + "org.slf4j:slf4j-api:1.7.22", + "org.slf4j:slf4j-api:1.7.24", + "org.slf4j:slf4j-api:1.7.25", + "com.fasterxml.jackson.core:jackson-databind:2.9.0 ->" + + " com.fasterxml.jackson.core:jackson-annotations:2.9.0", + "com.fasterxml.jackson.core:jackson-annotations:2.9.0", + "com.fasterxml.jackson.core:jackson-annotations:2.9.7", + "com.fasterxml.jackson.core:jackson-databind:2.9.7"); + PomUtils.makePlatformPom(mavenRepo, "org", "platform", "1.0"); + + rootProject.buildGradle().append(""" + buildscript { + repositories { + mavenCentral() + } + } + plugins { + id '%s' + } + allprojects { + repositories { + maven { url "file:///%s" } + } + } + + // Make it easy to verify what versions of dependencies you got. + allprojects { + configurations.matching { it.name == 'runtimeClasspath' }.all { + resolutionStrategy.activateDependencyLocking() + } + task resolveConfigurations { + doLast { + if (pluginManager.hasPlugin('java')) { + configurations.compileClasspath.resolve() + configurations.runtimeClasspath.resolve() + } + } + } + } + """, PLUGIN_NAME, mavenRepo.getAbsolutePath()); + } + + @Test + void star_dependency_constraint_is_injected_for_direct_dependency( + GradleInvoker gradle, RootProject rootProject, SubProject fooProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:* = 1.7.24 + """); + + rootProject.settingsGradle().include("foo"); + fooProject.buildGradle().append(""" + apply plugin: 'java' + dependencies { + implementation 'org.slf4j:slf4j-api' + } + """); + + gradle.withArgs("resolveConfigurations", "--write-locks").buildsSuccessfully(); + + verifyLockfile(fooProject.path(), "org.slf4j:slf4j-api:1.7.24"); + } + + @Test + void star_dependency_constraint_is_not_forcefully_downgraded_for_transitive_dependency( + GradleInvoker gradle, RootProject rootProject, SubProject fooProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:* = 1.7.21 + ch.qos.logback:logback-classic = 1.1.11 # brings in slf4j-api 1.7.22 + """); + + rootProject.settingsGradle().include("foo"); + fooProject.buildGradle().append(""" + apply plugin: 'java' + dependencies { + implementation 'ch.qos.logback:logback-classic' + } + """); + + gradle.withArgs("resolveConfigurations", "--write-locks").buildsSuccessfully(); + + verifyLockfile(fooProject.path(), "org.slf4j:slf4j-api:1.7.22"); + } + + @Test + void star_dependency_constraint_upgrades_transitive_dependency( + GradleInvoker gradle, RootProject rootProject, SubProject fooProject) { + rootProject.file("versions.props").overwrite(""" + org.slf4j:* = 1.7.25 + ch.qos.logback:logback-classic = 1.1.11 # brings in slf4j-api 1.7.22 + """); + + rootProject.settingsGradle().include("foo"); + fooProject.buildGradle().append(""" + apply plugin: 'java' + dependencies { + implementation 'ch.qos.logback:logback-classic' + } + """); + + gradle.withArgs("resolveConfigurations", "--write-locks").buildsSuccessfully(); + + verifyLockfile(fooProject.path(), "org.slf4j:slf4j-api:1.7.25"); + } + + @Test + void imported_platform_generated_correctly_in_pom( + GradleInvoker gradle, RootProject rootProject, SubProject fooProject) throws Exception { + rootProject.file("versions.props").overwrite(""" + org:platform = 1.0 + # This shouldn't end up in the POM + other:constraint = 1.0.0 + """); + + rootProject.settingsGradle().include("foo"); + fooProject.buildGradle().append(""" + apply plugin: 'java-library' + apply plugin: 'maven-publish' + dependencies { + rootConfiguration platform('org:platform') + } + publishing { + publications { + main(MavenPublication) { + from components.java + } + } + } + """); + + gradle.withArgs("foo:generatePomFile").buildsSuccessfully(); + + Path pomFile = fooProject.path().resolve("build/publications/main/pom-default.xml"); + assertThat(pomFile).exists(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document pom = builder.parse(pomFile.toFile()); + + NodeList dependencies = + pom.getElementsByTagName("dependencyManagement").item(0).getChildNodes(); + Element dependenciesElement = null; + for (int i = 0; i < dependencies.getLength(); i++) { + if (dependencies.item(i).getNodeName().equals("dependencies")) { + dependenciesElement = (Element) dependencies.item(i); + break; + } + } + + assertThat(dependenciesElement).isNotNull(); + NodeList dependencyList = dependenciesElement.getElementsByTagName("dependency"); + assertThat(dependencyList.getLength()).isEqualTo(1); + + Element dependency = (Element) dependencyList.item(0); + Map depMap = new HashMap<>(); + depMap.put("groupId", dependency.getElementsByTagName("groupId").item(0).getTextContent()); + depMap.put( + "artifactId", + dependency.getElementsByTagName("artifactId").item(0).getTextContent()); + depMap.put("version", dependency.getElementsByTagName("version").item(0).getTextContent()); + depMap.put("scope", dependency.getElementsByTagName("scope").item(0).getTextContent()); + depMap.put("type", dependency.getElementsByTagName("type").item(0).getTextContent()); + + assertThat(depMap).containsEntry("groupId", "org"); + assertThat(depMap).containsEntry("artifactId", "platform"); + assertThat(depMap).containsEntry("version", "1.0"); + assertThat(depMap).containsEntry("scope", "import"); + assertThat(depMap).containsEntry("type", "pom"); + } + + @Test + void non_glob_module_forces_do_not_get_added_to_a_matching_platform_too( + GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().append(""" + apply plugin: 'java' + dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind' + } + """); + rootProject.file("versions.props").overwrite(""" + com.fasterxml.jackson.core:jackson-databind = 2.9.0 + com.fasterxml.jackson.*:* = 2.9.7 + """); + + gradle.withArgs("resolveConfigurations", "--write-locks").buildsSuccessfully(); + + verifyLockfile( + rootProject.path(), + "com.fasterxml.jackson.core:jackson-databind:2.9.0", + "com.fasterxml.jackson.core:jackson-annotations:2.9.7"); + } + + @Test + void throws_if_resolving_configuration_in_after_evaluate(GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().append(""" + configurations { foo } + \s\s\s\s + afterEvaluate { + configurations.foo.resolve() + } + """); + rootProject.file("versions.props").overwrite(""); + + InvocationResult result = gradle.withArgs().buildsWithFailure(); + assertThat(result.output()).contains("Not allowed to resolve"); + } + + @Test + void does_not_throw_if_excluded_configuration_is_resolved_early(GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().append(""" + configurations { foo } + \s\s\s\s + versionRecommendations { + excludeConfigurations 'foo' + } + \s\s\s\s + afterEvaluate { + configurations.foo.resolve() + } + """); + rootProject.file("versions.props").overwrite(""); + + gradle.withArgs().buildsSuccessfully(); + } + + @Test + void creates_root_configuration_even_if_versions_props_file_missing(GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().append(""" + dependencies { + constraints { + rootConfiguration 'org.slf4j:slf4j-api:1.7.25' + } + } + """); + rootProject.path().resolve("versions.props").toFile().delete(); + + gradle.withArgs().buildsSuccessfully(); + } + + @Test + void build_succeeds_without_versions_props_or_versions_lock( + GradleInvoker gradle, RootProject rootProject, SubProject fooProject) { + rootProject.settingsGradle().include("foo"); + fooProject.buildGradle().append(""" + apply plugin: 'java' + """); + + gradle.withArgs("build").buildsSuccessfully(); + } + + private void verifyLockfile(Path projectDir, String... lines) { + Path lockfile = projectDir.resolve("gradle.lockfile"); + if (!lockfile.toFile().exists()) { + lockfile = projectDir.resolve("gradle/dependency-locks/runtimeClasspath.lockfile"); + } + assertThat(lockfile).exists(); + + try { + String lockfileContent = Files.readString(lockfile); + for (String line : lines) { + if (lockfile.getFileName().toString().equals("gradle.lockfile")) { + assertThat(lockfileContent).contains(line + "=runtimeClasspath"); + } else { + assertThat(lockfileContent).contains(line); + } + } + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + } +} diff --git a/versions.lock b/versions.lock index 219bed39c..cd3211b43 100644 --- a/versions.lock +++ b/versions.lock @@ -44,11 +44,11 @@ cglib:cglib-nodep:3.2.2 (1 constraints: 490ded24) com.netflix.nebula:nebula-test:10.6.2 (2 constraints: d61de72d) -com.palantir.gradle.plugintesting:configuration-cache-spec:0.19.0 (1 constraints: 3c053e3b) +com.palantir.gradle.plugintesting:configuration-cache-spec:0.23.0 (1 constraints: 3705303b) -com.palantir.gradle.plugintesting:gradle-plugin-testing-junit:0.19.0 (1 constraints: 3c053e3b) +com.palantir.gradle.plugintesting:gradle-plugin-testing-junit:0.23.0 (1 constraints: 3705303b) -com.palantir.gradle.plugintesting:plugin-testing-core:0.19.0 (1 constraints: 3c053e3b) +com.palantir.gradle.plugintesting:plugin-testing-core:0.23.0 (1 constraints: 3705303b) commons-io:commons-io:2.19.0 (1 constraints: db190cdf) diff --git a/versions.props b/versions.props index df95f0b5e..fb93acd1a 100644 --- a/versions.props +++ b/versions.props @@ -1,6 +1,7 @@ com.fasterxml.jackson.*:* = 2.20.0 com.fasterxml.jackson.core:jackson-annotations = 2.20 com.google.guava:guava = 33.5.0-jre +com.palantir.gradle.plugintesting:gradle-plugin-testing-junit = 0.23.0 org.assertj:* = 3.27.6 org.immutables:value = 2.11.6 org.junit.jupiter:* = 6.0.1