# Test evidence: `zset-ffi` What each test guards and where a rival failed. Keyed by test name; the reasoning is in `context/code/zset-ffi.md`. The tests are `crates/zset-ffi/tests/{abi,header,dylib}.rs`. **Injected in the source**, one at a time, each reverted and the suite rerun (a build that printed `Running` was confirmed each time). The failing tests, by name: - `guard` without `catch_unwind`: the overflow tests abort the test binary (`SIGABRT`, a panic across `extern "C"`), which is the failure this crate exists to prevent; - `zs_zset_add` that adds in place: `overflow_is_an_error_code_and_leaves_the_set_unchanged`; - every panic classed `ZS_ERR_PANIC`: `overflow_is_an_error_code_and_leaves_the_set_unchanged`, `a_weight_overflow_in_a_step_is_a_code_and_poisons_the_circuit`, `a_loaded_library_adds_queries_frees_and_reports_overflow`; - a failed step that does not poison: `a_weight_overflow_in_a_step_is_a_code_and_poisons_the_circuit`; - a recorded fault that does not poison: `a_malformed_pair_and_an_unknown_id_are_codes`; - `rebuild_step` that ignores the deadline: `a_rebuild_resumes_across_calls_and_matches_one_step`; - `Min` that reads non-positive weights: `min_and_max_read_only_values_of_positive_weight` (the first version of the suite did not catch this; the test was added for it); - a header declaration with a parameter dropped or retyped (`int64_t weight` as `int32_t`): `the_header_declares_exactly_the_exported_functions_with_their_types`; - a header code renumbered, and a new `Code` variant with no header line: `the_header_constants_are_the_rust_constants`; - `zs_zset_encode` that reports the wrong size: eight ABI tests, among them `a_small_buffer_reports_the_size_it_needs`. ### overflow_is_an_error_code_and_leaves_the_set_unchanged `a` holds `k = MAX` and `j = 1`; adding a set that overflows `k` must return the overflow code and leave **both** keys as they were. The key `j` sorts before `k`, so a partial in-place add changes `j` first; a fixture where the overflowing key came first would pass that rival. ### a_weight_overflow_in_a_step_is_a_code_and_poisons_the_circuit A weight reaches `MAX` in an `integrate`, then one more overflows inside `step`. The step returns the overflow code, the next step and a new operator return the poisoned code. ### a_rebuild_resumes_across_calls_and_matches_one_step Nine keys, chunks of two, a deadline callback that is always expired: the job needs five calls' worth of chunks (the callback is counted: five, one per chunk), and the sum of what the sink yields equals one uninterrupted step on a second circuit. The loop is bounded at 20. ### min_and_max_read_only_values_of_positive_weight Weights of mixed sign in one group, where the smallest value has a negative weight, so `> 0` and `!= 0` give different minima. ### a_loaded_library_adds_queries_frees_and_reports_overflow The built `libzset_ffi.{so,dylib}` is opened with `libloading` and driven through symbol lookups: the contract a CFFI caller meets, including the exported names, not the rlib. ### the_header_declares_exactly_the_exported_functions_with_their_types Parses `src/lib.rs` and the header (comments stripped), maps each Rust type to its C spelling and compares names, parameter types and return types; asserts the scanner saw more than 30 functions, so an empty parse cannot pass. The first version compared only arity and passed a retyped parameter. ### the_header_constants_are_the_rust_constants Every `ZS_ERR_*`, `ZS_ABI_VERSION`, `ZS_OK` and the four `ZS_AGG_*` equal the Rust values; the the header's `ZS_ERR_*` lines are counted against `Code::ALL`, which the `codes!` macro generates, so a new variant without a line fails (it did, injected as `Novel = 9`). ### a_chunked_rebuild_through_delay_is_one_tick After #345's review the rebuild is one logical tick. A snapshot of two keys, chunks of one, an always-expired deadline: the first call is `done == 0`, the second `done == 1` (the last chunk is Done despite the deadline), and a `delay` sink shows the previous tick's key once. Under the old per-chunk ticking the delay showed the first chunk instead. ## Review of #347 (8a76469b) **Injected afterwards**, one at a time, each failing by name: a null buffer written through (the test binary aborts, which is the failure); `zs_circuit_input` adding the input before checking `out`, and `zs_circuit_take` clearing the sink first: `a_null_out_pointer_is_refused_before_any_state_changes`; a rebuild stepped on another circuit: `a_rebuild_is_stepped_only_on_its_own_circuit`; the sink bit unchecked in `take`: `stream_ids_and_sink_ids_are_not_interchangeable`; a `flat_map` or `keep` failure ignored: `a_callback_that_reports_failure_fails_the_step_and_kills_the_circuit`; re-entry undetected, or `step` not setting `in_step`: `a_callback_that_calls_back_into_its_circuit_gets_poisoned_and_changes_nothing` (and the unit test below for the first); a count that drops its zero row: `a_count_or_sum_that_cancels_to_zero_keeps_its_row`; `len32` truncating: `a_length_of_4_gib_or_more_is_refused_not_truncated`. The aliasing finding (`zs_zset_add(a, a)`) is not an assertion failure on a native build: only Miri finds it, and `a_set_added_to_or_subtracted_from_itself_is_defined` is the test Miri failed before the fix and passes after (recipe in `zset-ffi.md`). ### a_set_added_to_or_subtracted_from_itself_is_defined Self-add doubles, self-sub empties, and a self-add that overflows leaves the set unchanged. ### a_null_buffer_is_refused_before_it_is_written `cap >= needed` with a null buffer is `ZS_ERR_NULL_ARGUMENT`; a null buffer with too small a `cap` still probes the size. ### a_null_out_pointer_is_refused_before_any_state_changes The next input id, the sink's contents and the job's progress are observed after a refused call with a null out-pointer; each is as before. ### a_rebuild_is_stepped_only_on_its_own_circuit A job made from one circuit stepped on another is `ZS_ERR_BAD_HANDLE` and neither sink moves. ### stream_ids_and_sink_ids_are_not_interchangeable A stream id given to `take`, and a sink id given to `delay`, `output` and `push`, are refused. ### a_callback_that_reports_failure_fails_the_step_and_kills_the_circuit `keep`, `flat_map` and `expired` callbacks each return their failure value: `ZS_ERR_CALLBACK`, then `ZS_ERR_POISONED`. ### a_callback_that_calls_back_into_its_circuit_gets_poisoned_and_changes_nothing A filter callback calls `zs_circuit_input` on its own circuit: it gets `ZS_ERR_POISONED`, the outer step completes, and no input was added. ### tests::a_circuit_left_mid_step_by_a_skipped_frame_answers_poisoned_to_everything What a Lisp non-local exit through `zs_circuit_step` leaves behind is the `in_step` flag set; every later call is `ZS_ERR_POISONED` and writes nothing. ### a_count_or_sum_that_cancels_to_zero_keeps_its_row Codex's finding: `Some(0)` was filtered away for a non-empty group. ### a_length_of_4_gib_or_more_is_refused_not_truncated `len32` refuses `1 << 32` and accepts `u32::MAX`. ## Delta review of #347 (9bcfa9d1) **Injected**, one at a time: a fault that overwrites the first (`note` replaced by a plain `set`) fails `an_index_by_callback_that_fails_reports_the_callback_code_not_a_later_fault`; `zs_rebuild_step` borrowing the job before checking the circuit (Miri only), and a `free` that ignores `in_step` or `busy`, fail `a_callback_cannot_step_or_free_the_job_or_circuit_it_runs_under` (and under Miri, as use-after-free); an owner compared by address instead of id fails `a_circuit_freed_and_another_made_does_not_inherit_its_jobs`; the interrupted check removed fails `a_host_step_between_rebuild_calls_is_interrupted_not_a_poison`; a retyped `ZsKeepFn` fails `the_header_callback_typedefs_match_the_rust_callback_types`. ### an_index_by_callback_that_fails_reports_the_callback_code_not_a_later_fault The callback fails (returns 3) and emits nothing, so `index_by` also sees zero keys. The step must report `ZS_ERR_CALLBACK`, the first fault, not the consequent `BAD_VALUE`. ### a_callback_cannot_step_or_free_the_job_or_circuit_it_runs_under An `expired` callback steps its own job, frees the job and frees the circuit. The step is refused with `ZS_ERR_POISONED`, both frees do nothing, and the job then finishes normally. ### a_circuit_freed_and_another_made_does_not_inherit_its_jobs A job outlives its circuit; sixty-four new circuits (some at the old address) all refuse it. ### a_host_step_between_rebuild_calls_is_interrupted_not_a_poison `ZS_ERR_INTERRUPTED`, and the circuit still steps afterwards. ### the_header_callback_typedefs_match_the_rust_callback_types Retyping `ZsKeepFn`'s return in the header fails. The `unary_op!` signatures are now read from the macro body rather than written into the test. ## Transactions (improvise-bw4p.5) Rivals were injected one at a time in `engine.rs` and watched fail by name; Miri passes on `abi`. ### a_rollback_after_a_failed_step_revives_the_circuit_in_its_old_state An integrate held at `i64::MAX`; inside a transaction a step overflows (`ZS_ERR_OVERFLOW`, then `ZS_ERR_POISONED`), `commit` of it is refused as poisoned, and `rollback` revives the circuit with the integral where it was: `-1` then gives `MAX - 1`, with neither the failed `+1` nor the key added beside it. Rivals: a rollback that leaves the poison on, and one that does not roll the circuit back, fail this test and the callback one; a `commit` that does not check the poison fails this test. ### a_committed_transaction_keeps_its_steps_and_a_new_one_may_follow `commit` keeps the step and its sink, and a later transaction can be opened and rolled back. ### a_failed_callback_step_is_rolled_back_too A failure the callback only reports (the step returns, the fault is noted) is rolled back like a panic: the sink holds nothing after the rollback. ### transaction_misuse_is_a_code_and_changes_nothing `ZS_ERR_TRANSACTION` for `commit` or `rollback` with none open, a second `begin`, and a `take` while one is open (nothing written to the out-pointer). A rollback after a poison from outside any transaction is also refused and the circuit stays poisoned. Rivals: `take` allowed in a transaction, and a second `begin` allowed, each fail this test. ## Review of #355 at 51438911 ### f1_rebuild_begun_before_a_txn_continued_inside_and_rolled_back Through the ABI, a job continued inside a rolled-back transaction returned 0 with `done` 1 and a partial load. It is now `ZS_ERR_INTERRUPTED`. ### f4_a_failed_rebuild_step_in_a_txn_then_rollback_then_resume A job whose first chunk failed inside the transaction resumed without that chunk; now `ZS_ERR_INTERRUPTED`. ### f2_a_rollback_from_inside_a_callback_is_refused_and_the_txn_stays_open `ZS_ERR_POISONED` from the callback's rollback, and the transaction still commits. ### f3_begin_on_a_circuit_poisoned_outside_a_txn_is_poisoned `begin` is `ZS_ERR_POISONED`; `commit` and `rollback` are `ZS_ERR_TRANSACTION`. ### f5_a_chunk_whose_push_overflows_in_a_txn_then_rollback_is_interrupted The same path through the ABI: `zs_rebuild_step` returns `ZS_ERR_OVERFLOW` from the chunk's push, `zs_circuit_rollback` succeeds, and the next `zs_rebuild_step` was a silent success; it is now `ZS_ERR_INTERRUPTED`. Rival: the mark before the push removed fails `[f5]`.