49d9552476
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# 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]`.
|
|
|
|
### the_opaque_handles_are_send
|
|
|
|
`ZsCircuit` and `ZsRebuild` are `Send` (the engine's fault cell is an `Arc<Mutex>` and the
|
|
callback context an `unsafe impl Send` wrapper). Rival: restoring the `Rc<Cell>` fault cell, or
|
|
dropping `unsafe impl Send for Ctx`, fails `engine.rs` to build (`*mut c_void cannot be sent`).
|
|
|
|
### a_circuit_stepped_on_two_threads_runs_its_callbacks_on_the_thread_that_steps
|
|
|
|
A filter callback records its thread; the circuit is stepped on the test thread, moved, and
|
|
stepped on another. The ids differ and match the stepping threads, so the callback is not
|
|
pinned to the thread that built the circuit. The second thread's push is seen by its own step.
|