Four small seams, each useful on its own, that together make a Flash app
testable without hand-rolled scaffolding.
- ServerHandle/ServerLifecycle/FlashApp gain port()/ports(). Listeners already
bind in the FlashApp constructor, so port(0) resolved to a real port that
nothing could read back; every integration test worked around this by
opening a ServerSocket(0), closing it and reusing the number, which races
anything else on the machine.
- FlashContext.override() replaces a binding instead of rejecting it. The
duplicate-is-an-error rule stays everywhere else; this is the single
deliberate exception, for swapping a service out in tests. A replacement is
logged at INFO so misuse in production is visible.
- FlashApplication + FlashApp.apply() name an application independently of the
port it runs on, so the same one can be booted twice. It takes FlashApp
rather than FlashRegistrar because ws() and mount() live there. Being a
functional interface, a lambda and a named class are the same thing.
- FlashContext.onClose() runs cleanup at stop(), children first and then in
reverse registration order. stop() previously closed sockets and the
executor and never touched the service graph, so a pooled DataSource was
only ever released by JVM exit — invisible with one app per process, a leak
per test class once a suite boots and stops many.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boots a real app on an OS-assigned port for a test class or a single test and
hands back a client pointed at it:
@RegisterExtension
static FlashTest app = FlashTest.of(new BlogApp())
.mock(UserService.class, new InMemoryUserService());
app.get("/api/users").expectStatus(200).expectBodyContains("alice");
A field rather than an annotation, because annotation values are compile-time
constants and so could never express a second server wired from the first —
FlashTest.of(new BlogApp(auth.baseUri())). Startup is lazy, so reading
baseUri() boots that server on the spot and declaration order does the wiring,
with no dependence on JUnit's extension ordering. A static field boots once per
class, a non-static field once per test; that is stock JUnit field semantics
rather than an option to configure.
Runs against a real loopback port instead of dispatching in-process. An
in-process dispatcher would be a third copy of the routing/handler/exception
sequence that Http1Connection and Http2StreamDispatcher already duplicate, kept
in sync by hand, and it would let a test pass while the status line,
content-length or HPACK encoding was broken.
mock() installs overrides as the last extension, after everything the
application and its extensions declare, so a fake always wins. Any object is
accepted, so a hand-written fake and a Mockito mock are equally welcome and
this module depends on no mocking library — only flash and junit-jupiter-api.
Teardown cancels the client before stopping the server: HttpClient holds
keep-alive sockets open and ServerLifecycle.stop() spins until the last one
closes, so the default 15s drain would otherwise be paid on every test class.
shutdownNow rather than close(), which blocks until every operation completes
and would hang on a leaked WebSocket.
Lives at the top level, not under flash-extensions/, which holds things you
install() onto an app; this carries junit-jupiter-api at compile scope and
nothing installable should.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First migration, chosen because it is the case that drove the harness design:
a Flash app plus a FakeOidcProvider, with tokens audience-bound to the app's
own port, so the port has to be readable after boot.
Drops the racy free-port dance, the hand-rolled HttpClient plumbing and the
per-test teardown; failures now report the response body. 120 to 106 code
lines, and what remains is tool-policy assertions rather than fixture code.
The two boot-rejection tests keep building their app directly — a harness whose
job is to boot an app is the wrong tool for asserting that booting fails — but
port(0) removes freePort() from those too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beforeEach called ensureStarted(), so every FlashTest field in a class booted
for every test whether or not that test touched it — a class holding four
servers paid for four boots per test. Booting is already lazy on first access,
so the hook was only ever forcing work forward.
Neither hook starts anything now. beforeAll still records that a static field
owns the class-scoped lifecycle, which is what keeps afterEach from tearing a
class-scoped server down after the first test.
This also lets an application read @TempDir inside configure(): JUnit populates
those during instance post-processing, before the first test body but after
extension beforeEach callbacks would have fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every integration test picked a port by opening ServerSocket(0), closing it and
reusing the number, which races anything else on the machine between the close
and the rebind. FlashApp.port() reports the port the listener actually bound,
so the guess is gone: 18 freePort() helpers deleted, 40 call sites now pass
port(0) and read the result back.
Two ServerSocket(0) uses remain and are correct. ConnectionRunnerTest accepts
on its socket rather than using it to pick a number. H2LoadMeasurementTest
hands its port to an external nghttpd process, which has no equivalent of
port() to read back; that one is now commented to say so.
HttpServerTlsTest's two-listener case reads both back through ports().
HttpServerTest and HttpServerConcurrencyTest also move from @BeforeEach to
@BeforeAll — every test in them is read-only against the same routes, so 11 and
3 boots respectively become 1. HttpServerConcurrencyTest's lazy-compile test
keeps building its own app, since a freshly compiled router is the thing it
tests. HttpServerTest now runs 11 tests in 0.06s.
The http2 interop suites (curl, nghttp, grpcurl, h2spec), the load measurement
and the soak test are skipped without their external binaries or system
property, so those edits are compile-verified here and exercised in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
McpExtensionIntegrationTest: 10 stateless JSON-RPC calls against one server
config, so one class-scoped server replaces a boot per test. 117 to 85 code
lines.
McpExtensionSecurityTest: the four server configurations it exercises — AUTO
without oidc, REQUIRED with a derived resource identifier, REQUIRED with an
explicit one, and REQUIRED with advertised scopes — become four named servers
sharing one FakeOidcProvider, replacing eight boots and a freePort() helper.
151 to 128 code lines. The boot-rejection test still builds its app directly:
a harness whose job is to boot an app is the wrong tool for asserting that
booting fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two frontend layouts, each laid out on disk inside its own application's
configure(). The harness runs that lazily at first access, so @TempDir is
populated by then and the server for whichever test is not running never boots.
Replaces three blocks of HttpRequest.newBuilder(URI.create(...)) per test with
single-line assertions. 98 to 60 code lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README covers the application handle, requests and assertions, service
replacement, multi-server wiring, scope, WebSockets, configuration and the
teardown ordering.
limits.md records the seven things the harness cannot do and what to use for
each: TLS, HTTP/2, WebSocket over HTTP/2, malformed requests, response framing,
the flash core module's dependency cycle, and scoped services. Each is a
consequence of a real constraint rather than an unfinished feature, so writing
them down stops the next person rediscovering them one at a time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HttpException carries the status the caller meant, and its own javadoc says
extensions map it to a structured response — but nothing did. Every one reached
the catch-all and came back as 500, including the 400s that RequestHelper
raises for a malformed query param and that flash-ext-jackson raises for an
unparseable body. A handler doing the documented thing produced the wrong
status.
The default handler now renders HttpException at its own status, in both dev
and prod modes, with the message JSON-escaped. Not pre-encoded like JSON_404
and JSON_500: the message is per-exception, and a path that already unwound a
stack does not need the allocation shaved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Standard jakarta.validation annotations, compiled once per type into a flat
check table. No configuration: constraints come from the annotations already on
your types, and ValidationException extends HttpException with status 422 so
the default handler renders it without this extension registering anything.
record CreateUser(@NotBlank @Size(max = 80) String name,
@Email String email,
@Min(18) int age) {}
CreateUser dto = validation.body(req, CreateUser.class);
Annotations only — Hibernate Validator's engine is deliberately absent. It
resolves constraints reflectively per call and pulls ~2 MB plus EL, which is
the per-request cost this module exists to avoid. jakarta.validation-api is
~90 KB of annotations.
The passing path allocates nothing. Constraints resolve at first use into an
opcode plus operands cached in a ClassValue, so there is no map lookup and no
lock. Fields are read through MethodHandles adapted to an exact signature —
(Object)Object for references, (Object)long for primitive integrals — so
invokeExact neither boxes nor builds the argument array Field.get and
Method.invoke allocate. Checks are a flat array walked by a tableswitch rather
than a class hierarchy behind a virtual call. @Size reads a length the object
already knows and @Email scans with indexOf, because Pattern.matcher allocates
a matcher and two int arrays per call. Messages are pre-rendered at compile
time. The violation list and the exception exist only once something fails.
@Pattern is the marked exception: its regex compiles once but matcher()
allocates per call.
Constraints are read from declared fields, so records and plain classes take
one code path — a constraint on a record component propagates to its backing
field.
Jakarta null semantics are exact: only @NotNull rejects null.
flash-ext-openapi now mirrors the same annotations into the generated schema —
minLength, maxLength, minItems, minimum, maximum, pattern, format: email and
required — via an optional jakarta.validation dependency detected at boot. A
type declares its rules once and both the validator and the published contract
read them. An explicit @Schema still wins; the bridge only fills keys nobody
set, and without the annotations on the classpath the bridge class is never
loaded.
flash-ext-jackson is optional too: validate(value) works without it, only
body(req, type) needs a codec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Relism
merged commit bd899d52bb into master2026-09-09 14:21:17 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Boots a real app on an OS-assigned port for a test class or a single test and hands back a client pointed at it: @RegisterExtension static FlashTest app = FlashTest.of(new BlogApp()) .mock(UserService.class, new InMemoryUserService()); app.get("/api/users").expectStatus(200).expectBodyContains("alice"); A field rather than an annotation, because annotation values are compile-time constants and so could never express a second server wired from the first — FlashTest.of(new BlogApp(auth.baseUri())). Startup is lazy, so reading baseUri() boots that server on the spot and declaration order does the wiring, with no dependence on JUnit's extension ordering. A static field boots once per class, a non-static field once per test; that is stock JUnit field semantics rather than an option to configure. Runs against a real loopback port instead of dispatching in-process. An in-process dispatcher would be a third copy of the routing/handler/exception sequence that Http1Connection and Http2StreamDispatcher already duplicate, kept in sync by hand, and it would let a test pass while the status line, content-length or HPACK encoding was broken. mock() installs overrides as the last extension, after everything the application and its extensions declare, so a fake always wins. Any object is accepted, so a hand-written fake and a Mockito mock are equally welcome and this module depends on no mocking library — only flash and junit-jupiter-api. Teardown cancels the client before stopping the server: HttpClient holds keep-alive sockets open and ServerLifecycle.stop() spins until the last one closes, so the default 15s drain would otherwise be paid on every test class. shutdownNow rather than close(), which blocks until every operation completes and would hang on a leaked WebSocket. Lives at the top level, not under flash-extensions/, which holds things you install() onto an app; this carries junit-jupiter-api at compile scope and nothing installable should. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Standard jakarta.validation annotations, compiled once per type into a flat check table. No configuration: constraints come from the annotations already on your types, and ValidationException extends HttpException with status 422 so the default handler renders it without this extension registering anything. record CreateUser(@NotBlank @Size(max = 80) String name, @Email String email, @Min(18) int age) {} CreateUser dto = validation.body(req, CreateUser.class); Annotations only — Hibernate Validator's engine is deliberately absent. It resolves constraints reflectively per call and pulls ~2 MB plus EL, which is the per-request cost this module exists to avoid. jakarta.validation-api is ~90 KB of annotations. The passing path allocates nothing. Constraints resolve at first use into an opcode plus operands cached in a ClassValue, so there is no map lookup and no lock. Fields are read through MethodHandles adapted to an exact signature — (Object)Object for references, (Object)long for primitive integrals — so invokeExact neither boxes nor builds the argument array Field.get and Method.invoke allocate. Checks are a flat array walked by a tableswitch rather than a class hierarchy behind a virtual call. @Size reads a length the object already knows and @Email scans with indexOf, because Pattern.matcher allocates a matcher and two int arrays per call. Messages are pre-rendered at compile time. The violation list and the exception exist only once something fails. @Pattern is the marked exception: its regex compiles once but matcher() allocates per call. Constraints are read from declared fields, so records and plain classes take one code path — a constraint on a record component propagates to its backing field. Jakarta null semantics are exact: only @NotNull rejects null. flash-ext-openapi now mirrors the same annotations into the generated schema — minLength, maxLength, minItems, minimum, maximum, pattern, format: email and required — via an optional jakarta.validation dependency detected at boot. A type declares its rules once and both the validator and the published contract read them. An explicit @Schema still wins; the bridge only fills keys nobody set, and without the annotations on the classpath the bridge class is never loaded. flash-ext-jackson is optional too: validate(value) works without it, only body(req, type) needs a codec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>