Skip to content

fix: pass Cowlib 2.22.0 cookie and date upstream suites natively - #5

Merged
mremond merged 6 commits into
mainfrom
fm/task-9b04259d
Nov 12, 2026
Merged

mremond merged 6 commits into
mainfrom
fm/task-9b04259d

Conversation

@mremond

@mremond mremond commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

Cowlib's own cookie and date test suites had not been run on the 2.22.0 release, so Nera's cookie and date parsing had no upstream check against BEAM.

What changed

  • Cowlib's original `cow_cookie` and `cow_date` EUnit suites now run natively and on BEAM, and the CI Cowlib job runs them.
  • `filename:flatten/1` is exported, and `io:format` `~W` and `~P` print depth-limited terms as OTP does. EUnit needed both.
  • Scheduler races are fixed. A `receive ... after` could wait forever, and a program could exit with status 0 before an EXIT or DOWN signal was delivered.
  • The Cowlib harness accepts a 2.22.0 pin, and the compatibility, limits and changelog docs record the results.

Before and after

Before: Natively, EUnit stopped on an undefined `filename:flatten/1` and then on a `badarg` from `~W`. About 1 run in 30 of the cookie suite hung or exited early, on unchanged main too.

After: Both suites pass natively and on BEAM: 157 of 157 cookie tests and 27 of 27 date tests. 1,200 consecutive native cookie-suite runs pass.

This changes the runtime, the standard library and the test harness; there is no screen to show.

Checked

  • Original cow_cookie and cow_date suites through nera test, natively and on BEAM, locally and in the Linux CI job: 157/157 and 27/27 on both engines; EUnit framework controls pass on both; no skipped or normalized assertions
  • Repeated native cow_cookie runs: 1,200 of 1,200 pass after the fixes (600 one at a time, 600 in four parallel loops); unchanged main hung 3 times in 80
  • New runtime unit tests against the old scheduler code: Each of the six tests fails on the old ordering and passes now
  • Independent review of the scheduler change: No lost or double-run process found; its one point, duplicate timer entries on wakes with no work, is fixed here
  • Receive-timer and shutdown probes from the contributing guide: All 18 timer cases within budget (median lateness 1.1 to 1.7 ms); shutdown timings within budget. The probe's handler case fails on output text, identically on unchanged main
  • Cowlib demo comparison with OTP 29: Passes; only the four known lines differ
How it was checked

cargo build --release --locked: passed

cargo clippy --workspace --all-targets --locked -- -D warnings: passed

cargo test --workspace --locked --no-fail-fast: passed (nera-runtime library 667 passed, 1 ignored)

python3 scripts/test-allocator-config.py: passed

./target/release/nera test -j 1 --timeout 30: 466 passed, 0 failed, 2 skipped of 468, on the final version

python3 scripts/ci/test_changes.py: passed

Six new runtime tests (stale_receive_timer_cannot_replace_a_later_one, receive_timer_taken_before_waiting_is_published_still_wakes, timer_expiring_during_a_wake_without_mail_still_wakes, waking_without_work_keeps_a_single_timer, exit_cleanup_in_progress_is_not_deadlock, delivery_leaves_the_receive_timer_to_the_worker): 10 of 10 runs pass

tests/io_format_depth.erl and tests/filename_flatten.erl: 10 of 10 runs pass natively; both match BEAM

python3 -m unittest discover -s tests/integration/cowlib -p 'test_*.py': 101 tests, 3 existing skips, 10 of 10 runs pass

tests/integration/cowlib/native_eunit.py --pin 2.22.0 --modules cow_cookie cow_date: passed locally and in the CI Cowlib job

scripts/test-native-timer.py: passed; scripts/test-native-shutdown.py: timings pass, handler output text differs as on unchanged main

scripts/test-cowlib-demo.py: 4 of 4 pass

Platform: macOS arm64, Rust 1.99.0, OTP 29.1.2 with EUnit 2.11, PropEr 1.5.0; CI covers Linux x86_64 and macOS arm64

Risk

Risk above low: Blast radius, medium — Receive timers and the idle check run in every native program

Full risk assessment
Dimension Level Why
Blast radius medium Receive timers and the idle check run in every native program
Behavior low Corrects lost timeouts, early exits and two missing library features
Undo low Revert and done
Evidence low Upstream suites pass end to end; 1,200 repeated runs; ordering tests
Sensitivity low No sign-ins, keys, saved data or release steps
Novelty low Edits existing scheduler, timer and format code
Dependencies low No new or updated packages
Not done

Cowlib's `prop_parse_date/0` property passes on BEAM but cannot run natively yet. `rand:uniform/1` is a built-in that `rand:seed/2` does not control, so seeded runs differ.

A separate cookie probe still prints `Set-Cookie` attributes in another order than BEAM, because Nera orders atom map keys differently. The upstream suite does not depend on that order.

The review noted two older scheduler gaps this change does not touch: a cascade kill of a just-woken process can keep the runnable count too high, and the main process can miss a signal sent during its last slice.

@mremond mremond added this to the 0.1.0 milestone Nov 12, 2026
OTP's EUnit server calls filename:flatten/1 for watched paths, so a native
EUnit project failed to build with "function filename:flatten/1 is not
exported". Nera's filename module already defined it privately; it is now
exported with OTP's behaviour.

The filename_flatten fixture checks strings, binaries, atoms, nested and
improper lists, function_clause on a non-list, apply/3 and a remote fun
against BEAM's results.
~W and ~P printed the literal text "~W" or "~P". EUnit's reporter formats a
cancelled test group with ~tP and passes that text to io:fwrite/2 again, so
the leftover directive made the reporter fail with badarg.

Both directives now take a term and a depth and print what lies deeper as
"...", following io_lib:write/2 for ~W and the pretty printer for ~P; the two
differ only for maps. Depth 0 prints "...", a negative depth the whole term,
and ~W honours field width as ~w does.

The io_format_depth fixture checks 38 results taken from BEAM, including
off-heap binaries, sub-binaries, trailing bits, maps, improper lists, field
widths and EUnit's second formatting pass.
Cowlib's original cow_cookie and cow_date EUnit suites now run on Cowlib
2.22.0, the release examples/cowlib_demo builds, on BEAM and through
nera test with OTP's EUnit: 157/157 and 27/27 on both, with the EUnit
framework controls passing.

- upstream-2.22.0.json pins the release commit; native_eunit.py and
  native_date.py take --pin 2.22.0, while 2.20.0 stays the default for the
  earlier campaigns.
- cow_cookie preparation keeps expiry_in_window/3, a test helper the new
  Max-Age/Expires limit test calls.
- native_date.py runs 2.22.0's renamed prop_parse_date/0. It passes on BEAM;
  natively it is blocked because rand:uniform/1 is a built-in that
  rand:seed/2 does not control.
- The Linux workflow fetches the pinned commit, runs both suites and keeps
  the report as an artifact.
- COMPATIBILITY.md, KNOWN_LIMITS.md, the harness README, the example README
  and the changelog record the results and limits.
A process in `receive ... after` could wait forever, and a program could
exit with status 0 too early. Repeated native runs of Cowlib's cow_cookie
suite hit both about once in 30 runs, also on unchanged main.

- A timed receive now registers its timer while the process is still
  locked, before its wait becomes visible. A late registration can no
  longer replace the timer of the process's next receive, and a timer
  that expires before the wait is published still wakes the process.
- Message, exit and timer senders no longer cancel the receiver's timer
  after queueing it. The receiver could already be in a later timed
  receive; the worker that runs it cancels the timer under the process
  lock.
- The idle check treats a process whose exit cleanup is still running as
  live work, so linked or monitoring processes get their EXIT or DOWN
  signal before the program stops.

Runtime unit tests replay each ordering and fail on the old code.
… again

A process woken with nothing to do, such as by socket readiness or an
ignored exit signal, goes back to its wait. It registered its still
pending timer again each time, leaving one more heap entry per such wake
until the deadline. The timer already belongs to that wait, so it is now
kept; a timer that fires meanwhile is still caught by the wait's race
check. A unit test wakes a waiting process 100 times and checks that one
timer entry remains and the timeout still fires.
@mremond mremond modified the milestones: 0.1.0, 0.1.1 Nov 12, 2026
@mremond
mremond merged commit e573d8f into main Nov 12, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant