org.gradle.integtests.resolve.transform.ArtifactTransformCachingIntegrationTest.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 2017 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.ivyservice.CacheLayout
import org.gradle.cache.internal.LeastRecentlyUsedCacheCleanup
import org.gradle.integtests.fixtures.AbstractHttpDependencyResolutionTest
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.integtests.fixtures.cache.FileAccessTimeJournalFixture
import org.gradle.integtests.fixtures.executer.GradleContextualExecuter
import org.gradle.internal.reflect.problems.ValidationProblemId
import org.gradle.internal.reflect.validation.ValidationMessageChecker
import org.gradle.internal.reflect.validation.ValidationTestFor
import org.gradle.test.fixtures.Flaky
import org.gradle.test.fixtures.file.LeaksFileHandles
import org.gradle.test.fixtures.file.TestFile
import org.gradle.test.fixtures.server.http.BlockingHttpServer
import org.junit.Rule
import spock.lang.Issue
import java.util.regex.Pattern
import static java.util.concurrent.TimeUnit.MILLISECONDS
import static java.util.concurrent.TimeUnit.SECONDS
import static org.gradle.internal.service.scopes.DefaultGradleUserHomeScopeServiceRegistry.REUSE_USER_HOME_SERVICES
import static org.gradle.test.fixtures.ConcurrentTestUtil.poll
import static org.hamcrest.Matchers.containsString
class ArtifactTransformCachingIntegrationTest extends AbstractHttpDependencyResolutionTest implements FileAccessTimeJournalFixture, ValidationMessageChecker {
private final static long MAX_CACHE_AGE_IN_DAYS = LeastRecentlyUsedCacheCleanup.DEFAULT_MAX_AGE_IN_DAYS_FOR_RECREATABLE_CACHE_ENTRIES
@Rule
BlockingHttpServer blockingHttpServer = new BlockingHttpServer()
def setup() {
expectReindentedValidationMessage()
settingsFile << """
rootProject.name = 'root'
include 'lib'
include 'util'
include 'app'
"""
buildFile << resolveTask << """
import org.gradle.api.artifacts.transform.TransformParameters
allprojects {
repositories {
maven { url '${mavenHttpRepo.uri}' }
}
}
"""
}
def "transform is applied to each file once per build"() {
given:
buildFile << declareAttributes() << multiProjectWithJarSizeTransform() << withJarTasks() << withFileLibDependency("lib3.jar") << withExternalLibDependency("lib4")
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files: [lib1.jar.txt, lib2.jar.txt, lib3.jar.txt, lib4-1.0.jar.txt]") == 2
output.count("ids: [lib1.jar.txt (project :lib), lib2.jar.txt (project :lib), lib3.jar.txt (lib3.jar), lib4-1.0.jar.txt (org.test.foo:lib4:1.0)]") == 2
output.count("components: [project :lib, project :lib, lib3.jar, org.test.foo:lib4:1.0]") == 2
output.count("Transformed") == 4
isTransformed("lib1.jar", "lib1.jar.txt")
isTransformed("lib2.jar", "lib2.jar.txt")
isTransformed("lib3.jar", "lib3.jar.txt")
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files: [lib1.jar.txt, lib2.jar.txt, lib3.jar.txt, lib4-1.0.jar.txt]") == 2
output.count("Transformed") == 0
}
@Issue("https://github.com/gradle/gradle/issues/15604")
@LeaksFileHandles
def "transforms of file dependencies are not kept in the in-memory cache between builds"() {
given:
def projectDir1 = file("project1")
def projectDir2 = file("project2")
setupProjectInDir(projectDir1)
setupProjectInDir(projectDir2)
executer.requireIsolatedDaemons()
executer.beforeExecute {
if (!GradleContextualExecuter.embedded) {
executer.withArgument("-D$REUSE_USER_HOME_SERVICES=true")
}
}
when:
executer.inDirectory(projectDir1)
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files: [lib1.jar.txt, lib1.jar]") == 2
output.count("ids: [lib1.jar.txt (lib1.jar), lib1.jar (lib1.jar)]") == 2
output.count("components: [lib1.jar, lib1.jar]") == 2
output.count("Transformed") == 1
isTransformed("lib1.jar", "lib1.jar")
when:
projectDir1.deleteDir()
executer.inDirectory(projectDir2)
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files: [lib1.jar.txt, lib1.jar]") == 2
// From the Gradle user home cache
output.count("Transformed") == 0
}
private void setupProjectInDir(TestFile projectDir) {
projectDir.file("build.gradle") << resolveTask << """
import org.gradle.api.artifacts.transform.TransformParameters
""" << declareAttributes() << """
abstract class FileSizer implements TransformAction {
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputArtifact
abstract Provider getInputArtifact()
private File getInput() {
inputArtifact.get().asFile
}
void transform(TransformOutputs outputs) {
def output = outputs.file(input.name + ".txt")
println "Transformed \$input.name to \$input.name into \${output.parentFile}"
outputs.file(inputArtifact)
output.text = String.valueOf(input.length())
}
}
allprojects {
dependencies {
registerTransform(FileSizer) {
from.attribute(artifactType, "jar")
to.attribute(artifactType, "size")
}
}
tasks.register("resolve", Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
}
}
project(':util') {
dependencies {
compile project(':lib')
}
}
project(':app') {
dependencies {
compile project(':util')
}
}
""" << withFileLibDependency("lib1.jar", projectDir)
projectDir.file("settings.gradle") << """
rootProject.name = 'root'
include 'lib'
include 'util'
include 'app'
"""
}
@ValidationTestFor(
ValidationProblemId.CANNOT_WRITE_TO_RESERVED_LOCATION
)
def "task cannot write into transform directory"() {
def forbiddenPath = ".transforms/not-allowed.txt"
buildFile << """
subprojects {
task badTask {
outputs.file { project.layout.buildDirectory.file("${forbiddenPath}") } withPropertyName "output"
doLast { }
}
}
"""
when:
fails "badTask", "--continue"
then:
['lib', 'app', 'util'].each {
def reserved = file("${it}/build/${forbiddenPath}")
failure.assertHasDescription("A problem was found with the configuration of task ':${it}:badTask' (type 'DefaultTask').")
failure.assertThatDescription(containsString(cannotWriteToReservedLocation {
property('output')
.forbiddenAt(reserved)
.includeLink()
}))
}
}
def "scheduled transformation is invoked before consuming task is executed"() {
given:
buildFile << declareAttributes() << multiProjectWithJarSizeTransform() << withJarTasks()
when:
succeeds ":util:resolve"
def transformationPosition1 = output.indexOf("> Transform lib1.jar (project :lib) with FileSizer")
def transformationPosition2 = output.indexOf("> Transform lib2.jar (project :lib) with FileSizer")
def taskPosition = output.indexOf("> Task :util:resolve")
then:
transformationPosition1 >= 0
transformationPosition2 >= 0
taskPosition >= 0
transformationPosition1 < taskPosition
transformationPosition2 < taskPosition
}
def "scheduled transformation is only invoked once per subject"() {
given:
settingsFile << """
include 'util2'
"""
buildFile << declareAttributes() << multiProjectWithJarSizeTransform() << withJarTasks()
buildFile << """
project(':util2') {
dependencies {
compile project(':lib')
}
}
"""
when:
succeeds ":util:resolve", ":util2:resolve"
then:
output.count("> Transform lib1.jar (project :lib) with FileSizer") == 1
output.count("> Transform lib2.jar (project :lib) with FileSizer") == 1
}
def "scheduled chained transformation is only invoked once per subject"() {
given:
settingsFile << """
include 'app1'
include 'app2'
"""
buildFile << """
def color = Attribute.of('color', String)
allprojects {
dependencies {
attributesSchema {
attribute(color)
}
}
configurations {
compile {
attributes.attribute color, 'blue'
}
}
task resolveRed(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(color, 'red') }
}.artifacts
}
task resolveYellow(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(color, 'yellow') }
}.artifacts
}
}
configure([project(':app1'), project(':app2')]) {
dependencies {
compile project(':lib')
registerTransform(MakeBlueToGreenThings) {
from.attribute(Attribute.of('color', String), "blue")
to.attribute(Attribute.of('color', String), "green")
}
registerTransform(MakeGreenToRedThings) {
from.attribute(Attribute.of('color', String), "green")
to.attribute(Attribute.of('color', String), "red")
}
registerTransform(MakeGreenToYellowThings) {
from.attribute(Attribute.of('color', String), "green")
to.attribute(Attribute.of('color', String), "yellow")
}
}
}
abstract class MakeThingsColored implements TransformAction {
private final String targetColor
MakeThingsColored(String targetColor) {
this.targetColor = targetColor
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".\${targetColor}")
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 MakeGreenToRedThings extends MakeThingsColored {
MakeGreenToRedThings() {
super('red')
}
}
abstract class MakeGreenToYellowThings extends MakeThingsColored {
MakeGreenToYellowThings() {
super('yellow')
}
}
abstract class MakeBlueToGreenThings extends MakeThingsColored {
MakeBlueToGreenThings() {
super('green')
}
}
""" << withJarTasks()
when:
run ":app1:resolveRed", ":app2:resolveYellow"
then:
output.count("> Transform lib1.jar (project :lib) with MakeBlueToGreenThings") == 1
output.count("> Transform lib2.jar (project :lib) with MakeBlueToGreenThings") == 1
output.count("> Transform lib1.jar (project :lib) with MakeGreenToYellowThings") == 1
output.count("> Transform lib2.jar (project :lib) with MakeGreenToYellowThings") == 1
output.count("> Transform lib1.jar (project :lib) with MakeGreenToRedThings") == 1
output.count("> Transform lib2.jar (project :lib) with MakeGreenToRedThings") == 1
}
// This shows current behaviour, where the transform is executed even though the input artifact has not been created yet
// This should become an error eventually
def "executes transform immediately when required during task graph building"() {
buildFile << declareAttributes() << withJarTasks() << """
import org.gradle.api.artifacts.transform.TransformParameters
abstract class MakeGreen implements TransformAction {
@InputArtifact
abstract Provider getInputArtifact()
@Override
void transform(TransformOutputs outputs) {
def file = inputArtifact.get().asFile
println "Transforming \${file.name} with MakeGreen"
outputs.file(file.name + ".green").text = "very green"
}
}
project(':util') {
dependencies {
compile project(':lib')
}
}
project(':app') {
dependencies {
compile project(':util')
registerTransform(MakeGreen) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'green')
}
}
configurations {
green {
extendsFrom(compile)
canBeResolved = true
canBeConsumed = false
attributes {
attribute(artifactType, 'green')
}
}
}
tasks.register("resolveAtConfigurationTime").configure {
inputs.files(configurations.green)
configurations.green.each { println it }
doLast { }
}
tasks.register("declareTransformAsInput").configure {
def files = configurations.green
inputs.files(files)
doLast {
files.each { println it }
}
}
tasks.register("withDependency").configure {
dependsOn("resolveAtConfigurationTime")
}
tasks.register("toBeFinalized").configure {
// We require the task via a finalizer, so the transform node is in UNKNOWN state.
finalizedBy("declareTransformAsInput")
}
}
"""
when:
run(":app:toBeFinalized", "withDependency")
then:
output.count("Transforming lib1.jar with MakeGreen") == 1
output.count("Transforming lib2.jar with MakeGreen") == 1
when:
run(":app:toBeFinalized", "withDependency")
then:
output.count("Transforming lib1.jar with MakeGreen") == 1
output.count("Transforming lib2.jar with MakeGreen") == 1
}
def "each file is transformed once per set of configuration parameters"() {
given:
buildFile << declareAttributes() << withJarTasks() << withFileLibDependency("lib3.jar") << withExternalLibDependency("lib4") << """
abstract class TransformWithMultipleTargets implements TransformAction {
interface Parameters extends TransformParameters {
@Input
Property getTarget()
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
assert input.exists()
def output = outputs.file(input.name + ".\${parameters.target.get()}")
def outputDirectory = output.parentFile
assert outputDirectory.directory && outputDirectory.list().length == 0
if (parameters.target.get() == "size") {
output.text = String.valueOf(input.length())
} else if (parameters.target.get() == "hash") {
output.text = 'hash'
}
println "Transformed \$input.name to \$output.name into \$outputDirectory"
}
}
allprojects {
dependencies {
registerTransform(TransformWithMultipleTargets) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
parameters {
target = 'size'
}
}
registerTransform(TransformWithMultipleTargets) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'hash')
parameters {
target = 'hash'
}
}
}
task resolveSize(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
identifier = "1"
}
task resolveHash(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'hash') }
}.artifacts
identifier = "2"
}
task resolve {
dependsOn(resolveHash, resolveSize)
}
}
project(':util') {
dependencies {
compile project(':lib')
}
}
project(':app') {
dependencies {
compile project(':util')
}
}
"""
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.size, lib2.jar.size, lib3.jar.size, lib4-1.0.jar.size]") == 2
output.count("ids 1: [lib1.jar.size (project :lib), lib2.jar.size (project :lib), lib3.jar.size (lib3.jar), lib4-1.0.jar.size (org.test.foo:lib4:1.0)]") == 2
output.count("components 1: [project :lib, project :lib, lib3.jar, org.test.foo:lib4:1.0]") == 2
output.count("files 2: [lib1.jar.hash, lib2.jar.hash, lib3.jar.hash, lib4-1.0.jar.hash]") == 2
output.count("ids 2: [lib1.jar.hash (project :lib), lib2.jar.hash (project :lib), lib3.jar.hash (lib3.jar), lib4-1.0.jar.hash (org.test.foo:lib4:1.0)]") == 2
output.count("components 2: [project :lib, project :lib, lib3.jar, org.test.foo:lib4:1.0]") == 2
output.count("Transformed") == 8
isTransformed("lib1.jar", "lib1.jar.size")
isTransformed("lib2.jar", "lib2.jar.size")
isTransformed("lib3.jar", "lib3.jar.size")
isTransformed("lib4-1.0.jar", "lib4-1.0.jar.size")
isTransformed("lib1.jar", "lib1.jar.hash")
isTransformed("lib2.jar", "lib2.jar.hash")
isTransformed("lib3.jar", "lib3.jar.hash")
isTransformed("lib4-1.0.jar", "lib4-1.0.jar.hash")
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.size, lib2.jar.size, lib3.jar.size, lib4-1.0.jar.size]") == 2
output.count("Transformed") == 0
}
def "can use custom type that does not implement equals() for transform configuration"() {
given:
buildFile << declareAttributes() << withJarTasks() << """
class CustomType implements Serializable {
String value
}
abstract class TransformWithMultipleTargets implements TransformAction {
interface Parameters extends TransformParameters {
@Input
CustomType getTarget()
void setTarget(CustomType target)
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
def output = outputs.file(input.name + ".\${parameters.target.value}")
def outputDirectory = output.parentFile
if (parameters.target.value == "size") {
output.text = String.valueOf(input.length())
}
if (parameters.target.value == "hash") {
output.text = 'hash'
}
println "Transformed \$input.name to \$output.name into \$outputDirectory"
}
}
allprojects {
dependencies {
registerTransform(TransformWithMultipleTargets) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'size')
parameters {
target = new CustomType(value: 'size')
}
}
registerTransform(TransformWithMultipleTargets) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'hash')
parameters {
target = new CustomType(value: 'hash')
}
}
}
task resolveSize(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'size') }
}.artifacts
identifier = "1"
}
task resolveHash(type: Resolve) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'hash') }
}.artifacts
identifier = "2"
}
task resolve {
dependsOn(resolveSize, resolveHash)
}
}
project(':util') {
dependencies {
compile project(':lib')
}
}
project(':app') {
dependencies {
compile project(':util')
}
}
"""
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.size, lib2.jar.size]") == 2
output.count("files 2: [lib1.jar.hash, lib2.jar.hash]") == 2
output.count("Transformed") == 4
isTransformed("lib1.jar", "lib1.jar.size")
isTransformed("lib2.jar", "lib2.jar.size")
isTransformed("lib1.jar", "lib1.jar.hash")
isTransformed("lib2.jar", "lib2.jar.hash")
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.size, lib2.jar.size]") == 2
output.count("Transformed") == 0
}
@Flaky
def "can use configuration parameter of type #type"() {
given:
buildFile << declareAttributes() << withJarTasks() << """
abstract class TransformWithMultipleTargets implements TransformAction {
interface Parameters extends TransformParameters {
@Input
$type getTarget()
void setTarget($type target)
}
@InputArtifact
abstract Provider getInputArtifact()
void transform(TransformOutputs outputs) {
def input = inputArtifact.get().asFile
assert input.exists()
def output = outputs.file(input.name + ".value")
println "Transformed \$input.name to \$output.name into \$output.parentFile"
output.text = String.valueOf(input.length()) + String.valueOf(parameters.target)
}
}
allprojects {
dependencies {
registerTransform(TransformWithMultipleTargets) {
from.attribute(artifactType, 'jar')
to.attribute(artifactType, 'value')
parameters {
target = $value
}
}
}
task resolve1(type: Resolve) {
identifier = "1"
}
task resolve2(type: Resolve) {
identifier = "2"
}
configure([resolve1, resolve2]) {
artifacts = configurations.compile.incoming.artifactView {
attributes { it.attribute(artifactType, 'value') }
}.artifacts
}
task resolve {
dependsOn(resolve1, resolve2)
}
}
project(':util') {
dependencies {
compile project(':lib')
}
}
project(':app') {
dependencies {
compile project(':util')
}
}
"""
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.value, lib2.jar.value]") == 2
output.count("files 2: [lib1.jar.value, lib2.jar.value]") == 2
output.count("Transformed") == 2
isTransformed("lib1.jar", "lib1.jar.value")
isTransformed("lib2.jar", "lib2.jar.value")
when:
succeeds ":util:resolve", ":app:resolve"
then:
output.count("files 1: [lib1.jar.value, lib2.jar.value]") == 2
output.count("Transformed") == 0
where:
type | value
"boolean" | "true"
"int" | "123"
"List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy