72de24e59a
Reviewed-by: dfuchs
868 lines
35 KiB
Java
868 lines
35 KiB
Java
/*
|
|
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8252374
|
|
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
|
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters
|
|
* ReferenceTracker AggregateRequestBodyTest
|
|
* @run testng/othervm -Djdk.internal.httpclient.debug=true
|
|
* -Djdk.httpclient.HttpClient.log=requests,responses,errors
|
|
* AggregateRequestBodyTest
|
|
* @summary Tests HttpRequest.BodyPublishers::concat
|
|
*/
|
|
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpRequest.BodyPublisher;
|
|
import java.net.http.HttpRequest.BodyPublishers;
|
|
import java.net.http.HttpResponse;
|
|
import java.net.http.HttpResponse.BodyHandlers;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Arrays;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionException;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.Flow;
|
|
import java.util.concurrent.Flow.Subscriber;
|
|
import java.util.concurrent.Flow.Subscription;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Supplier;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.LongStream;
|
|
import java.util.stream.Stream;
|
|
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
|
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
|
import javax.net.ssl.SSLContext;
|
|
|
|
import com.sun.net.httpserver.HttpServer;
|
|
import com.sun.net.httpserver.HttpsConfigurator;
|
|
import com.sun.net.httpserver.HttpsServer;
|
|
import jdk.test.lib.net.SimpleSSLContext;
|
|
import org.testng.Assert;
|
|
import org.testng.ITestContext;
|
|
import org.testng.ITestResult;
|
|
import org.testng.SkipException;
|
|
import org.testng.annotations.AfterClass;
|
|
import org.testng.annotations.AfterTest;
|
|
import org.testng.annotations.BeforeMethod;
|
|
import org.testng.annotations.BeforeTest;
|
|
import org.testng.annotations.DataProvider;
|
|
import org.testng.annotations.Test;
|
|
|
|
import static java.lang.System.out;
|
|
import static java.net.http.HttpClient.Version.HTTP_1_1;
|
|
import static java.net.http.HttpClient.Version.HTTP_2;
|
|
import static org.testng.Assert.assertEquals;
|
|
import static org.testng.Assert.assertFalse;
|
|
import static org.testng.Assert.assertTrue;
|
|
import static org.testng.Assert.expectThrows;
|
|
|
|
public class AggregateRequestBodyTest implements HttpServerAdapters {
|
|
|
|
SSLContext sslContext;
|
|
HttpTestServer http1TestServer; // HTTP/1.1 ( http )
|
|
HttpTestServer https1TestServer; // HTTPS/1.1 ( https )
|
|
HttpTestServer http2TestServer; // HTTP/2 ( h2c )
|
|
HttpTestServer https2TestServer; // HTTP/2 ( h2 )
|
|
String http1URI;
|
|
String https1URI;
|
|
String http2URI;
|
|
String https2URI;
|
|
|
|
static final int RESPONSE_CODE = 200;
|
|
static final int ITERATION_COUNT = 4;
|
|
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
|
|
static final Class<CompletionException> CE = CompletionException.class;
|
|
// a shared executor helps reduce the amount of threads created by the test
|
|
static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
|
|
static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
|
|
static volatile boolean tasksFailed;
|
|
static final AtomicLong serverCount = new AtomicLong();
|
|
static final AtomicLong clientCount = new AtomicLong();
|
|
static final long start = System.nanoTime();
|
|
public static String now() {
|
|
long now = System.nanoTime() - start;
|
|
long secs = now / 1000_000_000;
|
|
long mill = (now % 1000_000_000) / 1000_000;
|
|
long nan = now % 1000_000;
|
|
return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
|
|
}
|
|
|
|
final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
|
|
private volatile HttpClient sharedClient;
|
|
|
|
static class TestExecutor implements Executor {
|
|
final AtomicLong tasks = new AtomicLong();
|
|
Executor executor;
|
|
TestExecutor(Executor executor) {
|
|
this.executor = executor;
|
|
}
|
|
|
|
@Override
|
|
public void execute(Runnable command) {
|
|
long id = tasks.incrementAndGet();
|
|
executor.execute(() -> {
|
|
try {
|
|
command.run();
|
|
} catch (Throwable t) {
|
|
tasksFailed = true;
|
|
System.out.printf(now() + "Task %s failed: %s%n", id, t);
|
|
System.err.printf(now() + "Task %s failed: %s%n", id, t);
|
|
FAILURES.putIfAbsent("Task " + id, t);
|
|
throw t;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
protected boolean stopAfterFirstFailure() {
|
|
return Boolean.getBoolean("jdk.internal.httpclient.debug");
|
|
}
|
|
|
|
final AtomicReference<SkipException> skiptests = new AtomicReference<>();
|
|
void checkSkip() {
|
|
var skip = skiptests.get();
|
|
if (skip != null) throw skip;
|
|
}
|
|
static String name(ITestResult result) {
|
|
var params = result.getParameters();
|
|
return result.getName()
|
|
+ (params == null ? "()" : Arrays.toString(result.getParameters()));
|
|
}
|
|
|
|
@BeforeMethod
|
|
void beforeMethod(ITestContext context) {
|
|
if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) {
|
|
if (skiptests.get() == null) {
|
|
SkipException skip = new SkipException("some tests failed");
|
|
skip.setStackTrace(new StackTraceElement[0]);
|
|
skiptests.compareAndSet(null, skip);
|
|
}
|
|
}
|
|
}
|
|
|
|
@AfterClass
|
|
static final void printFailedTests(ITestContext context) {
|
|
out.println("\n=========================");
|
|
try {
|
|
var failed = context.getFailedTests().getAllResults().stream()
|
|
.collect(Collectors.toMap(r -> name(r), ITestResult::getThrowable));
|
|
FAILURES.putAll(failed);
|
|
|
|
out.printf("%n%sCreated %d servers and %d clients%n",
|
|
now(), serverCount.get(), clientCount.get());
|
|
if (FAILURES.isEmpty()) return;
|
|
out.println("Failed tests: ");
|
|
FAILURES.entrySet().forEach((e) -> {
|
|
out.printf("\t%s: %s%n", e.getKey(), e.getValue());
|
|
e.getValue().printStackTrace(out);
|
|
e.getValue().printStackTrace();
|
|
});
|
|
if (tasksFailed) {
|
|
System.out.println("WARNING: Some tasks failed");
|
|
}
|
|
} finally {
|
|
out.println("\n=========================\n");
|
|
}
|
|
}
|
|
|
|
private String[] uris() {
|
|
return new String[] {
|
|
http1URI,
|
|
https1URI,
|
|
http2URI,
|
|
https2URI,
|
|
};
|
|
}
|
|
|
|
static AtomicLong URICOUNT = new AtomicLong();
|
|
|
|
@DataProvider(name = "variants")
|
|
public Object[][] variants(ITestContext context) {
|
|
if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) {
|
|
return new Object[0][];
|
|
}
|
|
String[] uris = uris();
|
|
Object[][] result = new Object[uris.length * 2][];
|
|
int i = 0;
|
|
for (boolean sameClient : List.of(false, true)) {
|
|
for (String uri : uris()) {
|
|
result[i++] = new Object[]{uri, sameClient};
|
|
}
|
|
}
|
|
assert i == uris.length * 2;
|
|
return result;
|
|
}
|
|
|
|
private HttpClient makeNewClient() {
|
|
clientCount.incrementAndGet();
|
|
HttpClient client = HttpClient.newBuilder()
|
|
.proxy(HttpClient.Builder.NO_PROXY)
|
|
.executor(executor)
|
|
.sslContext(sslContext)
|
|
.build();
|
|
return TRACKER.track(client);
|
|
}
|
|
|
|
HttpClient newHttpClient(boolean share) {
|
|
if (!share) return makeNewClient();
|
|
HttpClient shared = sharedClient;
|
|
if (shared != null) return shared;
|
|
synchronized (this) {
|
|
shared = sharedClient;
|
|
if (shared == null) {
|
|
shared = sharedClient = makeNewClient();
|
|
}
|
|
return shared;
|
|
}
|
|
}
|
|
|
|
static final List<String> BODIES = List.of(
|
|
"Lorem ipsum",
|
|
"dolor sit amet",
|
|
"consectetur adipiscing elit, sed do eiusmod tempor",
|
|
"quis nostrud exercitation ullamco",
|
|
"laboris nisi",
|
|
"ut",
|
|
"aliquip ex ea commodo consequat." +
|
|
"Duis aute irure dolor in reprehenderit in voluptate velit esse" +
|
|
"cillum dolore eu fugiat nulla pariatur.",
|
|
"Excepteur sint occaecat cupidatat non proident."
|
|
);
|
|
|
|
static BodyPublisher[] publishers(String... content) {
|
|
if (content == null) return null;
|
|
BodyPublisher[] result = new BodyPublisher[content.length];
|
|
for (int i=0; i < content.length ; i++) {
|
|
result[i] = content[i] == null ? null : BodyPublishers.ofString(content[i]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static String[] strings(String... s) {
|
|
return s;
|
|
}
|
|
|
|
@DataProvider(name = "sparseContent")
|
|
Object[][] nulls() {
|
|
return new Object[][] {
|
|
{"null array", null},
|
|
{"null element", strings((String)null)},
|
|
{"null first element", strings(null, "one")},
|
|
{"null second element", strings( "one", null)},
|
|
{"null third element", strings( "one", "two", null)},
|
|
{"null fourth element", strings( "one", "two", "three", null)},
|
|
{"null random element", strings( "one", "two", "three", null, "five")},
|
|
};
|
|
}
|
|
|
|
static List<Long> lengths(long... lengths) {
|
|
return LongStream.of(lengths)
|
|
.mapToObj(Long::valueOf)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
@DataProvider(name = "contentLengths")
|
|
Object[][] contentLengths() {
|
|
return new Object[][] {
|
|
{-1, lengths(-1)},
|
|
{-42, lengths(-42)},
|
|
{42, lengths(42)},
|
|
{42, lengths(10, 0, 20, 0, 12)},
|
|
{-1, lengths(10, 0, 20, -1, 12)},
|
|
{-1, lengths(-1, 0, 20, 10, 12)},
|
|
{-1, lengths(10, 0, 20, 12, -1)},
|
|
{-1, lengths(10, 0, 20, -10, 12)},
|
|
{-1, lengths(-10, 0, 20, 10, 12)},
|
|
{-1, lengths(10, 0, 20, 12, -10)},
|
|
{-1, lengths(10, 0, Long.MIN_VALUE, -1, 12)},
|
|
{-1, lengths(-1, 0, Long.MIN_VALUE, 10, 12)},
|
|
{-1, lengths(10, Long.MIN_VALUE, 20, 12, -1)},
|
|
{Long.MAX_VALUE, lengths(10, Long.MAX_VALUE - 42L, 20, 0, 12)},
|
|
{-1, lengths(10, Long.MAX_VALUE - 40L, 20, 0, 12)},
|
|
{-1, lengths(10, Long.MAX_VALUE - 12L, 20, 0, 12)},
|
|
{-1, lengths(10, Long.MAX_VALUE/2L, Long.MAX_VALUE/2L + 1L, 0, 12)},
|
|
{-1, lengths(10, Long.MAX_VALUE/2L, -1, Long.MAX_VALUE/2L + 1L, 12)},
|
|
{-1, lengths(10, Long.MAX_VALUE, 12, Long.MAX_VALUE, 20)},
|
|
{-1, lengths(10, Long.MAX_VALUE, Long.MAX_VALUE, 12, 20)},
|
|
{-1, lengths(0, Long.MAX_VALUE, Long.MAX_VALUE, 12, 20)},
|
|
{-1, lengths(Long.MAX_VALUE, Long.MAX_VALUE, 12, 0, 20)}
|
|
};
|
|
}
|
|
|
|
@DataProvider(name="negativeRequests")
|
|
Object[][] negativeRequests() {
|
|
return new Object[][] {
|
|
{0L}, {-1L}, {-2L}, {Long.MIN_VALUE + 1L}, {Long.MIN_VALUE}
|
|
};
|
|
}
|
|
|
|
|
|
static class ContentLengthPublisher implements BodyPublisher {
|
|
final long length;
|
|
ContentLengthPublisher(long length) {
|
|
this.length = length;
|
|
}
|
|
@Override
|
|
public long contentLength() {
|
|
return length;
|
|
}
|
|
|
|
@Override
|
|
public void subscribe(Subscriber<? super ByteBuffer> subscriber) {
|
|
}
|
|
|
|
static ContentLengthPublisher[] of(List<Long> lengths) {
|
|
return lengths.stream()
|
|
.map(ContentLengthPublisher::new)
|
|
.toArray(ContentLengthPublisher[]::new);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A dummy publisher that allows to call onError on its subscriber (or not...).
|
|
*/
|
|
static class PublishWithError implements BodyPublisher {
|
|
final ConcurrentHashMap<Subscriber<?>, ErrorSubscription> subscribers = new ConcurrentHashMap<>();
|
|
final long length;
|
|
final List<String> content;
|
|
final int errorAt;
|
|
final Supplier<? extends Throwable> errorSupplier;
|
|
PublishWithError(List<String> content, int errorAt, Supplier<? extends Throwable> supplier) {
|
|
this.content = content;
|
|
this.errorAt = errorAt;
|
|
this.errorSupplier = supplier;
|
|
length = content.stream().mapToInt(String::length).sum();
|
|
}
|
|
|
|
boolean hasErrors() {
|
|
return errorAt < content.size();
|
|
}
|
|
|
|
@Override
|
|
public long contentLength() {
|
|
return length;
|
|
}
|
|
|
|
@Override
|
|
public void subscribe(Subscriber<? super ByteBuffer> subscriber) {
|
|
ErrorSubscription subscription = new ErrorSubscription(subscriber);
|
|
subscribers.put(subscriber, subscription);
|
|
subscriber.onSubscribe(subscription);
|
|
}
|
|
|
|
class ErrorSubscription implements Flow.Subscription {
|
|
volatile boolean cancelled;
|
|
volatile int at;
|
|
final Subscriber<? super ByteBuffer> subscriber;
|
|
ErrorSubscription(Subscriber<? super ByteBuffer> subscriber) {
|
|
this.subscriber = subscriber;
|
|
}
|
|
@Override
|
|
public void request(long n) {
|
|
while (!cancelled && --n >= 0 && at < Math.min(errorAt+1, content.size())) {
|
|
if (at++ == errorAt) {
|
|
subscriber.onError(errorSupplier.get());
|
|
return;
|
|
} else if (at <= content.size()){
|
|
subscriber.onNext(ByteBuffer.wrap(
|
|
content.get(at-1).getBytes()));
|
|
if (at == content.size()) {
|
|
subscriber.onComplete();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void cancel() {
|
|
cancelled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static class RequestSubscriber implements Flow.Subscriber<ByteBuffer> {
|
|
CompletableFuture<Subscription> subscriptionCF = new CompletableFuture<>();
|
|
ConcurrentLinkedDeque<ByteBuffer> items = new ConcurrentLinkedDeque<>();
|
|
CompletableFuture<List<ByteBuffer>> resultCF = new CompletableFuture<>();
|
|
|
|
@Override
|
|
public void onSubscribe(Subscription subscription) {
|
|
this.subscriptionCF.complete(subscription);
|
|
}
|
|
|
|
@Override
|
|
public void onNext(ByteBuffer item) {
|
|
items.addLast(item);
|
|
}
|
|
|
|
@Override
|
|
public void onError(Throwable throwable) {
|
|
resultCF.completeExceptionally(throwable);
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
resultCF.complete(items.stream().collect(Collectors.toUnmodifiableList()));
|
|
}
|
|
|
|
CompletableFuture<List<ByteBuffer>> resultCF() { return resultCF; }
|
|
}
|
|
|
|
static String stringFromBuffer(ByteBuffer buffer) {
|
|
byte[] bytes = new byte[buffer.remaining()];
|
|
buffer.get(bytes);
|
|
return new String(bytes);
|
|
}
|
|
|
|
String stringFromBytes(Stream<ByteBuffer> buffers) {
|
|
return buffers.map(AggregateRequestBodyTest::stringFromBuffer)
|
|
.collect(Collectors.joining());
|
|
}
|
|
|
|
static PublishWithError withNoError(String content) {
|
|
return new PublishWithError(List.of(content), 1,
|
|
() -> new AssertionError("Should not happen!"));
|
|
}
|
|
|
|
static PublishWithError withNoError(List<String> content) {
|
|
return new PublishWithError(content, content.size(),
|
|
() -> new AssertionError("Should not happen!"));
|
|
}
|
|
|
|
@Test(dataProvider = "sparseContent") // checks that NPE is thrown
|
|
public void testNullPointerException(String description, String[] content) {
|
|
checkSkip();
|
|
BodyPublisher[] publishers = publishers(content);
|
|
Assert.assertThrows(NullPointerException.class, () -> BodyPublishers.concat(publishers));
|
|
}
|
|
|
|
// Verifies that an empty array creates a "noBody" publisher
|
|
@Test
|
|
public void testEmpty() {
|
|
checkSkip();
|
|
BodyPublisher publisher = BodyPublishers.concat();
|
|
RequestSubscriber subscriber = new RequestSubscriber();
|
|
assertEquals(publisher.contentLength(), 0);
|
|
publisher.subscribe(subscriber);
|
|
subscriber.subscriptionCF.thenAccept(s -> s.request(1));
|
|
List<ByteBuffer> result = subscriber.resultCF.join();
|
|
assertEquals(result, List.of());
|
|
assertTrue(subscriber.items.isEmpty());
|
|
}
|
|
|
|
// verifies that error emitted by upstream publishers are propagated downstream.
|
|
@Test(dataProvider = "sparseContent") // nulls are replaced with error publisher
|
|
public void testOnError(String description, String[] content) {
|
|
checkSkip();
|
|
final RequestSubscriber subscriber = new RequestSubscriber();
|
|
final PublishWithError errorPublisher;
|
|
final BodyPublisher[] publishers;
|
|
String result = BODIES.stream().collect(Collectors.joining());
|
|
if (content == null) {
|
|
content = List.of(result).toArray(String[]::new);
|
|
errorPublisher = new PublishWithError(BODIES, BODIES.size(),
|
|
() -> new AssertionError("Unexpected!!"));
|
|
publishers = List.of(errorPublisher).toArray(new BodyPublisher[0]);
|
|
description = "No error";
|
|
} else {
|
|
publishers = publishers(content);
|
|
description = description.replace("null", "error at");
|
|
errorPublisher = new PublishWithError(BODIES, 2, () -> new Exception("expected"));
|
|
}
|
|
result = "";
|
|
boolean hasErrors = false;
|
|
for (int i=0; i < content.length; i++) {
|
|
if (content[i] == null) {
|
|
publishers[i] = errorPublisher;
|
|
if (hasErrors) continue;
|
|
if (!errorPublisher.hasErrors()) {
|
|
result = result + errorPublisher
|
|
.content.stream().collect(Collectors.joining());
|
|
} else {
|
|
result = result + errorPublisher.content
|
|
.stream().limit(errorPublisher.errorAt)
|
|
.collect(Collectors.joining());
|
|
result = result + "<error>";
|
|
hasErrors = true;
|
|
}
|
|
} else if (!hasErrors) {
|
|
result = result + content[i];
|
|
}
|
|
}
|
|
BodyPublisher publisher = BodyPublishers.concat(publishers);
|
|
publisher.subscribe(subscriber);
|
|
subscriber.subscriptionCF.thenAccept(s -> s.request(Long.MAX_VALUE));
|
|
if (errorPublisher.hasErrors()) {
|
|
CompletionException ce = expectThrows(CompletionException.class,
|
|
() -> subscriber.resultCF.join());
|
|
out.println(description + ": got expected " + ce);
|
|
assertEquals(ce.getCause().getClass(), Exception.class);
|
|
assertEquals(stringFromBytes(subscriber.items.stream()) + "<error>", result);
|
|
} else {
|
|
assertEquals(stringFromBytes(subscriber.resultCF.join().stream()), result);
|
|
out.println(description + ": got expected result: " + result);
|
|
}
|
|
}
|
|
|
|
// Verifies that if an upstream publisher has an unknown length, the
|
|
// aggregate publisher will have an unknown length as well. Otherwise
|
|
// the length should be known.
|
|
@Test(dataProvider = "sparseContent") // nulls are replaced with unknown length
|
|
public void testUnknownContentLength(String description, String[] content) {
|
|
checkSkip();
|
|
if (content == null) {
|
|
content = BODIES.toArray(String[]::new);
|
|
description = "BODIES (known length)";
|
|
} else {
|
|
description = description.replace("null", "length(-1)");
|
|
}
|
|
BodyPublisher[] publishers = publishers(content);
|
|
BodyPublisher nolength = new BodyPublisher() {
|
|
final BodyPublisher missing = BodyPublishers.ofString("missing");
|
|
@Override
|
|
public long contentLength() { return -1; }
|
|
@Override
|
|
public void subscribe(Subscriber<? super ByteBuffer> subscriber) {
|
|
missing.subscribe(subscriber);
|
|
}
|
|
};
|
|
long length = 0;
|
|
for (int i=0; i < content.length; i++) {
|
|
if (content[i] == null) {
|
|
publishers[i] = nolength;
|
|
length = -1;
|
|
} else if (length >= 0) {
|
|
length += content[i].length();
|
|
}
|
|
}
|
|
out.printf("testUnknownContentLength(%s): %d%n", description, length);
|
|
BodyPublisher publisher = BodyPublishers.concat(publishers);
|
|
assertEquals(publisher.contentLength(), length,
|
|
description.replace("null", "length(-1)"));
|
|
}
|
|
|
|
private static final Throwable completionCause(CompletionException x) {
|
|
while (x.getCause() instanceof CompletionException) {
|
|
x = (CompletionException)x.getCause();
|
|
}
|
|
return x.getCause();
|
|
}
|
|
|
|
@Test(dataProvider = "negativeRequests")
|
|
public void testNegativeRequest(long n) {
|
|
checkSkip();
|
|
assert n <= 0 : "test for negative request called with n > 0 : " + n;
|
|
BodyPublisher[] publishers = ContentLengthPublisher.of(List.of(1L, 2L, 3L));
|
|
BodyPublisher publisher = BodyPublishers.concat(publishers);
|
|
RequestSubscriber subscriber = new RequestSubscriber();
|
|
publisher.subscribe(subscriber);
|
|
Subscription subscription = subscriber.subscriptionCF.join();
|
|
subscription.request(n);
|
|
CompletionException expected = expectThrows(CE, () -> subscriber.resultCF.join());
|
|
Throwable cause = completionCause(expected);
|
|
if (cause instanceof IllegalArgumentException) {
|
|
System.out.printf("Got expected IAE for %d: %s%n", n, cause);
|
|
} else {
|
|
throw new AssertionError("Unexpected exception: " + cause,
|
|
(cause == null) ? expected : cause);
|
|
}
|
|
}
|
|
|
|
static BodyPublisher[] ofStrings(String... strings) {
|
|
return Stream.of(strings).map(BodyPublishers::ofString).toArray(BodyPublisher[]::new);
|
|
}
|
|
|
|
@Test
|
|
public void testPositiveRequests() {
|
|
checkSkip();
|
|
// A composite array of publishers
|
|
BodyPublisher[] publishers = Stream.of(
|
|
Stream.of(ofStrings("Lorem", " ", "ipsum", " ")),
|
|
Stream.of(BodyPublishers.concat(ofStrings("dolor", " ", "sit", " ", "amet", ", "))),
|
|
Stream.<BodyPublisher>of(withNoError(List.of("consectetur", " ", "adipiscing"))),
|
|
Stream.of(ofStrings(" ")),
|
|
Stream.of(BodyPublishers.concat(ofStrings("elit", ".")))
|
|
).flatMap((s) -> s).toArray(BodyPublisher[]::new);
|
|
BodyPublisher publisher = BodyPublishers.concat(publishers);
|
|
|
|
// Test that we can request all 13 items in a single request call.
|
|
RequestSubscriber requestSubscriber1 = new RequestSubscriber();
|
|
publisher.subscribe(requestSubscriber1);
|
|
Subscription subscription1 = requestSubscriber1.subscriptionCF.join();
|
|
subscription1.request(16);
|
|
assertTrue(requestSubscriber1.resultCF().isDone());
|
|
List<ByteBuffer> list1 = requestSubscriber1.resultCF().join();
|
|
String result1 = stringFromBytes(list1.stream());
|
|
assertEquals(result1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
|
|
System.out.println("Got expected sentence with one request: \"%s\"".formatted(result1));
|
|
|
|
// Test that we can split our requests call any which way we want
|
|
// (whether in the 'middle of a publisher' or at the boundaries.
|
|
RequestSubscriber requestSubscriber2 = new RequestSubscriber();
|
|
publisher.subscribe(requestSubscriber2);
|
|
Subscription subscription2 = requestSubscriber2.subscriptionCF.join();
|
|
subscription2.request(1);
|
|
assertFalse(requestSubscriber2.resultCF().isDone());
|
|
subscription2.request(10);
|
|
assertFalse(requestSubscriber2.resultCF().isDone());
|
|
subscription2.request(4);
|
|
assertFalse(requestSubscriber2.resultCF().isDone());
|
|
subscription2.request(1);
|
|
assertTrue(requestSubscriber2.resultCF().isDone());
|
|
List<ByteBuffer> list2 = requestSubscriber2.resultCF().join();
|
|
String result2 = stringFromBytes(list2.stream());
|
|
assertEquals(result2, "Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
|
|
System.out.println("Got expected sentence with 4 requests: \"%s\"".formatted(result1));
|
|
}
|
|
|
|
@Test(dataProvider = "contentLengths")
|
|
public void testContentLength(long expected, List<Long> lengths) {
|
|
checkSkip();
|
|
BodyPublisher[] publishers = ContentLengthPublisher.of(lengths);
|
|
BodyPublisher aggregate = BodyPublishers.concat(publishers);
|
|
assertEquals(aggregate.contentLength(), expected,
|
|
"Unexpected result for %s".formatted(lengths));
|
|
}
|
|
|
|
// Verifies that cancelling the subscription ensure that downstream
|
|
// publishers are no longer subscribed etc...
|
|
@Test
|
|
public void testCancel() {
|
|
checkSkip();
|
|
BodyPublisher[] publishers = BODIES.stream()
|
|
.map(BodyPublishers::ofString)
|
|
.toArray(BodyPublisher[]::new);
|
|
BodyPublisher publisher = BodyPublishers.concat(publishers);
|
|
|
|
assertEquals(publisher.contentLength(),
|
|
BODIES.stream().mapToInt(String::length).sum());
|
|
Map<RequestSubscriber, String> subscribers = new LinkedHashMap<>();
|
|
|
|
for (int n=0; n < BODIES.size(); n++) {
|
|
|
|
String description = String.format(
|
|
"cancel after %d/%d onNext() invocations",
|
|
n, BODIES.size());
|
|
RequestSubscriber subscriber = new RequestSubscriber();
|
|
publisher.subscribe(subscriber);
|
|
Subscription subscription = subscriber.subscriptionCF.join();
|
|
subscribers.put(subscriber, description);
|
|
|
|
// receive half the data
|
|
for (int i = 0; i < n; i++) {
|
|
subscription.request(1);
|
|
ByteBuffer buffer = subscriber.items.pop();
|
|
}
|
|
|
|
// cancel subscription
|
|
subscription.cancel();
|
|
// request the rest...
|
|
subscription.request(Long.MAX_VALUE);
|
|
}
|
|
|
|
CompletableFuture[] results = subscribers.keySet()
|
|
.stream().map(RequestSubscriber::resultCF)
|
|
.toArray(CompletableFuture[]::new);
|
|
CompletableFuture<?> any = CompletableFuture.anyOf(results);
|
|
|
|
// subscription was cancelled, so nothing should be received...
|
|
try {
|
|
TimeoutException x = Assert.expectThrows(TimeoutException.class,
|
|
() -> any.get(5, TimeUnit.SECONDS));
|
|
out.println("Got expected " + x);
|
|
} finally {
|
|
subscribers.keySet().stream()
|
|
.filter(rs -> rs.resultCF.isDone())
|
|
.forEach(rs -> System.err.printf(
|
|
"Failed: %s completed with %s",
|
|
subscribers.get(rs), rs.resultCF));
|
|
}
|
|
Consumer<RequestSubscriber> check = (rs) -> {
|
|
Assert.assertTrue(rs.items.isEmpty(), subscribers.get(rs) + " has items");
|
|
Assert.assertFalse(rs.resultCF.isDone(), subscribers.get(rs) + " was not cancelled");
|
|
out.println(subscribers.get(rs) + ": PASSED");
|
|
};
|
|
subscribers.keySet().stream().forEach(check);
|
|
}
|
|
|
|
// Verifies that cancelling the subscription is propagated downstream
|
|
@Test
|
|
public void testCancelSubscription() {
|
|
checkSkip();
|
|
PublishWithError upstream = new PublishWithError(BODIES, BODIES.size(),
|
|
() -> new AssertionError("should not come here"));
|
|
BodyPublisher publisher = BodyPublishers.concat(upstream);
|
|
|
|
assertEquals(publisher.contentLength(),
|
|
BODIES.stream().mapToInt(String::length).sum());
|
|
Map<RequestSubscriber, String> subscribers = new LinkedHashMap<>();
|
|
|
|
for (int n=0; n < BODIES.size(); n++) {
|
|
|
|
String description = String.format(
|
|
"cancel after %d/%d onNext() invocations",
|
|
n, BODIES.size());
|
|
RequestSubscriber subscriber = new RequestSubscriber();
|
|
publisher.subscribe(subscriber);
|
|
Subscription subscription = subscriber.subscriptionCF.join();
|
|
subscribers.put(subscriber, description);
|
|
|
|
// receive half the data
|
|
for (int i = 0; i < n; i++) {
|
|
subscription.request(1);
|
|
ByteBuffer buffer = subscriber.items.pop();
|
|
}
|
|
|
|
// cancel subscription
|
|
subscription.cancel();
|
|
// request the rest...
|
|
subscription.request(Long.MAX_VALUE);
|
|
assertTrue(upstream.subscribers.get(subscriber).cancelled,
|
|
description + " upstream subscription not cancelled");
|
|
out.println(description + " upstream subscription was properly cancelled");
|
|
}
|
|
|
|
CompletableFuture[] results = subscribers.keySet()
|
|
.stream().map(RequestSubscriber::resultCF)
|
|
.toArray(CompletableFuture[]::new);
|
|
CompletableFuture<?> any = CompletableFuture.anyOf(results);
|
|
|
|
// subscription was cancelled, so nothing should be received...
|
|
try {
|
|
TimeoutException x = Assert.expectThrows(TimeoutException.class,
|
|
() -> any.get(5, TimeUnit.SECONDS));
|
|
out.println("Got expected " + x);
|
|
} finally {
|
|
subscribers.keySet().stream()
|
|
.filter(rs -> rs.resultCF.isDone())
|
|
.forEach(rs -> System.err.printf(
|
|
"Failed: %s completed with %s",
|
|
subscribers.get(rs), rs.resultCF));
|
|
}
|
|
Consumer<RequestSubscriber> check = (rs) -> {
|
|
Assert.assertTrue(rs.items.isEmpty(), subscribers.get(rs) + " has items");
|
|
Assert.assertFalse(rs.resultCF.isDone(), subscribers.get(rs) + " was not cancelled");
|
|
out.println(subscribers.get(rs) + ": PASSED");
|
|
};
|
|
subscribers.keySet().stream().forEach(check);
|
|
|
|
}
|
|
|
|
@Test(dataProvider = "variants")
|
|
public void test(String uri, boolean sameClient) throws Exception {
|
|
checkSkip();
|
|
System.out.println("Request to " + uri);
|
|
|
|
HttpClient client = newHttpClient(sameClient);
|
|
|
|
BodyPublisher publisher = BodyPublishers.concat(
|
|
BODIES.stream()
|
|
.map(BodyPublishers::ofString)
|
|
.toArray(HttpRequest.BodyPublisher[]::new)
|
|
);
|
|
HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
|
|
.POST(publisher)
|
|
.build();
|
|
for (int i = 0; i < ITERATION_COUNT; i++) {
|
|
System.out.println("Iteration: " + i);
|
|
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
|
|
int expectedResponse = RESPONSE_CODE;
|
|
if (response.statusCode() != expectedResponse)
|
|
throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode()));
|
|
assertEquals(response.body(), BODIES.stream().collect(Collectors.joining()));
|
|
}
|
|
System.out.println("test: DONE");
|
|
}
|
|
|
|
@BeforeTest
|
|
public void setup() throws Exception {
|
|
sslContext = new SimpleSSLContext().get();
|
|
if (sslContext == null)
|
|
throw new AssertionError("Unexpected null sslContext");
|
|
|
|
HttpTestHandler handler = new HttpTestEchoHandler();
|
|
http1TestServer = HttpTestServer.create(HTTP_1_1);
|
|
http1TestServer.addHandler(handler, "/http1/echo/");
|
|
http1URI = "http://" + http1TestServer.serverAuthority() + "/http1/echo/x";
|
|
|
|
https1TestServer = HttpTestServer.create(HTTP_1_1, sslContext);
|
|
https1TestServer.addHandler(handler, "/https1/echo/");
|
|
https1URI = "https://" + https1TestServer.serverAuthority() + "/https1/echo/x";
|
|
|
|
// HTTP/2
|
|
http2TestServer = HttpTestServer.create(HTTP_2);
|
|
http2TestServer.addHandler(handler, "/http2/echo/");
|
|
http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo/x";
|
|
|
|
https2TestServer = HttpTestServer.create(HTTP_2, sslContext);
|
|
https2TestServer.addHandler(handler, "/https2/echo/");
|
|
https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo/x";
|
|
|
|
serverCount.addAndGet(4);
|
|
http1TestServer.start();
|
|
https1TestServer.start();
|
|
http2TestServer.start();
|
|
https2TestServer.start();
|
|
}
|
|
|
|
@AfterTest
|
|
public void teardown() throws Exception {
|
|
String sharedClientName =
|
|
sharedClient == null ? null : sharedClient.toString();
|
|
sharedClient = null;
|
|
Thread.sleep(100);
|
|
AssertionError fail = TRACKER.check(500);
|
|
try {
|
|
http1TestServer.stop();
|
|
https1TestServer.stop();
|
|
http2TestServer.stop();
|
|
https2TestServer.stop();
|
|
} finally {
|
|
if (fail != null) {
|
|
if (sharedClientName != null) {
|
|
System.err.println("Shared client name is: " + sharedClientName);
|
|
}
|
|
throw fail;
|
|
}
|
|
}
|
|
}
|
|
}
|