From 2f76441609514ed3e0c94c06ae9b308f60dcd56d Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:57:39 +0900 Subject: [PATCH 1/6] Fix #36601: Replace createTempFile+move with direct file write Files.createTempFile() creates files with owner-only permissions (600) on POSIX systems. When moved to the final results-generic.bin location, these restrictive permissions were preserved, unlike other output files. Remove the temporary file pattern and write directly to the target file via Files.newOutputStream(), which respects the system umask. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../serializable/SerializableTestResultStore.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java index 7fa93787b3773..26e5effaad7c1 100644 --- a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java +++ b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java @@ -40,7 +40,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -97,9 +96,7 @@ private static int depth(TestDescriptorInternal descriptor) { private final Set flatteningIds; private final List extraFlattenedDescriptors; private final List extraFlattenedResults; - private final Path serializedResultsFile; private final int diskSkipLevels; - private final Path temporaryResultsFile; /** * Encoder storing the serialized test results. */ @@ -111,15 +108,13 @@ private static int depth(TestDescriptorInternal descriptor) { private final Multimap metadatas = LinkedHashMultimap.create(); private Writer(Path serializedResultsFile, Path outputEventsFile, int diskSkipLevels) throws IOException { - this.serializedResultsFile = serializedResultsFile; this.diskSkipLevels = diskSkipLevels; // Use constants to avoid allocating empty collections if flattening is not enabled flatteningIds = isDiskSkipEnabled() ? new HashSet<>() : Collections.emptySet(); extraFlattenedDescriptors = isDiskSkipEnabled() ? new ArrayList<>() : Collections.emptyList(); extraFlattenedResults = isDiskSkipEnabled() ? new ArrayList<>() : Collections.emptyList(); Files.createDirectories(serializedResultsFile.getParent()); - temporaryResultsFile = Files.createTempFile(serializedResultsFile.getParent(), "in-progress-results-generic", ".bin"); - resultsEncoder = new KryoBackedEncoder(Files.newOutputStream(temporaryResultsFile)); + resultsEncoder = new KryoBackedEncoder(Files.newOutputStream(serializedResultsFile)); Serializer testOutputEventSerializer = TestEventSerializer.create().build(TestOutputEvent.class); try { resultsEncoder.writeSmallInt(STORE_VERSION); @@ -296,8 +291,6 @@ public void close() throws IOException { } finally { CompositeStoppable.stoppable(resultsEncoder, outputWriter).stop(); } - // Move the temporary results file to the final location, if successful - Files.move(temporaryResultsFile, serializedResultsFile, StandardCopyOption.REPLACE_EXISTING); } } From 42cbfe99433fca6ad9c058a600a7ba060ee70b2b Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:57:53 +0900 Subject: [PATCH 2/6] Add integration test for binary test result file permissions Verify that all files in the binary test results directory have group-read and others-read permissions on POSIX systems. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../AbstractTestTaskIntegrationTest.groovy | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy index 4b37f9df04273..94508e3d52541 100644 --- a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy +++ b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy @@ -27,6 +27,8 @@ import org.gradle.testing.fixture.AbstractTestingMultiVersionIntegrationTest import spock.lang.Issue import java.time.Duration +import java.nio.file.Files as NioFiles +import java.nio.file.attribute.PosixFilePermission import static org.hamcrest.Matchers.greaterThanOrEqualTo @@ -409,4 +411,22 @@ abstract class AbstractTestTaskIntegrationTest extends AbstractTestingMultiVersi private static JavaVersion classFormat(TestFile path) { JavaVersion.forClassVersion(path.bytes[7] & 0xFF) } + + @Requires(UnitTestPreconditions.FilePermissions) + def "binary test result files have correct permissions"() { + given: + file('src/test/java/MyTest.java') << standaloneTestClass + + when: + succeeds 'test' + + then: + def binaryDir = file("build/test-results/test/binary") + binaryDir.listFiles().each { File f -> + assert f.canRead() + def perms = NioFiles.getPosixFilePermissions(f.toPath()) + assert perms.contains(PosixFilePermission.OTHERS_READ) + assert perms.contains(PosixFilePermission.GROUP_READ) + } + } } From 74fad2cfa940b31d80e808f30c3b2b0c932c61b9 Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:41:50 +0900 Subject: [PATCH 3/6] refactor: move permission test above private methods for consistency Reorder 'binary test result files have correct permissions' test to appear before private utility methods in AbstractTestTaskIntegrationTest, following Spock test class conventions. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../AbstractTestTaskIntegrationTest.groovy | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy index 94508e3d52541..69ef7c284cdc9 100644 --- a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy +++ b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy @@ -399,19 +399,6 @@ abstract class AbstractTestTaskIntegrationTest extends AbstractTestingMultiVersi "after" | afterClassAnnotation } - private String buildRequestingNewerJavaVersion() { - """ - java { - sourceCompatibility = 17 - targetCompatibility = 17 - } - """ - } - - private static JavaVersion classFormat(TestFile path) { - JavaVersion.forClassVersion(path.bytes[7] & 0xFF) - } - @Requires(UnitTestPreconditions.FilePermissions) def "binary test result files have correct permissions"() { given: @@ -429,4 +416,17 @@ abstract class AbstractTestTaskIntegrationTest extends AbstractTestingMultiVersi assert perms.contains(PosixFilePermission.GROUP_READ) } } + + private String buildRequestingNewerJavaVersion() { + """ + java { + sourceCompatibility = 17 + targetCompatibility = 17 + } + """ + } + + private static JavaVersion classFormat(TestFile path) { + JavaVersion.forClassVersion(path.bytes[7] & 0xFF) + } } From 02c35439259585e12d66cc052017653fef5f04ce Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:53:22 +0900 Subject: [PATCH 4/6] refactor: use Files.walk() and fully-qualified names in permission test Replace listFiles() with Files.walk() to validate permissions for all files including subdirectories. Remove NioFiles import alias and use java.nio.file.Files fully-qualified name for better readability outside IDE. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../testing/AbstractTestTaskIntegrationTest.groovy | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy index 69ef7c284cdc9..c184eeb36353d 100644 --- a/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy +++ b/platforms/jvm/testing-jvm/src/integTest/groovy/org/gradle/testing/AbstractTestTaskIntegrationTest.groovy @@ -27,7 +27,6 @@ import org.gradle.testing.fixture.AbstractTestingMultiVersionIntegrationTest import spock.lang.Issue import java.time.Duration -import java.nio.file.Files as NioFiles import java.nio.file.attribute.PosixFilePermission import static org.hamcrest.Matchers.greaterThanOrEqualTo @@ -409,12 +408,13 @@ abstract class AbstractTestTaskIntegrationTest extends AbstractTestingMultiVersi then: def binaryDir = file("build/test-results/test/binary") - binaryDir.listFiles().each { File f -> - assert f.canRead() - def perms = NioFiles.getPosixFilePermissions(f.toPath()) - assert perms.contains(PosixFilePermission.OTHERS_READ) - assert perms.contains(PosixFilePermission.GROUP_READ) - } + java.nio.file.Files.walk(binaryDir.toPath()) + .filter { java.nio.file.Files.isRegularFile(it) } + .each { path -> + def perms = java.nio.file.Files.getPosixFilePermissions(path) + assert perms.contains(PosixFilePermission.OTHERS_READ) + assert perms.contains(PosixFilePermission.GROUP_READ) + } } private String buildRequestingNewerJavaVersion() { From cbd2fd6006b3d1b6d8d2c70cb3ce5369d513c55f Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:13:44 +0900 Subject: [PATCH 5/6] fix: use fixed-name in-progress file instead of createTempFile Replace Files.createTempFile() with a fixed-name in-progress file written via Files.newOutputStream() to respect system umask, while preserving the temporary file + move pattern to avoid leaving incomplete results on write failure. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../serializable/SerializableTestResultStore.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java index 26e5effaad7c1..1d1b169c933b9 100644 --- a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java +++ b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java @@ -40,6 +40,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -100,6 +101,8 @@ private static int depth(TestDescriptorInternal descriptor) { /** * Encoder storing the serialized test results. */ + private final Path serializedResultsFile; + private final Path temporaryResultsFile; private final KryoBackedEncoder resultsEncoder; private final TestOutputWriter outputWriter; private long nextId = 1; @@ -109,12 +112,14 @@ private static int depth(TestDescriptorInternal descriptor) { private Writer(Path serializedResultsFile, Path outputEventsFile, int diskSkipLevels) throws IOException { this.diskSkipLevels = diskSkipLevels; + this.serializedResultsFile = serializedResultsFile; + this.temporaryResultsFile = serializedResultsFile.getParent().resolve("in-progress-results-generic.bin"); // Use constants to avoid allocating empty collections if flattening is not enabled flatteningIds = isDiskSkipEnabled() ? new HashSet<>() : Collections.emptySet(); extraFlattenedDescriptors = isDiskSkipEnabled() ? new ArrayList<>() : Collections.emptyList(); extraFlattenedResults = isDiskSkipEnabled() ? new ArrayList<>() : Collections.emptyList(); Files.createDirectories(serializedResultsFile.getParent()); - resultsEncoder = new KryoBackedEncoder(Files.newOutputStream(serializedResultsFile)); + resultsEncoder = new KryoBackedEncoder(Files.newOutputStream(temporaryResultsFile)); Serializer testOutputEventSerializer = TestEventSerializer.create().build(TestOutputEvent.class); try { resultsEncoder.writeSmallInt(STORE_VERSION); @@ -291,6 +296,8 @@ public void close() throws IOException { } finally { CompositeStoppable.stoppable(resultsEncoder, outputWriter).stop(); } + // Move the completed in-progress file to the final location + Files.move(temporaryResultsFile, serializedResultsFile, StandardCopyOption.REPLACE_EXISTING); } } From 908c1a4dcd621352568f4cf85b91f99fd102cfc2 Mon Sep 17 00:00:00 2001 From: CHANHAN <130114269+chanani@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:18:32 +0900 Subject: [PATCH 6/6] refactor : path resolution using resolveSibling() Updated the temporary results file path logic to use Path.resolveSibling() instead of getParent().resolve() based on code review feedback. Signed-off-by: CHANHAN <130114269+chanani@users.noreply.github.com> --- .../results/serializable/SerializableTestResultStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java index 1d1b169c933b9..35c946076d28e 100644 --- a/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java +++ b/platforms/software/testing-base/src/main/java/org/gradle/api/internal/tasks/testing/results/serializable/SerializableTestResultStore.java @@ -113,7 +113,7 @@ private static int depth(TestDescriptorInternal descriptor) { private Writer(Path serializedResultsFile, Path outputEventsFile, int diskSkipLevels) throws IOException { this.diskSkipLevels = diskSkipLevels; this.serializedResultsFile = serializedResultsFile; - this.temporaryResultsFile = serializedResultsFile.getParent().resolve("in-progress-results-generic.bin"); + this.temporaryResultsFile = serializedResultsFile.resolveSibling("in-progress-results-generic.bin"); // Use constants to avoid allocating empty collections if flattening is not enabled flatteningIds = isDiskSkipEnabled() ? new HashSet<>() : Collections.emptySet(); extraFlattenedDescriptors = isDiskSkipEnabled() ? new ArrayList<>() : Collections.emptyList();