Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Kotlin trailing lambda syntax for custom output formatters #690

Merged
merged 4 commits into from Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -21,7 +21,7 @@ class DependencyUpdates @JvmOverloads constructor(
val project: Project,
val resolutionStrategy: Action<in ResolutionStrategyWithCurrent>?,
val revision: String,
val outputFormatter: Any?,
private val outputFormatterArgument: OutputFormatterArgument,
val outputDir: String,
val reportfileName: String?,
val checkForGradleUpdate: Boolean,
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand All @@ -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).
Expand All @@ -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<Map<String, String>, Coordinate>,
Expand All @@ -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)
}
}
}
Expand Down
@@ -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
Expand All @@ -12,6 +14,7 @@ import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import org.gradle.util.ConfigureUtil
import java.lang.IllegalArgumentException
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import should not be needed in Kotlin.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, removed!

import javax.annotation.Nullable

/**
Expand Down Expand Up @@ -41,16 +44,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<Result>)` 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
}
}
}

Expand Down Expand Up @@ -125,8 +157,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<Result>) {
outputFormatterArgument = OutputFormatterArgument.CustomAction(action)
}

private fun callIncompatibleWithConfigurationCache() {
Expand Down
@@ -0,0 +1,33 @@
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<Result>) : OutputFormatterArgument

companion object {
@JvmField
val DEFAULT = BuiltIn(formatterNames = "text")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to be a @JvmField?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to be able to access this from Groovy code in tests. But I have removed it now as it is not necessary in production code.

}
}
Expand Up @@ -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

Expand Down Expand Up @@ -300,7 +303,7 @@ final class DependencyUpdatesSpec extends Specification {
}
}

def 'Single project with a Closure as Reporter'() {
def 'Single project with an Action<Result> as Reporter'() {
given:
def project = singleProject()
addRepositoryTo(project)
Expand All @@ -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<Result>

when:
def reporter = evaluate(project, 'release', customReporter)
Expand Down Expand Up @@ -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<Result>) {
return new OutputFormatterArgument.CustomAction(outputFormatter)
} else {
return OutputFormatterArgument.DEFAULT
}
}

private void addRepositoryTo(project) {
def localMavenRepo = getClass().getResource('/maven/')
project.repositories {
Expand Down
Expand Up @@ -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<DependencyUpdatesTask>("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'
]
}
}
Expand Up @@ -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")
Expand Down