NAME
DBIO::Test::AsyncParity - The same ResultSet operations, synchronous and asynchronous, on one database, compared
VERSION
version 0.900004
SYNOPSIS
package DBIO::PostgreSQL::EV::Test::AsyncParity;
use base 'DBIO::Test::AsyncParity';
sub parity_deviations {{
'upsert' => {
kind => 'pending', # not built yet
card => 'k58', # required for pending
reason => qr/upsert/,
},
'row-copy' => {
kind => 'unsupported', # this transport cannot do it
reason => qr/copy_async/,
},
}}
# the seams with a working default, only when the dist needs its own:
sub parity_reset {
my ($class, $schema, $harness) = @_;
$class->SUPER::parity_reset($schema, $harness);
$schema->storage->dbh_do(sub { $_[1]->do('ALTER SEQUENCE ... RESTART') });
}
1;
and in the test file DBIO::PostgreSQL::EV::Test::AsyncParity->run(...).
DESCRIPTION
An async transport (future_io, ev, ...) answers the same ORM surface a blocking connection does. This module is the one list of cases that proves it, stated once in core: every case is driven twice on the same connected schema and the same database -- once through the synchronous method, once through its *_async twin -- and what the two ways produced is compared. Compared are the rows (as plain data), the counts, the state the case left behind, and, for the cases that are about failing, the exception class.
The suite takes the harness (DBIO::Test::AsyncHarness) a transport already has and is called by a thin test file:
use DBIO::PostgreSQL::EV::TestHarness;
use DBIO::Test::AsyncParity;
DBIO::PostgreSQL::EV::TestHarness->skip_all_unless_live;
my $h = DBIO::PostgreSQL::EV::TestHarness->new(
schema_class => 'DBIO::Test::Schema',
storage_type => 'DBIO::PostgreSQL::Storage', # the driver; the schema has none
);
DBIO::Test::AsyncParity->run(harness => $h, schema_class => 'DBIO::Test::Schema');
done_testing;
There are exactly three acceptable answers for a case:
- pass
-
The two ways agree, and the case's own expectation (if it has one) holds.
- deviation
-
The transport states, in "parity_deviations", that this case does not agree and why. A declared deviation is a test, not a skip: the case is driven, it has to fail the way the declaration says, and a declared deviation of a case that passes is a failure -- the declaration is stale.
- absent
-
Core has no asynchronous twin for the operation at all (
populate). The table says so itself; the case is driven, the missing twin is observed, and it turns red when core grows one.
Anything else is a failure, and a failure names the number and the name of the case. A case is not skipped by choice: the only exception is a missing capability ("Capabilities"), and then it is a test of its own, labelled skipped with the capability named, and listed on the closing line. Without a live harness the file is skipped as a whole by the harness, which is where that decision belongs.
The model is DBIO::Test::Conformance (ADR 0060): one table in core, the transport answers it, a deviation is declared and checked.
METHODS
table_version
The version of the case table, 2 (version 1 had the 41 cases of the ResultSet surface; 2 adds the cases of the async scaffold, 42 to 56).
cases
my $cases = DBIO::Test::AsyncParity->cases;
The arrayref of cases, in number order, a fresh copy. See DBIO::Test::AsyncParity::Cases.
parity_deviations
Seam with a default: none. See "Declaring deviations".
parity_seed
Seam with a working default, see "WRITING A TRANSPORT'S CLASS".
parity_reset
Seam with a working default, see "WRITING A TRANSPORT'S CLASS".
parity_capabilities
my @names = $class->parity_capabilities($harness);
The capabilities of the transport under test, the names a case lists in requires. The default gives what can be read off the transport: its transport_capabilities (listen, notify, ...), pipeline when it overrides the pipeline seams ("_pipeline_refusal" in DBIO::Storage::Async), cancel when its Future class can cancel, and pool_queue when its pool is a DBIO::Storage::PoolBase. A transport adds what only it knows (see "Capabilities") and calls this method first.
parity_fault
my $guard = $class->parity_fault($schema, $harness, $name, @args);
Make the transport fail in the way $name says, until the returned guard goes out of scope. The default throws: a transport that offers a fault lists fault:NAME in "parity_capabilities" and implements it here. Names: rollback_fails (a ROLLBACK of a transaction is not confirmed), disconnect (the connection of the transaction context passed as the first argument is lost).
parity_session_sql
my ($set, $probe) = $class->parity_session_sql($harness);
Two statements for the case session-reset: one that leaves state in the session of the connection it runs on, and one that fails when that state is gone. The default is a temporary table, CREATE TEMPORARY TABLE and a SELECT from it. An engine without that syntax overrides it.
parity_unreachable
my $schema = $class->parity_unreachable($harness, $password);
A connected schema in the harness's async mode whose connection cannot be made, built with $password; the case connect-error-masked asks it for the count of artists and checks the error does not carry the password. The default throws; a transport that implements it lists unreachable_connection in "parity_capabilities".
parity_sync_schema
Seam with a working default, see "WRITING A TRANSPORT'S CLASS".
check_declarations
my @problems = $class->check_declarations;
What can be checked without a server: every declared deviation names a case of the table, has kind unsupported, pending or absent, a regex reason, a card for pending, and does not redeclare a case the table itself marks absent. Returns the problems as strings, none when the declaration is whole. "run" makes it the first test.
results
my @results = $class->results(harness => $h, schema_class => 'DBIO::Test::Schema');
my @results = $class->results(harness => $h, only => [ 'find-pk' ]);
Drive every case against the harness's schema and return one hashref per case: { no, case, kind, status, message } and, for deviation, the deviation kind and for pending the card. It does not touch Test::More. status is pass, deviation, absent or fail. only restricts the run to the named cases.
schema_class names the schema the table was written for (default DBIO::Test::Schema); the runner only uses it to say what is missing when the connected schema lacks a source the table needs.
summary_line
my $line = $class->summary_line(\@results);
The one line that closes a run: the declared deviations with their cards and the absent operations. undef when there is nothing to say.
run
DBIO::Test::AsyncParity->run(harness => $h, schema_class => 'DBIO::Test::Schema');
Write the tests: first one for the declarations ("check_declarations"), then one per case, labelled with the case number, its name and its status. A failure's message is printed with diag, the closing summary line too. Does not call done_testing.
WRITING A TRANSPORT'S CLASS
Every method is a class method. Nothing must be provided: a transport that answers every case needs no class of its own and calls DBIO::Test::AsyncParity->run directly.
- parity_deviations
-
Hashref keyed by case name (see "CASES"), default
{}. See "Declaring deviations". - parity_seed($schema, $harness)
-
Put the fixture of "fixture" in DBIO::Test::AsyncParity::Cases into the empty database: three artists, four CDs, four tracks. The default creates them through the ResultSet API, without naming a key, and works on any schema built from DBIO::Test::Schema. Override when the database wants something else (rows by
COPY, a different schema). - parity_reset($schema, $harness)
-
Remove everything a case could have left and everything the seed made, so the next run starts empty. The default deletes the rows of
OneKey,Track,CDandArtist, children first, with oneDELETEeach. It runs before each way of each case and once more after the case, also after a failure. Override it when the tables carry something it must not delete or when a sequence is to be put back. - parity_sync_schema($harness)
-
A second, purely synchronous connection to the same database, for the case that is about an
*_asynccall on a connection without an async mode. The default connects the harness's schema class with the harness's own DSN, user and password and noasyncattribute. When the harness carries astorage_type(see "new" in DBIO::Test::AsyncHarness), it connects a clone of the schema class declaring that driver, as the harness's own schema does; otherwise the schema class must declare its driver itself (ADR 0067). Disconnected when the run ends.
Declaring deviations
parity_deviations returns a hashref keyed by case name:
{ case_name => { kind => 'unsupported' | 'pending' | 'absent',
reason => qr/.../, card => 'k58' } }
- kind
unsupported -
The transport cannot do it, by design. Permanent; needs no card.
- kind
pending -
The transport does not do it yet.
cardis required: a card of the transport's own board, in the formk58. - kind
absent -
The operation has no asynchronous twin. Cases of the table that are absent in core carry that themselves; a transport only uses it for its own gap.
- reason
-
A regex the failure's message has to match. The message is what the runner reports for the case: the exception the asynchronous way threw, or the difference between the two ways.
The run ends with one line, printed by "run" and returned by "summary_line":
2 deviations: 34 upsert (pending, karr k58), 27 row-copy (unsupported); 1 absent: 41 populate
Capabilities
A case may list requires => [ 'name', ... ]. A transport that does not have every one of them does not run the case: its result is skipped, a passing test labelled NN name: skipped (needs NAME), and the closing line lists it. The capabilities of a transport are the names "parity_capabilities" answers; the default reads them off the transport:
listen, notify, ... the transport's transport_capabilities
pipeline it implements the pipeline seams
cancel its Future class can cancel
pool_queue its pool is a DBIO::Storage::PoolBase
A transport adds the ones only it knows, and implements the seams behind them:
fault:rollback_fails parity_fault 'rollback_fails'
fault:disconnect parity_fault 'disconnect'
unreachable_connection parity_unreachable
A skipped case is a gap of that transport, not of the table. The capability is the single place that decides, so a case never skips by a driver name.
CASES
The table is DBIO::Test::AsyncParity::Cases. A case has a number, the name a declaration is keyed by, and a kind: direct (the synchronous method has an *_async twin), composed (core has none, the case composes the operation from the ones it has) or async-only (nothing to compare against; the case states what the asynchronous way must answer).
Reads
1 find-pk find by primary key
2 find-unique find through a named unique constraint (ADR 0034)
3 find-miss find that matches nothing: undef, no error
4 find-rel-key find whose key is a relationship value
5 search-all search with a condition, all rows
6 order-rows-offset order_by with rows and offset
7 group-by group_by over a column subset
8 prefetch has_many prefetch, collapsed
9 join join on a relationship
10 search-related related resultset of a row (composed)
11 first first row of an ordered set
12 single single row by a condition
13 count count of a condition
14 count-literal count with a literal SQL condition
15 results-exist the EXISTS question, hit and miss
16 rsc-all all values of a column
17 rsc-single single value of a column
18 rsc-min-max-sum min, max and sum of a column
19 rsc-func aggregate by name
Writes
20 create create a row
21 create-cascade create with nested related rows (composed)
22 row-insert insert a new_result row
23 row-update update a fetched row
24 row-update-rs-loop update of a set, row by row (composed)
25 row-delete delete a fetched row (OneKey)
26 row-delete-rs-loop delete of a set, row by row (composed)
27 row-copy copy a row (OneKey)
28 row-reload get_from_storage and discard_changes
29 update-or-insert update_or_insert of an unstored row
30 find-or-create find_or_create of a missing row
31 find-or-new find_or_new of a missing row
32 update-or-create update_or_create of a present row
33 update-or-new update_or_new of a present row
34 upsert single-statement upsert (capability)
Transactions and context
35 txn-commit txn_do / txn_do_async that commits
36 txn-rollback txn_do / txn_do_async that fails and rolls back
37 txn-with-context a ResultSet bound to the transaction context (composed)
38 txn-adopts-future the returned Future is adopted (async-only)
Errors
39 err-unique a unique violation: the same exception class
40 async-on-sync-connection *_async without an async mode croaks (async-only)
No async twin
41 populate named, not invented (absent)
Pipeline and cancel
42 pipeline-commit a pipeline of two inserts (pipeline)
43 pipeline-fail a failing statement fails the batch, the pool serves (pipeline)
44 cancel-txn a cancelled transaction rolls back (cancel)
45 cancel-statement a cancelled statement gives its connection back (cancel)
46 cancel-batch a cancelled pipeline retires its connection (pipeline, cancel)
Failure inside a transaction
47 rollback-failed a ROLLBACK not confirmed retires the connection (fault:rollback_fails)
48 txn-statement-in-flight COMMIT waits for a statement still running
49 txn-statement-in-flight-fails a dropped statement that fails fails the transaction
50 disconnect-in-txn the connection is lost mid-transaction (fault:disconnect)
Pool and session
51 pool-self-deadlock a call queued for the transaction's own pool is refused (pool_queue)
52 pool-max-waiters the queue is bounded (pool_queue)
53 pool-acquire-timeout a wait is bounded (pool_queue)
54 session-reset session state is gone after release (pool_queue)
Transport
55 listen-notify LISTEN and NOTIFY (listen, notify)
56 connect-error-masked a connect error carries no password (unreachable_connection)
Cases 1 to 40 cover the ResultSet surface that has an async twin; 41 names the one operation of that surface that has no twin, so that its absence is a line of the run and not a gap in the list. Cases 42 to 56 run the pipeline, cancel, transaction, pool and transport behaviour of the async layer on a real transport. Not in the table: a disconnect in the middle of a deploy and the rotation of an AccessBroker's credentials need a harness of their own; core covers both offline (t/test/64_async_deploy_across_disconnect.t, t/test/63_async_pool_set_access_broker_built.t).
AUTHOR
DBIO & DBIx::Class Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.