test(control): wait until the bridge listens, and reap it when a quit check fails #457
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!457
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/control-quit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: test · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and Rosetta x86_64; no game run) · Needs: Linux and Windows, any GPU (game-free checks only)
Why
Anyone running the game-free suite on Linux or macOS can have it stall:
tests/test_control.py's quit case can hang forits whole 180 s limit and leave a bridge process running after the suite ends, whenever the bridge it starts is a little
late to listen. On this Mac under a loaded suite it happened in about 1 run of 150. This PR fixes the test fixture; the
game's bridge is unchanged. Part of the macOS port (#420).
quit_caseintests/control_test.cforks: the child runs the bridge, and the parent waits until the socket's fileexists, then connects once (
CHECK(connect(...) == 0)).bindand listens afterwards (transport_openinhost/control.c). A connect betweenthe two is refused (
ECONNREFUSED, measured on macOS; Linux refuses it too [inference: not run here]).CHECKprints and callsexit(1). The child keeps serving and holds the parent's stdout and stderr, sotest_control.py'ssubprocess.run(..., capture_output=True, timeout=180)waits for those pipes until its limit.running on this Mac, with a listening socket and no client.
longshot_ctl.py launchretries until the bridge answers.
What changed
The quit case now waits for the child's word that its bridge listens, a failed check ends and reaps the forked bridge,
and two new runs make the race certain (
quit slow,quit fail) on Linux and macOS. The game's bridge is unchanged: itsonly new lines are a hook that only the test build compiles.
quit_caseandcheck_failedintests/control_test.c.tests/control_test.c:hos_control_servehas returned (afterlisten),instead of for the socket's file;
CHECKends and reaps the forked bridge before the process exits (check_failed, POSIX only; on Windowsit only prints and exits, as before);
quit slow(the test build's bridge waits 300 ms betweenbindandlisten) andquit fail(a checkfails once the child serves; it must end with 1 and leave nothing running).
host/control.c: a hook for the test build only, under#ifdef HOS_CONTROL_NO_GAME:hos_control_test_bound,called between
bindandlistenwhen set. The game never definesHOS_CONTROL_NO_GAME; outside those blocks the fileis byte for byte the control-sockets PR's. The hook is in the POSIX transport, so Windows' test build has none.
tests/test_control.py: on Linux and macOS it also runsquit slow(must pass) andquit fail(must end with 1and the injected failure). Windows' run list is unchanged.
listen, rename). That changes the game on every platform, and the real clients already retry.
mac/control-sockets): this branch carries its two commits,1ffdbe50and60dd4e0c, the rebased#456's head. Review only
58a56b55, this PR's own commit;1ffdbe50..60dd4e0cis the control-sockets PR.5e072fc8:test_control.py's run list sits under #58's compile command; #58's command iskept as it is.
6c9edffe: onto #456's rebased head60dd4e0c(rebase --onto);58a56b55is26959952rebased.One conflict, with #544 (
225c37f8, the Windowspeersidcase):tests/control_test.c's usage comment andmain()dispatch, and
tests/test_control.py's run list. Both kept:peersidon Windows,quit slow/quit failon POSIX(
if windows: … else: …). No end commit.Measurements
Verdict: fixed, a game-free proof (a test fixture, nothing to time): after the rebase
tests/test_control.pypasses here for arm64 and x86_64 (40 PASS lines each, the two quit runs among them) and the whole suite changes from
mainonly by that check (FAIL → pass). Before the rebase the two new runs hung 5 of 5 each without the fix, with theforked bridge left running; with it
quit slowpassed 20 of 20 andquit failended with 1 at once, 20 of 20, nothingleft running.
Commits: main
6c9edffe5d(2026-11-14); this PRmac/control-quit@58a56b555d(on #456 @60dd4e0c). Before the rebase: main5e072fc8c2(2026-11-14); this PRmac/control-quit@2695995219.Baseline (the second table, before the rebase):
mac/control-sockets@f1f1843d, with this PR's hook and two runs butnot its fix (
maincannot build the control fixture on macOS at all, so the race cannot be shown there).Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21.0.0. No GPU.
Scene: none. Game-free checks.
Method: after the rebase,
tests/test_control.py --cc clang --arch arm64, then--arch x86_64, at58a56b55throughtools/slot.pyin the suite's clean environment, and the whole suite on this PR compared by test name withmain'srun at
6c9edffe. Before the rebase:control_testbuilt astest_control.pybuilds it on macOS (native arm64), thenrun
<folder> quit slowand<folder> quit failwith stdout and stderr on pipes: 5 times each without the fix, 20times each with it, 20 s at most per run (a run past 20 s is a hang). Then
tests/test_control.py --cc clang --arch arm64and--arch x86_64, and the control checks throughtests/run_all.pyonmainand on this PR, interleaved, tworuns each.
How to reproduce:
python -I -B tests/test_control.py --cc clang --arch arm64(and--arch x86_64), with a shortTMPDIR.6c9edffe58a56b55tests/test_control.pyhost/control.cdoes not compile on macOS)--arch arm64, the same 40 with--arch x86_64; 12.6 s in the suiteBefore the rebase:
f1f1843d+ hook, no fix26959952quit slowconnect, its child serves onquit failtest_control.py --arch arm64/x86_64tests/run_all.py(run 1 / run 2)5e072fc826959952tests/test_control.pycontrol.cdoes not compile on macOS)tests/test_longshot_ctl.pytests/test_oot_state_tap.pyvsnprintfmacro, which the runner PR repairs)Earlier, on the old base (
ab92a95f, on the port's integration branch, the same fixture and hook): the originalquit case hung 8 times in 1,200 runs under load (7 of 900 arm64, 1 of 300 x86_64), each the refused
connect. With thefix, 0 hangs in 1,900 runs (arm64 and x86_64, idle and loaded). Not rerun here.
Screenshots: none; nothing on screen.
Flags and settings
None.
Testing
Tested:
58a56b55on6c9edffe):python -I -B tests/test_control.py --cc clang --arch arm64,then
--arch x86_64: exit 0, 40 PASS lines each, the two quit runs among them.tests/run_all.py, 6 at once, on the same Mac, after the rebase, compared by test name withmain(the summary lines are under Measurements):
tests/test_control.py, FAIL onmain(the bridge does not compile on macOSthere) → pass here in 12.6 s, with the quit runs. At
26959952, before the rebase, no control fixture was leftrunning after the suite. (The control-sockets PR's own suite run then, without this fix, met the quit race under
load and left one running.)
main. Fifteen were named here before the rebase:test_appimage_package_retry,test_collect_wide,test_game_layout,test_low_memory_notice,test_mem_gate,test_mem_gate_children,test_mirror_trim,test_oot_state_tap,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant. The port's other PRs fix them. The three namedbefore as stale on
main(test_framegen_resources,test_module_ids,test_pad_layout) now pass onmainandhere.
main's list since5e072fc8, and fail on both:test_appimage_player_build,test_cpm_discovery,test_crash_dialog,test_crash_record,test_env_on,test_lift_cleanup_error,test_linux_preflight,test_longshot_ctl,test_make_game_cache,test_pipeline_tour,test_readback_late,test_report_run,test_split_giants,test_vk_pgo,test_vram_budget_row,test_walk_key.mainand here, twointerleaved runs each, as under Measurements.
unifdefagainstmain@6c9edffe:host/control.cwith-U__APPLE__ -UHOS_CONTROL_NO_GAME(the game build) identical; with-U__APPLE__ -D_WIN32(Windows, the test buildtoo) identical; the Linux test build (
-U__APPLE__ -U_WIN32 -DHOS_CONTROL_NO_GAME) differs by the hook only (itsdeclaration and its one call between
bindandlisten).tests/control_test.cdiffers on every platform:CHECKcallscheck_failed(on Windows it prints and exits 1, as before) and the POSIX quit case is the new one.aa95d35f, atfe3e8c91(58a56b55before the rebases ontoe090cc48andaa95d35f):Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
apply) gave the same results as before;
python -I -B tests/run_all.pyon wave S1's merge11a5b4e7, which carriesthis head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
(
reports/2026-11-15-rebase-sweep-3.md).Not tested:
check_failedonly prints and exits there, as before. Not runhere.
Must be tested:
python -I -B tests/test_control.pyon thisPR (head
fe3e8c91) and on mainaa95d35fin the same checkout, expecting it to pass on both, with no hang; on Linuxthis PR prints two more PASS lines than main (the quit case with its bridge slow to listen, and
quit: a check failing once the child serves ends the child with it), and on Windows the same PASS lines as main (by hand: testrequest T-B6ML).
Risks
What could break:
HOS_CONTROL_NO_GAME) compiles.hanging it.
CHECKis now a call tocheck_failedon every platform, Windows included (where it prints and exits as before).mac/control-sockets), whose two commits it carries; best merged right after it.Turning it off: revert the PR.
Compatibility: Nothing changes: the game build of
host/control.cismain's on Windows and Linux; saves, theoptions file, the shader caches and the vk ABI are unchanged.
Issues
See #420.
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headfe3e8c91, on #456fc2f1040, without aconflict; this PR's own commits, old → new:
58a56b55→fe3e8c91; the commit names elsewhere in this descriptionare from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (532 passed, 17 skipped,and 31 failures that also fail on
mainat6c9edffe(531 passed, 32 failed, 17 skipped), named under Testing. Afterthe rebase onto main
aa95d35f(headfe3e8c91): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's543/33/17, no new failure.)
third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here. (The two new runs hang without the fix andpass with it; on
mainthe fixture does not build on macOS.)tests/that reads changed source updated in this PR, not weakened.(
docs/control.md, in #456's carried commits; this PR's own commit changes test code and the test-build hook only.)269599521958a56b555dWIP: test(control): wait until the bridge listens, and reap it when a quit check failsto test(control): wait until the bridge listens, and reap it when a quit check fails58a56b555db767e799c5b767e799c5fe3e8c9178Folded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
Pull request closed