From c81d1d541206d9b7b7eeba762c73f54ff4b76bd6 Mon Sep 17 00:00:00 2001 From: Aubrey Chipman Date: Tue, 30 Nov 2021 10:52:01 -0800 Subject: [PATCH 1/2] Tests for dependency locking interaction with resolution rules plugin --- .../ResolutionRulesLockabilitySpec.groovy | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy diff --git a/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy b/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy new file mode 100644 index 0000000..848ec78 --- /dev/null +++ b/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy @@ -0,0 +1,331 @@ +/** + * + * Copyright 2021 Netflix, Inc. + * + * 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 nebula.plugin.dependencylock + +import nebula.plugin.dependencylock.util.LockGenerator +import nebula.test.IntegrationTestKitSpec +import org.gradle.testkit.runner.TaskOutcome + +import java.util.jar.Attributes +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.jar.Manifest + +class ResolutionRulesLockabilitySpec extends IntegrationTestKitSpec { + def mavenForRules + + def setup() { + debug = true + keepFiles = true + + setupRules() + + buildFile << """\ + buildscript { + repositories { mavenCentral() } + dependencies { + classpath 'com.netflix.nebula:gradle-resolution-rules-plugin:8.0.0' + } + } + plugins { + id 'nebula.dependency-lock' + id 'java' + } + apply plugin: 'nebula.resolution-rules' + allprojects { + repositories { + mavenCentral() + maven { url '${mavenForRules.absolutePath}' } + } + } + dependencies { + resolutionRules 'test.rules:resolution-rules:1.+' + } + """.stripIndent() + + addSubproject('sub1', """ + plugins { + id 'nebula.dependency-lock' + id 'nebula.resolution-rules' + id 'java' + } + dependencies { + implementation 'org.apache.commons:commons-lang3:3.12.0' + } + """) + + addSubproject('sub2', """ + plugins { + id 'nebula.dependency-lock' + id 'nebula.resolution-rules' + id 'java' + } + dependencies { + implementation project(':sub1') + implementation 'commons-io:commons-io:2.11.0' + } + """) + + addSubproject('sub3', """ + plugins { + id 'nebula.dependency-lock' + id 'nebula.resolution-rules' + id 'java' + } + dependencies { + implementation 'commons-logging:commons-logging:1.2' + } + """) + } + + def 'global locking works'() { + when: + def result = runTasks('generateGlobalLock', 'saveGlobalLock') + + then: + result.task(":generateGlobalLock").outcome == TaskOutcome.SUCCESS + result.task(":saveGlobalLock").outcome == TaskOutcome.SUCCESS + + def globalLockFile = new File(projectDir, 'global.lock') + def globalLockText = """\ +{ + "_global_": { + "commons-io:commons-io": { + "firstLevelTransitive": [ + "$moduleName:sub2" + ], + "locked": "2.11.0" + }, + "commons-logging:commons-logging": { + "firstLevelTransitive": [ + "$moduleName:sub3" + ], + "locked": "1.2" + }, + "$moduleName:sub1": { + "firstLevelTransitive": [ + "$moduleName:sub2" + ], + "project": true + }, + "$moduleName:sub2": { + "project": true + }, + "$moduleName:sub3": { + "project": true + }, + "org.apache.commons:commons-lang3": { + "firstLevelTransitive": [ + "$moduleName:sub1", + "$moduleName:sub1" + ], + "locked": "3.12.0" + } + }, + "resolutionRules": { + "test.rules:resolution-rules": { + "locked": "1.0.0" + } + } +}""".stripIndent() + + globalLockFile.text == globalLockText + } + + def 'project locking works'() { + when: + def result = runTasks('generateLock', 'saveLock') + + then: + result.task(":generateLock").outcome == TaskOutcome.SUCCESS + result.task(":saveLock").outcome == TaskOutcome.SUCCESS + + def rootLockFile = new File(projectDir, 'dependencies.lock') + def sub1LockFile = new File(projectDir, 'sub1/dependencies.lock') + def sub2LockFile = new File(projectDir, 'sub2/dependencies.lock') + def sub3LockFile = new File(projectDir, 'sub3/dependencies.lock') + + def rootLockText = '''\ + { + "resolutionRules": { + "test.rules:resolution-rules": { + "locked": "1.0.0" + } + } + }'''.stripIndent() + + def sub1LockText = LockGenerator.duplicateIntoConfigs('''\ + "org.apache.commons:commons-lang3": { + "locked": "3.12.0" + } + '''.stripIndent()) + + def sub2LockText = LockGenerator.duplicateIntoConfigs("""\ + "commons-io:commons-io": { + "locked": "2.11.0" + }, + "org.apache.commons:commons-lang3": { + "firstLevelTransitive": [ + "$moduleName:sub1" + ], + "locked": "3.12.0" + }, + "$moduleName:sub1": { + "project": true + } + """.stripIndent(), ['runtimeClasspath', 'testRuntimeClasspath'], """\ + "commons-io:commons-io": { + "locked": "2.11.0" + }, + "$moduleName:sub1": { + "project": true + } + """.stripIndent(), ['compileClasspath', 'testCompileClasspath']) + + def sub3LockText = LockGenerator.duplicateIntoConfigs('''\ + "commons-logging:commons-logging": { + "locked": "1.2" + } + '''.stripIndent()) + + rootLockFile.text == rootLockText + sub1LockFile.text == sub1LockText + sub2LockFile.text == sub2LockText + sub3LockFile.text == sub3LockText + } + + def 'Gradle core locking works'() { + given: + new File("${projectDir}/gradle.properties").text = """ + systemProp.nebula.features.coreLockingSupport=true + dependencyLock.additionalConfigurationsToLock=resolutionRules + """.stripIndent() + buildFile << ''' + allprojects { + task dependenciesForAll(type: DependencyReportTask) {} + } + '''.stripIndent() + + when: + def result = runTasks('dependenciesForAll', '--write-locks') + + then: + def rootLockFile = coreLockContent(new File(projectDir, 'gradle.lockfile')) + def sub1LockFile = coreLockContent(new File(projectDir, 'sub1/gradle.lockfile')) + def sub2LockFile = coreLockContent(new File(projectDir, 'sub2/gradle.lockfile')) + def sub3LockFile = coreLockContent(new File(projectDir, 'sub3/gradle.lockfile')) + + rootLockFile.get('test.rules:resolution-rules:1.0.0') == 'resolutionRules' + rootLockFile.get('empty') == 'annotationProcessor,compileClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath' + + sub1LockFile.get('org.apache.commons:commons-lang3:3.12.0') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' + sub1LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' + + sub2LockFile.get('commons-io:commons-io:2.11.0') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' + sub2LockFile.get('org.apache.commons:commons-lang3:3.12.0') == 'runtimeClasspath,testRuntimeClasspath' + sub2LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' + + sub3LockFile.get('commons-logging:commons-logging:1.2') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' + sub3LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' + } + + private void setupRules() { + def rulesFolder = new File(projectDir, 'rules') + rulesFolder.mkdirs() + def rulesJsonFile = new File(rulesFolder, 'rules.json') + rulesJsonFile.text = '''\ + { + "deny": [], "reject": [], "substitute": [], "replace": [], "align": [] + } + '''.stripIndent() + + mavenForRules = new File(projectDir, 'repo') + mavenForRules.mkdirs() + def locked = new File(mavenForRules, 'test/rules/resolution-rules/1.0.0') + locked.mkdirs() + createRulesJar([rulesFolder], projectDir, new File(locked, 'resolution-rules-1.0.0.jar')) + createPom('test.rules', 'resolution-rules', '1.0.0', locked) + + def mavenMetadataXml = new File(mavenForRules, 'test/rules/resolution-rules/maven-metadata.xml') + mavenMetadataXml.createNewFile() + mavenMetadataXml << ''' + + test.rules + resolution-rules + + 1.0.0 + 1.0.0 + + 1.0.0 + + 20211130014943 + + +''' + } + + private createRulesJar(Collection files, File unneededRoot, File destination) { + Manifest manifest = new Manifest() + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, '1.0') + JarOutputStream target = new JarOutputStream(new FileOutputStream(destination), manifest) + files.each { add(it, unneededRoot, target) } + target.close() + } + + private createPom(String group, String name, String version, File dir) { + def pom = new File(dir, "${name}-${version}.pom") + pom.text = """\ + + + 4.0.0 + ${group} + ${name} + ${version} + + """.stripIndent() + } + + private void add(File source, File unneededRoot, JarOutputStream target) throws IOException { + def prefix = "${unneededRoot.path}/" + if (source.isDirectory()) { + String dirName = source.path - prefix + if (!dirName.endsWith('/')) { + dirName += '/' + } + def entry = new JarEntry(dirName) + target.putNextEntry(entry) + target.closeEntry() + source.listFiles().each { nested -> + add(nested, unneededRoot, target) + } + } else { + def entry = new JarEntry(source.path - prefix) + target.putNextEntry(entry) + target << source.bytes + target.closeEntry() + } + } + + private Map coreLockContent(File lockFile) { + lockFile.readLines().findAll {!it.startsWith("#")}.collectEntries { + it.split('=').toList() + } + } +} From 0b62397c5a06998d8a79f77c2aa619ce231d2d04 Mon Sep 17 00:00:00 2001 From: Aubrey Chipman Date: Tue, 30 Nov 2021 14:10:49 -0800 Subject: [PATCH 2/2] Dependency locking interaction with resolution rules plugin which uses a build service for resolution rules In resolution-rules plugin 9.0.0, the resolution rules configuration was added to each subproject with a project dependency to the root project. By adding the resolutionRules configuration to the set of configurations to skip for each subproject, it remains locked from the parent project but does not run into an error --- .../DependencyLockTaskConfigurer.groovy | 3 +- .../ResolutionRulesLockabilitySpec.groovy | 38 +++++++++++++++++-- .../dependencylock/util/LockGenerator.groovy | 7 +++- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockTaskConfigurer.groovy b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockTaskConfigurer.groovy index c996409..9b7fb0a 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockTaskConfigurer.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockTaskConfigurer.groovy @@ -53,7 +53,8 @@ class DependencyLockTaskConfigurer { public static final String SAVE_LOCK_TASK_NAME = 'saveLock' public static final String SAVE_GLOBAL_LOCK_TASK_NAME = 'saveGlobalLock' - final Set configurationsToSkipForGlobalLock = ['checkstyle', 'findbugs', 'findbugsPlugins', 'jacocoAgent', 'jacocoAnt', 'spotbugs', 'spotbugsPlugins', 'zinc', 'pmd'] + // these get skipped for subproject's configurations + final Set configurationsToSkipForGlobalLock = ['checkstyle', 'findbugs', 'findbugsPlugins', 'jacocoAgent', 'jacocoAnt', 'spotbugs', 'spotbugsPlugins', 'zinc', 'pmd', 'resolutionRules'] Project project diff --git a/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy b/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy index 848ec78..fba6b4b 100644 --- a/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy +++ b/src/test/groovy/nebula/plugin/dependencylock/ResolutionRulesLockabilitySpec.groovy @@ -40,7 +40,7 @@ class ResolutionRulesLockabilitySpec extends IntegrationTestKitSpec { buildscript { repositories { mavenCentral() } dependencies { - classpath 'com.netflix.nebula:gradle-resolution-rules-plugin:8.0.0' + classpath 'com.netflix.nebula:gradle-resolution-rules-plugin:latest.release' } } plugins { @@ -174,7 +174,16 @@ class ResolutionRulesLockabilitySpec extends IntegrationTestKitSpec { "org.apache.commons:commons-lang3": { "locked": "3.12.0" } - '''.stripIndent()) + '''.stripIndent(), ['compileClasspath', 'testCompileClasspath', 'runtimeClasspath', 'testRuntimeClasspath'], """\ + ":$moduleName": { + "project": true + }, + "test.rules:resolution-rules": { + "firstLevelTransitive": [ + ":$moduleName" + ], + "locked": "1.0.0" + }""".stripIndent(), ['resolutionRules']) def sub2LockText = LockGenerator.duplicateIntoConfigs("""\ "commons-io:commons-io": { @@ -196,13 +205,31 @@ class ResolutionRulesLockabilitySpec extends IntegrationTestKitSpec { "$moduleName:sub1": { "project": true } - """.stripIndent(), ['compileClasspath', 'testCompileClasspath']) + """.stripIndent(), ['compileClasspath', 'testCompileClasspath'], """\ + ":$moduleName": { + "project": true + }, + "test.rules:resolution-rules": { + "firstLevelTransitive": [ + ":$moduleName" + ], + "locked": "1.0.0" + }""".stripIndent(), ['resolutionRules']) def sub3LockText = LockGenerator.duplicateIntoConfigs('''\ "commons-logging:commons-logging": { "locked": "1.2" } - '''.stripIndent()) + '''.stripIndent(), ['compileClasspath', 'testCompileClasspath', 'runtimeClasspath', 'testRuntimeClasspath'], """\ + ":$moduleName": { + "project": true + }, + "test.rules:resolution-rules": { + "firstLevelTransitive": [ + ":$moduleName" + ], + "locked": "1.0.0" + }""".stripIndent(), ['resolutionRules']) rootLockFile.text == rootLockText sub1LockFile.text == sub1LockText @@ -235,13 +262,16 @@ class ResolutionRulesLockabilitySpec extends IntegrationTestKitSpec { rootLockFile.get('empty') == 'annotationProcessor,compileClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath' sub1LockFile.get('org.apache.commons:commons-lang3:3.12.0') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' + sub1LockFile.get('test.rules:resolution-rules:1.0.0') == 'resolutionRules' sub1LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' sub2LockFile.get('commons-io:commons-io:2.11.0') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' sub2LockFile.get('org.apache.commons:commons-lang3:3.12.0') == 'runtimeClasspath,testRuntimeClasspath' + sub2LockFile.get('test.rules:resolution-rules:1.0.0') == 'resolutionRules' sub2LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' sub3LockFile.get('commons-logging:commons-logging:1.2') == 'compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath' + sub3LockFile.get('test.rules:resolution-rules:1.0.0') == 'resolutionRules' sub3LockFile.get('empty') == 'annotationProcessor,testAnnotationProcessor' } diff --git a/src/test/groovy/nebula/plugin/dependencylock/util/LockGenerator.groovy b/src/test/groovy/nebula/plugin/dependencylock/util/LockGenerator.groovy index b8f5c3d..5949e43 100644 --- a/src/test/groovy/nebula/plugin/dependencylock/util/LockGenerator.groovy +++ b/src/test/groovy/nebula/plugin/dependencylock/util/LockGenerator.groovy @@ -83,7 +83,9 @@ class LockGenerator { * @param secondConfigs configurations to duplicate the second set of dependencies into * @return the String to put into the file */ - static String duplicateIntoConfigs(String firstDeps, Collection firstConfigs, String secondDeps, Collection secondConfigs) { + static String duplicateIntoConfigs(String firstDeps, Collection firstConfigs, + String secondDeps, Collection secondConfigs, + String thirdDeps = '', Collection thirdConfigs = new ArrayList<>()) { Map configNameToDependencyContents = new HashMap() firstConfigs.each { configName -> @@ -92,6 +94,9 @@ class LockGenerator { secondConfigs.each { configName -> configNameToDependencyContents.put(configName, secondDeps) } + thirdConfigs.each { configName -> + configNameToDependencyContents.put(configName, thirdDeps) + } def sortedConfigNameToDependencyContents = configNameToDependencyContents.sort() List configBlocks = new ArrayList()