Skip to content

Commit 12e4627

Browse files
dfa1claude
andcommitted
test(reader): cover open(uri, registry) via default-client seam
#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 <noreply@anthropic.com>
1 parent 0958a9d commit 12e4627

2 files changed

Lines changed: 111 additions & 12 deletions

File tree

reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ public final class VortexHttpReader implements VortexHandle {
3333
/// Shared across all instances. JDK HttpClient is heavyweight and designed for reuse;
3434
/// per-reader instantiation would create redundant connection pools and selector threads.
3535
/// Never closed: lifetime tracks the JVM.
36-
private static final HttpClient DEFAULT_HTTP_CLIENT = HttpClient.newHttpClient();
36+
///
37+
/// Package-private and non-final purely as a unit-test seam: tests substitute a mocked
38+
/// client to drive the default-client [#open(URI, ReadRegistry)] overload without real
39+
/// network I/O. Production code never reassigns it.
40+
static HttpClient defaultHttpClient = HttpClient.newHttpClient();
3741

3842
private final URI uri;
3943
private final HttpClient client;
@@ -66,7 +70,7 @@ public static VortexHttpReader open(URI uri) throws IOException {
6670
}
6771

6872
public static VortexHttpReader open(URI uri, ReadRegistry registry) throws IOException {
69-
return open(uri, registry, DEFAULT_HTTP_CLIENT);
73+
return open(uri, registry, defaultHttpClient);
7074
}
7175

7276
/// Opens a remote Vortex file using a caller-supplied [HttpClient].
Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,118 @@
11
package io.github.dfa1.vortex.reader;
22

3+
import io.github.dfa1.vortex.core.VortexFormat;
34
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.ExtendWith;
6+
import org.mockito.Mock;
7+
import org.mockito.junit.jupiter.MockitoExtension;
48

9+
import java.io.IOException;
10+
import java.io.InputStream;
511
import java.net.URI;
12+
import java.net.http.HttpClient;
13+
import java.net.http.HttpHeaders;
14+
import java.net.http.HttpRequest;
15+
import java.net.http.HttpResponse;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Optional;
19+
import javax.net.ssl.SSLSession;
620

7-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.mockito.ArgumentMatchers.any;
23+
import static org.mockito.Mockito.doReturn;
824

9-
/// Unit coverage for the [VortexHttpReader#open(URI, ReadRegistry)] overload, which wires the
10-
/// shared default [java.net.http.HttpClient]. A non-HTTP URI makes the request builder reject
11-
/// the scheme before any socket is opened, so the delegation runs without real network I/O.
25+
/// Covers the [VortexHttpReader#open(URI, ReadRegistry)] overload, which wires the shared
26+
/// default [HttpClient]. The three-arg overload is covered by
27+
/// [VortexHttpReaderTailFetchTest]; this drives the two-arg overload to completion by
28+
/// substituting the package-private default-client seam with a mocked client serving a
29+
/// committed fixture, so it stays deterministic and network-free.
30+
@ExtendWith(MockitoExtension.class)
1231
class VortexHttpReaderOpenOverloadTest {
1332

33+
@Mock
34+
private HttpClient client;
35+
36+
private static final URI URI = java.net.URI.create("http://example.com/primitives.vortex");
37+
1438
@Test
15-
void open_uriAndRegistry_delegatesToDefaultClient() {
16-
// Given a URI whose scheme HttpRequest.newBuilder rejects (no network performed)
17-
URI uri = URI.create("ftp://example.invalid/file.vortex");
39+
void open_uriAndRegistry_usesDefaultClient() throws Exception {
40+
// Given the fixture fits the tail window and the default-client seam is the mock
41+
byte[] file = fixtureBytes();
42+
doReturn(response(206, "bytes 0-" + (file.length - 1) + "/" + file.length, file))
43+
.when(client).send(any(), any());
44+
HttpClient original = VortexHttpReader.defaultHttpClient;
45+
VortexHttpReader.defaultHttpClient = client;
46+
47+
// When the two-arg overload is used (no explicit client)
48+
try (var sut = VortexHttpReader.open(URI, ReadRegistry.empty())) {
49+
50+
// Then it delegates through the default client and parses metadata
51+
assertThat(sut.fileSize()).isEqualTo(file.length);
52+
assertThat(sut.fileSize()).isGreaterThan(VortexFormat.TRAILER_SIZE);
53+
assertThat(sut.dtype()).isNotNull();
54+
} finally {
55+
VortexHttpReader.defaultHttpClient = original;
56+
}
57+
}
58+
59+
// ── helpers ───────────────────────────────────────────────────────────────
60+
61+
private static byte[] fixtureBytes() throws IOException {
62+
try (InputStream in = VortexHttpReaderOpenOverloadTest.class
63+
.getResourceAsStream("/fixtures/primitives.vortex")) {
64+
if (in == null) {
65+
throw new IOException("missing test fixture: /fixtures/primitives.vortex");
66+
}
67+
return in.readAllBytes();
68+
}
69+
}
70+
71+
@SuppressWarnings("unchecked")
72+
private static HttpResponse<byte[]> response(int status, String contentRange, byte[] body) {
73+
return new HttpResponse<>() {
74+
@Override
75+
public int statusCode() {
76+
return status;
77+
}
78+
79+
@Override
80+
public byte[] body() {
81+
return body;
82+
}
83+
84+
@Override
85+
public HttpHeaders headers() {
86+
Map<String, List<String>> map = contentRange == null
87+
? Map.of()
88+
: Map.of("content-range", List.of(contentRange));
89+
return HttpHeaders.of(map, (k, v) -> true);
90+
}
91+
92+
@Override
93+
public HttpRequest request() {
94+
return null;
95+
}
96+
97+
@Override
98+
public Optional<HttpResponse<byte[]>> previousResponse() {
99+
return Optional.empty();
100+
}
101+
102+
@Override
103+
public Optional<SSLSession> sslSession() {
104+
return Optional.empty();
105+
}
106+
107+
@Override
108+
public java.net.URI uri() {
109+
return URI;
110+
}
18111

19-
// When / Then the two-arg overload runs and surfaces the rejection
20-
assertThatThrownBy(() -> VortexHttpReader.open(uri, ReadRegistry.empty()))
21-
.isInstanceOf(IllegalArgumentException.class);
112+
@Override
113+
public HttpClient.Version version() {
114+
return HttpClient.Version.HTTP_1_1;
115+
}
116+
};
22117
}
23118
}

0 commit comments

Comments
 (0)