Copilot commented on code in PR #2433:
URL: https://github.com/apache/groovy/pull/2433#discussion_r3035498166


##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,230 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        AsynchronousFileChannel channel = null;
+        try {
+            channel = AsynchronousFileChannel.open(self, 
StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new IOException("File too large 
to read into a single byte array: " + size + " bytes"));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            readFully(channel, buf, 0, result);
+        } catch (IOException | RuntimeException e) {
+            if (channel != null) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+            }
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void readFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                  long position, CompletableFuture<byte[]> 
result) {
+        channel.read(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesRead, Void attachment) {
+                if (bytesRead == -1 || !buf.hasRemaining()) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                } else {
+                    readFully(channel, buf, position + bytesRead, result);
+                }
+            }
+
+            @Override
+            public void failed(Throwable exc, Void attachment) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+                result.completeExceptionally(exc);
+            }
+        });
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * platform's default charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file to write to
+     * @param text the text to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text) {
+        return writeAsync(self, text, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async', 'UTF-8').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file to write to
+     * @param text    the text to write
+     * @param charset the charset used to encode the text
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text, 
String charset) {
+        return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously write the byte array to the Path, creating the file
+     * if it doesn't exist and truncating it if it does.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeBytesAsync([72, 105] as byte[]).get()
+     * assert path.bytes == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self  the file to write to
+     * @param bytes the byte array to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeBytesAsync(Path self, byte[] 
bytes) {
+        CompletableFuture<Void> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self,
+                    StandardOpenOption.WRITE, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+            ByteBuffer buf = ByteBuffer.wrap(bytes);
+            writeFully(channel, buf, 0, result);
+        } catch (IOException e) {
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void writeFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                   long position, CompletableFuture<Void> 
result) {
+        channel.write(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesWritten, Void attachment) {
+                if (buf.hasRemaining()) {
+                    writeFully(channel, buf, position + bytesWritten, result);
+                } else {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {

Review Comment:
   `writeFully` recurses with `position + bytesWritten`. If `bytesWritten` is 
`0` while the buffer still has remaining data (possible for async writes), this 
can recurse forever without progress. Handle the `bytesWritten == 0` case 
explicitly to avoid infinite recursion / stack overflow.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+
+        then:
+        path.text == 'Hello async'
+    }
+
+    def testWriteAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Héllo àsync', 'UTF-8').get()
+

Review Comment:
   Using `CompletableFuture.get()` without a timeout can hang the test suite 
indefinitely if the async operation never completes. Prefer `get(timeout, 
unit)` (or equivalent) to bound failures.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+
+        then:
+        path.text == 'Hello async'
+    }
+
+    def testWriteAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Héllo àsync', 'UTF-8').get()
+
+        then:
+        path.text == 'Héllo àsync'

Review Comment:
   This assertion reads `path.text` using the platform default charset after 
writing using `writeAsync(..., 'UTF-8')`. If the default charset isn't UTF-8, 
the read-back text can be mis-decoded and the test will be flaky. Read back 
using UTF-8 (e.g., `path.getText('UTF-8')`) to match the write encoding.
   ```suggestion
           path.getText('UTF-8') == 'Héllo àsync'
   ```



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+
+        then:
+        path.text == 'Hello async'
+    }
+
+    def testWriteAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Héllo àsync', 'UTF-8').get()
+
+        then:
+        path.text == 'Héllo àsync'
+    }
+
+    def testWriteBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeBytesAsync([72, 105] as byte[]).get()
+

Review Comment:
   Using `CompletableFuture.get()` without a timeout can hang the test suite 
indefinitely if the async operation never completes. Prefer `get(timeout, 
unit)` (or equivalent) to bound failures.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'

Review Comment:
   Using `future.get()` without a timeout can hang the test suite indefinitely 
if the async operation never completes. Prefer `get(timeout, unit)` (or Spock 
polling conditions) to keep failures bounded.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+

Review Comment:
   This test writes using `path.text = ...` (platform default charset) but 
reads using `getTextAsync('UTF-8')`. On platforms where the default charset 
isn't UTF-8, this can fail. Write the file using UTF-8 as well (e.g., via 
`path.write(..., 'UTF-8')` or equivalent) so the encoding/decoding match.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+

Review Comment:
   Using `CompletableFuture.get()` without a timeout can hang the test suite 
indefinitely if the async operation never completes. Prefer `get(timeout, 
unit)` (or equivalent) to bound failures.



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,230 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        AsynchronousFileChannel channel = null;
+        try {
+            channel = AsynchronousFileChannel.open(self, 
StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new IOException("File too large 
to read into a single byte array: " + size + " bytes"));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            readFully(channel, buf, 0, result);
+        } catch (IOException | RuntimeException e) {
+            if (channel != null) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+            }
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void readFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                  long position, CompletableFuture<byte[]> 
result) {
+        channel.read(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesRead, Void attachment) {
+                if (bytesRead == -1 || !buf.hasRemaining()) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                } else {
+                    readFully(channel, buf, position + bytesRead, result);
+                }
+            }
+
+            @Override
+            public void failed(Throwable exc, Void attachment) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+                result.completeExceptionally(exc);
+            }
+        });
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * platform's default charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file to write to
+     * @param text the text to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text) {
+        return writeAsync(self, text, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async', 'UTF-8').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file to write to
+     * @param text    the text to write
+     * @param charset the charset used to encode the text
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text, 
String charset) {
+        return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));

Review Comment:
   `writeAsync(Path, String, String)` can throw synchronously (e.g., invalid 
charset via `Charset.forName(...)`, or `text` being null) before returning the 
`CompletableFuture`, which breaks the async contract compared to `getTextAsync` 
(where charset errors are surfaced via exceptional completion). Consider 
wrapping charset/encoding errors and returning a `CompletableFuture` already 
completed exceptionally instead of throwing.
   ```suggestion
           try {
               return writeBytesAsync(self, 
text.getBytes(Charset.forName(charset)));
           } catch (RuntimeException e) {
               CompletableFuture<Void> result = new CompletableFuture<>();
               result.completeExceptionally(e);
               return result;
           }
   ```



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+
+        then:
+        path.text == 'Hello async'
+    }
+
+    def testWriteAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Héllo àsync', 'UTF-8').get()
+
+        then:
+        path.text == 'Héllo àsync'
+    }
+
+    def testWriteBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeBytesAsync([72, 105] as byte[]).get()
+
+        then:
+        path.bytes == [72, 105] as byte[]
+    }
+
+    def testGetTextAsyncEmptyFile() {
+        given:
+        def path = tempFile.toPath()
+        path.text = ''
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == ''

Review Comment:
   Using `future.get()` without a timeout can hang the test suite indefinitely 
if the async operation never completes. Prefer `get(timeout, unit)` (or Spock 
polling conditions) to keep failures bounded.
   ```suggestion
           future.get(5, java.util.concurrent.TimeUnit.SECONDS) == ''
   ```



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]

Review Comment:
   Using `future.get()` without a timeout can hang the test suite indefinitely 
if the async operation never completes. Prefer `get(timeout, unit)` (or Spock 
polling conditions) to keep failures bounded.



##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,230 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        AsynchronousFileChannel channel = null;
+        try {
+            channel = AsynchronousFileChannel.open(self, 
StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new IOException("File too large 
to read into a single byte array: " + size + " bytes"));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            readFully(channel, buf, 0, result);
+        } catch (IOException | RuntimeException e) {
+            if (channel != null) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+            }
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void readFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                  long position, CompletableFuture<byte[]> 
result) {
+        channel.read(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesRead, Void attachment) {
+                if (bytesRead == -1 || !buf.hasRemaining()) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                } else {
+                    readFully(channel, buf, position + bytesRead, result);
+                }
+            }
+
+            @Override
+            public void failed(Throwable exc, Void attachment) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+                result.completeExceptionally(exc);
+            }
+        });
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * platform's default charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file to write to
+     * @param text the text to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text) {
+        return writeAsync(self, text, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async', 'UTF-8').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file to write to
+     * @param text    the text to write
+     * @param charset the charset used to encode the text
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text, 
String charset) {
+        return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously write the byte array to the Path, creating the file
+     * if it doesn't exist and truncating it if it does.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeBytesAsync([72, 105] as byte[]).get()
+     * assert path.bytes == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self  the file to write to
+     * @param bytes the byte array to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeBytesAsync(Path self, byte[] 
bytes) {
+        CompletableFuture<Void> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self,
+                    StandardOpenOption.WRITE, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+            ByteBuffer buf = ByteBuffer.wrap(bytes);
+            writeFully(channel, buf, 0, result);
+        } catch (IOException e) {

Review Comment:
   `writeBytesAsync` only catches `IOException`. If a runtime exception occurs 
after opening the channel (e.g., `bytes` is null causing 
`ByteBuffer.wrap(bytes)` to throw), the method will throw and the opened 
`AsynchronousFileChannel` may leak. Consider catching `RuntimeException` as 
well, ensuring the channel is closed on any failure, and completing the 
returned future exceptionally.
   ```suggestion
           AsynchronousFileChannel channel = null;
           try {
               channel = AsynchronousFileChannel.open(self,
                       StandardOpenOption.WRITE, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
               ByteBuffer buf = ByteBuffer.wrap(bytes);
               writeFully(channel, buf, 0, result);
           } catch (IOException | RuntimeException e) {
               if (channel != null) {
                   try {
                       channel.close();
                   } catch (IOException closeException) {
                       e.addSuppressed(closeException);
                   }
               }
   ```



##########
src/spec/test/gdk/WorkingWithIOSpecTest.groovy:
##########
@@ -79,6 +79,12 @@ Le bruit de l'eau.''')
             // tag::file_bytes[]
             byte[] contents = file.bytes
             // end::file_bytes[]
+
+            // tag::async_read[]
+            def path = file.toPath()
+            def future = path.textAsync           // returns 
CompletableFuture<String>
+            println future.get()                  // blocks until content is 
available
+            // end::async_read[]

Review Comment:
   This uses `future.get()` without a timeout. If the async read never 
completes (e.g., due to environment issues), this JUnit test can hang 
indefinitely. Prefer `get(timeout, unit)` to bound test execution time.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'
+    }
+
+    def testGetTextAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Héllo àsync'
+
+        when:
+        def future = path.getTextAsync('UTF-8')
+
+        then:
+        future.get() == 'Héllo àsync'
+    }
+
+    def testGetBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.bytes = [72, 101, 108, 108, 111] as byte[]
+
+        when:
+        def future = path.bytesAsync
+
+        then:
+        future.get() == [72, 101, 108, 108, 111] as byte[]
+    }
+
+    def testWriteAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Hello async').get()
+
+        then:
+        path.text == 'Hello async'
+    }
+
+    def testWriteAsyncWithCharset() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeAsync('Héllo àsync', 'UTF-8').get()
+
+        then:
+        path.text == 'Héllo àsync'
+    }
+
+    def testWriteBytesAsync() {
+        given:
+        def path = tempFile.toPath()
+
+        when:
+        path.writeBytesAsync([72, 105] as byte[]).get()
+
+        then:
+        path.bytes == [72, 105] as byte[]
+    }
+
+    def testGetTextAsyncEmptyFile() {
+        given:
+        def path = tempFile.toPath()
+        path.text = ''
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == ''
+    }
+
+    def testGetBytesAsyncNonExistentFile() {
+        given:
+        def path = tempDir.toPath().resolve('does-not-exist.txt')
+
+        when:
+        path.bytesAsync.get()
+

Review Comment:
   Using `CompletableFuture.get()` without a timeout can hang the test suite 
indefinitely if the async operation never completes. Prefer `get(timeout, 
unit)` to bound failures (even for exceptional-completion tests).



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // ---- Async extensions ----
+
+    def testGetTextAsync() {
+        given:
+        def path = tempFile.toPath()
+        path.text = 'Hello async'
+
+        when:
+        def future = path.textAsync
+
+        then:
+        future.get() == 'Hello async'

Review Comment:
   Using `future.get()` without a timeout can hang the test suite indefinitely 
if the async operation never completes (e.g., CI/environment issues). Prefer 
`get(timeout, unit)` (or Spock polling conditions) to bound failures.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to