Merge pull request #6110 from dmatej/TestHelperLog

Added log to TestHelper and fixed ChunkedInputClosedOnErrorTest
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
index 32cd089..86953dc 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
  * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -16,18 +17,9 @@
 
 package org.glassfish.jersey.netty.connector;
 
+import io.netty.buffer.ByteBuf;
 import io.netty.channel.Channel;
 import io.netty.handler.stream.ChunkedInput;
-import org.glassfish.jersey.client.ClientConfig;
-import org.glassfish.jersey.client.ClientProperties;
-import org.glassfish.jersey.client.ClientRequest;
-import org.glassfish.jersey.client.spi.Connector;
-import org.glassfish.jersey.client.spi.ConnectorProvider;
-import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
-import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.test.JerseyTest;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
 
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.WebApplicationException;
@@ -38,8 +30,8 @@
 import javax.ws.rs.core.Configuration;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.MessageBodyWriter;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
@@ -47,118 +39,143 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 /**
  * Bug 5837 reproducer
  */
 public class ChunkedInputClosedOnErrorTest extends JerseyTest {
 
-    private static Client initClient(ConnectorProvider provider) {
-        ClientConfig defaultConfig = new ClientConfig();
-        defaultConfig.property(ClientProperties.CONNECT_TIMEOUT, 10 * 1000);
-        defaultConfig.property(ClientProperties.READ_TIMEOUT, 10 * 1000);
-        defaultConfig.connectorProvider(provider);
-        Client client = ClientBuilder.newBuilder()
-                .withConfig(defaultConfig)
-                .build();
-        return client;
-    }
-
     @Override
     protected Application configure() {
         return new ResourceConfig();
     }
 
     @Test
-    public void testChunkedInputNotStuckedTimes() throws InterruptedException {
-        for (int i = 0; i != 10; i++) {
-            boolean ret = testChunkedInputNotStucked();
-            Assertions.assertTrue(ret, "JerseyChunkedInput was not closed on error");
+    public void testChunkedInputNotStuckedTimes() throws Exception {
+        long timeout = System.currentTimeMillis() + 100;
+        int counter = 0;
+        while (System.currentTimeMillis() < timeout || counter < 300) {
+            counter++;
+            TestNettyConnectorProvider provider = new TestNettyConnectorProvider();
+            Client client = provider.initClient();
+            ProcessingException processingException = assertThrows(ProcessingException.class,
+                () -> client.register(new MultipartWriter()).target(target().getUri()).request()
+                    .post(Entity.entity(new MultipartWriter(), MediaType.MULTIPART_FORM_DATA_TYPE)));
+            provider.waitForWriterSet();
+            assertAll("Iteration: " + counter,
+                () -> assertInstanceOf(IllegalArgumentException.class, processingException.getCause()),
+                () -> assertEquals("TestException", processingException.getCause().getMessage()),
+                () -> assertTrue(provider.isEndOfInput(), "JerseyChunkedInput was not closed on error")
+            );
         }
     }
 
-    public boolean testChunkedInputNotStucked() throws InterruptedException {
-        final AtomicReference<NettyEntityWriter> writer = new AtomicReference<>();
-        final CountDownLatch writerSetLatch = new CountDownLatch(1);
-        final CountDownLatch flushLatch = new CountDownLatch(1);
-        ConnectorProvider provider = new ConnectorProvider() {
-            @Override
-            public Connector getConnector(Client client, Configuration runtimeConfig) {
-                return new NettyConnector(client, NettyConnectorProvider.config().rw()) {
-                    @Override
-                    NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel,
-                                                        NettyConnectorProvider.Config.RW config) {
-                        writer.set(super.nettyEntityWriter(clientRequest, channel, config));
-                        writerSetLatch.countDown();
-                        return new NettyEntityWriter() {
-                            private boolean slept = false;
+    private static class TestNettyConnectorProvider implements ConnectorProvider {
 
-                            @Override
-                            public void write(Object object) {
-                                writer.get().write(object);
-                            }
+        private final AtomicReference<NettyEntityWriter> writer = new AtomicReference<>();
+        private final CountDownLatch writerSetLatch = new CountDownLatch(1);
+        private final CountDownLatch flushLatch = new CountDownLatch(1);
 
-                            @Override
-                            public void writeAndFlush(Object object) {
-                                writer.get().writeAndFlush(object);
-                            }
+        Client initClient() {
+            ClientConfig defaultConfig = new ClientConfig();
+            defaultConfig.property(ClientProperties.CONNECT_TIMEOUT, 1_000);
+            defaultConfig.property(ClientProperties.READ_TIMEOUT, 10_000);
+            defaultConfig.connectorProvider(this);
+            return ClientBuilder.newBuilder().withConfig(defaultConfig).build();
+        }
 
-                            @Override
-                            public void flush() throws IOException {
-                                writer.get().flush();
-                                flushLatch.countDown();
-                            }
+        void waitForWriterSet() throws InterruptedException {
+            writerSetLatch.await();
+        }
 
-                            @Override
-                            public ChunkedInput getChunkedInput() {
-                                for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
-                                    // caught from catch block in executorService.execute(new Runnable() {
-                                    // "sleep" to simulate race condition
-                                    if (element.getClassName().contains("NettyConnector")
-                                            && element.getMethodName().equals("run")) {
-                                        try {
-                                            flushLatch.await();
-                                        } catch (InterruptedException e) {
-                                            throw new RuntimeException(e);
-                                        }
+        boolean isEndOfInput() throws Exception {
+            // getChunkedInput blocks and waits for the flush call, but that doesn't mean
+            // that the ChunkedInput is closed, so we give it a bit more time.
+            ChunkedInput<ByteBuf> chunkedInput = writer.get().getChunkedInput();
+            long timeout = System.currentTimeMillis() + 100;
+            while (!chunkedInput.isEndOfInput() && System.currentTimeMillis() < timeout) {
+                Thread.yield();
+            }
+            return chunkedInput.isEndOfInput();
+        }
+
+        @Override
+        public Connector getConnector(Client client, Configuration runtimeConfig) {
+            return new NettyConnector(client, NettyConnectorProvider.config().rw()) {
+
+                @Override
+                NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel,
+                    NettyConnectorProvider.Config.RW config) {
+                    writer.set(super.nettyEntityWriter(clientRequest, channel, config));
+                    writerSetLatch.countDown();
+                    return new NettyEntityWriter() {
+
+                        @Override
+                        public void write(Object object) {
+                            writer.get().write(object);
+                        }
+
+                        @Override
+                        public void writeAndFlush(Object object) {
+                            writer.get().writeAndFlush(object);
+                        }
+
+                        @Override
+                        public void flush() throws IOException {
+                            writer.get().flush();
+                            flushLatch.countDown();
+                        }
+
+                        @Override
+                        public ChunkedInput<ByteBuf> getChunkedInput() {
+                            for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
+                                // caught from catch block in executorService.execute(new Runnable()
+                                // {
+                                // "sleep" to simulate race condition
+                                if (element.getClassName().contains("NettyConnector")
+                                    && element.getMethodName().equals("run")) {
+                                    try {
+                                        flushLatch.await();
+                                    } catch (InterruptedException e) {
+                                        throw new RuntimeException(e);
                                     }
                                 }
-                                return writer.get().getChunkedInput();
                             }
+                            return writer.get().getChunkedInput();
+                        }
 
-                            @Override
-                            public OutputStream getOutputStream() {
-                                return writer.get().getOutputStream();
-                            }
+                        @Override
+                        public OutputStream getOutputStream() {
+                            return writer.get().getOutputStream();
+                        }
 
-                            @Override
-                            public long getLength() {
-                                return writer.get().getLength();
-                            }
+                        @Override
+                        public long getLength() {
+                            return writer.get().getLength();
+                        }
 
-                            @Override
-                            public Type getType() {
-                                return writer.get().getType();
-                            }
-                        };
-                    }
-                };
-            }
-        };
-
-        Client client = initClient(provider);
-        try {
-            Response r = client
-                    .register(new MultipartWriter())
-                    .target(target().getUri()).request()
-                    .post(Entity.entity(new MultipartWriter(), MediaType.MULTIPART_FORM_DATA_TYPE));
-        } catch (ProcessingException expected) {
-
-        }
-        writerSetLatch.await();
-        try {
-            return writer.get().getChunkedInput().isEndOfInput();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
+                        @Override
+                        public Type getType() {
+                            return writer.get().getType();
+                        }
+                    };
+                }
+            };
         }
     }
 
@@ -170,11 +187,10 @@
         }
 
         @Override
-        public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
-                            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
-                WebApplicationException {
+        public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations,
+            MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
+            throws IOException, WebApplicationException {
             throw new IllegalArgumentException("TestException");
         }
     }
-
 }
diff --git a/test-framework/core/src/main/java/org/glassfish/jersey/test/spi/TestHelper.java b/test-framework/core/src/main/java/org/glassfish/jersey/test/spi/TestHelper.java
index 0aabca5..080cbdc 100644
--- a/test-framework/core/src/main/java/org/glassfish/jersey/test/spi/TestHelper.java
+++ b/test-framework/core/src/main/java/org/glassfish/jersey/test/spi/TestHelper.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
  * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -21,6 +22,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import org.junit.jupiter.api.DynamicContainer;
 import org.junit.jupiter.api.DynamicTest;
@@ -32,6 +35,7 @@
  * @author Michal Gajdos
  */
 public final class TestHelper {
+    private static final Logger LOG = Logger.getLogger(TestHelper.class.getName());
 
     /**
      * Create a human readable string from given URI. This method replaces {@code 0} port (start container at first available
@@ -65,6 +69,7 @@
         for (Method method : testMethods) {
             children.add(DynamicTest.dynamicTest(method.getName(), () -> {
                 try {
+                    LOG.log(Level.INFO, "Invoking test " + displayName + "." + method.getName());
                     for (Method beforeEachMethod : beforeEachMethods) {
                         beforeEachMethod.invoke(test);
                     }