Repository: gradle/gradle
Pull request #32801: Introduce execution time value for `SubtractingFileCollection`
Author: login: 6hundreds
Labels: name: a:chore
description: Minor issue without significant impact
name: in:configuration-cache
description: Configuration Caching
Created: 2025-03-21T16:39:43Z | Merged: 2025-03-21T21:51:30Z

Part of https://github.com/gradle/gradle/issues/28820


Diff:
diff --git a/platforms/core-configuration/file-collections/src/main/java/org/gradle/api/internal/file/SubtractingFileCollection.java b/platforms/core-configuration/file-collections/src/main/java/org/gradle/api/internal/file/SubtractingFileCollection.java
index e64c2205ea98e..a07d53ab64d47 100644
--- a/platforms/core-configuration/file-collections/src/main/java/org/gradle/api/internal/file/SubtractingFileCollection.java
+++ b/platforms/core-configuration/file-collections/src/main/java/org/gradle/api/internal/file/SubtractingFileCollection.java
@@ -21,6 +21,7 @@
 
 import java.io.File;
 import java.util.LinkedHashSet;
+import java.util.Optional;
 import java.util.Set;
 
 public class SubtractingFileCollection extends AbstractOpaqueFileCollection {
@@ -58,8 +59,31 @@ protected Set<File> getIntrinsicFiles() {
         return files;
     }
 
+    @Override
+    public Optional<FileCollectionExecutionTimeValue> calculateExecutionTimeValue() {
+        return left.calculateExecutionTimeValue()
+            .flatMap(leftEtv -> ((FileCollectionInternal) right).calculateExecutionTimeValue()
+                .map(rightEtv -> new SubtractingExecutionTimeValue(leftEtv, rightEtv)));
+    }
+
     @Override
     public boolean contains(File file) {
         return left.contains(file) && !right.contains(file);
     }
+
+    private static class SubtractingExecutionTimeValue implements FileCollectionExecutionTimeValue {
+        private final FileCollectionExecutionTimeValue left;
+        private final FileCollectionExecutionTimeValue right;
+
+        private SubtractingExecutionTimeValue(FileCollectionExecutionTimeValue left, FileCollectionExecutionTimeValue right) {
+            this.left = left;
+            this.right = right;
+        }
+
+        @Override
+        public FileCollectionInternal toFileCollection(FileCollectionFactory fileCollectionFactory) {
+            return (FileCollectionInternal) left.toFileCollection(fileCollectionFactory)
+                .minus(right.toFileCollection(fileCollectionFactory));
+        }
+    }
 }
diff --git a/platforms/core-configuration/file-collections/src/test/groovy/org/gradle/api/internal/file/SubtractingFileCollectionExecutionTimeValueTest.groovy b/platforms/core-configuration/file-collections/src/test/groovy/org/gradle/api/internal/file/SubtractingFileCollectionExecutionTimeValueTest.groovy
new file mode 100644
index 0000000000000..39f013041c13f
--- /dev/null
+++ b/platforms/core-configuration/file-collections/src/test/groovy/org/gradle/api/internal/file/SubtractingFileCollectionExecutionTimeValueTest.groovy
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 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.internal.file
+
+
+import spock.lang.Specification
+
+import static org.gradle.api.internal.file.TestFiles.fileCollectionFactory
+
+class SubtractingFileCollectionExecutionTimeValueTest extends Specification {
+    def "SubtractingFileCollection supports execution time value"() {
+        given:
+        def left = fileCollectionFactory().fixed(new File("foo.txt"), new File("bar.txt"))
+        def right = fileCollectionFactory().fixed(new File("bar.txt"))
+        def collection = new SubtractingFileCollection(left, right)
+
+        when:
+        def executionTimeValue = collection.calculateExecutionTimeValue()
+
+        then:
+        executionTimeValue.present
+
+        and:
+        def newCollection = executionTimeValue.get().toFileCollection(fileCollectionFactory())
+
+        then:
+        newCollection.files == [new File("foo.txt")] as Set
+    }
+
+    def "SubtractingFileCollection supports absence of execution time value"() {
+        given:
+        def collection = new SubtractingFileCollection(left, right)
+
+        when:
+        def executionTimeValue = collection.calculateExecutionTimeValue()
+
+        then:
+        executionTimeValue.present == isPresent
+
+        where:
+        left                                               | right                                              | isPresent
+        new EmptyExecutionTimeValueFileCollection()        | fileCollectionFactory().fixed(new File("foo.txt")) | false
+        fileCollectionFactory().fixed(new File("foo.txt")) | new EmptyExecutionTimeValueFileCollection()        | false
+        new EmptyExecutionTimeValueFileCollection()        | new EmptyExecutionTimeValueFileCollection()        | false
+        fileCollectionFactory().fixed(new File("foo.txt")) | fileCollectionFactory().fixed(new File("bar.txt")) | true
+    }
+}
+


Reviews:
user: login: alllex
body: 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T18:25:59Z
user: login: 6hundreds
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T18:40:08Z

Review comments:
diff_hunk: @@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 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.internal.file
+
+
+import spock.lang.Specification
+
+import static org.gradle.api.internal.file.TestFiles.fileCollectionFactory
+
+class SubtractingFileCollectionExecutionTimeValueTest extends Specification {
path: platforms/core-configuration/file-collections/src/test/groovy/org/gradle/api/internal/file/SubtractingFileCollectionExecutionTimeValueTest.groovy
user: login: alllex
body: 💅  The test class seems to be more about the `SubtractingFileCollection`. Testing execution time value behavior is only one aspect. I'd rename the test class to accommodate for future unit tests for the collection itself.
created_at: 2025-03-21T18:24:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 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.internal.file
+
+
+import spock.lang.Specification
+
+import static org.gradle.api.internal.file.TestFiles.fileCollectionFactory
+
+class SubtractingFileCollectionExecutionTimeValueTest extends Specification {
path: platforms/core-configuration/file-collections/src/test/groovy/org/gradle/api/internal/file/SubtractingFileCollectionExecutionTimeValueTest.groovy
user: login: 6hundreds
body: We have a bunch of tests similarly named like this (by me). They are targeting only ETV intentionally, since further ETV implementations will not so simple, and perhaps will require more extensive coverage. 
created_at: 2025-03-21T18:40:08Z
author_association: MEMBER

PR comments:
user: login: 6hundreds
created_at: 2025-03-21T16:40:09Z
body: @bot-gradle test this on linux
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-03-21T16:40:12Z
body: <!-- {"replyTargetCommentId":2743904228,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Linux_Trigger","buildId":"96142244","headRef":"5ae9170208f25079074a686ad9263c4bebdf2ec4"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger)(Linux) (Check)](https://builds.gradle.org/build/96142244)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=5ae9170208f25079074a686ad9263c4bebdf2ec4,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-03-21T17:54:22Z
body: The following builds have failed:
- [Pull Request Feedback (Trigger)(Linux) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Linux_Trigger/96142244)
  - [Build Scan](https://ge.gradle.org/scans?search.buildOutcome=failure&amp;search.tags=CI&amp;search.names=gitCommitId&amp;search.values=5ae9170208f25079074a686ad9263c4bebdf2ec4&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-03-21T18:40:18Z
body: <!-- {"replyTargetCommentId":null,"triggeredTeamCityBuilds":[]} -->
WARN: The milestone of this PR is 8.14 RC1, but the base branch version is 9.0
author_association: COLLABORATOR