<commit_before>/*
 * 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.normalization

import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.integtests.fixtures.UnsupportedWithConfigurationCache
import org.gradle.test.fixtures.file.TestFile
import spock.lang.Unroll

@Unroll
class ConfigureRuntimeClasspathNormalizationIntegrationTest extends AbstractIntegrationSpec {

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore files on runtime classpath in #tree (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(useRuntimeApi).withFilesIgnored()

        def ignoredResource = project[ignoredResourceName]
        def notIgnoredResource = project[notIgnoredResourceName]

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeContents()
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        notIgnoredResource.changeContents()
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        ignoredResource.remove()
        succeeds project.customTask

        then:
        skipped(project.customTask)

        when:
        ignoredResource.add()
        succeeds project.customTask

        then:
        skipped(project.customTask)

        where:
        tree                 | ignoredResourceName               | notIgnoredResourceName               | useRuntimeApi
        'directories'        | 'ignoredResourceInDirectory'      | 'notIgnoredResourceInDirectory'      | true
        'jars'               | 'ignoredResourceInJar'            | 'notIgnoredResourceInJar'            | true
        'nested jars'        | 'ignoredResourceInNestedJar'      | 'notIgnoredResourceInNestedJar'      | true
        'nested in dir jars' | 'ignoredResourceInNestedInDirJar' | 'notIgnoredResourceInNestedInDirJar' | true
        'directories'        | 'ignoredResourceInDirectory'      | 'notIgnoredResourceInDirectory'      | false
        'jars'               | 'ignoredResourceInJar'            | 'notIgnoredResourceInJar'            | false
        'nested jars'        | 'ignoredResourceInNestedJar'      | 'notIgnoredResourceInNestedJar'      | false
        'nested in dir jars' | 'ignoredResourceInNestedInDirJar' | 'notIgnoredResourceInNestedInDirJar' | false
    }

    def "can ignore manifest attributes on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestAttributesIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore entire manifest on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore all meta-inf files on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withAllMetaInfIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        project.jarManifestProperties.replaceContents("implementation-version=1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore manifest properties on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestPropertiesIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifestProperties.replaceContents("implementation-version=1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can configure ignore rules per project (using runtime API: #useRuntimeApi)"() {
        def projectWithIgnores = new ProjectWithRuntimeClasspathNormalization('a', useRuntimeApi).withFilesIgnored()
        def projectWithoutIgnores = new ProjectWithRuntimeClasspathNormalization('b', useRuntimeApi)
        def allProjects = [projectWithoutIgnores, projectWithIgnores]
        settingsFile << "include 'a', 'b'"

        when:
        succeeds(*allProjects*.customTask)
        then:
        executedAndNotSkipped(*allProjects*.customTask)

        when:
        projectWithIgnores.ignoredResourceInJar.changeContents()
        projectWithoutIgnores.ignoredResourceInJar.changeContents()
        succeeds(*allProjects*.customTask)
        then:
        skipped(projectWithIgnores.customTask)
        executedAndNotSkipped(projectWithoutIgnores.customTask)

        where:
        useRuntimeApi << [true, false]
    }

    @UnsupportedWithConfigurationCache(because = "Task.getProject() during execution")
    def "runtime classpath normalization to #change cannot be changed after first usage (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(useRuntimeApi)
        project.buildFile << """
            task configureNormalization() {
                dependsOn '${project.customTask}'
                doLast {
                    project.normalization {
                        runtimeClasspath {
                            ${config}
                        }
                    }
                }
            }
        """.stripIndent()

        when:
        fails 'configureNormalization'

        then:
        failureHasCause 'Cannot configure runtime classpath normalization after execution started.'

        where:
        change                      | config                                                    | useRuntimeApi
        'ignore file'               | "ignore '**/some-other-file.txt'"                         | true
        'ignore file'               | "ignore '**/some-other-file.txt'"                         | false
        'ignore manifest attribute' | "metaInf { ignoreAttribute 'Implementation-version' }"    | true
        'ignore manifest attribute' | "metaInf { ignoreAttribute 'Implementation-version' }"    | false
        'ignore property'           | "properties { ignoreProperty 'timestamp' }"               | true
        'ignore property'           | "properties { ignoreProperty 'timestamp' }"               | false
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties on runtime classpath in #tree (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withPropertiesIgnored()

        def ignoredResource = project[ignoredResourceName]

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeProperty('ignore-me', 'please ignore me')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeProperty('dont-ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        where:
        tree                 | ignoredResourceName              | useRuntimeApi
        'directories'        | 'propertiesFileInDir'            | true
        'jars'               | 'propertiesFileInJar'            | true
        'nested jars'        | 'propertiesFileInNestedJar'      | true
        'nested in dir jars' | 'propertiesFileInNestedInDirJar' | true
        'directories'        | 'propertiesFileInDir'            | false
        'jars'               | 'propertiesFileInJar'            | false
        'nested jars'        | 'propertiesFileInNestedJar'      | false
        'nested in dir jars' | 'propertiesFileInNestedInDirJar' | false
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties in selected files"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties in selected files defined in multiple rules"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'this should also be ignored')
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                    properties('some/path/to/foo.properties') {
                        ignoreProperty 'ignore-me-too'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'please ignore me too')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can add rules to the default properties rule"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'this should also be ignored')
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                    properties {
                        ignoreProperty 'ignore-me-too'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'please ignore me too')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    class ProjectWithRuntimeClasspathNormalization {
        final TestFile root
        TestResource ignoredResourceInDirectory
        TestResource notIgnoredResourceInDirectory
        TestResource ignoredResourceInJar
        TestResource ignoredResourceInNestedJar
        TestResource ignoredResourceInNestedInDirJar
        TestResource notIgnoredResourceInJar
        TestResource notIgnoredResourceInNestedJar
        TestResource notIgnoredResourceInNestedInDirJar
        TestResource jarManifest
        TestResource jarManifestProperties
        PropertiesResource propertiesFileInDir
        PropertiesResource propertiesFileInJar
        PropertiesResource propertiesFileInNestedJar
        PropertiesResource propertiesFileInNestedInDirJar
        TestFile libraryJar
        TestFile nestedJar
        TestFile nestedInDirJar
        private TestFile libraryJarContents
        private TestFile nestedJarContents
        private TestFile nestedInDirJarContents
        private final String projectName
        final TestFile buildFile

        ProjectWithRuntimeClasspathNormalization(String projectName = null, boolean useRuntimeApi) {
            this.projectName = projectName
            this.root = projectName ? file(projectName) : temporaryFolder.testDirectory

            buildFile = root.file('build.gradle') << """
                apply plugin: 'base'
            """

            buildFile << declareCustomTask(useRuntimeApi)

            nestedInDirJarContents = root.file('nestedInDirJarContents').create {
                ignoredResourceInNestedInDirJar = new TestResource(file('another/package/ignored.txt') << "This should be ignored", this.&createNestedInDirJar)
                notIgnoredResourceInNestedInDirJar = new TestResource(file('another/package/not-ignored.txt') << "This should not be ignored", this.&createNestedInDirJar)
                propertiesFileInNestedInDirJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createNestedInDirJar)
            }
            root.file('classpath/dirEntry').create {
                ignoredResourceInDirectory = new TestResource(file("ignored.txt") << "This should be ignored")
                notIgnoredResourceInDirectory = new TestResource(file("not-ignored.txt") << "This should not be ignored")
                nestedInDirJar = file('nestedInDir.jar')
                propertiesFileInDir = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'])
            }
            nestedJarContents = root.file('libraryContents').create {
                ignoredResourceInNestedJar = new TestResource(file('some/package/ignored.txt') << "This should be ignored", this.&createJar)
                notIgnoredResourceInNestedJar = new TestResource(file('some/package/not-ignored.txt') << "This should not be ignored", this.&createJar)
                propertiesFileInNestedJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createJar)
            }
            libraryJarContents = root.file('libraryContents').create {
                jarManifest = new TestResource(file('META-INF/MANIFEST.MF') << "Manifest-Version: 1.0\nImplementation-Version: 1.0.0", this.&createJar)
                jarManifestProperties = new TestResource(file('META-INF/build-info.properties') << "implementation-version=1.0.0", this.&createJar)
                ignoredResourceInJar = new TestResource(file('some/package/ignored.txt') << "This should be ignored", this.&createJar)
                notIgnoredResourceInJar = new TestResource(file('some/package/not-ignored.txt') << "This should not be ignored", this.&createJar)
                nestedJar = file('nested.jar')
                propertiesFileInJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createJar)
            }
            libraryJar = root.file('library.jar')
            createJar()
            createNestedInDirJar()
        }

        String declareCustomTask(boolean useRuntimeApi) {
            if (useRuntimeApi) {
                return """
                    task customTask {
                        def outputFile = file("\$temporaryDir/output.txt")
                        inputs.files("classpath/dirEntry", "library.jar")
                            .withPropertyName("classpath")
                            .withNormalizer(ClasspathNormalizer)
                        outputs.file(outputFile)
                            .withPropertyName("outputFile")

                        doLast {
                            outputFile.text = "done"
                        }
                    }
                """
            } else {
                return """
                    class CustomTask extends DefaultTask {
                        @OutputFile File outputFile = new File(temporaryDir, "output.txt")
                        @Classpath FileCollection classpath = project.layout.files("classpath/dirEntry", "library.jar")

                        @TaskAction void generate() {
                            outputFile.text = "done"
                        }
                    }

                    task customTask(type: CustomTask)
                """
            }
        }

        void createJar() {
            if (nestedJar.exists()) {
                nestedJar.delete()
            }
            nestedJarContents.zipTo(nestedJar)
            if (libraryJar.exists()) {
                libraryJar.delete()
            }
            libraryJarContents.zipTo(libraryJar)
        }

        void createNestedInDirJar() {
            if (nestedInDirJar.exists()) {
                nestedInDirJar.delete()
            }
            nestedInDirJarContents.zipTo(nestedInDirJar)
        }

        ProjectWithRuntimeClasspathNormalization withFilesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        ignore "**/ignored.txt"
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withAllMetaInfIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreCompletely()
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreManifest()
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestAttributesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreAttribute "Implementation-Version"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestPropertiesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreProperty "implementation-version"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withPropertiesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        properties {
                            ignoreProperty "ignore-me"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        String getCustomTask() {
            return "${projectName ? ":${projectName}" : ''}:customTask"
        }
    }

    class TestResource {
        final TestFile backingFile
        private final Closure onChange

        TestResource(TestFile backingFile, Closure onChange = {}) {
            this.backingFile = backingFile
            this.onChange = onChange
        }

        void replaceContents(String contents) {
            backingFile.withWriter { w ->
                w << contents
            }
            changed()
        }

        void changeContents() {
            backingFile << "More changes"
            changed()
        }

        void remove() {
            assert backingFile.delete()
            changed()
        }

        void add() {
            backingFile << "First creation of file"
            changed()
        }

        void changed() {
            onChange.call()
        }
    }

    class PropertiesResource extends TestResource {
        PropertiesResource(TestFile backingFile, Map<String, String> initialProps, Closure finalizedBy={}) {
            super(backingFile, finalizedBy)
            Properties props = new Properties()
            props.putAll(initialProps)
            props.store(backingFile.newOutputStream(), "")
        }

        PropertiesResource changeProperty(String key, String value) {
            Properties props = new Properties()
            props.load(backingFile.newInputStream())
            props.setProperty(key, value)
            props.store(backingFile.newOutputStream(), "")
            changed()
            return this
        }
    }
}<commit_msg>Fix file leak issue in test<commit_after>/*
 * 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.normalization

import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache
import org.gradle.integtests.fixtures.UnsupportedWithConfigurationCache
import org.gradle.test.fixtures.file.TestFile
import spock.lang.Unroll

@Unroll
class ConfigureRuntimeClasspathNormalizationIntegrationTest extends AbstractIntegrationSpec {

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore files on runtime classpath in #tree (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(useRuntimeApi).withFilesIgnored()

        def ignoredResource = project[ignoredResourceName]
        def notIgnoredResource = project[notIgnoredResourceName]

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeContents()
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        notIgnoredResource.changeContents()
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        ignoredResource.remove()
        succeeds project.customTask

        then:
        skipped(project.customTask)

        when:
        ignoredResource.add()
        succeeds project.customTask

        then:
        skipped(project.customTask)

        where:
        tree                 | ignoredResourceName               | notIgnoredResourceName               | useRuntimeApi
        'directories'        | 'ignoredResourceInDirectory'      | 'notIgnoredResourceInDirectory'      | true
        'jars'               | 'ignoredResourceInJar'            | 'notIgnoredResourceInJar'            | true
        'nested jars'        | 'ignoredResourceInNestedJar'      | 'notIgnoredResourceInNestedJar'      | true
        'nested in dir jars' | 'ignoredResourceInNestedInDirJar' | 'notIgnoredResourceInNestedInDirJar' | true
        'directories'        | 'ignoredResourceInDirectory'      | 'notIgnoredResourceInDirectory'      | false
        'jars'               | 'ignoredResourceInJar'            | 'notIgnoredResourceInJar'            | false
        'nested jars'        | 'ignoredResourceInNestedJar'      | 'notIgnoredResourceInNestedJar'      | false
        'nested in dir jars' | 'ignoredResourceInNestedInDirJar' | 'notIgnoredResourceInNestedInDirJar' | false
    }

    def "can ignore manifest attributes on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestAttributesIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore entire manifest on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore all meta-inf files on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withAllMetaInfIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifest.replaceContents("Manifest-Version: 1.0\nImplementation-Version: 1.0.1")
        project.jarManifestProperties.replaceContents("implementation-version=1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore manifest properties on runtime classpath"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withManifestPropertiesIgnored()

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.jarManifestProperties.replaceContents("implementation-version=1.0.1")
        succeeds project.customTask
        then:
        skipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can configure ignore rules per project (using runtime API: #useRuntimeApi)"() {
        def projectWithIgnores = new ProjectWithRuntimeClasspathNormalization('a', useRuntimeApi).withFilesIgnored()
        def projectWithoutIgnores = new ProjectWithRuntimeClasspathNormalization('b', useRuntimeApi)
        def allProjects = [projectWithoutIgnores, projectWithIgnores]
        settingsFile << "include 'a', 'b'"

        when:
        succeeds(*allProjects*.customTask)
        then:
        executedAndNotSkipped(*allProjects*.customTask)

        when:
        projectWithIgnores.ignoredResourceInJar.changeContents()
        projectWithoutIgnores.ignoredResourceInJar.changeContents()
        succeeds(*allProjects*.customTask)
        then:
        skipped(projectWithIgnores.customTask)
        executedAndNotSkipped(projectWithoutIgnores.customTask)

        where:
        useRuntimeApi << [true, false]
    }

    @UnsupportedWithConfigurationCache(because = "Task.getProject() during execution")
    def "runtime classpath normalization to #change cannot be changed after first usage (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(useRuntimeApi)
        project.buildFile << """
            task configureNormalization() {
                dependsOn '${project.customTask}'
                doLast {
                    project.normalization {
                        runtimeClasspath {
                            ${config}
                        }
                    }
                }
            }
        """.stripIndent()

        when:
        fails 'configureNormalization'

        then:
        failureHasCause 'Cannot configure runtime classpath normalization after execution started.'

        where:
        change                      | config                                                    | useRuntimeApi
        'ignore file'               | "ignore '**/some-other-file.txt'"                         | true
        'ignore file'               | "ignore '**/some-other-file.txt'"                         | false
        'ignore manifest attribute' | "metaInf { ignoreAttribute 'Implementation-version' }"    | true
        'ignore manifest attribute' | "metaInf { ignoreAttribute 'Implementation-version' }"    | false
        'ignore property'           | "properties { ignoreProperty 'timestamp' }"               | true
        'ignore property'           | "properties { ignoreProperty 'timestamp' }"               | false
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties on runtime classpath in #tree (using runtime API: #useRuntimeApi)"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true).withPropertiesIgnored()

        def ignoredResource = project[ignoredResourceName]

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeProperty('ignore-me', 'please ignore me')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        ignoredResource.changeProperty('dont-ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        where:
        tree                 | ignoredResourceName              | useRuntimeApi
        'directories'        | 'propertiesFileInDir'            | true
        'jars'               | 'propertiesFileInJar'            | true
        'nested jars'        | 'propertiesFileInNestedJar'      | true
        'nested in dir jars' | 'propertiesFileInNestedInDirJar' | true
        'directories'        | 'propertiesFileInDir'            | false
        'jars'               | 'propertiesFileInJar'            | false
        'nested jars'        | 'propertiesFileInNestedJar'      | false
        'nested in dir jars' | 'propertiesFileInNestedInDirJar' | false
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties in selected files"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can ignore properties in selected files defined in multiple rules"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'this should also be ignored')
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                    properties('some/path/to/foo.properties') {
                        ignoreProperty 'ignore-me-too'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'please ignore me too')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    @ToBeFixedForConfigurationCache(because = "classpath normalization")
    def "can add rules to the default properties rule"() {
        def project = new ProjectWithRuntimeClasspathNormalization(true)
        def barProperties = new PropertiesResource(project.root.file('classpath/dirEntry/bar.properties'), ['ignore-me': 'this should not actually be ignored'])
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'this should also be ignored')
        project.buildFile << '''
            normalization {
                runtimeClasspath {
                    properties('**/foo.properties') {
                        ignoreProperty 'ignore-me'
                    }
                    properties {
                        ignoreProperty 'ignore-me-too'
                    }
                }
            }
        '''

        when:
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)

        when:
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        project.propertiesFileInDir.changeProperty('ignore-me', 'please ignore me')
        project.propertiesFileInDir.changeProperty('ignore-me-too', 'please ignore me too')
        succeeds project.customTask
        then:
        skipped(project.customTask)

        when:
        barProperties.changeProperty('ignore-me', 'please dont ignore me')
        succeeds project.customTask
        then:
        executedAndNotSkipped(project.customTask)
    }

    class ProjectWithRuntimeClasspathNormalization {
        final TestFile root
        TestResource ignoredResourceInDirectory
        TestResource notIgnoredResourceInDirectory
        TestResource ignoredResourceInJar
        TestResource ignoredResourceInNestedJar
        TestResource ignoredResourceInNestedInDirJar
        TestResource notIgnoredResourceInJar
        TestResource notIgnoredResourceInNestedJar
        TestResource notIgnoredResourceInNestedInDirJar
        TestResource jarManifest
        TestResource jarManifestProperties
        PropertiesResource propertiesFileInDir
        PropertiesResource propertiesFileInJar
        PropertiesResource propertiesFileInNestedJar
        PropertiesResource propertiesFileInNestedInDirJar
        TestFile libraryJar
        TestFile nestedJar
        TestFile nestedInDirJar
        private TestFile libraryJarContents
        private TestFile nestedJarContents
        private TestFile nestedInDirJarContents
        private final String projectName
        final TestFile buildFile

        ProjectWithRuntimeClasspathNormalization(String projectName = null, boolean useRuntimeApi) {
            this.projectName = projectName
            this.root = projectName ? file(projectName) : temporaryFolder.testDirectory

            buildFile = root.file('build.gradle') << """
                apply plugin: 'base'
            """

            buildFile << declareCustomTask(useRuntimeApi)

            nestedInDirJarContents = root.file('nestedInDirJarContents').create {
                ignoredResourceInNestedInDirJar = new TestResource(file('another/package/ignored.txt') << "This should be ignored", this.&createNestedInDirJar)
                notIgnoredResourceInNestedInDirJar = new TestResource(file('another/package/not-ignored.txt') << "This should not be ignored", this.&createNestedInDirJar)
                propertiesFileInNestedInDirJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createNestedInDirJar)
            }
            root.file('classpath/dirEntry').create {
                ignoredResourceInDirectory = new TestResource(file("ignored.txt") << "This should be ignored")
                notIgnoredResourceInDirectory = new TestResource(file("not-ignored.txt") << "This should not be ignored")
                nestedInDirJar = file('nestedInDir.jar')
                propertiesFileInDir = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'])
            }
            nestedJarContents = root.file('libraryContents').create {
                ignoredResourceInNestedJar = new TestResource(file('some/package/ignored.txt') << "This should be ignored", this.&createJar)
                notIgnoredResourceInNestedJar = new TestResource(file('some/package/not-ignored.txt') << "This should not be ignored", this.&createJar)
                propertiesFileInNestedJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createJar)
            }
            libraryJarContents = root.file('libraryContents').create {
                jarManifest = new TestResource(file('META-INF/MANIFEST.MF') << "Manifest-Version: 1.0\nImplementation-Version: 1.0.0", this.&createJar)
                jarManifestProperties = new TestResource(file('META-INF/build-info.properties') << "implementation-version=1.0.0", this.&createJar)
                ignoredResourceInJar = new TestResource(file('some/package/ignored.txt') << "This should be ignored", this.&createJar)
                notIgnoredResourceInJar = new TestResource(file('some/package/not-ignored.txt') << "This should not be ignored", this.&createJar)
                nestedJar = file('nested.jar')
                propertiesFileInJar = new PropertiesResource(file('some/path/to/foo.properties'), ['ignore-me': 'this should be ignored', 'dont-ignore-me': 'this should not be ignored'], this.&createJar)
            }
            libraryJar = root.file('library.jar')
            createJar()
            createNestedInDirJar()
        }

        String declareCustomTask(boolean useRuntimeApi) {
            if (useRuntimeApi) {
                return """
                    task customTask {
                        def outputFile = file("\$temporaryDir/output.txt")
                        inputs.files("classpath/dirEntry", "library.jar")
                            .withPropertyName("classpath")
                            .withNormalizer(ClasspathNormalizer)
                        outputs.file(outputFile)
                            .withPropertyName("outputFile")

                        doLast {
                            outputFile.text = "done"
                        }
                    }
                """
            } else {
                return """
                    class CustomTask extends DefaultTask {
                        @OutputFile File outputFile = new File(temporaryDir, "output.txt")
                        @Classpath FileCollection classpath = project.layout.files("classpath/dirEntry", "library.jar")

                        @TaskAction void generate() {
                            outputFile.text = "done"
                        }
                    }

                    task customTask(type: CustomTask)
                """
            }
        }

        void createJar() {
            if (nestedJar.exists()) {
                nestedJar.delete()
            }
            nestedJarContents.zipTo(nestedJar)
            if (libraryJar.exists()) {
                libraryJar.delete()
            }
            libraryJarContents.zipTo(libraryJar)
        }

        void createNestedInDirJar() {
            if (nestedInDirJar.exists()) {
                nestedInDirJar.delete()
            }
            nestedInDirJarContents.zipTo(nestedInDirJar)
        }

        ProjectWithRuntimeClasspathNormalization withFilesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        ignore "**/ignored.txt"
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withAllMetaInfIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreCompletely()
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreManifest()
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestAttributesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreAttribute "Implementation-Version"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withManifestPropertiesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        metaInf {
                            ignoreProperty "implementation-version"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        ProjectWithRuntimeClasspathNormalization withPropertiesIgnored() {
            root.file('build.gradle') << """
                normalization {
                    runtimeClasspath {
                        properties {
                            ignoreProperty "ignore-me"
                        }
                    }
                }
            """.stripIndent()
            return this
        }

        String getCustomTask() {
            return "${projectName ? ":${projectName}" : ''}:customTask"
        }
    }

    class TestResource {
        final TestFile backingFile
        private final Closure onChange

        TestResource(TestFile backingFile, Closure onChange = {}) {
            this.backingFile = backingFile
            this.onChange = onChange
        }

        void replaceContents(String contents) {
            backingFile.withWriter { w ->
                w << contents
            }
            changed()
        }

        void changeContents() {
            backingFile << "More changes"
            changed()
        }

        void remove() {
            assert backingFile.delete()
            changed()
        }

        void add() {
            backingFile << "First creation of file"
            changed()
        }

        void changed() {
            onChange.call()
        }
    }

    class PropertiesResource extends TestResource {
        PropertiesResource(TestFile backingFile, Map<String, String> initialProps, Closure finalizedBy={}) {
            super(backingFile, finalizedBy)
            withProperties() { Properties props ->
                props.putAll(initialProps)
            }
        }

        PropertiesResource changeProperty(String key, String value) {
            withProperties(true) { Properties props ->
                props.setProperty(key, value)
            }
            changed()
            return this
        }

        private withProperties(boolean loadFromExisting = false, Closure action) {
            Properties props = new Properties()
            if (loadFromExisting) {
                def inputStream = backingFile.newInputStream()
                try {
                    props.load(inputStream)
                } finally {
                    inputStream.close()
                }
            }
            action.call(props)
            def outputStream = backingFile.newOutputStream()
            try {
                props.store(outputStream, "")
            } finally {
                outputStream.close()
            }
        }
    }
}
{'fraction_non_alphanumeric': 0.08139136739914198, 'fraction_numerical': 0.0014904630506153195, 'mean_word_length': 1.9390871958799503, 'pattern_counts': {'":': 2, '<': 113, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3027316', 'n_tokens_mistral': 12267, 'n_tokens_neox': 11921, 'n_words': 2451}