From 7c209523d33c7fec43f64405b95081903d3e47e1 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sun, 6 Oct 2024 21:51:25 +0200 Subject: [PATCH] Migrated Docker based integration tests to TestContainers --- .github/workflows/maven.yml | 2 +- httpcore5-testing/docker-compose.yml | 33 -- httpcore5-testing/docker/BUILDING.txt | 31 -- .../docker/apache-httpd/Dockerfile | 34 -- httpcore5-testing/docker/httpbin/Dockerfile | 17 - httpcore5-testing/docker/nginx/Dockerfile | 30 -- httpcore5-testing/pom.xml | 48 +- .../compatibility/ContainerImages.java | 94 ++++ .../core5/testing/compatibility/Result.java | 80 ++++ .../compatibility/http1/HttpBinCompatIT.java | 229 +++++++++ .../http2/AbstractHttp2CompatIT.java | 227 +++++++++ .../http2/ApacheHttpDCompatIT.java | 63 +++ .../http2/H2CompatibilityTest.java | 439 ------------------ .../{HttpBinIT.java => NginxCompatIT.java} | 33 +- .../resources/docker/httpd}/httpd-vhosts.conf | 22 +- .../test/resources/docker/ngnix}/default.conf | 11 +- pom.xml | 11 + 17 files changed, 746 insertions(+), 658 deletions(-) delete mode 100644 httpcore5-testing/docker-compose.yml delete mode 100644 httpcore5-testing/docker/BUILDING.txt delete mode 100644 httpcore5-testing/docker/apache-httpd/Dockerfile delete mode 100644 httpcore5-testing/docker/httpbin/Dockerfile delete mode 100644 httpcore5-testing/docker/nginx/Dockerfile create mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java create mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/Result.java create mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http1/HttpBinCompatIT.java create mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/AbstractHttp2CompatIT.java create mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/ApacheHttpDCompatIT.java delete mode 100644 httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java rename httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/{HttpBinIT.java => NginxCompatIT.java} (65%) rename httpcore5-testing/{docker/apache-httpd => src/test/resources/docker/httpd}/httpd-vhosts.conf (75%) rename httpcore5-testing/{docker/nginx => src/test/resources/docker/ngnix}/default.conf (76%) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 187832dab4..6a88469d58 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -49,4 +49,4 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java }} - name: Build with Maven - run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains + run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains,docker diff --git a/httpcore5-testing/docker-compose.yml b/httpcore5-testing/docker-compose.yml deleted file mode 100644 index 2162cb1012..0000000000 --- a/httpcore5-testing/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: '3.5' - -services: - test-httpd: - container_name: "my-hc-tests-httpd" - image: "hc-tests-httpd:latest" - ports: - - "8080:80" - test-nginx: - container_name: "my-hc-tests-nginx" - image: "hc-tests-nginx:latest" - ports: - - "8081:80" - test-httpbin: - container_name: "my-hc-tests-httpbin" - image: "hc-tests-httpbin:latest" - ports: - - "8082:80" diff --git a/httpcore5-testing/docker/BUILDING.txt b/httpcore5-testing/docker/BUILDING.txt deleted file mode 100644 index ed734fa22b..0000000000 --- a/httpcore5-testing/docker/BUILDING.txt +++ /dev/null @@ -1,31 +0,0 @@ -Building Docker containers for compatibility tests -======================================================== - -Remark: omit sudo command if executing as root - -= Build Apache HTTPD image ---- -sudo docker build -t hc-tests-httpd apache-httpd ---- - -= Build Nginx image ---- -sudo docker build -t hc-tests-nginx nginx ---- - -= Build HTTPBIN image ---- -sudo docker build -t hc-tests-httpbin httpbin ---- - -= Start containers - ---- -sudo docker-compose up ---- - -= Execute H2 compatibility tests - ---- -H2CompatibilityTest http://localhost:8080 APACHE-HTTPD -H2CompatibilityTest http://localhost:8081 NGINX \ No newline at end of file diff --git a/httpcore5-testing/docker/apache-httpd/Dockerfile b/httpcore5-testing/docker/apache-httpd/Dockerfile deleted file mode 100644 index ac1d2cbdef..0000000000 --- a/httpcore5-testing/docker/apache-httpd/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM httpd:2.4 -MAINTAINER dev@hc.apache.org - -ENV var_dir /var/httpd -ENV www_dir ${var_dir}/www - -RUN apt-get update -RUN apt-get install -y subversion - -RUN mkdir -p ${var_dir} -RUN svn co --depth immediates http://svn.apache.org/repos/asf/httpcomponents/site ${www_dir} -RUN svn up --set-depth infinity ${www_dir}/images -RUN svn up --set-depth infinity ${www_dir}/css - -RUN sed -i -E 's/^\s*#\s*(LoadModule\s+http2_module\s+modules\/mod_http2.so)/\1/' conf/httpd.conf - -RUN sed -i -E 's/^\s*ServerAdmin.*$/ServerAdmin dev@hc.apache.org/' conf/httpd.conf -RUN sed -i -E 's/^\s*#\s*(Include\s+conf\/extra\/httpd-vhosts.conf)/\1/' conf/httpd.conf -COPY httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf \ No newline at end of file diff --git a/httpcore5-testing/docker/httpbin/Dockerfile b/httpcore5-testing/docker/httpbin/Dockerfile deleted file mode 100644 index e4120c33cb..0000000000 --- a/httpcore5-testing/docker/httpbin/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM kennethreitz/httpbin:latest -MAINTAINER dev@hc.apache.org diff --git a/httpcore5-testing/docker/nginx/Dockerfile b/httpcore5-testing/docker/nginx/Dockerfile deleted file mode 100644 index 829587d20d..0000000000 --- a/httpcore5-testing/docker/nginx/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM nginx:1.22 -MAINTAINER dev@hc.apache.org - -ENV var_dir /var/nginx -ENV www_dir ${var_dir}/www - -RUN apt-get update -RUN apt-get install -y subversion - -RUN mkdir -p ${var_dir} -RUN svn co --depth immediates http://svn.apache.org/repos/asf/httpcomponents/site ${www_dir} -RUN svn up --set-depth infinity ${www_dir}/images -RUN svn up --set-depth infinity ${www_dir}/css - -COPY default.conf /etc/nginx/conf.d/default.conf diff --git a/httpcore5-testing/pom.xml b/httpcore5-testing/pom.xml index e942e28520..91c0a515f9 100644 --- a/httpcore5-testing/pom.xml +++ b/httpcore5-testing/pom.xml @@ -105,6 +105,16 @@ mockito-core test + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + @@ -127,44 +137,6 @@ - - io.fabric8 - docker-maven-plugin - 0.45.0 - - - start-httpbin - pre-integration-test - - start - - - - - kennethreitz/httpbin - httpbin - - - MAGENTA - - - 8082:80 - - - - - - - - stop-test-servers - post-integration-test - - stop - - - - - diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java new file mode 100644 index 0000000000..89d4678440 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java @@ -0,0 +1,94 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.testing.compatibility; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.utility.DockerImageName; + +public final class ContainerImages { + + public static final String AAA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + public static final String BBB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + public static final String CCC = "ccccccccccccccccccccccccccccccccccccccccccccccccc"; + public static final String PUSHY = "I am being very pushy"; + + public static int HTTP_PORT = 80; + + public static GenericContainer httpBin() { + return new GenericContainer<>(DockerImageName.parse("kennethreitz/httpbin:latest")) + .withExposedPorts(HTTP_PORT); + } + + public static GenericContainer apacheHttpD() { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("httpd-vhosts.conf", "docker/httpd/httpd-vhosts.conf") + .withFileFromString("pushy", PUSHY) + .withFileFromString("aaa", AAA) + .withFileFromString("bbb", BBB) + .withFileFromString("ccc", CCC) + .withDockerfileFromBuilder(builder -> + builder + .from("httpd:2.4") + .env("var_dir", "/var/httpd") + .env("www_dir", "${var_dir}/www") + .run("mkdir -p ${var_dir}") + .run("sed -i -E 's/^\\s*#\\s*(LoadModule\\s+http2_module\\s+modules\\/mod_http2.so)/\\1/' conf/httpd.conf") + .run("sed -i -E 's/^\\s*ServerAdmin.*$/ServerAdmin dev@hc.apache.org/' conf/httpd.conf") + .run("sed -i -E 's/^\\s*#\\s*(Include\\s+conf\\/extra\\/httpd-vhosts.conf)/\\1/' conf/httpd.conf") + .copy("httpd-vhosts.conf", "/usr/local/apache2/conf/extra/httpd-vhosts.conf") + .copy("pushy", "${www_dir}/") + .copy("aaa", "${www_dir}/") + .copy("bbb", "${www_dir}/") + .copy("ccc", "${www_dir}/") + .build())) + .withExposedPorts(HTTP_PORT); + } + + public static GenericContainer ngnix() { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("default.conf", "docker/ngnix/default.conf") + .withFileFromString("pushy", PUSHY) + .withFileFromString("aaa", AAA) + .withFileFromString("bbb", BBB) + .withFileFromString("ccc", CCC) + .withDockerfileFromBuilder(builder -> + builder + .from("nginx:1.22") + .env("var_dir", "/var/nginx") + .env("www_dir", "${var_dir}/www") + .run("mkdir -p ${var_dir}") + .copy("default.conf", "/etc/nginx/conf.d/default.conf") + .copy("pushy", "${www_dir}/") + .copy("aaa", "${www_dir}/") + .copy("bbb", "${www_dir}/") + .copy("ccc", "${www_dir}/") + .build())) + .withExposedPorts(HTTP_PORT); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/Result.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/Result.java new file mode 100644 index 0000000000..94c050c4f1 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/Result.java @@ -0,0 +1,80 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.testing.compatibility; + +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.message.RequestLine; +import org.apache.hc.core5.http.message.StatusLine; + +public final class Result { + + public final HttpRequest request; + public final HttpResponse response; + public final T content; + public final Exception exception; + + public enum Status { OK, NOK } + + public Result(final HttpRequest request, final Exception exception) { + this.request = request; + this.response = null; + this.content = null; + this.exception = exception; + } + + public Result(final HttpRequest request, final HttpResponse response, final T content) { + this.request = request; + this.response = response; + this.content = content; + this.exception = null; + } + + public Status getStatus() { + return exception != null ? Status.NOK : Status.OK; + } + + public boolean isOK() { + return exception == null; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append(new RequestLine(request)); + buf.append(" -> "); + if (exception != null) { + buf.append("NOK: ").append(exception); + } else { + if (response != null) { + buf.append("OK: ").append(new StatusLine(response)); + } + } + return buf.toString(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http1/HttpBinCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http1/HttpBinCompatIT.java new file mode 100644 index 0000000000..38b063d88e --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http1/HttpBinCompatIT.java @@ -0,0 +1,229 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.http1; + +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.config.CharCodingConfig; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.apache.hc.core5.testing.compatibility.Result; +import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; +import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; +import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; +import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; +import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class HttpBinCompatIT { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + @Container + static final GenericContainer CONTAINER = ContainerImages.httpBin(); + + static HttpHost targetHost() { + return new HttpHost("http", + CONTAINER.getHost(), + CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); + } + + HttpAsyncRequester client; + + @BeforeEach + void start() throws Exception { + client = H2RequesterBootstrap.bootstrap() + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) + .setStreamListener(LoggingH2StreamListener.INSTANCE) + .setConnPoolListener(LoggingConnPoolListener.INSTANCE) + .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) + .setExceptionCallback(LoggingExceptionCallback.INSTANCE) + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE) + .create(); + client.start(); + } + + @AfterEach + void shutdown() throws Exception { + if (client != null) { + client.close(CloseMode.GRACEFUL); + } + } + + @AfterAll + static void cleanup() { + CONTAINER.close(); + } + + @Test + void test_sequential_request_execution() throws Exception { + final HttpHost target = targetHost(); + final List> requestMessages = Arrays.asList( + new Message<>(new BasicHttpRequest(Method.GET, target, "/headers")), + new Message<>( + new BasicHttpRequest(Method.POST, target, "/anything"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>( + new BasicHttpRequest(Method.PUT, target, "/anything"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")), + new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")), + new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")), + new Message<>( + new BasicHttpRequest(Method.POST, target, "/delay/2"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>( + new BasicHttpRequest(Method.PUT, target, "/delay/2"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)) + ); + + for (final Message message : requestMessages) { + final HttpRequest request = message.getHead(); + final AsyncEntityProducer entityProducer = message.getBody(); + final Future> messageFuture = client.execute( + new BasicRequestProducer(request, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() + .setCharset(StandardCharsets.US_ASCII) + .setMalformedInputAction(CodingErrorAction.IGNORE) + .setUnmappableInputAction(CodingErrorAction.REPLACE) + .build())), + TIMEOUT, + null); + final Message response = messageFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getHead().getCode()); + } + } + + @Test + void test_pipelined_request_execution() throws Exception { + final HttpHost target = targetHost(); + final Future connectFuture = client.connect(target, TIMEOUT); + final AsyncClientEndpoint streamEndpoint = connectFuture.get(); + try { + final int n = 20; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + + final HttpRequest request; + final AsyncEntityProducer entityProducer; + if (i % 2 == 0) { + request = new BasicHttpRequest(Method.GET, target, "/headers"); + entityProducer = null; + } else { + request = new BasicHttpRequest(Method.POST, target, "/anything"); + entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN); + } + + streamEndpoint.execute( + new BasicRequestProducer(request, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() + .setCharset(StandardCharsets.US_ASCII) + .setMalformedInputAction(CodingErrorAction.IGNORE) + .setUnmappableInputAction(CodingErrorAction.REPLACE) + .build())), + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + request, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(request, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + streamEndpoint.releaseAndDiscard(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/AbstractHttp2CompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/AbstractHttp2CompatIT.java new file mode 100644 index 0000000000..6b42f66114 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/AbstractHttp2CompatIT.java @@ -0,0 +1,227 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.http2; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.apache.hc.core5.testing.compatibility.Result; +import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; +import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; +import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; +import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; +import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +abstract class AbstractHttp2CompatIT { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + abstract HttpHost targetHost(); + + HttpAsyncRequester client; + + @BeforeEach + void start() throws Exception { + client = H2RequesterBootstrap.bootstrap() + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setH2Config(H2Config.custom() + .setPushEnabled(true) + .build()) + .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) + .setStreamListener(LoggingH2StreamListener.INSTANCE) + .setConnPoolListener(LoggingConnPoolListener.INSTANCE) + .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) + .setExceptionCallback(LoggingExceptionCallback.INSTANCE) + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE) + .create(); + client.start(); + } + + @AfterEach + void shutdown() throws Exception { + if (client != null) { + client.close(CloseMode.GRACEFUL); + } + } + + @Test + void test_multiple_request_execution() throws Exception { + final HttpHost target = targetHost(); + final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); + final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + try { + final int n = 20; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/aaa"); + endpoint.execute( + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + Assertions.assertEquals(ContainerImages.AAA, result.content); + } else { + Assertions.fail(result.exception); + } + } + } finally { + endpoint.releaseAndDiscard(); + } + } + + @Test + void test_request_execution_with_push() throws Exception { + final HttpHost target = targetHost(); + final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); + final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + try { + final CountDownLatch countDownLatch = new CountDownLatch(4); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/pushy"); + endpoint.execute( + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + (request, context) -> + new AbstractAsyncPushHandler>(new BasicResponseConsumer<>(new StringAsyncEntityConsumer())) { + + @Override + protected void handleResponse(final HttpRequest promise, final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + protected void handleError(final HttpRequest promise, final Exception cause) { + resultQueue.add(new Result<>(httpget, cause)); + countDownLatch.countDown(); + } + + }, + null, + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + endpoint.releaseAndDiscard(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/ApacheHttpDCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/ApacheHttpDCompatIT.java new file mode 100644 index 0000000000..834229f308 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/ApacheHttpDCompatIT.java @@ -0,0 +1,63 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.http2; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class ApacheHttpDCompatIT extends AbstractHttp2CompatIT { + + @Container + static final GenericContainer CONTAINER = ContainerImages.apacheHttpD(); + + HttpHost targetHost() { + return new HttpHost("http", + CONTAINER.getHost(), + CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); + } + + @Override + @Test + @Disabled + void test_request_execution_with_push() throws Exception { + super.test_request_execution_with_push(); + } + + @AfterAll + static void cleanup() { + CONTAINER.close(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java deleted file mode 100644 index 915b4c4b3e..0000000000 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ -package org.apache.hc.core5.testing.compatibility.http2; - -import java.io.IOException; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeoutException; - -import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.Message; -import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.config.CharCodingConfig; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.message.BasicHttpRequest; -import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.AsyncEntityProducer; -import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; -import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler; -import org.apache.hc.core5.http.nio.support.BasicRequestProducer; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; -import org.apache.hc.core5.http2.HttpVersionPolicy; -import org.apache.hc.core5.http2.config.H2Config; -import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; -import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; -import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; -import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; -import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; -import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; -import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; -import org.apache.hc.core5.util.TextUtils; -import org.apache.hc.core5.util.Timeout; - -public class H2CompatibilityTest { - - private final HttpAsyncRequester client; - - public static void main(final String... args) throws Exception { - - final HttpHost[] h2servers = new HttpHost[]{ - new HttpHost("http", "localhost", 8080), - new HttpHost("http", "localhost", 8081) - }; - - final HttpHost httpbin = new HttpHost("http", "localhost", 8082); - - final H2CompatibilityTest test = new H2CompatibilityTest(); - try { - test.start(); - for (final HttpHost h2server : h2servers) { - test.executeH2(h2server); - } - test.executeHttpBin(httpbin); - } finally { - test.shutdown(); - } - } - - H2CompatibilityTest() { - this.client = H2RequesterBootstrap.bootstrap() - .setIOReactorConfig(IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setH2Config(H2Config.custom() - .setPushEnabled(true) - .build()) - .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) - .setStreamListener(LoggingH2StreamListener.INSTANCE) - .setConnPoolListener(LoggingConnPoolListener.INSTANCE) - .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) - .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE) - .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE) - .create(); - } - - void start() { - client.start(); - } - - void shutdown() { - client.close(CloseMode.GRACEFUL); - } - - private static final Timeout TIMEOUT = Timeout.ofSeconds(5); - - void executeH2(final HttpHost target) throws Exception { - { - System.out.println("*** HTTP/2 simple request execution ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final CountDownLatch countDownLatch = new CountDownLatch(1); - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html"); - endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - Objects.toString(response.getFirstHeader("server"))); - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, httpget, null, "(cancelled)"); - countDownLatch.countDown(); - } - - }); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(single request execution failed to complete in time)"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - { - System.out.println("*** HTTP/2 multiplexed request execution ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final int reqCount = 20; - final CountDownLatch countDownLatch = new CountDownLatch(reqCount); - for (int i = 0; i < reqCount; i++) { - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html"); - endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - "multiplexed / " + response.getFirstHeader("server")); - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, httpget, null, "(cancelled)"); - countDownLatch.countDown(); - } - - }); - } - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(multiplexed request execution failed to complete in time)"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - { - System.out.println("*** HTTP/2 request execution with push ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final CountDownLatch countDownLatch = new CountDownLatch(5); - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/index.html"); - final Future> future = endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - (request, context) -> new AbstractAsyncPushHandler>( - new BasicResponseConsumer<>(new DiscardingEntityConsumer<>())) { - - @Override - protected void handleResponse( - final HttpRequest promise, - final Message responseMessage) throws IOException, HttpException { - final HttpResponse response = responseMessage.getHead(); - logResult(TestResult.OK, target, promise, response, - "pushed / " + response.getFirstHeader("server")); - countDownLatch.countDown(); - } - - @Override - protected void handleError( - final HttpRequest promise, - final Exception cause) { - logResult(TestResult.NOK, target, promise, null, "(" + cause.getMessage() + ")"); - countDownLatch.countDown(); - } - }, - null, - null); - final Message message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - final HttpResponse response = message.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - Objects.toString(response.getFirstHeader("server"))); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "Push messages not received"); - } - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - } - - void executeHttpBin(final HttpHost target) throws Exception { - { - System.out.println("*** httpbin.org HTTP/1.1 simple request execution ***"); - - final List> requestMessages = Arrays.asList( - new Message<>(new BasicHttpRequest(Method.GET, target, "/headers")), - new Message<>( - new BasicHttpRequest(Method.POST, target, "/anything"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>( - new BasicHttpRequest(Method.PUT, target, "/anything"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")), - new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")), - new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")), - new Message<>( - new BasicHttpRequest(Method.POST, target, "/delay/2"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>( - new BasicHttpRequest(Method.PUT, target, "/delay/2"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)) - ); - - for (final Message message : requestMessages) { - final CountDownLatch countDownLatch = new CountDownLatch(1); - final HttpRequest request = message.getHead(); - final AsyncEntityProducer entityProducer = message.getBody(); - client.execute( - new BasicRequestProducer(request, entityProducer), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() - .setCharset(StandardCharsets.US_ASCII) - .setMalformedInputAction(CodingErrorAction.IGNORE) - .setUnmappableInputAction(CodingErrorAction.REPLACE) - .build())), - TIMEOUT, - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, request, response, - Objects.toString(response.getFirstHeader("server"))); - } else { - logResult(TestResult.NOK, target, request, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, request, null, "(cancelled)"); - countDownLatch.countDown(); - } - }); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)"); - } - } - } - { - System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***"); - - final Future connectFuture = client.connect(target, TIMEOUT); - final AsyncClientEndpoint streamEndpoint = connectFuture.get(); - - final int n = 10; - final CountDownLatch countDownLatch = new CountDownLatch(n); - for (int i = 0; i < n; i++) { - - final HttpRequest request; - final AsyncEntityProducer entityProducer; - if (i % 2 == 0) { - request = new BasicHttpRequest(Method.GET, target, "/headers"); - entityProducer = null; - } else { - request = new BasicHttpRequest(Method.POST, target, "/anything"); - entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN); - } - - streamEndpoint.execute( - new BasicRequestProducer(request, entityProducer), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() - .setCharset(StandardCharsets.US_ASCII) - .setMalformedInputAction(CodingErrorAction.IGNORE) - .setUnmappableInputAction(CodingErrorAction.REPLACE) - .build())), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, request, response, - "pipelined / " + response.getFirstHeader("server")); - } else { - logResult(TestResult.NOK, target, request, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, request, null, "(cancelled)"); - countDownLatch.countDown(); - } - }); - } - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)"); - } - } - } - - enum TestResult { OK, NOK } - - private void logResult( - final TestResult result, - final HttpHost httpHost, - final HttpRequest request, - final HttpResponse response, - final String message) { - final StringBuilder buf = new StringBuilder(); - buf.append(result); - if (buf.length() == 2) { - buf.append(" "); - } - buf.append(": ").append(httpHost).append(" "); - if (response != null) { - buf.append(response.getVersion()).append(" "); - } - if (request != null) { - buf.append(request.getMethod()).append(" ").append(request.getRequestUri()); - } - if (message != null && !TextUtils.isBlank(message)) { - buf.append(" -> ").append(message); - } - System.out.println(buf); - } - -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/NginxCompatIT.java similarity index 65% rename from httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java rename to httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/NginxCompatIT.java index 688f49298a..94eafdd119 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/NginxCompatIT.java @@ -28,26 +28,27 @@ package org.apache.hc.core5.testing.compatibility.http2; import org.apache.hc.core5.http.HttpHost; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.junit.jupiter.api.AfterAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; -class HttpBinIT { - private H2CompatibilityTest h2CompatibilityTest; +@Testcontainers(disabledWithoutDocker = true) +class NginxCompatIT extends AbstractHttp2CompatIT { - @BeforeEach - void start() throws Exception { - h2CompatibilityTest = new H2CompatibilityTest(); - h2CompatibilityTest.start(); - } + @Container + static final GenericContainer CONTAINER = ContainerImages.ngnix(); - @AfterEach - void shutdown() throws Exception { - h2CompatibilityTest.shutdown(); + HttpHost targetHost() { + return new HttpHost("http", + CONTAINER.getHost(), + CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); } - @Test - void executeHttpBin() throws Exception { - h2CompatibilityTest.executeHttpBin(new HttpHost("http", "localhost", 8082)); + @AfterAll + static void cleanup() { + CONTAINER.close(); } + } diff --git a/httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf b/httpcore5-testing/src/test/resources/docker/httpd/httpd-vhosts.conf similarity index 75% rename from httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf rename to httpcore5-testing/src/test/resources/docker/httpd/httpd-vhosts.conf index 160c1cc79b..fa29ea9c49 100644 --- a/httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf +++ b/httpcore5-testing/src/test/resources/docker/httpd/httpd-vhosts.conf @@ -17,11 +17,6 @@ ServerName localhost:80 Protocols h2c http/1.1 - - LogLevel http2:info - H2Push on - - AllowOverride none Require all denied @@ -34,12 +29,15 @@ DocumentRoot "/var/httpd/www" Require all granted - - - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" + + LogLevel http2:info + H2Push on + H2EarlyHints on + + + H2PushResource /aaa + H2PushResource /bbb + H2PushResource /ccc + diff --git a/httpcore5-testing/docker/nginx/default.conf b/httpcore5-testing/src/test/resources/docker/ngnix/default.conf similarity index 76% rename from httpcore5-testing/docker/nginx/default.conf rename to httpcore5-testing/src/test/resources/docker/ngnix/default.conf index b6160d6842..c48cd42483 100644 --- a/httpcore5-testing/docker/nginx/default.conf +++ b/httpcore5-testing/src/test/resources/docker/ngnix/default.conf @@ -19,14 +19,11 @@ server { location / { root /var/nginx/www; - index index.html; - location = /index.html { - http2_push "/css/site.css"; - http2_push "/css/maven-theme.css"; - http2_push "/css/maven-base.css"; - http2_push "/css/hc-maven.css"; - http2_push "/images/logos/httpcomponents.png"; + location = /pushy { + http2_push "/aaa"; + http2_push "/bbb"; + http2_push "/ccc"; } } diff --git a/pom.xml b/pom.xml index 4cc0c7ce60..d08bcc972a 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 2.23.1 2.2.21 3.1.9 + 1.20.2 5.3 javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer @@ -151,6 +152,16 @@ log4j-core ${log4j.version} + + org.testcontainers + testcontainers + ${testcontainers.version} + + + org.testcontainers + junit-jupiter + ${testcontainers.version} +