NAME
DBIO::MySQL::EV::TestHarness - Reusable live-MySQL harness for EV async on_connect-replay tests
VERSION
version 0.900001
SYNOPSIS
use Test::More;
use DBIO::MySQL::EV::TestHarness;
# Skip the whole file cleanly unless a live MySQL DSN + EV::MariaDB are available.
DBIO::MySQL::EV::TestHarness->skip_all_unless_live;
my $h = DBIO::MySQL::EV::TestHarness->new(
# each fresh pool connection replays these against the owning sync storage
connect_attrs => {
on_connect_do => [ q{SET @dbio_replay_tag = 'live'} ],
pool_size => 4,
},
);
# Force 3 DISTINCT pool connections to spawn (each replays the connect
# actions at spawn) and run an async op on each -- the replay-verification seam.
my @tags = $h->run_on_each_pooled_connection(3, sub {
my ($conn, $harness) = @_;
return $harness->query_on($conn, q{SELECT @dbio_replay_tag});
});
is $_->[0][0], 'live', 'connect action replayed on this pooled connection'
for @tags;
$h->disconnect;
done_testing;
DESCRIPTION
An installable test harness for the native ev async transport (DBIO::MySQL::EV::Storage). It removes the boilerplate every live EV test repeats -- parse DBIO_TEST_MYSQL_DSN, connect an { async => 'ev' } schema, reach the embedded async backend + its pool, drive the EV loop -- and, crucially, provides the one thing an on_connect-replay test needs and cannot get from a directly-constructed storage: an EV async backend wired to an owning sync storage whose on_connect_do / on_connect_call actually replay on every freshly-spawned pool connection (karr #18 / core #68).
It ships under lib/ (not t/) on purpose: it is consumed both by this distribution's own live tests and by downstream extension distributions, which cannot see another dist's t/lib. An extension that carries a develop/recommends dependency on DBIO::MySQL::EV reaches this class from its own t/.
METHODS
live_available
DBIO::MySQL::EV::TestHarness->live_available or ...;
Class method. True when DBIO_TEST_MYSQL_DSN is set and EV::MariaDB (and EV) can be loaded -- i.e. the live harness can run.
skip_all_unless_live
DBIO::MySQL::EV::TestHarness->skip_all_unless_live;
Class method. When the live harness is unavailable (see "live_available"), calls Test::More->plan(skip_all => ...), which skips and exits the test file cleanly. A no-op when live testing is available.
new
my $h = DBIO::MySQL::EV::TestHarness->new(%opts);
Connect an { async => 'ev' } schema and build its embedded async backend. Options:
dsn/user/pass-- connect info; default toDBIO_TEST_MYSQL_DSN/DBIO_TEST_MYSQL_USER/DBIO_TEST_MYSQL_PASS.schema_class-- the schema class to connect (default a generic sync MySQL schema). Supply your own to register storage layers / components (this is how an extension injects its behaviour).connect_attrs-- extra connect attributes merged into the{ async => 'ev', ... }attrs hash, e.g.on_connect_do,on_connect_call,pool_size. These are exactly the actions replayed on each pooled connection.timeout-- wall-clock seconds "await" waits before failing loudly (default 30).
Croaks unless "live_available" (call "skip_all_unless_live" first).
schema
The connected schema instance.
sync_storage
The sync DBIO::MySQL::Storage the schema connected to (the owner of the async backend). Forces driver determination on first call.
async
The embedded async backend (a DBIO::MySQL::EV::Storage, possibly with composed extension async layers). Built lazily on first access via the sync storage's async resolver, which also wires the owner back-reference.
pool
The DBIO::MySQL::EV::Pool of the async backend.
await
my @result = $h->await($future);
Drive the EV loop until $future is ready, then return its result (dies on a failed Future). Guarded by a wall-clock alarm so a hang fails loudly instead of spinning forever.
warm_pool
$h->warm_pool;
Drive every pool connection through its EV::MariaDB connect handshake to ready, then release them all back to the idle pool. The pool hands off a freshly-spawned connection before its async connect has completed ("acquire" in DBIO::Storage::PoolBase resolves immediately), and a bound query's prepare/execute needs an already-connected handle. A pooled *_async op (e.g. $h->async->select_async(...), or an extension async method that rides the inherited _query_async) acquires a connection internally, so the harness cannot warm it at the call site -- call warm_pool first to guarantee every idle connection an acquire can hand back is already connected. Mirrors the manual pool pre-warm the raw live tests do by hand. Returns nothing.
query_on
my @rows = $h->await( $h->query_on($conn, $sql, \@bind) );
Run $sql on the specific pooled connection $conn (pinned, not released), returning a Future of the raw result rows -- the natural way to assert that a connect action replayed on that connection. $sql uses ? placeholders (MySQL native). Returns a Future; wrap with "await".
run_on_each_pooled_connection
my @results = $h->run_on_each_pooled_connection($n, sub {
my ($conn, $harness) = @_;
return $harness->query_on($conn, $sql); # a Future
});
The replay-verification seam. Forces $n distinct pool connections to spawn by acquiring (and holding) them -- each spawn replays the owner's on_connect actions via "_run_pool_connect_statement" in DBIO::MySQL::EV::Storage. Then runs $code->($conn, $self) on each held connection, awaits the returned Future, releases every connection, and returns one entry per connection (in spawn order). Each entry is an arrayref of whatever that connection's Future resolved to -- e.g. for a "query_on" op it is the arrayref of raw result rows, so $results[$i][0] is connection $i's first row. Capturing per connection as an arrayref keeps multi-row results from flattening across connections.
$code must return a Future. Croaks if the pool's max_size is smaller than $n (raise pool_size in connect_attrs), or if the pool ever hands back a non-distinct connection (which would mean a spawn did not happen and the replay was not exercised on a fresh connection).
disconnect
Tear down the async backend (shutting the pool, which runs the on_disconnect actions on each live connection) and the schema storage.
REUSE BY AN EXTENSION
An async storage extension composes onto this transport via core's storage-layer composition (core karr #70); it does not ship its own transport. To live-test that its on_connect_call replays on pooled EV connections, an extension reuses this harness by supplying its own schema class (carrying the extension's storage layer + component) and its own per-connection assertion:
my $h = DBIO::MySQL::EV::TestHarness->new(
schema_class => 'MyApp::Ext::Schema',
connect_attrs => { on_connect_call => 'load_ext', pool_size => 3 },
);
$h->run_on_each_pooled_connection(3, sub {
my ($conn, $harness) = @_;
return $harness->query_on($conn, q{SELECT @@session.some_var});
});
The harness stays generic: it never mentions any extension, exposes the schema / sync storage / async backend / pool, and lets the caller add the layer (via schema_class) and assert whatever it needs on each pooled connection.
CAVEAT
This is a live harness: it opens real connections against DBIO_TEST_MYSQL_DSN. It is for driver/extension test suites, not production code. It is excluded from the mock-only offline discipline by design -- gate every consumer with "skip_all_unless_live".
AUTHOR
DBIO Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.