Files
improvise/context/test-evidence/zset-ffi.md
T

8.6 KiB

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.