org.gradle.integtests.resolve.transform.ArtifactTransformIntegrationTest.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-api Show documentation
Show all versions of gradle-api Show documentation
Gradle 6.9.1 API redistribution.
/*
* Copyright 2016 the original author or authors.
*
* 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 org.gradle.integtests.resolve.transform
import org.gradle.api.internal.artifacts.transform.ExecuteScheduledTransformationStepBuildOperationType
import org.gradle.integtests.fixtures.AbstractHttpDependencyResolutionTest
import org.gradle.integtests.fixtures.BuildOperationsFixture
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.internal.file.FileType
import org.gradle.test.fixtures.maven.MavenFileRepository
import org.hamcrest.Matcher
import spock.lang.Issue
import spock.lang.Unroll
import static org.gradle.util.Matchers.matchesRegexp
class ArtifactTransformIntegrationTest extends AbstractHttpDependencyResolutionTest implements ArtifactTransformTestFixture {
def setup() {
settingsFile << """
rootProject.name = 'root'
include 'lib'
include 'app'
"""
buildFile << """
import org.gradle.api.artifacts.transform.TransformParameters
def usage = Attribute.of('usage', String)
def artifactType = Attribute.of('artifactType', String)
def extraAttribute = Attribute.of('extra', String)
allprojects {
dependencies {
attributesSchema {
attribute(usage)
}
}
configurations {
compile {
attributes { attribute usage, 'api' }
}
}
}
$fileSizer
"""
}
private static String getFileSizer() {
"""
import org.gradle.api.artifacts.transform.InputArtifact
import org.gradle.api.artifacts.transform.TransformAction
import org.gradle.api.artifacts.transform.TransformOutputs
import org.gradle.api.artifacts.transform.TransformParameters
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
abstract class FileSizer implements TransformAction {
FileSizer() {
println "Creating FileSizer"
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".txt")
assert output.parentFile.directory && output.parentFile.list().length == 0
println "Transforming \${input.name} to \${output.name}"
output.text = String.valueOf(input.length())
}
}
"""
}
def "applies transforms to artifacts for external dependencies matching on implicit format attribute"() {
def m1 = mavenRepo.module("test", "test", "1.3").publish()
m1.artifactFile.text = "1234"
def m2 = mavenRepo.module("test", "test2", "2.3").publish()
m2.artifactFile.text = "12"
given:
buildFile << """
repositories {
maven { url "${mavenRepo.uri}" }
}
dependencies {
compile 'test:test:1.3'
compile 'test:test2:2.3'
}
${configurationAndTransform('FileSizer')}
"""
when:
run "resolve"
then:
outputContains("variants: [{artifactType=size, org.gradle.status=release}, {artifactType=size, org.gradle.status=release}]")
// transformed outputs should belong to same component as original
outputContains("ids: [test-1.3.jar.txt (test:test:1.3), test2-2.3.jar.txt (test:test2:2.3)]")
outputContains("components: [test:test:1.3, test:test2:2.3]")
file("build/libs").assertHasDescendants("test-1.3.jar.txt", "test2-2.3.jar.txt")
file("build/libs/test-1.3.jar.txt").text == "4"
file("build/libs/test2-2.3.jar.txt").text == "2"
and:
output.count("Transforming") == 2
output.count("Transforming test-1.3.jar to test-1.3.jar.txt") == 1
output.count("Transforming test2-2.3.jar to test2-2.3.jar.txt") == 1
when:
run "resolve"
then:
output.count("Transforming") == 0
}
def "can use transformations in build script dependencies"() {
file("buildSrc/src/main/groovy/FileSizer.groovy") << fileSizer
file("script-with-buildscript-block.gradle") << """
buildscript {
def artifactType = Attribute.of('artifactType', String)
dependencies {
registerTransform(FileSizer) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
}
classpath 'org.apache.commons:commons-math3:3.6.1'
}
${mavenCentralRepository()}
println(
configurations.classpath.incoming.artifactView {
attributes.attribute(artifactType, "size")
}.artifacts.artifactFiles.files
)
println(
configurations.classpath.incoming.artifactView {
attributes.attribute(artifactType, "size")
}.artifacts.artifactFiles.files
)
}
"""
buildFile << """
apply from: 'script-with-buildscript-block.gradle'
"""
expect:
succeeds("help", "--info")
output.count("Creating FileSizer") == 1
output.count("Transforming commons-math3-3.6.1.jar to commons-math3-3.6.1.jar.txt") == 1
}
def "applies transforms to files from file dependencies matching on implicit format attribute"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
def b = file('b.jar')
b.text = '12'
task jars
dependencies {
compile files([a, b]) { builtBy jars }
}
${configurationAndTransform('FileSizer')}
"""
when:
run "resolve"
then:
executed(":jars", ":resolve")
and:
outputContains("variants: [{artifactType=size}, {artifactType=size}]")
// transformed outputs should belong to same component as original
outputContains("ids: [a.jar.txt (a.jar), b.jar.txt (b.jar)]")
outputContains("components: [a.jar, b.jar]")
file("build/libs").assertHasDescendants("a.jar.txt", "b.jar.txt")
file("build/libs/a.jar.txt").text == "4"
file("build/libs/b.jar.txt").text == "2"
and:
output.count("Transforming") == 2
output.count("Transforming a.jar to a.jar.txt") == 1
output.count("Transforming b.jar to b.jar.txt") == 1
when:
run "resolve"
then:
executed(":jars", ":resolve")
and:
output.count("Transforming") == 0
}
def "applies transforms to artifacts from local projects matching on implicit format attribute"() {
given:
buildFile << """
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task jar2(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib2.jar'
}
artifacts {
compile jar1, jar2
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "resolve"
then:
executed(":lib:jar1", ":lib:jar2", ":app:resolve")
and:
outputContains("variants: [{artifactType=size, usage=api}, {artifactType=size, usage=api}]")
// transformed outputs should belong to same component as original
outputContains("ids: [lib1.jar.txt (project :lib), lib2.jar.txt (project :lib)]")
outputContains("components: [project :lib, project :lib]")
file("app/build/libs").assertHasDescendants("lib1.jar.txt", "lib2.jar.txt")
file("app/build/libs/lib1.jar.txt").text == file("lib/build/lib1.jar").length() as String
and:
output.count("Transforming") == 2
output.count("Transforming lib1.jar to lib1.jar.txt") == 1
output.count("Transforming lib2.jar to lib2.jar.txt") == 1
when:
run "resolve"
then:
executed(":lib:jar1", ":lib:jar2", ":app:resolve")
and:
output.count("Transforming") == 0
}
def "can map artifact extension to implicit attributes"() {
given:
settingsFile << """
include 'app2'
"""
taskTypeWithOutputFileProperty()
taskTypeLogsArtifactCollectionDetails()
buildFile << """
def contents = Attribute.of('contents', String)
project(':lib') {
task blueThing(type: FileProducer) {
output = layout.buildDir.file('lib.blue')
}
artifacts {
compile blueThing.output
}
}
project(':app') {
dependencies {
compile project(':lib')
}
dependencies {
artifactTypes {
blue {
attributes.attribute(contents, 'size')
}
}
}
task resolve(type: ShowArtifactCollection) {
collection = configurations.compile.incoming.artifactView {
attributes { it.attribute(contents, 'size') }
}.artifacts
}
}
project(':app2') {
dependencies {
compile project(':lib')
}
dependencies {
artifactTypes {
blue {
attributes.attribute(contents, 'bin')
}
}
registerTransform(FileSizer) {
from.attribute(contents, 'bin')
to.attribute(contents, 'size')
}
}
task resolve(type: ShowArtifactCollection) {
collection = configurations.compile.incoming.artifactView {
attributes { it.attribute(contents, 'size') }
}.artifacts
}
}
"""
when:
run "resolve"
then:
executed(":lib:blueThing", ":app:resolve", ":app2:resolve")
and:
def appOutput = result.groupedOutput.task(':app:resolve')
appOutput.assertOutputContains("variants = [{artifactType=blue, contents=size, usage=api}]")
appOutput.assertOutputContains("components = [project :lib]")
appOutput.assertOutputContains("artifacts = [lib.blue (project :lib)]")
appOutput.assertOutputContains("files = [lib.blue]")
def app2Output = result.groupedOutput.task(':app2:resolve')
app2Output.assertOutputContains("variants = [{artifactType=blue, contents=size, usage=api}]")
app2Output.assertOutputContains("components = [project :lib]")
app2Output.assertOutputContains("artifacts = [lib.blue.txt (project :lib)]")
app2Output.assertOutputContains("files = [lib.blue.txt]")
and:
output.count("Transforming") == 1
output.count("Transforming lib.blue to lib.blue.txt") == 1
when:
run "resolve"
then:
executed(":lib:blueThing", ":app:resolve", ":app2:resolve")
and:
output.count("Transforming") == 0
}
def "applies transforms to artifacts from local projects, files and external dependencies"() {
def dependency = mavenRepo.module("test", "test-dependency", "1.3").publish()
dependency.artifactFile.text = "dependency"
def binaryDependency = mavenRepo.module("test", "test", "1.3").dependsOn(dependency).publish()
binaryDependency.artifactFile.text = "1234"
settingsFile << """
include 'common'
"""
given:
buildFile << """
allprojects {
repositories {
maven { url "${mavenRepo.uri}" }
}
}
project(':common') {
task jar(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'common.jar'
}
artifacts {
compile jar
compile file("common-file.jar")
}
}
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task jar2(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib2.jar'
}
dependencies {
compile "${binaryDependency.groupId}:${binaryDependency.artifactId}:${binaryDependency.version}"
compile project(":common")
compile files("file1.jar")
}
artifacts {
compile jar1, jar2
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
file("lib/file1.jar").text = "first"
file("common/common-file.jar").text = "first"
when:
run "resolve"
then:
executed(":common:jar", ":lib:jar1", ":lib:jar2", ":app:resolve")
and:
outputContains("variants: [{artifactType=size, usage=api}, {artifactType=size, usage=api}, {artifactType=size}, {artifactType=size, org.gradle.status=release}, {artifactType=size, usage=api}, {artifactType=size, usage=api}, {artifactType=size, org.gradle.status=release}]")
// transformed outputs should belong to same component as original
outputContains("ids: [lib1.jar.txt (project :lib), lib2.jar.txt (project :lib), file1.jar.txt (file1.jar), test-1.3.jar.txt (test:test:1.3), common.jar.txt (project :common), common-file.jar.txt (project :common), test-dependency-1.3.jar.txt (test:test-dependency:1.3)]")
outputContains("components: [project :lib, project :lib, file1.jar, test:test:1.3, project :common, project :common, test:test-dependency:1.3]")
file("app/build/libs").assertHasDescendants("common.jar.txt", "common-file.jar.txt", "file1.jar.txt", "lib1.jar.txt", "lib2.jar.txt", "test-1.3.jar.txt", "test-dependency-1.3.jar.txt")
file("app/build/libs/lib1.jar.txt").text == file("lib/build/lib1.jar").length() as String
and:
output.count("Transforming") == 7
}
def "applies transforms to artifacts from local projects matching on explicit format attribute"() {
given:
buildFile << """
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task zip1(type: Zip) {
destinationDirectory = buildDir
archiveFileName = 'lib2.zip'
}
configurations {
compile.outgoing.variants {
files {
attributes.attribute(Attribute.of('artifactType', String), 'jar')
artifact jar1
artifact zip1
}
}
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "resolve"
then:
executed(":lib:jar1", ":lib:zip1", ":app:resolve")
and:
outputContains("variants: [{artifactType=size, usage=api}, {artifactType=size, usage=api}]")
file("app/build/libs").assertHasDescendants("lib1.jar.txt", "lib2.zip.txt")
file("app/build/libs/lib1.jar.txt").text == file("lib/build/lib1.jar").length() as String
and:
output.count("Transforming") == 2
output.count("Transforming lib1.jar to lib1.jar.txt") == 1
output.count("Transforming lib2.zip to lib2.zip.txt") == 1
when:
run "resolve"
then:
output.count("Transforming") == 0
}
def "does not apply transform to variants with requested implicit format attribute"() {
given:
buildFile << """
project(':lib') {
projectDir.mkdirs()
def file1 = file('lib1.size')
file1.text = 'some text'
def file2 = file('lib2.size')
file2.text = 'some text'
def jar1 = file('lib1.jar')
jar1.text = 'some text'
dependencies {
compile files(file1, jar1)
}
artifacts {
compile file2
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "resolve"
then:
outputContains("variants: [{artifactType=size, usage=api}, {artifactType=size}, {artifactType=size}]")
outputContains("ids: [lib2.size (project :lib), lib1.size, lib1.jar.txt (lib1.jar)]")
outputContains("components: [project :lib, lib1.size, lib1.jar]")
file("app/build/libs").assertHasDescendants("lib1.jar.txt", "lib1.size", "lib2.size")
file("app/build/libs/lib1.jar.txt").text == "9"
file("app/build/libs/lib1.size").text == "some text"
and:
output.count("Transforming") == 1
when:
run "resolve"
then:
output.count("Transforming") == 0
}
def "does not apply transforms to artifacts from local projects matching requested format attribute"() {
given:
buildFile << """
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task jar2(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib2.zip'
}
configurations {
compile.outgoing.variants {
files {
attributes.attribute(Attribute.of('artifactType', String), 'size')
artifact jar1
artifact jar2
}
}
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "resolve"
then:
executed(":lib:jar1", ":lib:jar2", ":app:resolve")
and:
outputContains("variants: [{artifactType=size, usage=api}, {artifactType=size, usage=api}]")
outputContains("ids: [lib1.jar (project :lib), lib2.zip.jar (project :lib)]")
outputContains("components: [project :lib, project :lib]")
file("app/build/libs").assertHasDescendants("lib1.jar", "lib2.zip")
and:
output.count("Transforming") == 0
}
def "applies transforms to artifacts from local projects matching on some variant attributes"() {
given:
buildFile << """
allprojects {
dependencies {
attributesSchema {
attribute(Attribute.of('javaVersion', String))
attribute(Attribute.of('color', String))
}
}
}
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task jar2(type: Zip) {
destinationDirectory = buildDir
archiveFileName = 'lib2.jar'
}
configurations {
compile.outgoing.variants {
java7 {
attributes.attribute(Attribute.of('javaVersion', String), '7')
attributes.attribute(Attribute.of('color', String), 'green')
artifact jar1
}
java8 {
attributes.attribute(Attribute.of('javaVersion', String), '8')
attributes.attribute(Attribute.of('color', String), 'red')
artifact jar2
}
}
}
}
project(':app') {
dependencies {
compile project(':lib')
registerTransform(MakeRedThings) {
from.attribute(Attribute.of('color', String), "green")
to.attribute(Attribute.of('color', String), "red")
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes {
it.attribute(artifactType, 'jar')
it.attribute(Attribute.of('javaVersion', String), '7')
it.attribute(Attribute.of('color', String), 'red')
}
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
doLast {
println "files: " + artifacts.collect { it.file.name }
println "variants: " + artifacts.collect { it.variant.attributes }
}
}
}
abstract class MakeRedThings implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".red")
assert output.parentFile.directory && output.parentFile.list().length == 0
println "Transforming \${input.name} to \${output.name}"
output.text = String.valueOf(input.length())
}
}
"""
when:
run "resolve"
then:
executed(":lib:jar1", ":app:resolve")
and:
outputContains("variants: [{artifactType=jar, color=red, javaVersion=7, usage=api}]")
file("app/build/libs").assertHasDescendants("lib1.jar.red")
and:
output.count("Transforming") == 1
output.count("Transforming lib1.jar to lib1.jar.red") == 1
when:
run "resolve"
then:
executed(":lib:jar1", ":app:resolve")
and:
output.count("Transforming") == 0
}
def "applies chain of transforms to artifacts from local projects matching on some variant attributes"() {
given:
buildFile << """
allprojects {
dependencies {
attributesSchema {
attribute(Attribute.of('javaVersion', String))
attribute(Attribute.of('color', String))
}
}
}
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
task jar2(type: Zip) {
destinationDirectory = buildDir
archiveFileName = 'lib2.jar'
}
configurations {
compile.outgoing.variants {
java7 {
attributes.attribute(Attribute.of('javaVersion', String), '7')
attributes.attribute(Attribute.of('color', String), 'green')
artifact jar1
}
java8 {
attributes.attribute(Attribute.of('javaVersion', String), '8')
attributes.attribute(Attribute.of('color', String), 'red')
artifact jar2
}
}
}
}
project(':app') {
dependencies {
compile project(':lib')
registerTransform(MakeBlueToRedThings) {
from.attribute(Attribute.of('color', String), "blue")
to.attribute(Attribute.of('color', String), "red")
}
registerTransform(MakeGreenToBlueThings) {
from.attribute(Attribute.of('color', String), "green")
to.attribute(Attribute.of('color', String), "blue")
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes {
it.attribute(artifactType, 'jar')
it.attribute(Attribute.of('javaVersion', String), '7')
it.attribute(Attribute.of('color', String), 'red')
}
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
doLast {
println "files: " + artifacts.collect { it.file.name }
println "variants: " + artifacts.collect { it.variant.attributes }
println "ids: " + artifacts.collect { it.id }
println "components: " + artifacts.collect { it.id.componentIdentifier }
}
}
}
abstract class MakeGreenToBlueThings implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".blue")
assert output.parentFile.directory && output.parentFile.list().length == 0
println "Transforming \${input.name} to \${output.name}"
println "Input exists: \${input.exists()}"
output.text = String.valueOf(input.length())
}
}
abstract class MakeBlueToRedThings implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".red")
assert output.parentFile.directory && output.parentFile.list().length == 0
println "Transforming \${input.name} to \${output.name}"
println "Input exists: \${input.exists()}"
output.text = String.valueOf(input.length())
}
}
"""
when:
run "resolve"
then:
executed(":lib:jar1", ":app:resolve")
and:
outputContains("variants: [{artifactType=jar, color=red, javaVersion=7, usage=api}]")
// Should belong to same component as the originals
outputContains("ids: [lib1.jar.blue.red (project :lib)]")
outputContains("components: [project :lib]")
file("app/build/libs").assertHasDescendants("lib1.jar.blue.red")
and:
output.count("Transforming") == 2
output.count("Transforming lib1.jar to lib1.jar.blue") == 1
output.count("Transforming lib1.jar.blue to lib1.jar.blue.red") == 1
when:
run "resolve"
then:
executed(":lib:jar1", ":app:resolve")
and:
output.count("Transforming") == 0
}
def "transforms can be applied to multiple files with the same name"() {
given:
buildFile << """
def f = file("lib.jar")
f.text = "1234"
dependencies {
compile files(f)
compile project(':lib')
}
project(':lib') {
def f2 = file("lib.jar")
f2.parentFile.mkdirs()
f2.text = "123"
artifacts { compile f2 }
}
dependencies {
registerTransform(FileSizer) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
}
}
task resolve {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
inputs.files artifacts.artifactFiles
doLast {
println "files: " + artifacts.collect { it.file.name }
println "ids: " + artifacts.collect { it.id }
println "components: " + artifacts.collect { it.id.componentIdentifier }
println "variants: " + artifacts.collect { it.variant.attributes }
println "content: " + artifacts.collect { it.file.text }
}
}
"""
when:
run "resolve"
then:
outputContains("variants: [{artifactType=size}, {artifactType=size, usage=api}]")
// transformed outputs should belong to same component as original
outputContains("ids: [lib.jar.txt (lib.jar), lib.jar.txt (project :lib)]")
outputContains("components: [lib.jar, project :lib]")
outputContains("files: [lib.jar.txt, lib.jar.txt]")
outputContains("content: [4, 3]")
and:
output.count("Transforming") == 2
output.count("Transforming lib.jar to lib.jar.txt") == 2
when:
run "resolve"
then:
output.count("Transforming") == 0
}
def "transform can register the input as an output"() {
buildFile << """
def f = file("lib.jar")
f.text = "1234"
dependencies {
compile files(f)
}
dependencies {
registerTransform(IdentityTransform) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'identity')
}
}
abstract class IdentityTransform implements TransformAction {
@InputArtifact
abstract Provider getInput()
void transform(TransformOutputs outputs) {
println("Transforming")
outputs.file(input)
}
}
task resolve {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'identity') }
}.artifacts
inputs.files artifacts.artifactFiles
doLast {
println "files: " + artifacts.collect { it.file.name }
}
}
"""
when:
run "resolve"
then:
output.count("Transforming") == 1
output.contains("files: [lib.jar]")
}
def "transform can generate multiple output files for a single input"() {
def m1 = mavenRepo.module("test", "test", "1.3").publish()
m1.artifactFile.text = "1234"
def m2 = mavenRepo.module("test", "test2", "2.3").publish()
m2.artifactFile.text = "12"
given:
buildFile << """
repositories {
maven { url "${mavenRepo.uri}" }
}
dependencies {
compile 'test:test:1.3'
compile 'test:test2:2.3'
}
${configurationAndTransform('LineSplitter')}
abstract class LineSplitter implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
File outputA = outputs.file(input.name + ".A.txt")
assert outputA.parentFile.directory && outputA.parentFile.list().length == 0
outputA.text = "Output A"
File outputB = outputs.file(input.name + ".B.txt")
outputB.text = "Output B"
}
}
"""
when:
succeeds "resolve"
then:
outputContains("variants: [{artifactType=size, org.gradle.status=release}, {artifactType=size, org.gradle.status=release}, {artifactType=size, org.gradle.status=release}, {artifactType=size, org.gradle.status=release}]")
outputContains("ids: [test-1.3.jar.A.txt (test:test:1.3), test-1.3.jar.B.txt (test:test:1.3), test2-2.3.jar.A.txt (test:test2:2.3), test2-2.3.jar.B.txt (test:test2:2.3)]")
outputContains("components: [test:test:1.3, test:test:1.3, test:test2:2.3, test:test2:2.3]")
file("build/libs").assertHasDescendants("test-1.3.jar.A.txt", "test-1.3.jar.B.txt", "test2-2.3.jar.A.txt", "test2-2.3.jar.B.txt")
file("build/libs").eachFile {
assert it.text =~ /Output \w/
}
}
def "transform can generate an empty output"() {
mavenRepo.module("test", "test", "1.3").publish()
mavenRepo.module("test", "test2", "2.3").publish()
given:
buildFile << """
repositories {
maven { url "${mavenRepo.uri}" }
}
dependencies {
compile 'test:test:1.3'
compile 'test:test2:2.3'
}
${configurationAndTransform('EmptyOutput')}
abstract class EmptyOutput implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
println "Transforming \${inputArtifact.get().asFile.name}"
}
}
"""
when:
run "resolve"
then:
output.count("Transforming") == 2
output.count("Transforming test-1.3.jar") == 1
output.count("Transforming test2-2.3.jar") == 1
file("build/libs").assertDoesNotExist()
when:
run "resolve"
then:
file("build/libs").assertDoesNotExist()
and:
output.count("Transforming") == 0
}
def "user receives reasonable error message when multiple transforms are available to produce requested variant"() {
given:
buildFile << """
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveBaseName = 'a'
archiveExtension = 'custom'
}
artifacts {
compile(jar1)
}
}
project(':app') {
dependencies {
compile project(':lib')
}
dependencies {
registerTransform(BrokenTransform) {
from.attribute(artifactType, 'custom')
to.attribute(artifactType, 'transformed')
from.attribute(extraAttribute, 'foo')
to.attribute(extraAttribute, 'bar')
}
registerTransform(BrokenTransform) {
from.attribute(artifactType, 'custom')
to.attribute(artifactType, 'transformed')
from.attribute(extraAttribute, 'foo')
to.attribute(extraAttribute, 'baz')
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute (artifactType, 'transformed') }
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
}
abstract class BrokenTransform implements TransformAction {
void transform(TransformOutputs outputs) {
throw new AssertionError("should not be used")
}
}
"""
when:
fails "resolve"
then:
failure.assertHasCause """Found multiple transforms that can produce a variant of project :lib with requested attributes:
- artifactType 'transformed'
- usage 'api'
Found the following transforms:
- From 'configuration ':lib:compile'':
- With source attributes:
- artifactType 'custom'
- usage 'api'
- Candidate transform(s):
- Transform 'BrokenTransform' producing attributes:
- artifactType 'transformed'
- extra 'bar'
- usage 'api'
- Transform 'BrokenTransform' producing attributes:
- artifactType 'transformed'
- extra 'baz'
- usage 'api'"""
}
def "user receives reasonable error message when multiple variants can be transformed to produce requested variant"() {
given:
buildFile << """
def buildType = Attribute.of("buildType", String)
def flavor = Attribute.of("flavor", String)
allprojects {
dependencies.attributesSchema.attribute(buildType)
dependencies.attributesSchema.attribute(flavor)
}
project(':lib') {
task jar1(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
configurations {
compile.outgoing.variants {
variant1 {
attributes.attribute(buildType, 'release')
attributes.attribute(flavor, 'free')
artifact jar1
}
variant2 {
attributes.attribute(buildType, 'release')
attributes.attribute(flavor, 'paid')
artifact jar1
}
variant3 {
attributes.attribute(buildType, 'debug')
attributes.attribute(flavor, 'free')
artifact jar1
}
}
}
}
project(':app') {
dependencies {
compile project(':lib')
}
dependencies {
registerTransform(BrokenTransform) {
from.attribute(artifactType, 'jar')
from.attribute(buildType, 'release')
to.attribute(artifactType, 'transformed')
}
registerTransform(BrokenTransform) {
from.attribute(artifactType, 'jar')
from.attribute(buildType, 'debug')
to.attribute(artifactType, 'transformed')
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes {
attribute(artifactType, 'transformed')
}
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
}
abstract class BrokenTransform implements TransformAction {
void transform(TransformOutputs outputs) {
throw new AssertionError("should not be used")
}
}
"""
when:
fails "resolve"
then:
failure.assertHasCause """Found multiple transforms that can produce a variant of project :lib with requested attributes:
- artifactType 'transformed'
- usage 'api'
Found the following transforms:
- From 'configuration ':lib:compile' variant variant1':
- With source attributes:
- artifactType 'jar'
- buildType 'release'
- flavor 'free'
- usage 'api'
- Candidate transform(s):
- Transform 'BrokenTransform' producing attributes:
- artifactType 'transformed'
- buildType 'release'
- flavor 'free'
- usage 'api'
- From 'configuration ':lib:compile' variant variant2':
- With source attributes:
- artifactType 'jar'
- buildType 'release'
- flavor 'paid'
- usage 'api'
- Candidate transform(s):
- Transform 'BrokenTransform' producing attributes:
- artifactType 'transformed'
- buildType 'release'
- flavor 'paid'
- usage 'api'
- From 'configuration ':lib:compile' variant variant3':
- With source attributes:
- artifactType 'jar'
- buildType 'debug'
- flavor 'free'
- usage 'api'
- Candidate transform(s):
- Transform 'BrokenTransform' producing attributes:
- artifactType 'transformed'
- buildType 'debug'
- flavor 'free'
- usage 'api'"""
}
def "result is applied for all query methods"() {
given:
buildFile << """
project(':lib') {
projectDir.mkdirs()
def jar = file('lib.jar')
jar.text = 'some text'
artifacts { compile jar }
}
project(':app') {
dependencies {
compile project(':lib')
}
configurations {
compile {
attributes.attribute(artifactType, 'size')
}
}
dependencies {
registerTransform(FileSizer) {
from.attribute(artifactType, "jar")
to.attribute(artifactType, "size")
}
}
ext.checkArtifacts = { artifacts ->
assert artifacts.collect { it.id.displayName } == ['lib.jar.txt (project :lib)']
assert artifacts.collect { it.file.name } == ['lib.jar.txt']
}
ext.checkLegacyArtifacts = { artifacts ->
assert artifacts.collect { it.id.displayName } == ['lib.jar.txt (project :lib)']
assert artifacts.collect { it.file.name } == ['lib.jar.txt']
}
ext.checkFiles = { config ->
assert config.collect { it.name } == ['lib.jar.txt']
}
task resolve {
doLast {
checkFiles configurations.compile
checkFiles configurations.compile.files
checkFiles configurations.compile.incoming.files
checkFiles configurations.compile.resolvedConfiguration.files
checkFiles configurations.compile.resolvedConfiguration.lenientConfiguration.files
checkFiles configurations.compile.resolve()
checkFiles configurations.compile.files { true }
checkFiles configurations.compile.fileCollection { true }
checkFiles configurations.compile.resolvedConfiguration.getFiles { true }
checkFiles configurations.compile.resolvedConfiguration.lenientConfiguration.getFiles { true }
checkLegacyArtifacts configurations.compile.resolvedConfiguration.resolvedArtifacts
checkLegacyArtifacts configurations.compile.resolvedConfiguration.lenientConfiguration.artifacts
checkArtifacts configurations.compile.incoming.artifacts
checkArtifacts configurations.compile.incoming.artifactView { }.artifacts
}
}
}
"""
expect:
succeeds "resolve"
}
def "transforms are applied lazily in file collections"() {
def m1 = mavenHttpRepo.module('org.test', 'test1', '1.0').publish()
def m2 = mavenHttpRepo.module('org.test', 'test2', '2.0').publish()
given:
buildFile << """
repositories {
maven { url '${mavenHttpRepo.uri}' }
}
configurations {
config1 {
attributes { attribute(artifactType, 'size') }
}
config2
}
dependencies {
config1 'org.test:test1:1.0'
config2 'org.test:test2:2.0'
}
${configurationAndTransform('FileSizer')}
def configFiles = configurations.config1.incoming.files
def configView = configurations.config2.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.files
task queryFiles {
doLast {
println configFiles.collect { it.name }
}
}
task queryView {
doLast {
println configView.collect { it.name }
}
}
"""
when:
succeeds "help"
then:
output.count("Transforming") == 0
output.count("Creating") == 0
when:
server.resetExpectations()
m1.pom.expectGet()
m1.artifact.expectGet()
succeeds "queryFiles"
then:
output.count("Creating FileSizer") == 1
output.count("Transforming") == 1
output.count("Transforming test1-1.0.jar to test1-1.0.jar.txt") == 1
when:
server.resetExpectations()
m2.pom.expectGet()
m2.artifact.expectGet()
succeeds "queryView"
then:
output.count("Creating FileSizer") == 1
output.count("Transforming") == 1
output.count("Transforming test2-2.0.jar to test2-2.0.jar.txt") == 1
when:
server.resetExpectations()
succeeds "queryView"
then:
output.count("Creating FileSizer") == 0
output.count("Transforming") == 0
}
@ToBeFixedForConfigurationCache(because = "task that uses file collection containing transforms but does not declare this as an input may be encoded before the transform nodes it references")
def "transforms are created as required and a new instance created for each file"() {
given:
buildFile << """
dependencies {
compile project(':lib')
}
project(':lib') {
task jar1(type: Jar) { archiveFileName = 'jar1.jar' }
task jar2(type: Jar) { archiveFileName = 'jar2.jar' }
tasks.withType(Jar) { destinationDirectory = buildDir }
artifacts { compile jar1, jar2 }
}
abstract class Hasher implements TransformAction {
private int count
Hasher() {
println "Creating Transform"
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".txt")
assert output.parentFile.directory && output.parentFile.list().length == 0
count++
println "Transforming \${input.name} to \${output.name} with count \${count}"
output.text = String.valueOf(count)
}
}
${configurationAndTransform('Hasher')}
def configFiles = configurations.compile.incoming.files
def configView = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.files
task queryFiles {
doLast {
println "files: " + configFiles.collect { it.name }
}
}
task queryView {
doLast {
println "files: " + configView.collect { it.name }
}
}
"""
when:
succeeds "help"
then:
output.count("Transforming") == 0
output.count("Creating Transform") == 0
when:
succeeds "queryFiles"
then:
output.count("Transforming") == 0
output.count("Creating Transform") == 0
outputContains("files: [jar1.jar, jar2.jar]")
when:
succeeds "queryView"
then:
output.count("Creating Transform") == 2
output.count("Transforming") == 2
output.count("Transforming jar1.jar to jar1.jar.txt with count 1") == 1
output.count("Transforming jar2.jar to jar2.jar.txt with count 1") == 1
outputContains("files: [jar1.jar.txt, jar2.jar.txt]")
when:
succeeds "queryView"
then:
output.count("Creating Transform") == 0
output.count("Transforming") == 0
outputContains("files: [jar1.jar.txt, jar2.jar.txt]")
}
def "user gets a reasonable error message when a transform throws exception and continues with other inputs"() {
given:
buildFile << """
def a = file('a.jar')
a << '1234'
def b = file('b.jar')
b << '321'
dependencies {
compile files(a, b)
}
abstract class TransformWithIllegalArgumentException implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
if (input.name == 'a.jar') {
throw new IllegalArgumentException("broken")
}
println "Transforming " + input.name
outputs.file(input)
}
}
${configurationAndTransform('TransformWithIllegalArgumentException')}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("broken")
and:
outputContains("Transforming b.jar")
when:
executer.withArgument("-Plenient=true")
succeeds("resolve")
then:
outputContains("files: [b.jar]")
}
@ToBeFixedForConfigurationCache(because = "treating file collection visit failures as a configuration cache problem adds an additional failure to the build summary; exception chain is different when transform input cannot be resolved")
def "user gets a reasonable error message when a transform input cannot be downloaded and proceeds with other inputs"() {
def m1 = ivyHttpRepo.module("test", "test", "1.3")
.artifact(type: 'jar', name: 'test-api')
.artifact(type: 'jar', name: 'test-impl')
.artifact(type: 'jar', name: 'test-impl2')
.publish()
def m2 = ivyHttpRepo.module("test", "test-2", "0.1")
.publish()
given:
buildFile << """
${configurationAndTransform('FileSizer')}
repositories {
ivy { url "${ivyHttpRepo.uri}" }
}
dependencies {
compile "test:test:1.3"
compile "test:test-2:0.1"
}
"""
when:
m1.ivy.expectGet()
m1.getArtifact(name: 'test-api').expectGet()
m1.getArtifact(name: 'test-impl').expectGetBroken()
m1.getArtifact(name: 'test-impl2').expectGet()
m2.ivy.expectGet()
m2.jar.expectGet()
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Could not download test-impl-1.3.jar (test:test:1.3)")
and:
outputContains("Transforming test-api-1.3.jar to test-api-1.3.jar.txt")
outputContains("Transforming test-impl2-1.3.jar to test-impl2-1.3.jar.txt")
outputContains("Transforming test-2-0.1.jar to test-2-0.1.jar.txt")
when:
m1.getArtifact(name: 'test-impl').expectGetBroken()
executer.withArguments("-Plenient=true")
succeeds("resolve")
then:
outputContains("files: [test-api-1.3.jar.txt, test-impl2-1.3.jar.txt, test-2-0.1.jar.txt]")
}
@ToBeFixedForConfigurationCache(because = "treating file collection visit failures as a configuration cache problem adds an additional failure to the build summary; exception chain is different when transform input cannot be resolved")
def "user gets a reasonable error message when file dependency cannot be listed and continues with other inputs"() {
given:
buildFile << """
${configurationAndTransform('FileSizer')}
def broken = false
gradle.taskGraph.whenReady { broken = true }
dependencies {
compile files('thing1.jar')
compile files { if (broken) { throw new RuntimeException("broken") }; [] }
compile files('thing2.jar')
}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("broken")
and:
outputContains("Transforming thing1.jar to thing1.jar.txt")
outputContains("Transforming thing2.jar to thing2.jar.txt")
when:
executer.withArguments("-Plenient=true")
succeeds("resolve")
then:
outputContains("files: [thing1.jar.txt, thing2.jar.txt]")
}
@Unroll
def "user gets a reasonable error message when null is registered via outputs.#method"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
abstract class ToNullTransform implements TransformAction {
void transform(TransformOutputs outputs) {
outputs.${method}(null)
}
}
${configurationAndTransform('ToNullTransform')}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("Execution failed for ToNullTransform: ${file("a.jar").absolutePath}.")
failure.assertHasCause("path may not be null or empty string. path='null'")
where:
method << ['dir', 'file']
}
def "user gets a reasonable error message when transform returns a non-existing file"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
abstract class NoExistTransform implements TransformAction {
void transform(TransformOutputs outputs) {
outputs.file('this_file_does_not.exist')
}
}
${configurationAndTransform('NoExistTransform')}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("Transform output this_file_does_not.exist must exist.")
when:
executer.withArguments("-Plenient=true")
succeeds("resolve")
then:
outputContains(":resolve NO-SOURCE")
}
@Unroll
def "user gets a reasonable error message when transform registers a #type output via #method"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
abstract class FailingTransform implements TransformAction {
void transform(TransformOutputs outputs) {
${
switch (type) {
case FileType.Missing:
return """
outputs.${method}('this_file_does_not.exist').delete()
"""
case FileType.Directory:
return """
def output = outputs.${method}('directory')
output.mkdirs()
"""
case FileType.RegularFile:
return """
def output = outputs.${method}('file')
output.delete()
output.text = 'some text'
"""
}
}
}
}
${declareTransformAction('FailingTransform')}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
lenient(providers.gradleProperty("lenient").forUseAtConfigurationTime().present)
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertThatCause(matchesRegexp("Transform ${failureMessage}."))
when:
executer.withArguments("-Plenient=true")
succeeds("resolve")
then:
outputContains(":resolve NO-SOURCE")
where:
method | type | failureMessage
'file' | FileType.Directory | 'output file .*directory must be a file, but is not'
'file' | FileType.Missing | 'output .*this_file_does_not.exist must exist'
'dir' | FileType.RegularFile | 'output directory .*file must be a directory, but is not'
'dir' | FileType.Missing | 'output .*this_file_does_not.exist must exist'
}
def "directories are created for outputs in the workspace"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
abstract class DirectoryTransform implements TransformAction {
void transform(TransformOutputs outputs) {
def outputFile = outputs.file("some/dir/output.txt")
assert outputFile.parentFile.directory
outputFile.text = "output"
def outputDir = outputs.dir("another/output/dir")
assert outputDir.directory
new File(outputDir, "in-dir.txt").text = "another output"
}
}
${declareTransformAction('DirectoryTransform')}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
"""
expect:
succeeds "resolve"
}
@Unroll
def "directories are not created for output #method which is part of the input"() {
given:
buildFile << """
def a = file('a.jar')
a.mkdirs()
new File(a, "subdir").mkdirs()
new File(a, "subfile.txt").text = "input file"
dependencies {
compile files(a)
}
abstract class MyTransform implements TransformAction {
@InputArtifact
abstract Provider getInput()
void transform(TransformOutputs outputs) {
println "Hello?"
def output = outputs.${method}(new File(input.get().asFile, "some/dir/does-not-exist"))
assert !output.parentFile.directory
}
}
dependencies {
registerTransform(MyTransform) {
from.attribute(artifactType, 'directory')
to.attribute(artifactType, 'size')
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
"""
expect:
fails "resolve"
failure.assertThatCause(matchesRegexp('Transform output .*does-not-exist must exist.'))
where:
method << ["file", "dir"]
}
def "user gets a reasonable error message when transform returns a file that is not part of the input artifact or in the output directory"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
SomewhereElseTransform.output = file("other.jar")
abstract class SomewhereElseTransform implements TransformAction {
static def output
void transform(TransformOutputs outputs) {
outputs.file(output)
output.text = "123"
}
}
${configurationAndTransform('SomewhereElseTransform')}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("Transform output ${testDirectory.file('other.jar')} must be a part of the input artifact or refer to a relative path.")
}
def "user gets a reasonable error message when transform registers an output that is not part of the input artifact or in the output directory"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
SomewhereElseTransform.output = file("other.jar")
abstract class SomewhereElseTransform implements TransformAction {
static def output
void transform(TransformOutputs outputs) {
def outputFile = outputs.file(output)
outputFile.text = "123"
}
}
${declareTransformAction('SomewhereElseTransform')}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("Transform output ${testDirectory.file('other.jar')} must be a part of the input artifact or refer to a relative path.")
}
def "user gets a reasonable error message when transform cannot be instantiated"() {
given:
buildFile << """
def a = file('a.jar')
a.text = '1234'
dependencies {
compile files(a)
}
abstract class BrokenTransform implements TransformAction {
BrokenTransform() {
throw new RuntimeException("broken")
}
void transform(TransformOutputs outputs) {
throw new IllegalArgumentException("broken")
}
}
${configurationAndTransform('BrokenTransform')}
"""
when:
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform a.jar to match attributes {artifactType=size}")
failure.assertHasCause("Could not create an instance of type BrokenTransform.")
failure.assertHasCause("broken")
}
@ToBeFixedForConfigurationCache(because = "treating file collection visit failures as a configuration cache problem adds an additional failure to the build summary; exception chain is different when transform input cannot be resolved")
def "collects multiple failures"() {
def m1 = mavenHttpRepo.module("test", "a", "1.3").publish()
def m2 = mavenHttpRepo.module("test", "broken", "2.0").publish()
def m3 = mavenHttpRepo.module("test", "c", "2.0").publish()
given:
buildFile << """
repositories {
maven { url '$mavenHttpRepo.uri' }
}
def a = file("a.jar")
a.text = '123'
def b = file("broken.jar")
b.text = '123'
def c = file("c.jar")
c.text = '123'
dependencies {
compile files(a, b, c)
compile 'test:a:1.3'
compile 'test:broken:2.0'
compile 'test:c:2.0'
}
abstract class TransformWithIllegalArgumentException implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
if (input.name.contains('broken')) {
throw new IllegalArgumentException("broken: " + input.name)
}
println "Transforming " + input.name
outputs.file(inputArtifact)
}
}
${configurationAndTransform('TransformWithIllegalArgumentException')}
"""
when:
m1.pom.expectGet()
m1.artifact.expectGetBroken()
m2.pom.expectGet()
m2.artifact.expectGet()
m3.pom.expectGet()
m3.artifact.expectGet()
fails "resolve"
then:
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
failure.assertHasCause("Failed to transform broken.jar to match attributes {artifactType=size}")
failure.assertHasCause("broken: broken.jar")
failure.assertHasCause("Could not download a-1.3.jar (test:a:1.3)")
failure.assertHasCause("Failed to transform broken-2.0.jar (test:broken:2.0) to match attributes {artifactType=size, org.gradle.status=release}")
failure.assertHasCause("broken: broken-2.0.jar")
and:
outputContains("Transforming a.jar")
outputContains("Transforming c.jar")
outputContains("Transforming c-2.0.jar")
when:
m1.artifact.expectGetBroken()
executer.withArguments("-Plenient=true")
succeeds("resolve")
then:
outputContains("files: [a.jar, c.jar, c-2.0.jar]")
}
def "provides useful error message when registration action fails"() {
when:
buildFile << """
dependencies {
registerTransform(FileSizer) {
throw new Exception("Bad registration")
}
}
"""
then:
fails "help"
and:
failure.assertHasDescription("A problem occurred evaluating root project 'root'.")
failure.assertHasCause("Bad registration")
}
def "provides useful error message when configuration value cannot be serialized"() {
when:
buildFile << """
// Not serializable
class CustomType {
String toString() { return "" }
}
class Custom extends ArtifactTransform {
Custom(CustomType value) { }
List transform(File input) { [] }
}
dependencies {
registerTransform {
from.attribute(usage, 'any')
to.attribute(usage, 'any')
artifactTransform(Custom) { params(new CustomType()) }
}
}
"""
then:
fails "help"
and:
failure.assertHasDescription("A problem occurred evaluating root project 'root'.")
failure.assertHasCause("Could not register artifact transform Custom (from {usage=any} to {usage=any})")
failure.assertHasCause("Could not isolate value [] of type Object[]")
failure.assertHasCause("Could not serialize value of type CustomType")
}
@Unroll
def "provides useful error message when parameter value cannot be isolated for #type transform"() {
mavenRepo.module("test", "a", "1.3").publish()
settingsFile << "include 'lib'"
buildFile << """
project(':lib') {
task jar(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib.jar'
}
artifacts {
compile jar
}
}
repositories {
maven { url '$mavenRepo.uri' }
}
dependencies {
compile ${dependency}
}
// Not serializable
class CustomType {
String toString() { return "" }
}
abstract class Custom implements TransformAction {
interface Parameters extends TransformParameters {
@Input
CustomType getInput()
void setInput(CustomType input)
}
void transform(TransformOutputs outputs) { }
}
dependencies {
registerTransform(Custom) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
parameters {
input = new CustomType()
}
}
}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
}
"""
when:
fails "resolve"
then:
Matcher matchesCannotIsolate = matchesRegexp("Could not isolate parameters Custom\\\$Parameters_Decorated@.* of artifact transform Custom")
failure.assertHasDescription("Execution failed for task ':resolve'.")
failure.assertThatCause(matchesCannotIsolate)
failure.assertHasCause("Could not serialize value of type CustomType")
where:
scheduled | dependency
true | 'project(":lib")'
false | '"test:a:1.3"'
type = scheduled ? 'scheduled' : 'immediate'
}
def "artifacts with same component id and extension, but different classifier remain distinguishable after transformation"() {
def module = mavenRepo.module("test", "test", "1.3").publish()
module.getArtifactFile(classifier: "foo").text = "1234"
module.getArtifactFile(classifier: "bar").text = "5678"
given:
buildFile << """
repositories {
maven { url "${mavenRepo.uri}" }
}
dependencies {
compile 'test:test:1.3:foo'
compile 'test:test:1.3:bar'
}
/*
* This transform creates a name that is independent of
* the original file name, thus losing the classifier that
* was encoded in it.
*/
abstract class NameManglingTransform implements TransformAction {
NameManglingTransform() {
println "Creating NameManglingTransform"
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def output = outputs.file("out.txt")
output.text = inputArtifact.get().asFile.text
}
}
${configurationAndTransform('NameManglingTransform')}
"""
when:
run "resolve"
then:
outputContains("ids: [out-foo.txt (test:test:1.3), out-bar.txt (test:test:1.3)]")
}
def "artifact excludes applied to external dependency on different graphs are honored"() {
def m1 = ivyRepo.module("test", "test", "1.3")
m1.artifact(name: "test-one", conf: "*")
m1.artifact(name: "test-two", conf: "*")
m1.publish()
def m2 = ivyRepo.module("test", "test2", "2.3").dependsOn(m1).exclude(module: "test", artifact: "test-one")
m2.publish()
def m3 = ivyRepo.module("test", "test3", "3.4").dependsOn(m1).exclude(module: "test", artifact: "test-two")
m3.publish()
given:
taskTypeLogsArtifactCollectionDetails()
buildFile << """
repositories {
ivy { url "${ivyRepo.uri}" }
}
configurations {
compile1 {
attributes { attribute usage, 'api' }
}
compile2 {
attributes { attribute usage, 'api' }
}
}
dependencies {
compile1 'test:test2:2.3'
compile2 'test:test3:3.4'
}
${declareTransform('FileSizer')}
task resolve1(type: ShowArtifactCollection) {
collection = configurations.compile1.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
}
task resolve2(type: ShowArtifactCollection) {
collection = configurations.compile2.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
}
"""
when:
run "resolve1", "resolve2"
then:
def task1 = result.groupedOutput.task(":resolve1")
task1.assertOutputContains("variants = [{artifactType=size, org.gradle.status=integration}, {artifactType=size, org.gradle.status=integration}]")
task1.assertOutputContains("components = [test:test2:2.3, test:test:1.3]")
task1.assertOutputContains("artifacts = [test2-2.3.jar.txt (test:test2:2.3), test-two-1.3.jar.txt (test:test:1.3)]")
task1.assertOutputContains("files = [test2-2.3.jar.txt, test-two-1.3.jar.txt]")
def task2 = result.groupedOutput.task(":resolve2")
task2.assertOutputContains("variants = [{artifactType=size, org.gradle.status=integration}, {artifactType=size, org.gradle.status=integration}]")
task2.assertOutputContains("components = [test:test3:3.4, test:test:1.3]")
task2.assertOutputContains("artifacts = [test3-3.4.jar.txt (test:test3:3.4), test-one-1.3.jar.txt (test:test:1.3)]")
task2.assertOutputContains("files = [test3-3.4.jar.txt, test-one-1.3.jar.txt]")
and:
output.count("Transforming") == 4
output.count("Transforming test-one-1.3.jar to test-one-1.3.jar.txt") == 1
output.count("Transforming test-two-1.3.jar to test-two-1.3.jar.txt") == 1
output.count("Transforming test2-2.3.jar to test2-2.3.jar.txt") == 1
output.count("Transforming test3-3.4.jar to test3-3.4.jar.txt") == 1
when:
run "resolve1", "resolve2"
then:
output.count("Transforming") == 0
}
def "artifacts with the same id but different content are transformed independently"() {
def repo1 = new MavenFileRepository(testDirectory.file("repo1"))
def repo2 = new MavenFileRepository(testDirectory.file("repo2"))
def m1 = repo1.module("test", "test", "1.3").publish()
m1.artifactFile.text = "1234"
def m2 = repo2.module("test", "test", "1.3").publish()
m2.artifactFile.text = "12345"
given:
settingsFile << """
include "a"
include "b"
"""
buildFile << """
project(":a") {
repositories {
maven { url "${repo1.uri}" }
}
}
project(":b") {
repositories {
maven { url "${repo2.uri}" }
}
}
allprojects {
dependencies {
compile 'test:test:1.3'
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "a:resolve", "b:resolve"
then:
def task1 = result.groupedOutput.task(":a:resolve")
task1.assertOutputContains("variants: [{artifactType=size, org.gradle.status=release}]")
task1.assertOutputContains("components: [test:test:1.3]")
task1.assertOutputContains("ids: [test-1.3.jar.txt (test:test:1.3)]")
task1.assertOutputContains("files: [test-1.3.jar.txt]")
def task2 = result.groupedOutput.task(":b:resolve")
task2.assertOutputContains("variants: [{artifactType=size, org.gradle.status=release}]")
task2.assertOutputContains("components: [test:test:1.3]")
task2.assertOutputContains("ids: [test-1.3.jar.txt (test:test:1.3)]")
task2.assertOutputContains("files: [test-1.3.jar.txt]")
file("a/build/libs/test-1.3.jar.txt").text == "4"
file("b/build/libs/test-1.3.jar.txt").text == "5"
and:
output.count("Transforming") == 2
output.count("Transforming test-1.3.jar to test-1.3.jar.txt") == 2
when:
run "a:resolve", "b:resolve"
then:
output.count("Transforming") == 0
}
def "transform runs only once even when variant is consumed from multiple projects"() {
given:
settingsFile << """
include 'app2'
"""
buildFile << """
project(':lib') {
projectDir.mkdirs()
def file1 = file('lib1.size')
file1.text = 'some text'
task lib1(type: Jar) {
destinationDirectory = buildDir
}
dependencies {
compile files(lib1)
}
artifacts {
compile file1
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
project(':app2') {
dependencies {
compile project(':lib')
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "app:resolve", "app2:resolve"
then:
output.count("Transforming") == 1
}
def "can resolve transformed variant during configuration time"() {
given:
buildFile << """
project(':lib') {
projectDir.mkdirs()
def jar1 = file('lib1.jar')
jar1.text = 'some text'
def file1 = file('lib1.size')
file1.text = 'some text'
dependencies {
compile files(jar1)
}
artifacts {
compile file1
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${declareTransform('FileSizer')}
task resolve(type: Copy) {
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
if (project.hasProperty("lenient")) {
lenient(true)
}
}.artifacts
// Resolve during configuration
from artifacts.artifactFiles.files
into "\${buildDir}/libs"
doLast {
// Do nothing
}
}
}
"""
when:
run "app:resolve"
then:
output.count("Transforming") == 1
}
def "notifies transform listeners and build operation listeners on successful execution"() {
def buildOperations = new BuildOperationsFixture(executer, temporaryFolder)
given:
buildFile << """
import org.gradle.api.internal.artifacts.transform.ArtifactTransformListener
import org.gradle.internal.event.ListenerManager
project.services.get(ListenerManager).addListener(new ArtifactTransformListener() {
@Override
void beforeTransformerInvocation(Describable transformer, Describable subject) {
println "Before transformer \${transformer.displayName} on \${subject.displayName}"
}
@Override
void afterTransformerInvocation(Describable transformer, Describable subject) {
println "After transformer \${transformer.displayName} on \${subject.displayName}"
}
})
project(":lib") {
task jar(type: Jar) {
archiveFileName = 'lib.jar'
destinationDirectory = buildDir
}
artifacts {
compile jar
}
}
project(":app") {
dependencies {
compile project(":lib")
}
${configurationAndTransform('FileSizer')}
}
"""
when:
run "app:resolve"
then:
outputContains("Before transformer FileSizer on lib.jar (project :lib)")
outputContains("After transformer FileSizer on lib.jar (project :lib)")
and:
with(buildOperations.only(ExecuteScheduledTransformationStepBuildOperationType)) {
it.failure == null
displayName == "Transform lib.jar (project :lib) with FileSizer"
details.transformerName == "FileSizer"
details.subjectName == "lib.jar (project :lib)"
}
}
def "notifies transform listeners and build operation listeners on failed execution"() {
def buildOperations = new BuildOperationsFixture(executer, temporaryFolder)
given:
buildFile << """
import org.gradle.api.internal.artifacts.transform.ArtifactTransformListener
import org.gradle.internal.event.ListenerManager
project.services.get(ListenerManager).addListener(new ArtifactTransformListener() {
@Override
void beforeTransformerInvocation(Describable transformer, Describable subject) {
println "Before transformer \${transformer.displayName} on \${subject.displayName}"
}
@Override
void afterTransformerInvocation(Describable transformer, Describable subject) {
println "After transformer \${transformer.displayName} on \${subject.displayName}"
}
})
project(":lib") {
task jar(type: Jar) {
archiveFileName = 'lib.jar'
destinationDirectory = buildDir
}
artifacts {
compile jar
}
}
project(":app") {
dependencies {
compile project(":lib")
}
${configurationAndTransform('BrokenTransform')}
}
abstract class BrokenTransform implements TransformAction {
void transform(TransformOutputs outputs) {
throw new GradleException('broken')
}
}
"""
when:
fails "app:resolve"
then:
outputContains("Before transformer BrokenTransform on lib.jar (project :lib)")
outputContains("After transformer BrokenTransform on lib.jar (project :lib)")
and:
with(buildOperations.only(ExecuteScheduledTransformationStepBuildOperationType)) {
displayName == "Transform lib.jar (project :lib) with BrokenTransform"
details.transformerName == "BrokenTransform"
details.subjectName == "lib.jar (project :lib)"
}
}
@Issue("https://github.com/gradle/gradle/issues/6156")
def "stops resolving dependencies of task when artifact transforms are encountered"() {
given:
buildFile << """
project(':lib') {
task jar(type: Jar) {
destinationDirectory = buildDir
archiveFileName = 'lib1.jar'
}
artifacts {
compile jar
}
}
project(':app') {
dependencies {
compile project(':lib')
}
${configurationAndTransform()}
task dependent {
dependsOn resolve
}
}
gradle.taskGraph.whenReady { taskGraph ->
taskGraph.allTasks.each { task ->
task.taskDependencies.getDependencies(task).each { dependency ->
println "> Dependency: \${task} -> \${dependency}"
}
}
}
"""
when:
run "dependent"
then:
output.count("> Dependency:") == 1
output.contains("> Dependency: task ':app:dependent' -> task ':app:resolve'")
output.contains("> Transform lib1.jar (project :lib) with FileSizer")
output.contains("> Task :app:resolve")
}
def declareTransform(String transformImplementation) {
"""
dependencies {
registerTransform(${transformImplementation}) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
}
}
"""
}
def declareTransformAction(String transformActionImplementation) {
"""
dependencies {
registerTransform($transformActionImplementation) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
}
}
"""
}
def configurationAndTransform(String transformImplementation = "FileSizer") {
"""
${declareTransform(transformImplementation)}
task resolve(type: Copy) {
duplicatesStrategy = 'INCLUDE'
def artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
lenient(providers.gradleProperty("lenient").forUseAtConfigurationTime().present)
}.artifacts
from artifacts.artifactFiles
into "\${buildDir}/libs"
doLast {
println "files: " + artifacts.collect { it.file.name }
println "ids: " + artifacts.collect { it.id }
println "components: " + artifacts.collect { it.id.componentIdentifier }
println "variants: " + artifacts.collect { it.variant.attributes }
}
}
"""
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy