From d03f56c23e799c197a05c667c8ea0d5b3abe91c6 Mon Sep 17 00:00:00 2001 From: David Cowden Date: Fri, 20 Feb 2015 00:08:29 -0800 Subject: [PATCH] Add updateLock task It is now possible to update only a subset of the locked dependencies rather than updating them all at once by regenerating the lock file. #42 --- README.md | 16 +- gradle.properties | 2 +- .../DependencyLockExtension.groovy | 1 + .../DependencyLockPlugin.groovy | 39 +- .../tasks/GenerateLockTask.groovy | 18 +- .../dependencylock/tasks/LockKey.groovy | 17 + .../tasks/UpdateLockTask.groovy | 99 +++ .../tasks/UpdateLockTaskSpec.groovy | 628 ++++++++++++++++++ 8 files changed, 790 insertions(+), 30 deletions(-) create mode 100644 src/main/groovy/nebula/plugin/dependencylock/tasks/LockKey.groovy create mode 100644 src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy create mode 100644 src/test/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTaskSpec.groovy diff --git a/README.md b/README.md index bcf0f3e6..19fff735 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ To include, add the following to your build.gradle Command line overrides via `-PdependencyLock.override` or `-PdependencyLock.overrideFile` will apply. -* generateLock - Generate a lock file into the build directory, any existing `dependency.lock` file will be ignored -* saveLock - copies generated lock into the project directory +* generateLock - Generate a lock file into the build directory. Any existing `dependency.lock` file will be ignored. +* updateLock - Update dependencies from the lock file into the build directory. By default, this task does the same thing as the `generateLock` task. This task also exposes an option `--dependencies` allowing the user to specify a comma-separated list, in the format `:`, of dependencies to update. +* saveLock - Copy the generated lock into the project directory. * commitLock - If a [gradle-scm-plugin](https://github.com/nebula-plugins/gradle-scm-plugin) implementation is applied. Will commit dependencies.lock to the configured SCM. Exists only on the rootProject. Assumes scm root is at the same level as the root build.gradle. #### Common Command Line Overrides @@ -49,6 +50,8 @@ Revert to normal gradle behavior even with plugin applied. ### Common Workflows +Generate lock: + 1. `./gradlew generateLock saveLock` 2. `./gradlew test` 3. if 2 passes `./gradlew commitLock` @@ -59,6 +62,11 @@ or 2. `./gradlew -PdependencyLock.useGeneratedLock=true test` 3. `./gradlew saveLock commitLock` +Update lock (the lock must still be saved/committed): + +* `./gradlew updateLock --dependencies com.example:foo,com.example:bar` + + ### Extensions Provided #### dependencyLock Extension @@ -68,15 +76,17 @@ or * lockFile - This field takes a String. The default is `dependencies.lock`. This filename will be what is generated by `generateLock` and read when locking dependencies. * configurationNames - This field takes a List. Defaults to the `testRuntime` conf which will include `compile`, `runtime`, and `testCompile`. These will be the configurations that are read when locking. * dependencyFilter - This field can be assigned a Closure that is used to filter the set of top-level dependencies as they are retrieved from the configurations. This happens before overrides are applied and before any dependencies are skipped. The Closure must accept the dependency's `group`, `name`, and `version` as its 3 parameters. The default implementation returns `true`, meaning all dependencies are used. +* updateDependencies - This field takes a List denoting the dependencies that should be updated when the `updateLock` task is run. If any dependencies are specified via the `--dependencies` option, this field is ignored. If any dependencies are listed during execution of the `updateLock` task either via the `--dependencies` option or this field, the `dependencyFilter` is bypassed. * skippedDependencies - This field takes a List. Defaults to empty. This list is used to list dependencies as ones that will never be locked. Strings should be of the format `:` * includeTransitives - This field is a boolean. Defaults to false. False will only lock direct dependencies. True will lock the entire transitive graph. -Use the extension if you wish to configure. Each project where gradle-dependency-lock will have its own dependencyLock extension. +Use the extension if you wish to configure. Each project where gradle-dependency-lock is applied will have its own dependencyLock extension. dependencyLock { lockFile = 'dependencies.lock' configurationNames = ['testRuntime'] dependencyFilter = { String group, String name, String version -> true } + updateDependencies = [] skippedDependencies = [] includeTransitives = false } diff --git a/gradle.properties b/gradle.properties index d9c120b4..8b9160c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.12.4-SNAPSHOT +version=1.12.5-SNAPSHOT diff --git a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy index d37bc17b..73959ba4 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy @@ -19,6 +19,7 @@ class DependencyLockExtension { String lockFile = 'dependencies.lock' Set configurationNames = ['testRuntime'] as Set Closure dependencyFilter = { String group, String name, String version -> true } + Set updateDependencies = [] as Set Set skippedDependencies = [] as Set boolean includeTransitives = false } diff --git a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy index 23dd02ad..a9a2fbfa 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy @@ -19,6 +19,7 @@ import groovy.json.JsonSlurper import nebula.plugin.dependencylock.tasks.CommitLockTask import nebula.plugin.dependencylock.tasks.GenerateLockTask import nebula.plugin.dependencylock.tasks.SaveLockTask +import nebula.plugin.dependencylock.tasks.UpdateLockTask import nebula.plugin.scm.ScmPlugin import org.gradle.api.GradleException import org.gradle.api.Plugin @@ -42,18 +43,29 @@ class DependencyLockPlugin implements Plugin { } Map overrides = loadOverrides() - GenerateLockTask lockTask = configureLockTask(clLockFileName, extension, overrides) + + GenerateLockTask genLockTask = project.tasks.create('generateLock', GenerateLockTask) + configureLockTask(genLockTask, clLockFileName, extension, overrides) if (project.hasProperty('dependencyLock.useGeneratedLock')) { - clLockFileName = lockTask.getDependenciesLock().path + clLockFileName = genLockTask.getDependenciesLock().path logger.lifecycle(clLockFileName) } - SaveLockTask saveTask = configureSaveTask(lockTask, extension) + + UpdateLockTask updateLockTask = project.tasks.create('updateLock', UpdateLockTask) + configureLockTask(updateLockTask, clLockFileName, extension, overrides) + configureUpdateTask(updateLockTask, extension) + + SaveLockTask saveTask = configureSaveTask(clLockFileName, extension) + saveTask.mustRunAfter genLockTask, updateLockTask + configureCommitTask(clLockFileName, saveTask, extension, commitExtension) project.gradle.taskGraph.whenReady { taskGraph -> File dependenciesLock = new File(project.projectDir, clLockFileName ?: extension.lockFile) - if (taskGraph.hasTask(lockTask)) { + def hasLockingTask = taskGraph.hasTask(genLockTask) || taskGraph.hasTask(updateLockTask) + + if (hasLockingTask) { project.configurations.all { resolutionStrategy { cacheDynamicVersionsFor 0, 'seconds' @@ -61,8 +73,7 @@ class DependencyLockPlugin implements Plugin { } } - if (!taskGraph.hasTask(lockTask) && dependenciesLock.exists() && - !shouldIgnoreDependencyLock()) { + if (!hasLockingTask && dependenciesLock.exists() && !shouldIgnoreDependencyLock()) { applyLock(dependenciesLock, overrides) } else if (!shouldIgnoreDependencyLock()) { applyOverrides(overrides) @@ -104,13 +115,12 @@ class DependencyLockPlugin implements Plugin { } } - private SaveLockTask configureSaveTask(GenerateLockTask lockTask, DependencyLockExtension extension) { + private SaveLockTask configureSaveTask(String clLockFileName, DependencyLockExtension extension) { SaveLockTask saveTask = project.tasks.create('saveLock', SaveLockTask) saveTask.conventionMapping.with { - generatedLock = { lockTask.dependenciesLock } + generatedLock = { new File(project.buildDir, clLockFileName ?: extension.lockFile) } outputLock = { new File(project.projectDir, extension.lockFile) } } - saveTask.mustRunAfter lockTask saveTask.outputs.upToDateWhen { if (saveTask.generatedLock.exists() && saveTask.outputLock.exists()) { saveTask.generatedLock.text == saveTask.outputLock.text @@ -122,8 +132,7 @@ class DependencyLockPlugin implements Plugin { saveTask } - private GenerateLockTask configureLockTask(String clLockFileName, DependencyLockExtension extension, Map overrides) { - GenerateLockTask lockTask = project.tasks.create('generateLock', GenerateLockTask) + private GenerateLockTask configureLockTask(GenerateLockTask lockTask, String clLockFileName, DependencyLockExtension extension, Map overrides) { lockTask.conventionMapping.with { dependenciesLock = { new File(project.buildDir, clLockFileName ?: extension.lockFile) @@ -138,6 +147,14 @@ class DependencyLockPlugin implements Plugin { lockTask } + private configureUpdateTask(UpdateLockTask lockTask, DependencyLockExtension extension) { + // You can't read a property at the same time you define the convention mapping ∞ + def updatesFromOption = lockTask.dependencies + lockTask.conventionMapping.dependencies = { updatesFromOption ?: extension.updateDependencies } + + lockTask + } + void applyOverrides(Map overrides) { if (project.hasProperty('dependencyLock.overrideFile')) { logger.info("Using override file ${project['dependencyLock.overrideFile']} to lock dependencies") diff --git a/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy b/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy index 1947d3f5..0bc1735a 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy @@ -15,7 +15,6 @@ */ package nebula.plugin.dependencylock.tasks -import groovy.transform.EqualsAndHashCode import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ExternalDependency @@ -23,7 +22,7 @@ import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.tasks.TaskAction class GenerateLockTask extends AbstractLockTask { - String description = 'Create a lock file in build/' + String description = 'Create a lock file in build/' Closure filter = { group, name, version -> true } Set configurationNames Set skippedDependencies = [] @@ -51,7 +50,7 @@ class GenerateLockTask extends AbstractLockTask { def filteredExternalDependencies = externalDependencies.findAll { Dependency dependency -> filter(dependency.group, dependency.name, dependency.version) } - filteredExternalDependencies.each { Dependency dependency -> + filteredExternalDependencies.each { ExternalDependency dependency -> def key = new LockKey(group: dependency.group, artifact: dependency.name) deps[key].requested = dependency.version } @@ -149,7 +148,7 @@ class GenerateLockTask extends AbstractLockTask { deps[key].transitive << parent } - private void writeLock(deps) { + void writeLock(deps) { def strings = deps.findAll { !getSkippedDependencies().contains(it.key.toString()) } .collect { LockKey k, Map v -> stringifyLock(k, v) } strings = strings.sort() @@ -186,15 +185,4 @@ class GenerateLockTask extends AbstractLockTask { return lockLine.toString() } - - @EqualsAndHashCode - private static class LockKey { - String group - String artifact - - @Override - String toString() { - "${group}:${artifact}" - } - } } diff --git a/src/main/groovy/nebula/plugin/dependencylock/tasks/LockKey.groovy b/src/main/groovy/nebula/plugin/dependencylock/tasks/LockKey.groovy new file mode 100644 index 00000000..346f320d --- /dev/null +++ b/src/main/groovy/nebula/plugin/dependencylock/tasks/LockKey.groovy @@ -0,0 +1,17 @@ +package nebula.plugin.dependencylock.tasks + +import groovy.transform.EqualsAndHashCode + +/** + * Map key for locked dependencies. + */ +@EqualsAndHashCode +class LockKey { + String group + String artifact + + @Override + String toString() { + "${group}:${artifact}" + } +} diff --git a/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy b/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy new file mode 100644 index 00000000..79134d61 --- /dev/null +++ b/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy @@ -0,0 +1,99 @@ +package nebula.plugin.dependencylock.tasks + +import groovy.json.JsonSlurper +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging + +import java.lang.Override +import org.gradle.api.GradleException +import org.gradle.api.internal.tasks.options.Option + +/** + * The update task is a generate task, it simply reads in the old locked dependencies and then overwrites the desired + * dependencies per user request. + */ +class UpdateLockTask extends GenerateLockTask { + private static Logger logger = Logging.getLogger(UpdateLockTask) + + String description = 'Apply updates to a preexisting lock file and write to build/' + Set dependencies + + @Option(option = "dependencies", description = "Specify which dependencies to update via a comma-separated list") + void setDependencies(String depsFromOption) { + setDependencies(depsFromOption.tokenize(',') as Set) + } + + void setDependencies(Set dependencyList) { + this.dependencies = dependencyList + } + + @Override + void lock() { + // If the user specifies dependencies to update, ignore any filter specified by the build file and use our + // own generated from the list of dependencies. + def updates = getDependencies() + + if (updates) { + filter = { group, artifact, version -> + updates.contains("${group}:${artifact}".toString()) + } + } + super.lock() + } + + @Override + void writeLock(updated) { + File currentLock = new File(project.projectDir, dependenciesLock.name) + def locked = loadLock(currentLock) + def pruned = pruneDeps(locked) + super.writeLock(pruned + (updated as Map)) + } + + private pruneDeps(locked) { + Set keys = [] + Set visited = [] + + // Visit all nodes in a list of dependency trees once and record the lock key. + Closure addKeysFrom + addKeysFrom = { Set nodes -> + keys += nodes.collect { ResolvedDependency dep -> + visited += dep + new LockKey(group: dep.moduleGroup, artifact: dep.moduleName) + } + Set unvisited = nodes*.children.flatten() - visited + if (unvisited.size() > 0) { + addKeysFrom.trampoline(unvisited) + } // bounce.. + }.trampoline() + + // Recursively generate keys for the entire dependency tree. + Set dependencies = project.configurations*.resolvedConfiguration.firstLevelModuleDependencies.flatten() + addKeysFrom(dependencies) + + // Prune dependencies from the lock file that are not needed by dependencies in the current build script. + locked.findAll { + keys.contains(it.key) + } + } + + private static loadLock(File lock) { + def json + try { + json = new JsonSlurper().parseText(lock.text) + } catch (ex) { + logger.debug('Unreadable json file: ' + lock.text) + logger.error('JSON unreadable') + throw new GradleException("${lock.name} is unreadable or invalid json, terminating run", ex) + } + // Load the dependencies in the same form as they are read from the configurations. + def lockKeyMap = [:].withDefault { + [transitive: [] as Set, firstLevelTransitive: [] as Set, childrenVisited: false] + } + json.each { key, value -> + def (group, artifact) = key.tokenize(':') + lockKeyMap.put(new LockKey(group: group, artifact: artifact), value) + } + lockKeyMap + } +} diff --git a/src/test/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTaskSpec.groovy b/src/test/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTaskSpec.groovy new file mode 100644 index 00000000..2c0e7b2e --- /dev/null +++ b/src/test/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTaskSpec.groovy @@ -0,0 +1,628 @@ +package nebula.plugin.dependencylock.tasks + +import nebula.plugin.dependencylock.dependencyfixture.Fixture +import nebula.test.ProjectSpec +import org.gradle.testfixtures.ProjectBuilder + + +class UpdateLockTaskSpec extends ProjectSpec { + final String taskName = 'updateLock' + + def setupSpec() { + Fixture.createFixtureIfNotCreated() + } + + def setup() { + project.apply plugin: 'java' + project.repositories { maven { url Fixture.repo } } + } + + UpdateLockTask createTask() { + def task = project.tasks.create(taskName, UpdateLockTask) + task.dependenciesLock = new File(project.buildDir, 'dependencies.lock') + task.configurationNames = [ 'testRuntime' ] + task + } + + def 'a dependency should only update relevant dependencies from the current lock file'() { + project.dependencies { + compile 'test.example:foo:2.0.0' + } + + def task = createTask() + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:foo": { "locked": "2.0.0", "requested": "2.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'by default a dependency is updated when another dependency requires a higher version transitively'(useTransitive, lockText) { + project.dependencies { + compile 'test.example:foo:1.0.0' + compile 'test.example:bar:1.1.0' + } + + def task = createTask() + task.includeTransitives = useTransitive + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + } + '''.stripIndent() + + when: + task.execute() + + then: + task.dependenciesLock.text == lockText + + where: + useTransitive || lockText + false || '''\ + { + "test.example:bar": { "locked": "1.1.0", "requested": "1.1.0" }, + "test.example:foo": { "locked": "1.0.1", "requested": "1.0.0" } + } + '''.stripIndent() + true || '''\ + { + "test.example:bar": { "locked": "1.1.0", "requested": "1.1.0" }, + "test.example:foo": { "locked": "1.0.1", "requested": "1.0.0", "transitive": [ "test.example:bar" ] } + } + '''.stripIndent() + } + + def 'by default the filter is respected'() { + project.dependencies { + compile 'test.example:baz:2.0.0' + compile 'test.example:foo:2.0.0' + } + + def task = createTask() + task.filter = { group, artifact, version -> + artifact == 'foo' + } + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "2.0.0", "requested": "2.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'when a top-level dependency is updated and transitives are included, its transitive dependencies are updated'() { + project.dependencies { + compile 'test.example:bar:1.1.0' + } + + def task = createTask() + task.includeTransitives = true + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "test.example:foo:1.0.0", "transitive": ["test.example:bar"] } + } + '''.stripIndent() + + when: + task.execute() + + then: + def lockText = lockFile.text = '''\ + { + "test.example:bar": { "locked": "1.1.0", "requested": "1.1.0" }, + "test.example:foo": { "locked": "1.0.1", "transitive": [ "test.example:bar" ] } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'specifying a set of dependencies restricts the dependencies updated'(dependencies, lockText) { + project.dependencies { + compile 'test.example:baz:2.0.0' + compile 'test.example:foo:2.0.0' + compile 'test.example:bar:1.1.0' + } + + def task = createTask() + task.dependencies = dependencies + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + when: + task.execute() + + then: + task.dependenciesLock.text == lockText + + where: + dependencies || lockText + [ 'test.example:foo' ] || '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "2.0.0", "requested": "2.0.0" } + } + '''.stripIndent() + [ 'test.example:foo', 'test.example:baz' ] || '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:baz": { "locked": "2.0.0", "requested": "2.0.0" }, + "test.example:foo": { "locked": "2.0.0", "requested": "2.0.0" } + } + '''.stripIndent() + } + + def 'when dependencies are specified, the filter is ignored' () { + project.dependencies { + compile 'test.example:baz:2.0.0' + compile 'test.example:foo:2.0.0' + } + + def task = createTask() + task.filter = { group, artifact, version -> + false + } + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + task.dependencies = [ 'test.example:foo' ] + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "2.0.0", "requested": "2.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'dependencies can be specified via a comma-separated list'(input, dependencies) { + def task = createTask() + + when: + task.setDependencies(input as String) + + then: + task.dependencies == dependencies as Set + + where: + input || dependencies + 'com.example:foo' || ['com.example:foo'] + 'com.example:baz,com.example:foo' || ['com.example:baz', 'com.example:foo'] + } + + + def 'by default filtered dependencies do not get updated when they are also included transitively'() { + project.dependencies { + compile 'test.example:foo:1.0.0' + compile 'test.example:foobaz:1.0.0' + } + + def task = createTask() + task.dependencies = ["test.example:foobaz"] + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" }, + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'by default a filtered dependency is not updated when it is also a transitive dependency of an updated top-level dependency'() { + project.dependencies { + compile 'test.example:foo:2.0.0' + compile 'test.example:foobaz:1.0.0' + + } + + def task = createTask() + task.dependencies = ["test.example:foobaz"] + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" }, + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + // The spec should probably be to do a minimal update where the hard version is ignored and only the latest + // greatest transitive version is included, but this is vastly more sophisticated. + def 'a filtered dependency is updated when the update is performed including transitives'() { + project.dependencies { + compile 'test.example:foo:2.0.0' + compile 'test.example:foobaz:1.0.0' + + } + + def task = createTask() + task.dependencies = ["test.example:foobaz"] + task.includeTransitives = true + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:baz": { "locked": "1.0.0", "transitive": [ "test.example:foobaz" ] }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "1.0.0", "transitive": [ "test.example:foobaz" ] }, + "test.example:foo": { "locked": "2.0.0", "transitive": [ "test.example:foobaz" ] }, + "test.example:foobaz": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'check circular dependency does not loop infinitely'() { + project.apply plugin: 'java' + + project.repositories { maven { url Fixture.repo } } + project.dependencies { + compile 'circular:a:1.+' + } + + def task = createTask() + task.includeTransitives = true + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "circular:a": { "locked": "1.0.0", "requested": "1.+", "transitive": [ "circular:b" ] }, + "circular:b": { "locked": "1.0.0", "transitive": [ "circular:a" ] } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "circular:a": { "locked": "1.0.0", "requested": "1.+", "transitive": [ "circular:b" ] }, + "circular:b": { "locked": "1.0.0", "transitive": [ "circular:a" ] } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'check for deeper circular dependency'() { + project.apply plugin: 'java' + + project.repositories { maven { url Fixture.repo } } + project.dependencies { + compile 'circular:oneleveldeep:1.+' + } + + def task = createTask() + task.includeTransitives = true + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "circular:a": { "locked": "1.0.0", "transitive": [ "circular:b", "circular:oneleveldeep" ] }, + "circular:b": { "locked": "1.0.0", "transitive": [ "circular:a" ] }, + "circular:oneleveldeep": { "locked": "1.0.0", "requested": "1.+" } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "circular:a": { "locked": "1.0.0", "transitive": [ "circular:b", "circular:oneleveldeep" ] }, + "circular:b": { "locked": "1.0.0", "transitive": [ "circular:a" ] }, + "circular:oneleveldeep": { "locked": "1.0.0", "requested": "1.+" } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'multi-project inter-project dependencies should be updated'() { + def common = ProjectBuilder.builder().withName('common').withProjectDir(new File(projectDir, 'common')).withParent(project).build() + project.subprojects.add(common) + def app = ProjectBuilder.builder().withName('app').withProjectDir(new File(projectDir, 'app')).withParent(project).build() + project.subprojects.add(app) + + project.subprojects { + apply plugin: 'java' + group = 'test.nebula' + repositories { maven { url Fixture.repo } } + projectDir.mkdir() + } + + app.dependencies { + compile app.project(':common') + compile 'test.example:foo:2.+' + } + + common.dependencies { + compile 'test.example:baz:2.0.0' + } + + GenerateLockTask task = app.tasks.create(taskName, UpdateLockTask) + task.dependenciesLock = new File(app.buildDir, 'dependencies.lock') + task.configurationNames = [ 'testRuntime' ] + + def lockFile = new File(app.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:azz": { "locked": "-42.0", "requested": "0.24-" }, + "test.example:foo": { "locked": "1.0.1", "requested": "1.0.+" }, + "test.nebula:common": { "project": true } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "2.0.0", "firstLevelTransitive": [ "test.nebula:common" ] }, + "test.example:foo": { "locked": "2.0.1", "requested": "2.+" }, + "test.nebula:common": { "project": true } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'updating a filtered multi-project inter-project dependency should updated first-level transitives'() { + def common = ProjectBuilder.builder().withName('common').withProjectDir(new File(projectDir, 'common')).withParent(project).build() + project.subprojects.add(common) + def app = ProjectBuilder.builder().withName('app').withProjectDir(new File(projectDir, 'app')).withParent(project).build() + project.subprojects.add(app) + + project.subprojects { + apply plugin: 'java' + group = 'test.nebula' + repositories { maven { url Fixture.repo } } + projectDir.mkdir() + } + + app.dependencies { + compile app.project(':common') + compile 'test.example:foo:2.+' + } + + common.dependencies { + compile 'test.example:baz:2.0.0' + } + + GenerateLockTask task = app.tasks.create(taskName, UpdateLockTask) + task.dependenciesLock = new File(app.buildDir, 'dependencies.lock') + task.configurationNames = [ 'testRuntime' ] + task.dependencies = [ 'test.nebula:common' ] + + def lockFile = new File(app.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:azz": { "locked": "-42.0", "requested": "0.24-" }, + "test.example:baz": { "locked": "1.0.0", "firstLevelTransitive": [ "test.nebula:common" ] }, + "test.example:foo": { "locked": "1.0.1", "requested": "2.+" }, + "test.nebula:common": { "project": true } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "2.0.0", "firstLevelTransitive": [ "test.nebula:common" ] }, + "test.example:foo": { "locked": "1.0.1", "requested": "2.+" }, + "test.nebula:common": { "project": true } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'a filtered dependency should still be updated when it is a first-level transitive of a project'() { + def app = ProjectBuilder.builder().withName('app').withProjectDir(new File(projectDir, 'app')).withParent(project).build() + project.subprojects.add(app) + + project.subprojects { + apply plugin: 'java' + group = 'test.nebula' + repositories { maven { url Fixture.repo } } + projectDir.mkdir() + } + + app.dependencies { + compile 'test.example:foo:2.0.0' + } + + project.dependencies { + compile project.project(':app') + compile 'test.example:bar:1.1.0' + compile 'test.example:foo:1.0.0' + } + + def task = createTask() + task.dependencies = [ 'test.nebula:app' ] + + def lockFile = new File(project.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "1.0.0", "requested": "1.0.0" } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:bar": { "locked": "1.0.0", "requested": "1.0.0" }, + "test.example:foo": { "locked": "2.0.0", "firstLevelTransitive": [ "test.nebula:app" ] }, + "test.nebula:app": { "project": true } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } + + def 'multi-project inter-project dependencies should update lock for first levels'() { + def common = ProjectBuilder.builder().withName('common').withProjectDir(new File(projectDir, 'common')).withParent(project).build() + project.subprojects.add(common) + def lib = ProjectBuilder.builder().withName('lib').withProjectDir(new File(projectDir, 'lib')).withParent(project).build() + project.subprojects.add(lib) + def app = ProjectBuilder.builder().withName('app').withProjectDir(new File(projectDir, 'app')).withParent(project).build() + project.subprojects.add(app) + + project.subprojects { + apply plugin: 'java' + group = 'test.nebula' + repositories { maven { url Fixture.repo } } + projectDir.mkdir() + } + + common.dependencies { + compile 'test.example:foo:2.+' + compile 'test.example:baz:2.+' + } + + lib.dependencies { + compile lib.project(':common') + compile 'test.example:baz:1.+' + } + + app.dependencies { + compile app.project(':lib') + } + + def task = app.tasks.create(taskName, UpdateLockTask) + + task.dependenciesLock = new File(app.buildDir, 'dependencies.lock') + task.configurationNames= [ 'testRuntime' ] + + def lockFile = new File(app.projectDir, 'dependencies.lock') + lockFile.text = '''\ + { + "test.example:azz": { "locked": "-42.0", "firstLevelTransitive": [ "test.nebula:lib" ] }, + "test.example:baz": { "locked": "1.0.0", "firstLevelTransitive": [ "test.nebula:common", "test.nebula:lib" ] }, + "test.example:foo": { "locked": "1.0.0", "firstLevelTransitive": [ "test.nebula:common" ] }, + "test.nebula:common": { "project": true, "firstLevelTransitive": [ "test.nebula:lib" ] }, + "test.nebula:lib": { "project": true } + } + '''.stripIndent() + + when: + task.execute() + + then: + String lockText = '''\ + { + "test.example:baz": { "locked": "2.0.0", "firstLevelTransitive": [ "test.nebula:common", "test.nebula:lib" ] }, + "test.example:foo": { "locked": "2.0.1", "firstLevelTransitive": [ "test.nebula:common" ] }, + "test.nebula:common": { "project": true, "firstLevelTransitive": [ "test.nebula:lib" ] }, + "test.nebula:lib": { "project": true } + } + '''.stripIndent() + + task.dependenciesLock.text == lockText + } +}