Skip to content

test(chaos): put the SocketWatcher into the concurrency chaos suite#47

Merged
donislawdev merged 1 commit into
masterfrom
test/chaos-with-the-socket-watcher
Jul 25, 2026
Merged

test(chaos): put the SocketWatcher into the concurrency chaos suite#47
donislawdev merged 1 commit into
masterfrom
test/chaos-with-the-socket-watcher

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

What and why

test_concurrency_chaos.py exists for one reason, and its own docstring says it: the suite tested
each thread's job in isolation, which is how an off-main-thread Tk call survived. Since the
SOCKET-layer work the session runs a fourth thread, and since #46 the capture thread reads the
watcher's live port -> pid map without a lock - yet no test in that file ever passed the engine
a socket_source. So the watcher's lifecycle and that lock-free read were exactly the thing the file
was written to prevent: concurrency covered only in isolation.

Two tests, both following this file's existing conventions rather than inventing new ones.

  • test_the_socket_watcher_survives_start_stop_cycles - CYCLES rounds with a real event stream
    running: fail-open (running implies a live capture thread and a watcher), the watcher cleared and
    its handle released on stop, and no bean-socket-watcher thread outliving its session. The thread
    check waits 0.3 s first, because SocketWatcher.stop() only joins for 0.25 s.
  • test_the_capture_thread_reads_the_live_socket_map_under_churn - the real new surface: the
    capture thread resolving pids while the watcher thread mutates the map, the watchdog republishes it
    wholesale, and settings and targeting churn underneath. It runs on the same ports the event
    source announces, because otherwise pid_for always misses and a green run proves nothing.

What a reviewer should know

  • The crashlog watch is the test, not decoration. _pid_for / _process_for swallow their
    exceptions into crashlog.once by design - a broken port table must not kill a session over a
    display name. So a read that started raising under concurrency would leave every other assertion
    in the test green, and the only trace would be a crash entry nobody looks at. Patching once()
    also defeats its _once_seen dedupe, so repeated failures stay visible instead of collapsing into
    one entry.
  • Both claims are mutation-confirmed, not asserted. A SocketWatcher.pid_for that raises turns
    the test red through the crashlog watch; an engine reverted to poller-only stamps 0 rows out of
    2.8 million packets
    . The first mutation also independently confirmed the failure-domain split
    from fix(engine): stamp the connection log from the live socket map #46 - it reported engine.ports and engine.ports.pid rather than one collapsed entry.
  • Stated limit, in the docstring: this does not catch putting the lock back into pid_for. A
    lock contends, it does not raise, and this test does not measure contention. That property has its
    own guard (test_socketwatch.py::test_pid_for_takes_no_lock_because_the_capture_thread_calls_it).
    This file documents what it cannot see, and I kept that habit.
  • Conclusiveness is a condition, never a duration. The loop waits for MIN_STAMPED rows carrying
    the event stream's pid, exactly as the model-worker test waits for MIN_BUILDS/MIN_ROWS - a
    wall-clock budget would let machine speed decide whether the test proved anything, which this file
    already learned the hard way (a threshold from a dev machine vs 8342 packets on a CI runner).
  • _LiveSocketSource is paced deliberately. Unpaced it saturates a core and starves the very
    threads the test is about, which would turn a green run into noise.
  • It did not reproduce the latent _start_locked race (the unsynchronised self._targeting read
    I flagged in the review). 25 start/stop cycles with concurrent apply_settings passed cleanly.
    That is not evidence the race is absent - it is evidence the window is too narrow to hit in a few
    seconds, and it will be handled as its own change.
  • Purely additive: +221 lines, zero deletions, so no existing test was touched.

Checklist

  • python -m pytest tests passes locally. (666 tests, 0 failures, on an elevated shell, so
    the two known non-admin failures are absent rather than excused. Verified through pytest's own
    exit code. smoke_gui.py: OK.)
  • New behaviour has tests (see tests/ for the style). - the change is tests, and both of
    their central claims were mutation-checked.
  • UI text goes through i18n keys, with both lang/en.json and lang/pl.json updated. -
    n/a: no UI text.
  • User-facing changes noted in CHANGELOG.md; technical ones and new tests in
    CHANGELOG-INTERNAL.md, under [Unreleased]. - internal only (convention 39): new tests, no
    behaviour change.
  • Commits follow Conventional Commits (type(scope): summary).
  • No version bump - the owner closes a version via VERSION.txt.

🤖 Generated with Claude Code

This file's charter is "many threads hammering one engine", and it names the
failure it exists to catch: threads that were only ever tested in isolation. Since
the SOCKET-layer work the session runs a FOURTH thread, and since the
connection-log fix the capture thread reads the watcher's live map WITHOUT a lock -
yet no test here ever passed the engine a socket_source, so neither the watcher's
lifecycle nor that lock-free read took part in any chaos at all.

- test_the_socket_watcher_survives_start_stop_cycles: CYCLES rounds with a live
  event stream, asserting fail-open (running implies a live capture thread AND a
  watcher), the watcher cleared on stop, and no bean-socket-watcher thread
  outliving its session - checked after a 0.3 s grace, because SocketWatcher.stop()
  only joins for 0.25 s
- test_the_capture_thread_reads_the_live_socket_map_under_churn: the capture thread
  resolving pids while the watcher mutates the map, the watchdog republishes it and
  settings and targeting churn underneath. It runs on the SAME ports the event
  source announces - otherwise pid_for always misses and a green run proves nothing
- the crashlog watch IS the test: _pid_for/_process_for swallow into crashlog.once
  by design, so a read that started raising would leave every other assertion
  green. Patching once() also defeats its _once_seen dedupe, so repeated failures
  stay visible instead of collapsing into a single entry
- conclusiveness is a CONDITION the test waits for (MIN_STAMPED rows carrying the
  event stream's pid), never a duration - same reasoning as MIN_BUILDS/MIN_ROWS
- both claims mutation-confirmed: a pid_for that raises turns it red through the
  crashlog watch, and an engine reverted to poller-only stamps 0 rows out of 2.8
  million packets. The first mutation also independently confirmed the
  failure-domain split from the previous commit - it reported engine.ports AND
  engine.ports.pid rather than one collapsed entry
- written into the docstring, so nobody assumes otherwise: this does NOT catch
  putting the lock back into pid_for. A lock contends, it does not raise, and this
  test does not measure contention - that guard lives in test_socketwatch.py
- _LiveSocketSource is paced deliberately; unpaced it saturates a core and starves
  the very threads the test is about

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 6b70e78 into master Jul 25, 2026
8 checks passed
@donislawdev
donislawdev deleted the test/chaos-with-the-socket-watcher branch July 25, 2026 10:55
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