diff --git a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdates.kt b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdates.kt index 946da78c..d656be00 100644 --- a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdates.kt +++ b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdates.kt @@ -21,7 +21,7 @@ class DependencyUpdates @JvmOverloads constructor( val project: Project, val resolutionStrategy: Action?, val revision: String, - val outputFormatter: Any?, + private val outputFormatterArgument: OutputFormatterArgument, val outputDir: String, val reportfileName: String?, val checkForGradleUpdate: Boolean, @@ -108,7 +108,7 @@ class DependencyUpdates @JvmOverloads constructor( val gradleUpdateChecker = GradleUpdateChecker(checkForGradleUpdate) return DependencyUpdatesReporter( - project, revision, outputFormatter, outputDir, + project, revision, outputFormatterArgument, outputDir, reportfileName, currentVersions, latestVersions, upToDateVersions, downgradeVersions, upgradeVersions, versions.undeclared, unresolved, projectUrls, gradleUpdateChecker, gradleReleaseChannel diff --git a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesReporter.kt b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesReporter.kt index 62ca39b1..8e9ae046 100644 --- a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesReporter.kt +++ b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesReporter.kt @@ -16,7 +16,6 @@ import com.github.benmanes.gradle.versions.updates.gradle.GradleReleaseChannel import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateChecker import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateResult import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateResults -import groovy.lang.Closure import org.gradle.api.Project import org.gradle.api.artifacts.ModuleVersionSelector import org.gradle.api.artifacts.UnresolvedDependency @@ -31,7 +30,7 @@ import java.util.TreeSet * * @property project The project evaluated against. * @property revision The revision strategy evaluated with. - * @property outputFormatter The output formatter strategy evaluated with. + * @property outputFormatterArgument The output formatter strategy evaluated with. * @property outputDir The outputDir for report. * @property reportfileName The filename of the report file. * @property currentVersions The current versions of each dependency declared in the project(s). @@ -50,7 +49,7 @@ import java.util.TreeSet class DependencyUpdatesReporter( val project: Project, val revision: String, - val outputFormatter: Any?, + private val outputFormatterArgument: OutputFormatterArgument, val outputDir: String, val reportfileName: String?, val currentVersions: Map, Coordinate>, @@ -67,34 +66,32 @@ class DependencyUpdatesReporter( @Synchronized fun write() { - if (outputFormatter !is Closure<*>) { + if (outputFormatterArgument !is OutputFormatterArgument.CustomAction) { val plainTextReporter = PlainTextReporter( project, revision, gradleReleaseChannel ) plainTextReporter.write(System.out, buildBaseObject()) } - if (outputFormatter == null || (outputFormatter is String && outputFormatter.isEmpty())) { + if (outputFormatterArgument is OutputFormatterArgument.BuiltIn && outputFormatterArgument.formatterNames.isEmpty()) { project.logger.lifecycle("Skip generating report to file (outputFormatter is empty)") return } - when (outputFormatter) { - is String -> { - for (it in outputFormatter.split(",")) { + + when (outputFormatterArgument) { + is OutputFormatterArgument.BuiltIn -> { + for (it in outputFormatterArgument.formatterNames.split(",")) { generateFileReport(getOutputReporter(it)) } } - is Reporter -> { - generateFileReport(outputFormatter) + + is OutputFormatterArgument.CustomReporter -> { + generateFileReport(outputFormatterArgument.reporter) } - is Closure<*> -> { + + is OutputFormatterArgument.CustomAction -> { val result = buildBaseObject() - outputFormatter.call(result) - } - else -> { - throw IllegalArgumentException( - "Cannot handle output formatter $outputFormatter, unsupported type" - ) + outputFormatterArgument.action.execute(result) } } } diff --git a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesTask.kt b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesTask.kt index 137d9251..90f7ed07 100644 --- a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesTask.kt +++ b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/DependencyUpdatesTask.kt @@ -1,5 +1,7 @@ package com.github.benmanes.gradle.versions.updates +import com.github.benmanes.gradle.versions.reporter.Reporter +import com.github.benmanes.gradle.versions.reporter.result.Result import com.github.benmanes.gradle.versions.updates.gradle.GradleReleaseChannel.RELEASE_CANDIDATE import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentFilter import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent @@ -41,16 +43,45 @@ open class DependencyUpdatesTask : DefaultTask() { // tasks can't be final var reportfileName: String = "report" get() = (System.getProperties()["reportfileName"] ?: field) as String - @Internal - var outputFormatter: Any = "plain" + /** + * Sets an output formatting for the task result. It can either be a [String] referencing one of + * the existing output formatters (i.e. "text", "xml", "json" or "html"), a [String] containing a + * comma-separated list with any combination of the existing output formatters (e.g. "xml,json"), + * or a [Reporter]/a [Closure] with a custom output formatting implementation. + * + * Use the [outputFormatter] function as an alternative to set a custom output formatting using + * the trailing closure/lambda syntax. + */ + var outputFormatter: Any? + @Internal get() = null + set(value) { + outputFormatterArgument = when (value) { + is String -> OutputFormatterArgument.BuiltIn(value) + is Reporter -> OutputFormatterArgument.CustomReporter(value) + // Kept for retro-compatibility with "outputFormatter = {}" usages. + is Closure<*> -> OutputFormatterArgument.CustomAction { value.call(it) } + else -> throw IllegalArgumentException( + "Unsupported output formatter provided $value. Please use a String, a Reporter/Closure, " + + "or alternatively provide a function using the `outputFormatter(Action)` API." + ) + } + } + + /** + * Keeps a reference to the latest [OutputFormatterArgument] provided either via the [outputFormatter] + * property or the [outputFormatter] function. + */ + private var outputFormatterArgument: OutputFormatterArgument = OutputFormatterArgument.DEFAULT @Input @Optional fun getOutputFormatterName(): String? { - return if (outputFormatter is String) { - outputFormatter as String - } else { - null + return with(outputFormatterArgument) { + if (this is OutputFormatterArgument.BuiltIn) { + formatterNames + } else { + null + } } } @@ -125,8 +156,20 @@ open class DependencyUpdatesTask : DefaultTask() { // tasks can't be final } /** Returns the outputDir format. */ - private fun outputFormatter(): Any { - return (System.getProperties()["outputFormatter"] ?: outputFormatter) + private fun outputFormatter(): OutputFormatterArgument { + val outputFormatterProperty = System.getProperties()["outputFormatter"] as? String + + return outputFormatterProperty?.let { OutputFormatterArgument.BuiltIn(it) } + ?: outputFormatterArgument + } + + /** + * Sets a custom output formatting for the task result. + * + * @param action [Action] implementing the desired custom output formatting. + */ + fun outputFormatter(action: Action) { + outputFormatterArgument = OutputFormatterArgument.CustomAction(action) } private fun callIncompatibleWithConfigurationCache() { diff --git a/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/OutputFormatterArgument.kt b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/OutputFormatterArgument.kt new file mode 100644 index 00000000..4035dd4b --- /dev/null +++ b/gradle-versions-plugin/src/main/kotlin/com/github/benmanes/gradle/versions/updates/OutputFormatterArgument.kt @@ -0,0 +1,32 @@ +package com.github.benmanes.gradle.versions.updates + +import com.github.benmanes.gradle.versions.reporter.Reporter +import com.github.benmanes.gradle.versions.reporter.result.Result +import org.gradle.api.Action + +/** + * Represents all the types of arguments for output formatting supported in [DependencyUpdatesTask]. + */ +sealed interface OutputFormatterArgument { + + /** + * A string representing one of the built-in output formatters (i.e. "json", "text", "html" or + * "xml"), or a comma-separated list with a combination of them (e.g. "json,text"). + */ + class BuiltIn(val formatterNames: String) : OutputFormatterArgument + + /** + * An implementation of the [Reporter] interface to provide a custom output formatting. + */ + class CustomReporter(val reporter: Reporter) : OutputFormatterArgument + + /** + * An [Action] to provide a custom output formatting. Enables the use of the trailing closure/lambda + * syntax for output formatting definition. + */ + class CustomAction(val action: Action) : OutputFormatterArgument + + companion object { + val DEFAULT = BuiltIn(formatterNames = "text") + } +} diff --git a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/DependencyUpdatesSpec.groovy b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/DependencyUpdatesSpec.groovy index cbbbe958..0bb69437 100644 --- a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/DependencyUpdatesSpec.groovy +++ b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/DependencyUpdatesSpec.groovy @@ -15,6 +15,9 @@ */ package com.github.benmanes.gradle.versions +import com.github.benmanes.gradle.versions.updates.OutputFormatterArgument +import org.gradle.api.Action + import static com.github.benmanes.gradle.versions.updates.gradle.GradleReleaseChannel.CURRENT import static com.github.benmanes.gradle.versions.updates.gradle.GradleReleaseChannel.RELEASE_CANDIDATE @@ -300,7 +303,7 @@ final class DependencyUpdatesSpec extends Specification { } } - def 'Single project with a Closure as Reporter'() { + def 'Single project with an Action as Reporter'() { given: def project = singleProject() addRepositoryTo(project) @@ -311,13 +314,13 @@ final class DependencyUpdatesSpec extends Specification { int undeclared = -1 int unresolved = -1 - def customReporter = { result -> + def customReporter = [execute: { result -> current = result.current.count outdated = result.outdated.count exceeded = result.exceeded.count undeclared = result.undeclared.count unresolved = result.unresolved.count - } + }] as Action when: def reporter = evaluate(project, 'release', customReporter) @@ -620,13 +623,25 @@ final class DependencyUpdatesSpec extends Specification { [rootProject, childProject, leafProject] } - private static def evaluate(project, revision = 'milestone', outputFormatter = 'plain', + private static def evaluate(project, revision = 'milestone', outputFormatter = null, outputDir = 'build', resolutionStrategy = null, reportfileName = null, checkForGradleUpdate = true, gradleReleaseChannel = RELEASE_CANDIDATE.id) { - new DependencyUpdates(project, resolutionStrategy, revision, outputFormatter, outputDir, + new DependencyUpdates(project, resolutionStrategy, revision, buildOutputFormatter(outputFormatter), outputDir, reportfileName, checkForGradleUpdate, gradleReleaseChannel).run() } + private static OutputFormatterArgument buildOutputFormatter(outputFormatter) { + if (outputFormatter instanceof String) { + return new OutputFormatterArgument.BuiltIn(outputFormatter) + } else if (outputFormatter instanceof Reporter) { + return new OutputFormatterArgument.CustomReporter(outputFormatter) + } else if (outputFormatter instanceof Action) { + return new OutputFormatterArgument.CustomAction(outputFormatter) + } else { + return new OutputFormatterArgument.BuiltIn("text") + } + } + private void addRepositoryTo(project) { def localMavenRepo = getClass().getResource('/maven/') project.repositories { diff --git a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/KotlinDslUsageSpec.groovy b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/KotlinDslUsageSpec.groovy index e6af95e8..b227d951 100644 --- a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/KotlinDslUsageSpec.groovy +++ b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/KotlinDslUsageSpec.groovy @@ -73,4 +73,48 @@ final class KotlinDslUsageSpec extends Specification { where: gradleVersion << ['5.6'] } + + @Unroll + def "user friendly kotlin-dsl with #outputFormatter produces #expectedOutput"() { + given: + buildFile << """ + tasks.named("dependencyUpdates") { + checkForGradleUpdate = true + $outputFormatter + outputDir = "build/dependencyUpdates" + reportfileName = "report" + resolutionStrategy { + componentSelection { + all { + if (candidate.version == "3.1" && currentVersion != "") { + reject("Guice 3.1 not allowed") + } + } + } + } + } + """ + + when: + def result = GradleRunner.create() + .withGradleVersion('5.6') + .withPluginClasspath() + .withProjectDir(testProjectDir.root) + .withArguments('dependencyUpdates') + .build() + + then: + result.output.contains(expectedOutput) + result.task(':dependencyUpdates').outcome == SUCCESS + + where: + outputFormatter << [ + 'outputFormatter = "json"', + 'outputFormatter { print("Custom report") }' + ] + expectedOutput << [ + 'com.google.inject:guice [2.0 -> 3.0]', + 'Custom report' + ] + } } diff --git a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/OutputFormatterSpec.groovy b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/OutputFormatterSpec.groovy index 26224420..c1359ab7 100644 --- a/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/OutputFormatterSpec.groovy +++ b/gradle-versions-plugin/src/test/groovy/com/github/benmanes/gradle/versions/OutputFormatterSpec.groovy @@ -73,6 +73,45 @@ final class OutputFormatterSpec extends Specification { result.task(':dependencyUpdates').outcome == SUCCESS } + def 'Build fails when unsupported outputFormatter is provided'() { + given: + buildFile = testProjectDir.newFile('build.gradle') + buildFile << + """ + buildscript { + dependencies { + classpath files($classpathString) + } + } + + apply plugin: 'java' + apply plugin: 'com.github.ben-manes.versions' + + repositories { + maven { + url '${mavenRepoUrl}' + } + } + + dependencies { + implementation 'com.google.inject:guice:2.0' + } + + dependencyUpdates { + outputFormatter = 13 + checkForGradleUpdate = false // future proof tests from breaking + } + """.stripIndent() + + when: + def runner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('dependencyUpdates') + + then: + runner.buildAndFail() + } + def 'outputFormatter defaults to text output'() { given: def reportFile = new File(reportFolder, "report.txt") diff --git a/gradle.properties b/gradle.properties index e67a0d3d..b4412552 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.github.ben-manes -VERSION_NAME=0.42.0-SNAPSHOT +VERSION_NAME=0.43.5-SNAPSHOT POM_INCEPTION_YEAR=2012 POM_PACKAGING=jar