8385891: Introduce a test for GZIPInputStream whose underlying stream is a blocking InputStream

Reviewed-by: lancea
This commit is contained in:
Jaikiran Pai 2026-06-23 11:58:09 +00:00
parent 96a1a9d2f5
commit 7ca9ae1dfe

View File

@ -0,0 +1,406 @@
/*
* Copyright (c) 2026, 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.
*/
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import jdk.test.lib.RandomFactory;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/*
* @test
* @summary Verifies that the GZIPInputStream works as expected when the underlying
* InputStream is a blocking stream
* @key randomness
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder jdk.test.lib.RandomFactory
* @run junit GZIPOverBlockingStreams
*/
class GZIPOverBlockingStreams {
private static final Random random = RandomFactory.getRandom();
private static final String MEMBER_CONTENT_FORMAT = "Hello member %d, foo bar hello world\n";
private static final ExecutorService httpServerExecutor = Executors.newCachedThreadPool();
private static Server nonHttpServer;
private static HttpServer httpServer;
@BeforeAll
static void beforeAll() throws Exception {
// create a socket based (non-HTTP) server
nonHttpServer = new Server();
nonHttpServer.start();
System.err.println("(non-HTTP) server started at " + nonHttpServer.getAddress());
// create a HTTP server
final InetAddress loopback = InetAddress.getLoopbackAddress();
final InetSocketAddress serverAddr = new InetSocketAddress(loopback, 0);
httpServer = HttpServer.create(serverAddr, 0);
httpServer.setExecutor(httpServerExecutor);
httpServer.createContext("/", new HttpReqHandler());
httpServer.start();
System.err.println("started HTTP server at " + httpServer.getAddress());
}
@AfterAll
static void afterAll() throws Exception {
if (nonHttpServer != null) {
System.err.println("stopping server " + nonHttpServer.getAddress());
nonHttpServer.close();
}
if (httpServer != null) {
System.err.println("stopping HTTP server " + httpServer.getAddress());
httpServer.stop(0);
}
httpServerExecutor.shutdownNow();
}
static List<Integer> numGZIPMembers() {
return List.of(1,
13,
42,
random.nextInt(2, 101) // a reasonable number of members, not too many
);
}
static List<Arguments> socketStreamTestArgs() {
final List<Arguments> args = new ArrayList<>();
final List<Integer> numMembers = numGZIPMembers();
for (boolean shouldCloseSocket : new boolean[]{true, false}) {
for (int n : numMembers) {
args.add(Arguments.of(n, shouldCloseSocket));
}
}
return args;
}
/*
* Verifies that when the GZIPInputStream is used to read GZIP content
* over a socket stream, it does not block when reading past a member trailer to determine
* the presence of a subsequent member.
*/
@ParameterizedTest
@MethodSource("socketStreamTestArgs")
void testSocketStream(final int numMembers, final boolean shouldCloseSocket) throws Exception {
final InetSocketAddress serverAddr = nonHttpServer.getAddress();
try (final Socket socket = new Socket(serverAddr.getAddress(), serverAddr.getPort())) {
System.err.println("connect established " + socket);
try (final OutputStream os = socket.getOutputStream();
final DataOutputStream dos = new DataOutputStream(os)) {
// instruct the server side the number of GZIP members we want in the response
dos.writeInt(numMembers);
// instruct the server side whether to close the socket after writing out the
// response
dos.writeBoolean(shouldCloseSocket);
System.err.println("sent request for GZIP stream with " + numMembers + " members");
// read the response
try (final InputStream in = socket.getInputStream();
final GZIPInputStream gzipInputStream = new GZIPInputStream(in)) {
final byte[] decompressed = gzipInputStream.readAllBytes();
System.err.println("read " + decompressed.length
+ " bytes of decompressed response");
// verify it's the expected content
assertDecompressedContent(numMembers, decompressed);
}
}
}
final Throwable serverFailure = nonHttpServer.failure;
if (serverFailure != null) {
fail("Server ran into an error", serverFailure);
}
}
static List<Arguments> httpTestArgs() {
final List<Arguments> args = new ArrayList<>();
final List<Integer> numMembers = numGZIPMembers();
for (boolean chunkedOrNot : new boolean[]{true, false}) {
for (int n : numMembers) {
args.add(Arguments.of(n, chunkedOrNot));
}
}
return args;
}
/*
* Verifies that when the GZIPInputStream is used to read GZIP content,
* over a stream obtained from a HTTP response, it does not block when reading past a member
* trailer to determine the presence of a subsequent member.
*/
@ParameterizedTest
@MethodSource("httpTestArgs")
void testHttpStream(final int numMembers, final boolean httpResponseChunked) throws Exception {
final URI reqURI = URIBuilder.newBuilder()
.scheme("http")
.loopback()
.port(httpServer.getAddress().getPort())
.path("/")
.build();
final HttpURLConnection conn = (HttpURLConnection) reqURI.toURL().openConnection();
conn.setRequestProperty("numMembers", String.valueOf(numMembers));
conn.setRequestProperty("chunkedResponse", String.valueOf(httpResponseChunked));
System.err.println("issuing request " + reqURI);
try (final InputStream in = conn.getInputStream();
final GZIPInputStream gzipInputStream = new GZIPInputStream(in)) {
final byte[] decompressed = gzipInputStream.readAllBytes();
System.err.println("read " + decompressed.length
+ " bytes of decompressed response");
assertDecompressedContent(numMembers, decompressed);
}
}
/*
* Creates and returns bytes representing a GZIP stream consisting of the given number of
* members.
*/
private static byte[] createGZIPStream(final int numMembers) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i = 1; i <= numMembers; i++) {
final ByteArrayOutputStream member = new ByteArrayOutputStream();
try (final OutputStream gzip = new GZIPOutputStream(member)) {
final String memberContent = String.format(MEMBER_CONTENT_FORMAT, i);
gzip.write(memberContent.getBytes(US_ASCII));
}
// write out the GZIP member to the stream which accumulates all the members
baos.write(member.toByteArray());
}
return baos.toByteArray();
}
/*
* Verifies that the given decompressed bytes, representing the given number of
* GZIP members, do match the expected content.
*/
private static void assertDecompressedContent(final int numMembers,
final byte[] decompressed) {
final String actual = new String(decompressed, US_ASCII);
final StringBuilder sb = new StringBuilder();
for (int i = 1; i <= numMembers; i++) {
sb.append(String.format(MEMBER_CONTENT_FORMAT, i));
}
final String expected = sb.toString();
assertEquals(expected, actual, "unexpected decompressed content");
}
/*
* A server which communicates over a socket to receive a request consisting of an integer
* representing the number of GZIP members to respond with. The server then responds back
* on the socket's OutputStream with GZIP content representing those many members.
*/
private static final class Server implements AutoCloseable, Runnable {
private final ServerSocket serverSocket;
private volatile boolean stop;
private volatile Throwable failure;
private Server() throws IOException {
this.serverSocket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress());
}
private InetSocketAddress getAddress() {
return (InetSocketAddress) this.serverSocket.getLocalSocketAddress();
}
@Override
public void close() throws IOException {
this.stop = true;
System.err.println("closing server: " + this.serverSocket);
this.serverSocket.close();
}
private void start() {
final Thread t = new Thread(this);
t.setName("server");
t.setDaemon(true);
t.start();
}
private synchronized void recordServerFailure(final Throwable t) {
Throwable previous = this.failure;
if (previous != null && previous != t) {
previous.addSuppressed(t);
return;
}
this.failure = t;
}
@Override
public void run() {
System.err.println("server started accepting requests at " + this.serverSocket);
try {
doRun();
} catch (Throwable t) {
if (!stop) { // ignore failures if the server is stopped
recordServerFailure(t);
System.err.println("server ran into error: " + t);
t.printStackTrace();
}
} finally {
try {
this.close();
} catch (IOException ioe) {
System.err.println("ignoring excpetion " +
"that happened during closing server: " + ioe);
ioe.printStackTrace();
}
}
}
private void doRun() throws Exception {
while (!this.stop) {
// we intentionally do not close the Socket. It's upto the
// sendGZIPResponse(...) method to do that only if the test
// request has instructed it to do so. This allows the test
// method to exercise the case where the socket is open
// but doesn't have any more data to send (and thus read() blocks)
final Socket socket = this.serverSocket.accept();
System.err.println("accepted connection from " + socket);
// handle the request on a separate thread
final Thread handler = new Thread(() -> {
try {
handleRequest(socket);
} catch (Throwable t) {
// keep track of the failure
recordServerFailure(t);
System.err.println("failure when handling request on socket "
+ socket + ", exception: " + t);
t.printStackTrace();
}
});
handler.setName("request-handler-" + socket.getRemoteSocketAddress());
handler.setDaemon(true);
handler.start();
}
}
private static void handleRequest(final Socket socket) throws IOException {
final int numMembers;
final boolean shouldCloseSocket;
try {
final InputStream in = socket.getInputStream();
final DataInputStream dis = new DataInputStream(in);
// read the socket's inputstream to determine how many GZIP members are
// expected in the response stream, by the client
numMembers = dis.readInt();
// whether the socket should be closed after writing out the response
shouldCloseSocket = dis.readBoolean();
} catch (IOException ioe) {
// could be a socket connection from an unexpected client, so ignore any
// failure when reading the request
System.err.println("Ignoring exception that happened when reading" +
" request from client socket " + socket + ", exception: " + ioe);
ioe.printStackTrace();
// close the unexpected client connection
socket.close();
return;
}
// valid request, respond to it with a GZIP response
sendGZIPResponse(socket, numMembers, shouldCloseSocket);
}
/*
* Sends GZIP content over the socket's OutputStream. This method closes the socket
* only if the test request (read over the socket's InputStream) instructs it to do so.
*/
private static void sendGZIPResponse(final Socket socket, final int numMembers,
final boolean shouldCloseSocket) throws IOException {
// respond back with a GZIP output, containing the expected number of members
final byte[] gzipResponse = createGZIPStream(numMembers);
System.err.println("responding to " + socket + " with a GZIP stream of size "
+ gzipResponse.length + " with " + numMembers + " members");
final OutputStream os = socket.getOutputStream();
os.write(gzipResponse);
System.err.println("done responding to " + socket);
// close the socket only if the test request wants us to
if (shouldCloseSocket) {
System.err.println("closing " + socket);
socket.close();
}
}
}
/*
* A HTTP request handler which responds back with chunked or non-chunked
* response containing GZIP content.
*/
private static final class HttpReqHandler implements HttpHandler {
@Override
public void handle(final HttpExchange exchange) throws IOException {
final URI reqURI = exchange.getRequestURI();
System.err.println("handling request: " + reqURI + " from "
+ exchange.getRemoteAddress());
final String val = exchange.getRequestHeaders().getFirst("numMembers");
final int numMembers = Integer.parseInt(val);
final boolean respChunked = Boolean.parseBoolean(
exchange.getRequestHeaders().getFirst("chunkedResponse"));
final byte[] gzipResponse = createGZIPStream(numMembers);
System.err.println("responding to " + reqURI
+ " with a GZIP stream of size " + gzipResponse.length
+ " with " + numMembers + " members"
+ " with chunked response = " + respChunked);
// drain the inputstream and write out the response
exchange.getRequestBody().readAllBytes();
if (respChunked) {
exchange.sendResponseHeaders(200, 0); // 0 = Chunked response
} else {
exchange.sendResponseHeaders(200, gzipResponse.length);
}
try (final OutputStream os = exchange.getResponseBody()) {
os.write(gzipResponse);
}
System.err.println("done responding to " + reqURI);
}
}
}