Skip to content

Commit 092cae8

Browse files
authored
Add tests to check that an entity's contents is closed or not when an (#740)
HTTPException or AssertionError is thrown
1 parent 26d51f6 commit 092cae8

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestClassicOverAsync.java

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,31 +27,90 @@
2727
package org.apache.hc.client5.testing.async;
2828

2929
import java.io.IOException;
30+
import java.io.InputStream;
3031
import java.util.Queue;
3132
import java.util.Random;
3233
import java.util.concurrent.ConcurrentLinkedQueue;
3334
import java.util.concurrent.CountDownLatch;
3435
import java.util.concurrent.ExecutorService;
3536
import java.util.concurrent.Executors;
3637
import java.util.concurrent.Future;
38+
import java.util.concurrent.atomic.AtomicReference;
3739

40+
import org.apache.commons.io.input.ProxyInputStream;
41+
import org.apache.hc.client5.http.ClientProtocolException;
3842
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
3943
import org.apache.hc.client5.testing.Result;
4044
import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel;
4145
import org.apache.hc.client5.testing.extension.async.ServerProtocolLevel;
4246
import org.apache.hc.core5.http.ClassicHttpRequest;
4347
import org.apache.hc.core5.http.ContentType;
48+
import org.apache.hc.core5.http.HttpEntity;
49+
import org.apache.hc.core5.http.HttpException;
4450
import org.apache.hc.core5.http.HttpHost;
4551
import org.apache.hc.core5.http.URIScheme;
4652
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
4753
import org.apache.hc.core5.http.io.entity.EntityUtils;
54+
import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
4855
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
4956
import org.junit.jupiter.api.Assertions;
5057
import org.junit.jupiter.params.ParameterizedTest;
5158
import org.junit.jupiter.params.provider.ValueSource;
5259

5360
public abstract class TestClassicOverAsync extends AbstractClassicOverAsyncIntegrationTestBase {
5461

62+
/**
63+
* Mocks an InputStream that records whether it has been closed.
64+
*/
65+
private static final class MockInputStream extends ProxyInputStream {
66+
67+
boolean closed;
68+
69+
public MockInputStream(final InputStream proxy) {
70+
super(proxy);
71+
}
72+
73+
@Override
74+
public synchronized void close() throws IOException {
75+
super.close();
76+
closed = true;
77+
}
78+
79+
}
80+
81+
/**
82+
* Mocks an HttpEntity that records whether it has been closed.
83+
*/
84+
private static final class MockHttpEntityWrapper extends HttpEntityWrapper {
85+
86+
boolean closed;
87+
final boolean streaming;
88+
final InputStream content;
89+
90+
public MockHttpEntityWrapper(final HttpEntity wrappedEntity, final boolean streaming) throws UnsupportedOperationException, IOException {
91+
super(wrappedEntity);
92+
this.streaming = streaming;
93+
this.content = new MockInputStream(wrappedEntity.getContent());
94+
}
95+
96+
@Override
97+
public synchronized void close() throws IOException {
98+
super.close();
99+
closed = true;
100+
}
101+
102+
@Override
103+
public synchronized InputStream getContent() throws IOException {
104+
return content;
105+
}
106+
107+
@Override
108+
public boolean isStreaming() {
109+
return streaming;
110+
}
111+
112+
}
113+
55114
public TestClassicOverAsync(final URIScheme scheme,
56115
final ClientProtocolLevel clientProtocolLevel,
57116
final ServerProtocolLevel serverProtocolLevel) {
@@ -84,6 +143,113 @@ void testSequentialGetRequests(final int contentSize) throws Exception {
84143
}
85144
}
86145

146+
/**
147+
* Tests that the response entity is the client when an AssertionError is thrown from the response handler.
148+
* The content stream is <strong>not closed</strong> because the entity is non-streaming.
149+
*/
150+
@ValueSource(ints = {0, 2048, 10240})
151+
@ParameterizedTest(name = "{displayName}; content length: {0}")
152+
void testSequentialGetRequestsAssertionError(final int contentSize) throws Exception {
153+
configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
154+
final HttpHost target = startServer();
155+
final CloseableHttpClient client = startClient();
156+
final int n = 10;
157+
final String detailMessage = "Simulated unit test failure";
158+
for (int i = 0; i < n; i++) {
159+
final AtomicReference<MockHttpEntityWrapper> entityRef = new AtomicReference<>();
160+
final AtomicReference<MockInputStream> contentRef = new AtomicReference<>();
161+
try {
162+
entityRef.set(null);
163+
client.execute(
164+
ClassicRequestBuilder.get()
165+
.setHttpHost(target)
166+
.setPath("/random/" + contentSize)
167+
.build(),
168+
response -> {
169+
final MockHttpEntityWrapper entity = new MockHttpEntityWrapper(response.getEntity(), false);
170+
entityRef.set(entity);
171+
contentRef.set((MockInputStream) entity.getContent());
172+
response.setEntity(entityRef.get());
173+
throw new AssertionError(detailMessage);
174+
});
175+
Assertions.fail("AssertionError expected from execute()");
176+
} catch (final AssertionError e) {
177+
// Note that we can't use Assertions.assertThrows() because it doesn't catch AssertionError.
178+
Assertions.assertEquals(detailMessage, e.getMessage());
179+
}
180+
Assertions.assertNotNull(entityRef.get());
181+
Assertions.assertTrue(entityRef.get().closed);
182+
Assertions.assertFalse(contentRef.get().closed);
183+
}
184+
}
185+
186+
/**
187+
* Tests that the response entity is the client when an HttpException is thrown from the response handler.
188+
* The content stream is <strong>not closed</strong> because the entity is non-streaming.
189+
*/
190+
@ValueSource(ints = {0, 2048, 10240})
191+
@ParameterizedTest(name = "{displayName}; content length: {0}")
192+
void testSequentialGetRequestsHttpException(final int contentSize) throws Exception {
193+
configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
194+
final HttpHost target = startServer();
195+
final CloseableHttpClient client = startClient();
196+
final int n = 10;
197+
final String detailMessage = "Simulated HttpException failure";
198+
for (int i = 0; i < n; i++) {
199+
final AtomicReference<MockHttpEntityWrapper> entityRef = new AtomicReference<>();
200+
final AtomicReference<MockInputStream> contentRef = new AtomicReference<>();
201+
final ClientProtocolException e = Assertions.assertThrows(ClientProtocolException.class, () -> {
202+
client.execute(
203+
ClassicRequestBuilder.get()
204+
.setHttpHost(target)
205+
.setPath("/random/" + contentSize)
206+
.build(),
207+
response -> {
208+
final MockHttpEntityWrapper entity = new MockHttpEntityWrapper(response.getEntity(), false);
209+
entityRef.set(entity);
210+
contentRef.set((MockInputStream) entity.getContent());
211+
response.setEntity(entityRef.get());
212+
throw new HttpException(detailMessage);
213+
});
214+
});
215+
// If an HttpException is thrown from the handler, and the entity is non-streaming, the stream is left open.
216+
Assertions.assertEquals(detailMessage, e.getCause().getMessage());
217+
Assertions.assertTrue(entityRef.get().closed);
218+
Assertions.assertFalse(contentRef.get().closed);
219+
}
220+
}
221+
222+
/**
223+
* Tests that the response entity and its content are closed by the client.
224+
*/
225+
@ValueSource(ints = {0, 2048, 10240})
226+
@ParameterizedTest(name = "{displayName}; content length: {0}")
227+
void testSequentialGetRequestsStreaming(final int contentSize) throws Exception {
228+
configureServer(bootstrap -> bootstrap.register("/random/*", AsyncRandomHandler::new));
229+
final HttpHost target = startServer();
230+
final CloseableHttpClient client = startClient();
231+
final int n = 10;
232+
for (int i = 0; i < n; i++) {
233+
final AtomicReference<MockInputStream> contentRef = new AtomicReference<>();
234+
final MockHttpEntityWrapper result = (MockHttpEntityWrapper) client.execute(
235+
ClassicRequestBuilder.get()
236+
.setHttpHost(target)
237+
.setPath("/random/" + contentSize)
238+
.build(),
239+
response -> {
240+
Assertions.assertEquals(200, response.getCode());
241+
final MockHttpEntityWrapper entity = new MockHttpEntityWrapper(response.getEntity(), true);
242+
contentRef.set((MockInputStream) entity.getContent());
243+
response.setEntity(entity);
244+
return response.getEntity();
245+
});
246+
// The plain use case where the entity and its input stream are closed.
247+
Assertions.assertTrue(result.closed);
248+
Assertions.assertTrue(contentRef.get().closed);
249+
}
250+
}
251+
252+
87253
@ValueSource(ints = {0, 2048, 10240})
88254
@ParameterizedTest(name = "{displayName}; content length: {0}")
89255
void testConcurrentGetRequests(final int contentSize) throws Exception {

0 commit comments

Comments
 (0)