Repository navigation
Test gaps at 3fa3533: surviving mutants and test names that claim more than they check #104
Description
Follow-up to #81.
After #92, #93 and #94, 22 of the 24 mutants I ran on 3fa3533 for the items of #81 fail a test. The mutants below still pass, and some tests check less than their names say. "The lock tests" are the 87 tests in test/advisory_locks_test.exs, test/advisory_locks_protocol_test.exs, test/storage/subscription_lock_test.exs and test/subscriptions/subscription_locking_test.exs. Each item has its details below.
-
fix(subscriptions): stop a stale verification retry from polling alongside the pending one #67
AdvisoryLocks: removingcancel_verify_retry/1from the lock pool:DOWNhandler still passes the lock tests, as in Test gaps at a2ce825: surviving mutants and two unreliable test barriers #81. -
fix(migrations): keep the fork's migrations out of upstream's schema versions #89
Tasks.Migrations: readingfork_schema_migrationsfrompublicinstead of the event store's schema passes the three migration test files. -
fix(subscriptions): tell lock owners their session is gone when the call that hit it replies before the connection says so #87
AdvisoryLocksandAdvisoryLocks.Protocol: four mutants of the wait for the lost-session notice pass the lock tests. -
fix(subscriptions): make caught up subscriptions read events appended while the notification connection was not listening #82
Listener: removingsend(self(), :listening)fromlisten_for_events/1, or the clause for a one-argument:configurefunction fromconnecting/3, passes the listen-gap tests andtest/notifications. -
fix(subscriptions): stop retrying a failed checkpoint write on every ack #91
SubscriptionFsm: three mutants ofcheckpoint_unwrittenandcheckpoints_pendingpasssubscription_fsm_test.exsand 318 tests intest/subscriptionsandtest/storage. -
fix(subscriptions): stop a losing claim from writing the subscription row #90
Storage.Lock: removing the{:error, _error} = reply -> replyclause fromtry_acquire_exclusive_transaction_lock/4passes three of the four lock test files, and with it an error from that statement crashesAdvisoryLocks. -
"should leave other event stores in the same database untouched", as chore(test): catch migration regressions and make two test barriers reliable #94 reworked it, compares the migration history and the columns of
publicbut no other objects: a migration of another schema that creates an index onpublic.streamspasses it. -
fix(subscriptions): stop retrying a failed checkpoint write on every ack #91 "waits for another threshold of acks before writing again" never reaches the next threshold, and "writes the latest acknowledged position when the retry fires" fires no retry.
-
chore(test): catch subscription regressions the suite let through #93 "writes the position when the retry timer fires after the checkpoint interval" calls
SubscriptionFsm.checkpoint/1itself after the timer's message arrives. -
"event store that recorded upstream's 1.5.0" in
list_event_store_migrations_test.exshas no 1.5.0 row since fix(migrations): keep the fork's migrations out of upstream's schema versions #89. -
chore(test): catch lock regressions the lock tests let through #92 "... gives up after a second even with an infinite timeout" passes with a 2 s cap, and "a claim gives up on a locked subscription row at the stricter session lock timeout" passes when the claim waits 1.9 s.
Details
Each mutant was applied to 3fa3533 and the named test files were run against it; every test passed unless the item says otherwise.
-
AdvisoryLocks(fix(subscriptions): stop a stale verification retry from polling alongside the pending one #67): withstate = cancel_verify_retry(state)replaced bystate = statein the lock pool:DOWNhandler, the lock tests passed (87), and so did 377 tests inadvisory_locks_test.exs,advisory_locks_protocol_test.exs,test/storageandtest/subscriptions. -
Tasks.Migrations(fix(migrations): keep the fork's migrations out of upstream's schema versions #89):FROM "public".fork_schema_migrationsin place ofFROM "#{schema}".fork_schema_migrationsinquery_fork_schema_migrations/2passed all 22 tests oflist_event_store_migrations_test.exs,migrate_event_store_test.exsandmigrated_event_store_test.exsunderMIX_ENV=migration --include migration. fix(migrations): keep the fork's migrations out of upstream's schema versions #89 removedlock_generation?/2, so I made new mutants that drop or hardcode the schema in the queries ofTasks.MigrateandTasks.Migrations; of five, this is the only one no test catches. Leaving the schema out of theto_regclasscheck, or reading"public".schema_migrationsin the same task, fails "event store in its own schema beside a migrated one lists the migrations of its own schema". -
AdvisoryLocksandAdvisoryLocks.Protocol(fix(subscriptions): tell lock owners their session is gone when the call that hit it replies before the connection says so #87): each of these passed the lock tests in 3 of 3 runs:without_retry({:disconnect_and_retry, error, state})returning{:disconnect, error, state}directly, so no:query_disconnectedmarker is sent;0 -> statein place of0 -> receive_session_loss(state, 0)inhandle_pending_session_loss/1, so a:session_lostnotice already in the mailbox is not handled before the next call;handle_info({AdvisoryLocks, :query_disconnected}, state)returning{:noreply, state}without waiting for the notice;@session_lost_waitset to:infinity. In no lock test does the notice fail to arrive, so nothing checks that the wait ends.
-
Listener(fix(subscriptions): make caught up subscriptions read events appended while the notification connection was not listening #82): each mutant passed 20 of 20 runs ofsubscription_listen_gap_test.exs(3 tests) and one run oftest/notifications(8 tests, 2 doctests). The listen-gap tests do not restart the notification children, and the only test that sets:configurepasses it as{module, function, args}. -
SubscriptionFsm(fix(subscriptions): stop retrying a failed checkpoint write on every ack #91): with seeds 1, 2 and 3, each passed the 33 tests ofsubscription_fsm_test.exs, and with seed 1 the 318 tests oftest/subscriptionsandtest/storage:- the
:okbranch ofwrite_checkpoint/1withoutcheckpoint_unwritten: false, so after a failed write and a good one the flag stays set and every laterpersist_checkpoint/1writes again with nothing pending; - the same in the
:okbranch ofdelete/1; checkpoints_pending: -1in place of0in the error branch ofwrite_checkpoint/1, which moves the next write after a failure one ack later.
- the
-
Storage.Lock(fix(subscriptions): stop a losing claim from writing the subscription row #90): without the{:error, _error} = reply -> replyclause intry_acquire_exclusive_transaction_lock/4,advisory_locks_test.exs,subscription_lock_test.exsandsubscription_locking_test.exspassed (86 tests) with seeds 1, 2 and 3. To see what the clause does, I revokedEXECUTEonpg_try_advisory_xact_lock(int, int)fromPUBLICand ran the session mode connection as a role without it, so that exactly this statement returns{:error, %Postgrex.Error{}}. On the real code the claim returned the error andAdvisoryLocksstayed up. With the mutant the call exited andAdvisoryLocksterminated with{:case_clause, {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}}}:with_bounded_lock_wait/3rescues onlyDBConnection.ConnectionErrorandPostgrex.Error, not aCaseClauseError. -
"migrate an event store in its own schema should leave other event stores in the same database untouched" (as chore(test): catch migration regressions and make two test barriers reliable #94 reworked it): besides the migration history,
table_structure/2readsinformation_schema.columnsforpublic, so an added column is caught (the Test gaps at a2ce825: surviving mutants and two unreliable test barriers #81 mutant that adds one topublic.streamsnow fails it), but other objects are not. Asubscription_lock_generation.sqlthat runsCREATE INDEX IF NOT EXISTS mutant_idx ON public.streams (stream_version)whencurrent_schema() <> 'public'passed all 8 tests ofmigrate_event_store_test.exsin 3 of 3 runs. WithRAISE EXCEPTIONunder the same condition, exactly that test fails, so the statement does run during it. -
"ack/3 after a checkpoint write failed waits for another threshold of acks before writing again" (fix(subscriptions): stop retrying a failed checkpoint write on every ack #91) uses threshold 5 and acks 1 to 8, then asserts one write attempt and no second one; the next threshold, at ack 10, is never reached, which is why the
checkpoints_pending: -1mutant above passes. "... writes the latest acknowledged position when the retry fires" swaps the schema and callsSubscriptionFsm.checkpoint/1directly; no timer and no:checkpointmessage are involved. -
"checkpoint/1 writes the position when the retry timer fires after the checkpoint interval" (chore(test): catch subscription regressions the suite let through #93) does
assert_receive :checkpoint, 1_000and then callsSubscriptionFsm.checkpoint/1itself, so the message never goes throughSubscription.handle_info(:checkpoint, ...). "checkpoint/1 retries a failed write once the checkpoint interval has passed" and "... after the retry interval when it writes on every ack" (fix(subscriptions): keep a checkpoint that failed to write so the next one retries it #71) assert only aProcess.read_timer/1range. -
"event store that recorded upstream's 1.5.0 lists the lock generation migration as pending" calls
create_new_database/1, dropsfork_schema_migrationsandlock_generation, and inserts no 1.5.0 row. Since fix(migrations): keep the fork's migrations out of upstream's schema versions #89,Initrecords 1.3.2, so the test runs against a database with no 1.5.0 row at all; at a2ce825Initrecorded 1.5.0 and the name matched. -
"closing a connection that cannot cancel its query gives up after a second even with an infinite timeout" (chore(test): catch lock regressions the lock tests let through #92) allows 5 s (
Task.yield(closing, 5_000)), and passed with@cancel_timeoutset to 2_000. That mutant still fails the lock tests in 3 of 3 runs, through the two "lock session ends while a release is queued and new connections hang" tests, which wait 1500 ms. "a claim gives up on a locked subscription row at the stricter session lock timeout" (chore(test): catch lock regressions the lock tests let through #92) setslock_timeoutto 100ms and asserts that the claim returns within 2 s; a claim that always waits 1.9 s and ignores the stricter session value passed it in 3 of 3 runs, and was caught by "should keep the lock held when a repeated claim cannot wait for a table lock" and "should keep every lock on the node while a table lock outlasts a claim". A claim with a finite:timeoutwhose wait is capped at 2 s (min(div(timeout, 2), 2_000)) passed all 87 lock tests in 3 of 3 runs.
Activity