All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gradle.api.tasks.TaskParametersIntegrationTest.groovy Maven / Gradle / Ivy

There is a newer version: 8.11.1
Show newest version
/*
 * Copyright 2014 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.api.tasks

import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.TestBuildCache
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.integtests.fixtures.executer.GradleContextualExecuter
import org.gradle.internal.Actions
import org.gradle.internal.reflect.validation.ValidationMessageChecker
import org.gradle.test.precondition.Requires
import org.gradle.test.preconditions.IntegTestPreconditions
import spock.lang.Issue

import static org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache.Skip.INVESTIGATE

class TaskParametersIntegrationTest extends AbstractIntegrationSpec implements ValidationMessageChecker {

    def "reports which properties are not serializable"() {
        buildFile << """
            task foo {
                inputs.property "a", "hello"
                inputs.property "b", new Foo()
                outputs.file "foo.txt"
                doLast { file("foo.txt") << "" }
            }

            class Foo {
                int x
                String toString() { "xxx" }
            }
        """

        when:
        fails "foo"
        then:
        failure.assertHasDescription("Execution failed for task ':foo'.")
        failure.assertHasCause("Cannot fingerprint input property 'b': value 'xxx' cannot be serialized.")
    }

    @ToBeFixedForConfigurationCache(skip = INVESTIGATE)
    def "deals gracefully with not serializable contents of GStrings"() {
        buildFile << """
            task foo {
                inputs.property "a", "hello \${new Foo()}"
                outputs.file "foo.txt"
                doLast { file("foo.txt") << "" }
            }

            class Foo {
                int x
                String toString() { "xxx" }
            }
        """

        expect:
        run("foo").assertTaskNotSkipped(":foo")
        run("foo").assertTaskSkipped(":foo")
    }

    @Issue("https://issues.gradle.org/browse/GRADLE-3435")
    def "task is not up-to-date after file moved between input properties"() {
        (1..3).each {
            file("input${it}.txt").createNewFile()
        }
        file("buildSrc/src/main/groovy/TaskWithTwoFileCollectionInputs.groovy") << """
            import org.gradle.api.*
            import org.gradle.api.file.*
            import org.gradle.api.tasks.*

            class TaskWithTwoFileCollectionInputs extends DefaultTask {
                @InputFiles FileCollection inputs1
                @InputFiles FileCollection inputs2

                @OutputDirectory File output = project.buildDir

                @TaskAction void action() {}
            }
        """

        buildFile << """
            task test(type: TaskWithTwoFileCollectionInputs) {
                inputs1 = files("input1.txt", "input2.txt")
                inputs2 = files("input3.txt")
            }
        """

        when:
        succeeds "test"

        then:
        executedAndNotSkipped ':test'

        when:
        succeeds "test"

        then:
        skipped ':test'

        // Keep the same files, but move one of them to the other property
        buildFile << """
            test {
                inputs1 = files("input1.txt")
                inputs2 = files("input2.txt", "input3.txt")
            }
        """

        when:
        succeeds "test", "--info"

        then:
        executedAndNotSkipped ':test'
        outputContains "Input property 'inputs1' file ${file("input2.txt")} has been removed."
        outputContains "Input property 'inputs2' file ${file("input2.txt")} has been added."

        when:
        succeeds "test"

        then:
        skipped ':test'
    }

    @Issue("https://issues.gradle.org/browse/GRADLE-3435")
    def "task is not up-to-date after swapping directories between output properties"() {
        file("buildSrc/src/main/groovy/TaskWithTwoOutputDirectoriesProperties.groovy") << """
            import org.gradle.api.*
            import org.gradle.api.tasks.*

            class TaskWithTwoOutputDirectoriesProperties extends DefaultTask {
                @InputFiles def inputFiles = project.layout.files()

                @OutputDirectory File outputs1
                @OutputDirectory File outputs2

                @TaskAction void action() {
                    new File(outputs1, "output1.txt").text = "output1"
                    new File(outputs2, "output2.txt").text = "output2"
                }
            }
        """

        buildFile << """
            task test(type: TaskWithTwoOutputDirectoriesProperties) {
                outputs1 = file("\$buildDir/output1")
                outputs2 = file("\$buildDir/output2")
            }
        """

        when:
        succeeds "test"

        then:
        executedAndNotSkipped ':test'

        when:
        succeeds "test"

        then:
        skipped ':test'

        // Keep the same files, but move one of them to the other property
        buildFile.delete()
        buildFile << """
            task test(type: TaskWithTwoOutputDirectoriesProperties) {
                outputs1 = file("\$buildDir/output2")
                outputs2 = file("\$buildDir/output1")
            }
        """

        when:
        succeeds "test", "--info"

        then:
        executedAndNotSkipped ':test'
        outputContains "Output property 'outputs1' file ${file("build/output1")} has been removed."
        outputContains "Output property 'outputs1' file ${file("build/output1/output1.txt")} has been removed."
        outputContains "Output property 'outputs2' file ${file("build/output2")} has been removed."

        when:
        succeeds "test"

        then:
        skipped ':test'
    }

    def "can annotate Map property with @OutputDirectories and @OutputFiles"() {
        file("buildSrc/src/main/groovy/TaskWithOutputFilesProperty.groovy") << """
            import org.gradle.api.*
            import org.gradle.api.tasks.*

            class TaskWithValidOutputFilesAndOutputDirectoriesProperty extends DefaultTask {
                @InputFiles def inputFiles = project.layout.files()
                @OutputFiles Map outputFiles = [:]
                @OutputDirectories Map outputDirs = [:]
                @TaskAction void action() {}
            }
        """

        buildFile << """
            task test(type: TaskWithValidOutputFilesAndOutputDirectoriesProperty) {
            }
        """

        expect:
        succeeds "test"
    }

    def "fails when input file calls are chained (properties(Map))"() {
        buildFile << """
            task test {
                inputs.properties(input: 1).properties(input2: 2)
            }
        """

        expect:
        fails "test"
        failureCauseContains "Chaining of the TaskInputs.properties(Map) method is not supported since Gradle 5.0."
    }

    def "task depends on other task whose outputs are its inputs"() {
        buildFile << """
            task a {
                outputs.file 'a.txt'
                def outputFile = file('a.txt')
                doLast {
                    outputFile << "Data"
                }
            }

            task b {
                inputs.files tasks.a.outputs.files
            }
        """

        expect:
        succeeds "b" assertTasksExecutedInOrder ":a", ":b"
    }

    def "task is out of date when property added"() {
        buildFile << """
task someTask {
    inputs.property("a", "value1")
    outputs.file "out"
    doLast org.gradle.internal.Actions.doNothing() // attach an action that is not defined by the build script
}
"""
        given:
        succeeds "someTask"

        when:
        run "someTask"

        then:
        skipped(":someTask")

        // add property
        when:
        buildFile << """
someTask.inputs.property("b", 12)
"""
        and:
        executer.withArgument("-i")
        run "someTask"

        then:
        executedAndNotSkipped(":someTask")
        outputContains("Input property 'b' has been added for task ':someTask'")

        when:
        run "someTask"

        then:
        skipped(":someTask")
    }

    def "task is out of date when property removed"() {
        buildFile << """
task someTask {
    inputs.property("a", "value1")
    inputs.property("b", "value2")
    outputs.file "out"
    doLast ${Actions.name}.doNothing() // attach an action that is not defined by the build script
}
"""
        given:
        succeeds "someTask"

        when:
        run "someTask"

        then:
        skipped(":someTask")

        // add property
        when:
        buildFile.text = """
task someTask {
    inputs.property("b", "value2")
    outputs.file "out"
    doLast ${Actions.name}.doNothing()
}
"""
        and:
        executer.withArgument("-i")
        run "someTask"

        then:
        executedAndNotSkipped(":someTask")
        outputContains("Input property 'a' has been removed for task ':someTask'")

        when:
        run "someTask"

        then:
        skipped(":someTask")
    }

    def "task is out of date when property type changes #oldValue -> #newValue"() {
        buildFile << """
task someTask {
    inputs.property("a", $oldValue).optional(true)
    outputs.file "out"
    doLast ${Actions.name}.doNothing() // attach an action that is not defined by the build script
}
"""
        given:
        succeeds "someTask"

        when:
        run "someTask"

        then:
        skipped(":someTask")

        // change property type
        when:
        buildFile.replace(oldValue, newValue)

        and:
        executer.withArgument("-i")
        run "someTask"

        then:
        executedAndNotSkipped(":someTask")
        outputContains("Value of input property 'a' has changed for task ':someTask'")

        when:
        run "someTask"

        then:
        skipped(":someTask")

        where:
        oldValue             | newValue
        "'value1'"           | "['value1']"
        "'value1'"           | "null"
        "null"               | "123"
        "[123]"              | "123"
        "false"              | "12.3"
        "[345, 'hi'] as Set" | "[345, 'hi']"
        "file('1')"          | "files('1')"
        "123"                | "123L"
        "123"                | "123 as short"
    }

    @Requires(IntegTestPreconditions.IsEmbeddedExecutor)
    // this test only works in embedded mode because of the use of validation test fixtures
    def "invalid task causes VFS to drop"() {
        buildFile << """
            import org.gradle.integtests.fixtures.validation.ValidationProblem

            class InvalidTask extends DefaultTask {
                @ValidationProblem inputFile

                @TaskAction void execute() {
                    println "Executed"
                }
            }

            task invalid(type: InvalidTask)
        """

        expectThatExecutionOptimizationDisabledWarningIsDisplayed(executer, dummyValidationProblem('InvalidTask', 'inputFile'), 'id', 'section')

        when:
        run "invalid", "--info"
        then:
        executedAndNotSkipped(":invalid")
        outputContains("Invalidating VFS because task ':invalid' failed validation")
    }

    @Requires(IntegTestPreconditions.IsEmbeddedExecutor)
    // this test only works in embedded mode because of the use of validation test fixtures
    def "validation warnings are displayed once"() {
        buildFile << """
            import org.gradle.integtests.fixtures.validation.ValidationProblem

            class InvalidTask extends DefaultTask {
                @ValidationProblem File inputFile

                @TaskAction void execute() {
                    println "Executed"
                }
            }

            task invalid(type: InvalidTask)
        """

        expectThatExecutionOptimizationDisabledWarningIsDisplayed(executer, dummyValidationProblem('InvalidTask', 'inputFile'), 'id', 'section')

        when:
        run "invalid"
        then:
        executedAndNotSkipped(":invalid")
        output.count("- Type 'InvalidTask' property 'inputFile' test problem. Reason: This is a test.") == 1
    }

    @Requires(IntegTestPreconditions.IsEmbeddedExecutor)
    // this test only works in embedded mode because of the use of validation test fixtures
    def "validation warnings are reported even when task is skipped"() {
        buildFile << """
            import org.gradle.integtests.fixtures.validation.ValidationProblem

            class InvalidTask extends SourceTask {
                @ValidationProblem File inputFile

                @TaskAction void execute() {
                    println "Executed"
                }
            }

            task invalid(type: InvalidTask)
        """

        expectThatExecutionOptimizationDisabledWarningIsDisplayed(executer, dummyValidationProblem('InvalidTask', 'inputFile'), 'id', 'section')

        when:
        run "invalid"
        then:
        skipped(":invalid")
    }

    def "task can use input property of type #type"() {
        file("buildSrc/src/main/java/SomeTask.java") << """
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.Optional;
import java.io.File;

public class SomeTask extends DefaultTask {
    private $type v;
    @Input
    public $type getV() { return v; }
    void setV($type v) { this.v = v; }

    File d;
    @OutputDirectory
    public File getD() { return d; }

    @TaskAction
    public void go() { }
}
"""

        buildFile << """
task someTask(type: SomeTask) {
    v = $initialValue
    d = file("build/out")
}
"""
        def isError = expectedValidationProblem != null

        when:
        if (isError) {
            fails 'someTask'
        } else {
            succeeds "someTask"
        }

        then:
        if (isError) {
            failure.error.contains(expectedValidationProblem)
        }

        when:
        buildFile.replace("v = $initialValue", "v = $newValue")
        executer.withArgument("--info")
        if (isError) {
            fails 'someTask'
        } else {
            succeeds "someTask"
        }

        then:
        if (isError) {
            failure.error.contains(expectedValidationProblem)
        } else {
            outputContains("Value of input property 'v' has changed for task ':someTask'")
        }

        and:
        if (isError) {
            fails 'someTask'
        } else {
            succeeds "someTask"
        }

        where:
        type                                  | initialValue                                          | newValue                                                     | expectedValidationProblem
        "String"                              | "'value 1'"                                           | "'value 2'"                                                  | null
        "java.io.File"                        | "file('file1')"                                       | "file('file2')"                                              | incorrectUseOfInputAnnotation { type('SomeTask') property('v') propertyType('File') }
        "boolean"                             | "true"                                                | "false"                                                      | null
        "Boolean"                             | "Boolean.TRUE"                                        | "Boolean.FALSE"                                              | null
        "int"                                 | "123"                                                 | "-45"                                                        | null
        "Integer"                             | "123"                                                 | "-45"                                                        | null
        "long"                                | "123"                                                 | "-45"                                                        | null
        "Long"                                | "123"                                                 | "-45"                                                        | null
        "short"                               | "123"                                                 | "-45"                                                        | null
        "Short"                               | "123"                                                 | "-45"                                                        | null
        "java.math.BigDecimal"                | "12.3"                                                | "-45.432"                                                    | null
        "java.math.BigInteger"                | "12"                                                  | "-45"                                                        | null
        "java.util.List"              | "['value1', 'value2']"                                | "['value1']"                                                 | null
        "java.util.List"              | "[]"                                                  | "['value1', null, false, 123, 12.4, ['abc'], [true] as Set]" | null
        "String[]"                            | "new String[0]"                                       | "['abc'] as String[]"                                        | null
        "Object[]"                            | "[123, 'abc'] as Object[]"                            | "['abc'] as String[]"                                        | null
        "java.util.Collection"        | "['value1', 'value2']"                                | "['value1'] as SortedSet"                                    | null
        "java.util.Set"               | "['value1', 'value2'] as Set"                         | "['value1'] as Set"                                          | null
        "Iterable"              | "[file('1'), file('2')] as Set"                       | "files('1')"                                                 | null
        FileCollection.name                   | "files('1', '2')"                                     | "configurations.create('empty')"                             | incorrectUseOfInputAnnotation { type('SomeTask') property('v') propertyType('FileCollection') }
        "java.util.Map"      | "[a: true, b: false]"                                 | "[a: true, b: true]"                                         | null
        "${Provider.name}"            | "providers.provider { 'a' }"                          | "providers.provider { 'b' }"                                 | null
        "${Property.name}"            | "objects.property(String); v.set('abc')"              | "objects.property(String); v.set('123')"                     | "Type 'SomeTask': property 'v' of mutable type 'org.gradle.api.provider.Property' is writable. Properties of type 'org.gradle.api.provider.Property' are already mutable. Possible solution: Remove the 'setV' method. ${learnAt("validation_problems", "mutable_type_with_setter")}."
        "${ListProperty.name}"        | "objects.listProperty(String); v.set(['abc'])"        | "objects.listProperty(String); v.set(['123'])"               | null
        "${SetProperty.name}"         | "objects.setProperty(String); v.set(['abc'])"         | "objects.setProperty(String); v.set(['123'])"                | null
        "${MapProperty.name}" | "objects.mapProperty(String, Number); v.set([a: 12])" | "objects.mapProperty(String, Number); v.set([a: 10])"        | null
    }

    def "null input properties registered via TaskInputs.property are not allowed"() {
        expectReindentedValidationMessage()
        buildFile << """
            task test {
                inputs.property("input", { null })
                doLast {}
            }
        """
        expect:
        fails "test"
        failure.assertHasDescription("A problem was found with the configuration of task ':test' (type 'DefaultTask').")
        failureDescriptionContains(missingValueMessage { property('input') })
    }

    def "optional null input properties registered via TaskInputs.property are allowed"() {
        buildFile << """
            task test {
                inputs.property("input", { null }).optional(true)
                doLast {}
            }
        """
        expect:
        succeeds "test"
    }

    @ToBeFixedForConfigurationCache(skip = INVESTIGATE)
    def "null input files registered via TaskInputs.#method are not allowed"() {
        expectReindentedValidationMessage()
        buildFile << """
            task test {
                inputs.${method}({ null }) withPropertyName "input"
                doLast {}
            }
        """
        expect:
        fails "test"
        failure.assertHasDescription("A problem was found with the configuration of task ':test' (type 'DefaultTask').")
        failureDescriptionContains(missingValueMessage { property('input') })

        where:
        method << ["file", "files", "dir"]
    }

    def "optional null input files registered via TaskInputs.#method are allowed"() {
        buildFile << """
            task test {
                inputs.${method}({ null }) withPropertyName "input" optional(true)
                doLast {}
            }
        """
        expect:
        succeeds "test"

        where:
        method << ["file", "files", "dir"]
    }

    @ToBeFixedForConfigurationCache(skip = INVESTIGATE)
    def "null output files registered via TaskOutputs.#method are not allowed"() {
        expectReindentedValidationMessage()
        buildFile << """
            task test {
                outputs.${method}({ null }) withPropertyName "output"
                doLast {}
            }
        """
        expect:
        fails "test"
        failure.assertHasDescription("A problem was found with the configuration of task ':test' (type 'DefaultTask').")
        failureDescriptionContains(missingValueMessage { property('output') })

        where:
        method << ["file", "files", "dir", "dirs"]
    }

    def "optional null output files registered via TaskOutputs.#method are allowed"() {
        buildFile << """
            task test {
                outputs.${method}({ null }) withPropertyName "output" optional(true)
                doLast {}
            }
        """
        expect:
        succeeds "test"

        where:
        method << ["file", "files", "dir", "dirs"]
    }

    def "missing input files registered via TaskInputs.#method are not allowed"() {
        expectReindentedValidationMessage()
        buildFile << """
            task test {
                inputs.${method}({ "missing" }) withPropertyName "input"
                doLast {}
            }
        """

        expect:
        def missingFile = file('missing')
        fails "test"
        failure.assertHasDescription("A problem was found with the configuration of task ':test' (type 'DefaultTask').")
        failureDescriptionContains(inputDoesNotExist {
            property('input')
                .kind(fileType)
                .missing(missingFile)
                .includeLink()
        })

        where:
        method | fileType
        "file" | "File"
        "dir"  | "Directory"
    }

    def "wrong input file type registered via TaskInputs.#method is not allowed"() {
        enableProblemsApiCheck()
        expectReindentedValidationMessage()
        file("input-file.txt").touch()

        def inputDir = file("input-dir")
        inputDir.createDir()
        buildFile """
            task test {
                inputs.${method}({ "$path" }) withPropertyName "input"
                doLast {}
            }
        """

        expect:
        def unexpected = file(path)
        fails "test"
        failure.assertHasDescription("A problem was found with the configuration of task ':test' (type 'DefaultTask').")
        failureDescriptionContains(unexpectedInputType {
            property('input')
                .kind(fileType)
                .missing(unexpected)
                .includeLink()
        })

        verifyAll(receivedProblem(0)) {
            fqid == 'validation:property-validation:unexpected-input-file-type'
            contextualLabel == "Property \'input\' $fileType \'${unexpected.absolutePath}\' is not a $fileType"
            details == "Expected an input to be a $fileType but it was a ${getOppositeKind(fileType)}"
            solutions == [
                "Use a $fileType as an input",
                "Declare the input as a ${getOppositeKind(fileType)} instead",
            ].collect { it.toString() }
            additionalData.asMap == [
                'typeName': 'org.gradle.api.DefaultTask',
                'propertyName': 'input',
            ]
        }

        where:
        method | path             | fileType
        "file" | "input-dir"      | "file"
        "dir"  | "input-file.txt" | "directory"
    }

    def "wrong output file type registered via TaskOutputs.#method is not allowed (files)"() {
        enableProblemsApiCheck()
        expectReindentedValidationMessage()
        def outputDir = file("output-dir")
        outputDir.createDir()
        buildFile << """
            task test {
                outputs.${method}({ "$path" }) withPropertyName "output"
                doLast {}
            }
        """

        expect:
        fails "test"
        failureDescriptionContains(cannotWriteFileToDirectory {
            property('output')
                .file(outputDir)
                .isNotFile()
                .includeLink()
        })

        verifyAll(receivedProblem(0)) {
            fqid == 'validation:property-validation:cannot-write-output'
            contextualLabel == "Property \'output\' is not writable because \'${outputDir.absolutePath}\' is not a file"
            details == 'Cannot write a file to a location pointing at a directory'
            solutions == [
                'Configure \'output\' to point to a file, not a directory',
                'Annotate \'output\' with @OutputDirectory instead of @OutputFiles',
            ]
            additionalData.asMap == [
                'typeName' : 'org.gradle.api.DefaultTask',
                'propertyName' : 'output',
            ]
        }
        where:
        method  | path
        "file"  | "output-dir"
        "files" | "output-dir"
    }

    def "wrong output file type registered via TaskOutputs.#method is not allowed (directories)"() {
        enableProblemsApiCheck()
        expectReindentedValidationMessage()
        def outputFile = file("output-file.txt")
        outputFile.touch()
        buildFile << """
            task test {
                outputs.${method}({ "$path" }) withPropertyName "output"
                doLast {}
            }
        """

        expect:
        fails "test"
        failureDescriptionContains(cannotWriteToDir {
            property('output')
                .dir(outputFile)
                .isNotDirectory()
                .includeLink()
        })

        verifyAll(receivedProblem(0)) {
            fqid == 'validation:property-validation:cannot-write-output'
            contextualLabel == "Property \'output\' is not writable because \'${outputFile.absolutePath}\' is not a directory"
            details == "Expected \'${outputFile.absolutePath}\' to be a directory but it\'s a file"
            solutions == [ 'Make sure that the \'output\' is configured to a directory' ]
            additionalData.asMap == [
                'typeName' : 'org.gradle.api.DefaultTask',
                'propertyName' : 'output',
            ]
        }
        where:
        method | path
        "dir"  | "output-file.txt"
        "dirs" | "output-file.txt"
    }

    @Issue("https://github.com/gradle/gradle/issues/15679")
    def "fileTrees with regular file roots cannot be used as output files"() {
        enableProblemsApiCheck()
        expectReindentedValidationMessage()
        buildScript """
            task myTask {
                inputs.file file('input.txt')
                outputs.files(files('build/output.txt').asFileTree).withPropertyName('output')
                doLast {
                    file('build/output.txt').text = new File('input.txt').text
                }
            }
        """.stripIndent()


        def outputFile = file('build/output.txt')
        outputFile.text = "pre-existing"
        file('input.txt').text = 'input file'

        expect:
        fails('myTask')
        failureDescriptionContains(cannotCreateRootOfFileTree {
            property('output')
                .dir(outputFile)
                .includeLink()
        })

        verifyAll(receivedProblem(0)) {
            fqid == 'validation:property-validation:cannot-write-output'
            contextualLabel == "Property \'output\' is not writable because \'${outputFile.absolutePath}\' is not a directory"
            details == "Expected the root of the file tree \'${outputFile.absolutePath}\' to be a directory but it\'s a file"
            solutions == [ 'Make sure that the root of the file tree \'output\' is configured to a directory' ]
            additionalData.asMap == [
                'typeName' : 'org.gradle.api.DefaultTask',
                'propertyName' : 'output',
            ]
        }
    }

    def "can specify null as an input property in ad-hoc task"() {
        buildFile << """
            task foo {
                inputs.property("a", null).optional(true)
                doLast {}
            }
        """

        expect:
        succeeds "foo"
    }

    @Issue("https://github.com/gradle/gradle/issues/3366")
    def "can specify null as an input property in Java task"() {
        file("buildSrc/src/main/java/Foo.java") << """
            import org.gradle.api.DefaultTask;
            import org.gradle.api.tasks.TaskAction;

            public class Foo extends DefaultTask {
                public Foo() {
                    getInputs().property("a", null).optional(true);
                }

                @TaskAction
                public void doSomething() {}
            }
        """

        buildFile << """
            task foo(type: Foo)
        """

        expect:
        succeeds "foo"
    }

    def "reports the input property which failed to evaluate"() {
        buildFile("""
            abstract class FailingTask extends DefaultTask {
                @Input
                abstract Property getStringInput()

                @TaskAction
                void doStuff() {
                    println("Hello world")
                }
            }

            tasks.register("failingTask", FailingTask) {
                stringInput.set(provider { throw new RuntimeException("BOOM") })
            }
        """)

        when:
        fails "failingTask"

        then:
        if (GradleContextualExecuter.isConfigCache()) {
            failureDescriptionContains("Configuration cache state could not be cached: field `__stringInput__` of task `:failingTask` of type `FailingTask`: error writing value of type 'org.gradle.api.internal.provider.DefaultProperty'")
        } else {
            failureHasCause("Failed to calculate the value of task ':failingTask' property 'stringInput'.")
        }
        failureHasCause("BOOM")
    }

    @ToBeFixedForConfigurationCache
    def "input and output properties are not evaluated too often"() {
        buildFile << """
            import org.gradle.api.services.BuildServiceParameters

            abstract class EvaluationCountBuildService implements BuildService {
                int outputFileCount = 0
                int inputFileCount = 0
                int inputValueCount = 0
                int nestedInputCount = 0
                int nestedInputValueCount = 0

                void outputFile() {
                    outputFileCount++
                }

                void inputFile() {
                    inputFileCount++
                }

                void inputValue() {
                    inputValueCount++
                }

                void nestedInput() {
                    nestedInputCount++
                }

                void nestedInputValue() {
                    nestedInputValueCount++
                }
            }
            def evaluationCount = project.getGradle().getSharedServices().registerIfAbsent("evaluationCount", EvaluationCountBuildService) {}

            @CacheableTask
            abstract class CustomTask extends DefaultTask {

                @Inject
                abstract ProjectLayout getLayout()

                @Internal
                abstract Property getEvaluationCountService()

                private NestedBean bean = new NestedBean()

                @OutputFile
                File getOutputFile() {
                    count("outputFile")
                    return layout.buildDirectory.file("foo.bar").get().asFile
                }

                @InputFile
                @PathSensitive(PathSensitivity.NONE)
                File getInputFile() {
                    count("inputFile")
                    return layout.projectDirectory.file("input.txt").asFile
                }

                @Input
                String getInput() {
                    count("inputValue")
                    return "Input"
                }

                @Nested
                Object getBean() {
                    count("nestedInput")
                    return bean
                }

                @TaskAction
                void doStuff() {
                    outputFile.text = inputFile.text
                }

                void count(String name) {
                    def service = evaluationCountService.get()
                    service."\${name}"()
                    def currentValue = service."\${name}Count"
                    println "Evaluating \${name} \${currentValue}"
                }

                class NestedBean {
                    @Input getFirst() {
                        count("nestedInputValue")
                        return "first"
                    }

                    @Input getSecond() {
                        return "second"
                    }
                }
            }

            task myTask(type: CustomTask) {
                evaluationCountService = evaluationCount
            }

            task printCounts {
                dependsOn myTask
                doLast {
                    println(['outputFileCount', 'inputFileCount', 'inputValueCount', 'nestedInputCount', 'nestedInputValueCount'].collect { name ->
                        def actualCount = evaluationCount.get()."\$name"
                        return "\$name = \$actualCount"
                    }.join(", "))
                }
            }
        """
        def inputFile = file('input.txt')
        inputFile.text = "input"
        def localCache = new TestBuildCache(file('cache-dir'))
        settingsFile << localCache.localCacheConfiguration()

        when:
        succeeds("printCounts")
        then:
        executedAndNotSkipped(':myTask')
        outputContains("outputFileCount = 2, inputFileCount = 3, inputValueCount = 1, nestedInputCount = 3, nestedInputValueCount = 1")

        when:
        inputFile.text = "changed"
        withBuildCache().succeeds("printCounts")
        then:
        executedAndNotSkipped(':myTask')
        outputContains("outputFileCount = 2, inputFileCount = 3, inputValueCount = 1, nestedInputCount = 3, nestedInputValueCount = 1")

        when:
        succeeds("printCounts")
        then:
        skipped(':myTask')
        outputContains("outputFileCount = 1, inputFileCount = 2, inputValueCount = 1, nestedInputCount = 3, nestedInputValueCount = 1")

        when:
        file('build').deleteDir()
        withBuildCache().succeeds("printCounts")
        then:
        skipped(':myTask')
        outputContains("outputFileCount = 1, inputFileCount = 2, inputValueCount = 1, nestedInputCount = 3, nestedInputValueCount = 1")
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy