From 4422f73a341a655e4ed95539c531cd82aa1226ac Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Sun, 21 Jun 2026 12:56:34 +0200 Subject: [PATCH] test(reader): cover open(uri, registry) via default-client seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #117's unit test threw before the delegation completed, so JaCoCo's end-of-line probe never fired and new-code coverage stayed at 50% (JaCoCo marks an always-throwing line as missed). Make the shared default HttpClient a package-private non-final seam so a unit test can substitute the mock fixture used by VortexHttpReaderTailFetchTest and drive the two-arg overload to a normal return — now the probe fires and the line is covered. Production never reassigns the field. Co-Authored-By: Claude Opus 4.8 --- .../dfa1/vortex/reader/VortexHttpReader.java | 8 +- .../VortexHttpReaderOpenOverloadTest.java | 115 ++++++++++++++++-- 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java b/reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java index 77e392ba..382cf5da 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java @@ -33,7 +33,11 @@ public final class VortexHttpReader implements VortexHandle { /// Shared across all instances. JDK HttpClient is heavyweight and designed for reuse; /// per-reader instantiation would create redundant connection pools and selector threads. /// Never closed: lifetime tracks the JVM. - private static final HttpClient DEFAULT_HTTP_CLIENT = HttpClient.newHttpClient(); + /// + /// Package-private and non-final purely as a unit-test seam: tests substitute a mocked + /// client to drive the default-client [#open(URI, ReadRegistry)] overload without real + /// network I/O. Production code never reassigns it. + static HttpClient defaultHttpClient = HttpClient.newHttpClient(); private final URI uri; private final HttpClient client; @@ -66,7 +70,7 @@ public static VortexHttpReader open(URI uri) throws IOException { } public static VortexHttpReader open(URI uri, ReadRegistry registry) throws IOException { - return open(uri, registry, DEFAULT_HTTP_CLIENT); + return open(uri, registry, defaultHttpClient); } /// Opens a remote Vortex file using a caller-supplied [HttpClient]. diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderOpenOverloadTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderOpenOverloadTest.java index 9ba24d22..113e21b8 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderOpenOverloadTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderOpenOverloadTest.java @@ -1,23 +1,118 @@ package io.github.dfa1.vortex.reader; +import io.github.dfa1.vortex.core.VortexFormat; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.net.ssl.SSLSession; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; -/// Unit coverage for the [VortexHttpReader#open(URI, ReadRegistry)] overload, which wires the -/// shared default [java.net.http.HttpClient]. A non-HTTP URI makes the request builder reject -/// the scheme before any socket is opened, so the delegation runs without real network I/O. +/// Covers the [VortexHttpReader#open(URI, ReadRegistry)] overload, which wires the shared +/// default [HttpClient]. The three-arg overload is covered by +/// [VortexHttpReaderTailFetchTest]; this drives the two-arg overload to completion by +/// substituting the package-private default-client seam with a mocked client serving a +/// committed fixture, so it stays deterministic and network-free. +@ExtendWith(MockitoExtension.class) class VortexHttpReaderOpenOverloadTest { + @Mock + private HttpClient client; + + private static final URI URI = java.net.URI.create("http://example.com/primitives.vortex"); + @Test - void open_uriAndRegistry_delegatesToDefaultClient() { - // Given a URI whose scheme HttpRequest.newBuilder rejects (no network performed) - URI uri = URI.create("ftp://example.invalid/file.vortex"); + void open_uriAndRegistry_usesDefaultClient() throws Exception { + // Given the fixture fits the tail window and the default-client seam is the mock + byte[] file = fixtureBytes(); + doReturn(response(206, "bytes 0-" + (file.length - 1) + "/" + file.length, file)) + .when(client).send(any(), any()); + HttpClient original = VortexHttpReader.defaultHttpClient; + VortexHttpReader.defaultHttpClient = client; + + // When the two-arg overload is used (no explicit client) + try (var sut = VortexHttpReader.open(URI, ReadRegistry.empty())) { + + // Then it delegates through the default client and parses metadata + assertThat(sut.fileSize()).isEqualTo(file.length); + assertThat(sut.fileSize()).isGreaterThan(VortexFormat.TRAILER_SIZE); + assertThat(sut.dtype()).isNotNull(); + } finally { + VortexHttpReader.defaultHttpClient = original; + } + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + private static byte[] fixtureBytes() throws IOException { + try (InputStream in = VortexHttpReaderOpenOverloadTest.class + .getResourceAsStream("/fixtures/primitives.vortex")) { + if (in == null) { + throw new IOException("missing test fixture: /fixtures/primitives.vortex"); + } + return in.readAllBytes(); + } + } + + @SuppressWarnings("unchecked") + private static HttpResponse response(int status, String contentRange, byte[] body) { + return new HttpResponse<>() { + @Override + public int statusCode() { + return status; + } + + @Override + public byte[] body() { + return body; + } + + @Override + public HttpHeaders headers() { + Map> map = contentRange == null + ? Map.of() + : Map.of("content-range", List.of(contentRange)); + return HttpHeaders.of(map, (k, v) -> true); + } + + @Override + public HttpRequest request() { + return null; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public java.net.URI uri() { + return URI; + } - // When / Then the two-arg overload runs and surfaces the rejection - assertThatThrownBy(() -> VortexHttpReader.open(uri, ReadRegistry.empty())) - .isInstanceOf(IllegalArgumentException.class); + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + }; } }