test(app): threaded twins for the remaining app tests (#1039) (#1130)

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Joe MattielloandClaude Opus 5.5 authored and GitHub committed 2026-10-31 15:01:42 -04:00
1 parent fe80b36a2b
commit 06fa8a7193
7 files changed
+264 -70

No files matched your search

+76 -26
View File
@@ -17,6 +17,9 @@
# cheat_tempest_* (corpus + private ROM) the real database's Tempest 2000
# "Infinite Lives" is found through the core's own title
# identity and reaches RAM
# <each of the above through the real binary>_threaded the same run with --emu-thread on (#967, label emu_thread, so the
# TSan job runs it); the persist chain gets its own data dirs and fixture names, so the two
# chains never share state
set(_fx "${CMAKE_CURRENT_LIST_DIR}/../tests/fixtures")
get_filename_component(_fx "${_fx}" ABSOLUTE)
@@ -45,41 +48,68 @@ add_test(NAME cheat_apply
add_test(NAME cheat_apply_control
COMMAND virtualjaguar --headless --frames 60 --expect-ram 1F0000=AB "${_yarc}")
set_tests_properties(cheat_apply_control PROPERTIES PASS_REGULAR_EXPRESSION "${_no_cheat}")
add_test(NAME cheat_apply_control_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 60 --expect-ram 1F0000=AB "${_yarc}")
set_tests_properties(cheat_apply_control_threaded PROPERTIES PASS_REGULAR_EXPRESSION "${_no_cheat}")
add_test(NAME cheat_state
COMMAND virtualjaguar --headless --frames 240 --state-roundtrip
--cheat 001F0000:AB --expect-ram 1F0000=AB "${VJ_ROMS}/jagniccc.j64")
add_test(NAME cheat_state_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 240 --state-roundtrip
--cheat 001F0000:AB --expect-ram 1F0000=AB "${VJ_ROMS}/jagniccc.j64")
set(_pd "${CMAKE_BINARY_DIR}/cheat-persist-data")
set(_pc "${CMAKE_BINARY_DIR}/cheat-persist-control")
add_test(NAME cheat_persist_clean
COMMAND ${CMAKE_COMMAND} -E remove_directory "${_pd}" "${_pc}")
# Run 1: yarc.cht (a fixture whose name matches the ROM's file name) supplies "Lives byte".
add_test(NAME cheat_persist_save
COMMAND virtualjaguar --headless --frames 60 --data-dir "${_pd}" --cheats-dir "${_fx}"
--cheat-on "Lives byte" --expect-ram 1F0010=05 "${_yarc}")
# Run 2: a new process, same data directory, NO cheat option: it must still be on.
add_test(NAME cheat_persist_restart
COMMAND virtualjaguar --headless --frames 60 --data-dir "${_pd}" --cheats-dir "${_fx}"
--expect-ram 1F0010=05 "${_yarc}")
# Control: the same, but a data directory nothing was saved to.
add_test(NAME cheat_persist_control
COMMAND virtualjaguar --headless --frames 60 --data-dir "${_pc}" --cheats-dir "${_fx}"
--expect-ram 1F0010=05 "${_yarc}")
set_tests_properties(cheat_persist_control PROPERTIES
PASS_REGULAR_EXPRESSION "headless: FAIL ram 0x1F0010 is [0-9A-F]+, expected 05")
add_test(NAME cheat_persist_cleanup
COMMAND ${CMAKE_COMMAND} -E remove_directory "${_pd}" "${_pc}")
set_tests_properties(cheat_persist_clean PROPERTIES FIXTURES_SETUP cheat_persist_fresh)
set_tests_properties(cheat_persist_save PROPERTIES
FIXTURES_REQUIRED cheat_persist_fresh FIXTURES_SETUP cheat_persist_saved)
set_tests_properties(cheat_persist_restart cheat_persist_control PROPERTIES
FIXTURES_REQUIRED "cheat_persist_fresh;cheat_persist_saved")
set_tests_properties(cheat_persist_cleanup PROPERTIES FIXTURES_CLEANUP "cheat_persist_fresh;cheat_persist_saved")
# The save -> restart -> control chain, once inline and once threaded: <_th> is "" or "_threaded", and the twin has its
# own data dirs and fixture names, so the two chains never share state.
foreach(_th "" "_threaded")
if(_th)
set(_thargs --emu-thread on)
else()
set(_thargs)
endif()
set(_pd "${CMAKE_BINARY_DIR}/cheat-persist-data${_th}")
set(_pc "${CMAKE_BINARY_DIR}/cheat-persist-control${_th}")
add_test(NAME cheat_persist_clean${_th}
COMMAND ${CMAKE_COMMAND} -E remove_directory "${_pd}" "${_pc}")
# Run 1: yarc.cht (a fixture whose name matches the ROM's file name) supplies "Lives byte".
add_test(NAME cheat_persist_save${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 60 --data-dir "${_pd}" --cheats-dir "${_fx}"
--cheat-on "Lives byte" --expect-ram 1F0010=05 "${_yarc}")
# Run 2: a new process, same data directory, NO cheat option: it must still be on.
add_test(NAME cheat_persist_restart${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 60 --data-dir "${_pd}" --cheats-dir "${_fx}"
--expect-ram 1F0010=05 "${_yarc}")
# Control: the same, but a data directory nothing was saved to.
add_test(NAME cheat_persist_control${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 60 --data-dir "${_pc}" --cheats-dir "${_fx}"
--expect-ram 1F0010=05 "${_yarc}")
set_tests_properties(cheat_persist_control${_th} PROPERTIES
PASS_REGULAR_EXPRESSION "headless: FAIL ram 0x1F0010 is [0-9A-F]+, expected 05")
add_test(NAME cheat_persist_cleanup${_th}
COMMAND ${CMAKE_COMMAND} -E remove_directory "${_pd}" "${_pc}")
set_tests_properties(cheat_persist_clean${_th} PROPERTIES FIXTURES_SETUP cheat_persist_fresh${_th})
set_tests_properties(cheat_persist_save${_th} PROPERTIES
FIXTURES_REQUIRED cheat_persist_fresh${_th} FIXTURES_SETUP cheat_persist_saved${_th})
set_tests_properties(cheat_persist_restart${_th} cheat_persist_control${_th} PROPERTIES
FIXTURES_REQUIRED "cheat_persist_fresh${_th};cheat_persist_saved${_th}")
set_tests_properties(cheat_persist_cleanup${_th} PROPERTIES
FIXTURES_CLEANUP "cheat_persist_fresh${_th};cheat_persist_saved${_th}")
endforeach()
set_tests_properties(cheat_persist_clean_threaded cheat_persist_save_threaded cheat_persist_restart_threaded
cheat_persist_control_threaded cheat_persist_cleanup_threaded PROPERTIES LABELS emu_thread)
set_tests_properties(cheats_unit cheat_apply cheat_state cheat_persist_save cheat_persist_restart PROPERTIES
TIMEOUT 300
FAIL_REGULAR_EXPRESSION "headless: FAIL")
# The twins: also the threaded OK line (the run really was threaded), and no owner violation / TSan report. The controls
# pass by failing for their stated reason, so they only guard against the threading errors.
set_tests_properties(cheat_state_threaded cheat_persist_save_threaded cheat_persist_restart_threaded PROPERTIES
TIMEOUT 300 LABELS emu_thread
PASS_REGULAR_EXPRESSION "${_vj_threaded_ok}"
FAIL_REGULAR_EXPRESSION "headless: FAIL|owner violation|ThreadSanitizer")
set_tests_properties(cheat_apply_control_threaded cheat_persist_control_threaded PROPERTIES
TIMEOUT 300 LABELS emu_thread
FAIL_REGULAR_EXPRESSION "owner violation|ThreadSanitizer")
# ---- the real database, when it was downloaded -------------------------------------------
if(VJ_CHEATS_DIR AND EXISTS "${VJ_CHEATS_DIR}/Tempest 2000.cht")
@@ -115,6 +145,19 @@ if(VJ_CHEATS_DIR AND EXISTS "${VJ_CHEATS_DIR}/Tempest 2000.cht")
set_tests_properties(cheat_tempest_control PROPERTIES
PASS_REGULAR_EXPRESSION "headless: FAIL ram 0x007D3D is [0-9A-F]+, expected 05")
set_tests_properties(cheat_tempest_lives PROPERTIES TIMEOUT 300 FAIL_REGULAR_EXPRESSION "headless: FAIL")
add_test(NAME cheat_tempest_lives_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 600 --cheats-dir "${VJ_CHEATS_DIR}"
--cheat-on "Infinite Lives" --expect-ram 7D3D=05 "${_t2k}")
add_test(NAME cheat_tempest_control_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 600 --cheats-dir "${VJ_CHEATS_DIR}"
--expect-ram 7D3D=05 "${_t2k}")
set_tests_properties(cheat_tempest_control_threaded PROPERTIES
PASS_REGULAR_EXPRESSION "headless: FAIL ram 0x007D3D is [0-9A-F]+, expected 05"
FAIL_REGULAR_EXPRESSION "owner violation|ThreadSanitizer")
set_tests_properties(cheat_tempest_lives_threaded PROPERTIES TIMEOUT 300
PASS_REGULAR_EXPRESSION "${_vj_threaded_ok}"
FAIL_REGULAR_EXPRESSION "headless: FAIL|owner violation|ThreadSanitizer")
set_tests_properties(cheat_tempest_lives_threaded cheat_tempest_control_threaded PROPERTIES LABELS emu_thread)
else()
message(STATUS "VJCheatTests: no private Tempest 2000 ROM; the database end-to-end test is not registered")
endif()
@@ -138,6 +181,13 @@ if(Python3_Interpreter_FOUND AND _vj_bash2 AND EXISTS "${VJ_ROOT}/site/cheats/pa
"$<TARGET_FILE:virtualjaguar>" "${VJ_ROOT}/site/cheats" "${_evil}" 5E2CDBC0
"${CMAKE_BINARY_DIR}/cheat-community-test")
set_tests_properties(cheat_community_install PROPERTIES TIMEOUT 300 FAIL_REGULAR_EXPRESSION "FAIL ")
# Threaded: the script runs every app launch with --emu-thread on (its own work dir) and fails on an owner violation.
add_test(NAME cheat_community_install_threaded
COMMAND "${Python3_EXECUTABLE}" -I "${CMAKE_CURRENT_LIST_DIR}/../tests/cheat_community_smoke.py"
"$<TARGET_FILE:virtualjaguar>" "${VJ_ROOT}/site/cheats" "${_evil}" 5E2CDBC0
"${CMAKE_BINARY_DIR}/cheat-community-test_threaded" on)
set_tests_properties(cheat_community_install_threaded PROPERTIES TIMEOUT 300 LABELS emu_thread
FAIL_REGULAR_EXPRESSION "FAIL |owner violation|ThreadSanitizer")
else()
message(STATUS "VJCheatTests: no private Doom - Evil Unleashed ROM; cheat_community_install is not registered")
endif()
+40 -2
View File
@@ -25,11 +25,15 @@
# gdb_inline_halt the same four steps with the emulation thread off (the headless loop syncs the halt after each frame)
# gdb_inline_halt_hardcore inline: the client continues a boot halt, then a hardcore sign-in reloads the game (needs the halt mirror synced)
# smoke_*_threaded the headless smoke tests with --emu-thread on: the inline hashes, and no owner violation
# smoke_{cd,boot_cart_rom,bad_option,locked_option,disc_insert_cart_hidden}_threaded the rest of the smoke set, same expectations
# controls_overlay_*_threaded / cheat_{state,apply_control,persist_*,tempest_*,community_install}_threaded (VJSmokeTests / VJCheatTests)
# the controls overlay and cheat tests with --emu-thread on, each on its own data dir; netplay_pair_shared_threaded
# smoke_disc_swap_threaded File > Disc on a loaded CD through EmuThread::call, read back from the pinned snapshot
# smoke_disc_swap_bios_threaded the same in real-BIOS boot mode, where each insert re-stages the CD BIOS
# smoke_disc_insert_bios_threaded the CD BIOS with no disc: Add disc image... inserts the first disc through call()
# smoke_m3u_playlist_threaded an .m3u's discs registered inside EmuThread::call, read back from the pinned snapshot
# smoke_busy_threaded a slow load call shows the busy state (log line headless)
# smoke_quit_during_call_threaded SIGTERM while that slow call is in flight: the call finishes, exit 0, no owner violation
# emu_thread_start_fallback a failed thread start runs inline (nothing to lift any more)
# debug_on_threaded / gdb_enabled(_threaded) nothing is off while threaded; the silent gdb host attaches at load
# emu_thread_default_unit build channel -> emulation-thread default; a stored choice always wins
@@ -195,7 +199,7 @@ set_tests_properties(perf_trace_unit PROPERTIES TIMEOUT 60 LABELS emu_thread FAI
# The headless smoke tests (VJSmokeTests.cmake) again with --emu-thread on: the same hashes as inline, nothing on the
# UI thread calling into the core, and the run really threaded (the OK line says so only then).
set(_vj_threaded_ok "headless: OK \\([0-9]+ load\\(s\\), emulation thread\\)")
# (_vj_threaded_ok, the OK line of a threaded run, is set in VJSmokeTests.cmake.)
add_test(NAME smoke_yarc_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames ${VJ_SMOKE_FRAMES} ${_yarc_hash_args}
"${VJ_ROMS}/yarc.j64")
@@ -210,12 +214,35 @@ add_test(NAME smoke_boot_cd_bios_threaded
add_test(NAME cheat_apply_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 60 --cheat 001F0000:AB --expect-ram 1F0000=AB
"${VJ_ROMS}/yarc.j64")
add_test(NAME smoke_cd_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 120 --allow-blank "${VJ_ROMS}/synth_jagcd.chd")
add_test(NAME smoke_boot_cart_rom_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 400 --boot-bios cart --expect-bios cart:builtin
--expect-hash 3cc9c43e)
set_tests_properties(smoke_yarc_threaded smoke_reload_threaded smoke_state_threaded smoke_boot_cd_bios_threaded
cheat_apply_threaded PROPERTIES
smoke_cd_threaded smoke_boot_cart_rom_threaded cheat_apply_threaded PROPERTIES
TIMEOUT 300 LABELS emu_thread
PASS_REGULAR_EXPRESSION "${_vj_threaded_ok}"
FAIL_REGULAR_EXPRESSION "headless: FAIL|owner violation|ThreadSanitizer")
# The negative controls threaded: they pass by failing for the stated reason (the inline originals' PASS lines), and must
# not trip an owner violation on the way.
add_test(NAME smoke_bad_option_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 5
--option virtualjaguar_no_such_option=1 "${VJ_ROMS}/yarc.j64")
set_tests_properties(smoke_bad_option_threaded PROPERTIES PASS_REGULAR_EXPRESSION "rejected by the core")
add_test(NAME smoke_locked_option_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 5
--option virtualjaguar_alt_inputs=enabled "${VJ_ROMS}/yarc.j64")
set_tests_properties(smoke_locked_option_threaded PROPERTIES PASS_REGULAR_EXPRESSION "locked by the app.*rejected by the core")
add_test(NAME smoke_disc_insert_cart_hidden_threaded
COMMAND virtualjaguar --headless --emu-thread on --frames 30 --allow-blank --boot-bios cart
--disc-insert "${VJ_ROMS}/synth_jagcd.chd")
set_tests_properties(smoke_disc_insert_cart_hidden_threaded PROPERTIES
PASS_REGULAR_EXPRESSION "headless: FAIL --disc-insert: the disc menu is hidden")
set_tests_properties(smoke_bad_option_threaded smoke_locked_option_threaded smoke_disc_insert_cart_hidden_threaded PROPERTIES
TIMEOUT 120 LABELS emu_thread FAIL_REGULAR_EXPRESSION "owner violation|ThreadSanitizer")
# A thread that fails to start (VJ_TEST_EMU_START_FAIL takes the real failure branch): the run falls back inline. Nothing
# is switched off while threaded any more, so there is nothing to give back (the old "are back on" line must be gone).
add_test(NAME emu_thread_start_fallback
@@ -468,3 +495,14 @@ add_test(NAME smoke_busy_threaded
set_tests_properties(smoke_busy_threaded PROPERTIES TIMEOUT 120 LABELS emu_thread
PASS_REGULAR_EXPRESSION "\\[emu\\] busy: a call has taken over 250 ms"
FAIL_REGULAR_EXPRESSION "headless: FAIL|owner violation|ThreadSanitizer")
# A quit (SIGTERM) while that same slow call is in flight: the call completes, the frame loops are skipped, the app exits 0
# and the thread stops. A script sends the signal once the busy line shows (so the UI thread is inside the call); not on Windows.
if(Python3_Interpreter_FOUND AND NOT WIN32)
add_test(NAME smoke_quit_during_call_threaded
COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_LIST_DIR}/../tests/quit_during_call.py"
$<TARGET_FILE:virtualjaguar> "${VJ_ROMS}/yarc.j64")
set_tests_properties(smoke_quit_during_call_threaded PROPERTIES TIMEOUT 120 LABELS emu_thread
PASS_REGULAR_EXPRESSION "PASS: quit during a slow call"
FAIL_REGULAR_EXPRESSION "FAIL |owner violation|ThreadSanitizer")
endif()
+4 -1
View File
@@ -12,6 +12,7 @@
# and the client are instances 2 and 3. The link must still come up, the shared
# config.ini must be untouched, each copy gets its own save folder, and the
# client hands the core its address through the environment (no vj_netlink.txt).
# (netplay_pair_shared_threaded: both copies with --emu-thread on.)
# netplay_no_partner control: with nobody to talk to both must FAIL, so the check can fail.
# netplay_discovery a client must FIND the host by LAN discovery and join it. Skipped (77) when
# no host is discovered: CI runners and many VMs do not deliver broadcast.
@@ -58,9 +59,11 @@ add_test(NAME netplay_pair_voicemodem_threaded
COMMAND vj_netplay_test pair $<TARGET_FILE:virtualjaguar> "${_yarc}" voicemodem on on)
add_test(NAME netplay_pair_mixed COMMAND vj_netplay_test pair $<TARGET_FILE:virtualjaguar> "${_yarc}" jaglink on off)
add_test(NAME netplay_no_partner_threaded COMMAND vj_netplay_test control $<TARGET_FILE:virtualjaguar> "${_yarc}" on)
add_test(NAME netplay_pair_shared_threaded COMMAND vj_netplay_test pair-shared $<TARGET_FILE:virtualjaguar> "${_yarc}" on)
set_tests_properties(netplay_pair_threaded netplay_pair_voicemodem_threaded netplay_pair_mixed netplay_no_partner_threaded
netplay_pair_shared_threaded
PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE LABELS "emu_thread")
set_tests_properties(netplay_pair_threaded netplay_pair_voicemodem_threaded netplay_pair_mixed
set_tests_properties(netplay_pair_threaded netplay_pair_voicemodem_threaded netplay_pair_mixed netplay_pair_shared_threaded
PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL |owner violation|ThreadSanitizer")
set_tests_properties(netplay_no_partner_threaded PROPERTIES FAIL_REGULAR_EXPRESSION "owner violation|ThreadSanitizer")
+48 -26
View File
@@ -51,6 +51,10 @@ set(VJ_ROMS "${VJ_ROOT}/test/roms")
set(VJ_SMOKE_FRAMES 300 CACHE STRING "Frames per game in the smoke tests")
set(VJ_SMOKE_YARC_HASH "f4c30ea3" CACHE STRING "Expected final-frame hash of yarc.j64 (hex); empty disables the check")
# The OK line of a headless run on the emulation thread (it says "emulation thread" only when the thread really ran):
# what every _threaded twin requires (VJEmuThreadTests.cmake, VJCheatTests.cmake, the controls tests below).
set(_vj_threaded_ok "headless: OK \\([0-9]+ load\\(s\\), emulation thread\\)")
if(VJ_SMOKE_YARC_HASH)
set(_yarc_hash_args --expect-hash ${VJ_SMOKE_YARC_HASH})
else()
@@ -308,33 +312,51 @@ set_tests_properties(remap_unit input_e2e devices_unit devices_e2e bios_e2e PROP
# controls_overlay_rebound a saved global rebinding (B = M, scancode 16) changes the text;
# the per-game layer is covered by devices_unit.
# ---------------------------------------------------------------------------
set(VJ_CTL_DISMISSED "${CMAKE_BINARY_DIR}/ctl_dismissed")
set(VJ_CTL_REBOUND "${CMAKE_BINARY_DIR}/ctl_rebound")
set(VJ_CTL_DEFAULT "${CMAKE_BINARY_DIR}/ctl_default")
file(MAKE_DIRECTORY "${VJ_CTL_DISMISSED}" "${VJ_CTL_REBOUND}" "${VJ_CTL_DEFAULT}")
set(VJ_CTL_ON "${CMAKE_BINARY_DIR}/ctl_on")
file(MAKE_DIRECTORY "${VJ_CTL_ON}")
file(WRITE "${VJ_CTL_ON}/config.ini" "ui.controls.autoshow=1\n")
file(WRITE "${VJ_CTL_DISMISSED}/config.ini" "ui.controls.autoshow=0\n")
file(WRITE "${VJ_CTL_REBOUND}/config.ini" "input.kb.b=k16\nui.controls.autoshow=1\n")
# Each test also runs as <name>_threaded (--emu-thread on, label emu_thread: the TSan job): the same expectations, and its
# own data folder, since a headless run saves config.ini and the two must never share it.
foreach(_th "" "_threaded")
if(_th)
set(_thargs --emu-thread on)
else()
set(_thargs)
endif()
set(VJ_CTL_DISMISSED "${CMAKE_BINARY_DIR}/ctl_dismissed${_th}")
set(VJ_CTL_REBOUND "${CMAKE_BINARY_DIR}/ctl_rebound${_th}")
set(VJ_CTL_DEFAULT "${CMAKE_BINARY_DIR}/ctl_default${_th}")
set(VJ_CTL_ON "${CMAKE_BINARY_DIR}/ctl_on${_th}")
file(MAKE_DIRECTORY "${VJ_CTL_DISMISSED}" "${VJ_CTL_REBOUND}" "${VJ_CTL_DEFAULT}" "${VJ_CTL_ON}")
file(WRITE "${VJ_CTL_ON}/config.ini" "ui.controls.autoshow=1\n")
file(WRITE "${VJ_CTL_DISMISSED}/config.ini" "ui.controls.autoshow=0\n")
file(WRITE "${VJ_CTL_REBOUND}/config.ini" "input.kb.b=k16\nui.controls.autoshow=1\n")
add_test(NAME controls_overlay_default
COMMAND virtualjaguar --headless --frames 5 --data-dir "${VJ_CTL_DEFAULT}" --expect-controls-overlay hidden "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_default PROPERTIES
PASS_REGULAR_EXPRESSION "\\[osd\\] Press F3 for the controls")
add_test(NAME controls_overlay_default${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 5 --data-dir "${VJ_CTL_DEFAULT}" --expect-controls-overlay hidden "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_default${_th} PROPERTIES
PASS_REGULAR_EXPRESSION "\\[osd\\] Press F3 for the controls")
add_test(NAME controls_overlay_keyboard
COMMAND virtualjaguar --headless --frames 5 --data-dir "${VJ_CTL_ON}" --expect-controls-overlay shown --print-controls "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_keyboard PROPERTIES
PASS_REGULAR_EXPRESSION "Player 1: Standard Joypad - keyboard.*B: Z.*Keypad 9 \\(Pro: X\\): [^\n]*, A\n")
add_test(NAME controls_overlay_keyboard${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 5 --data-dir "${VJ_CTL_ON}" --expect-controls-overlay shown --print-controls "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_keyboard${_th} PROPERTIES
PASS_REGULAR_EXPRESSION "Player 1: Standard Joypad - keyboard.*B: Z.*Keypad 9 \\(Pro: X\\): [^\n]*, A\n")
add_test(NAME controls_overlay_dismissed
COMMAND virtualjaguar --headless --frames 5 --data-dir "${VJ_CTL_DISMISSED}" --expect-controls-overlay hidden "${VJ_ROMS}/yarc.j64")
add_test(NAME controls_overlay_dismissed${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 5 --data-dir "${VJ_CTL_DISMISSED}" --expect-controls-overlay hidden "${VJ_ROMS}/yarc.j64")
if(_th) # no text of its own to pass on: the run must say it was threaded
set_tests_properties(controls_overlay_dismissed${_th} PROPERTIES PASS_REGULAR_EXPRESSION "${_vj_threaded_ok}")
endif()
add_test(NAME controls_overlay_rebound
COMMAND virtualjaguar --headless --frames 5 --data-dir "${VJ_CTL_REBOUND}" --expect-controls-overlay shown --print-controls "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_rebound PROPERTIES
PASS_REGULAR_EXPRESSION " B: M\n")
set_tests_properties(controls_overlay_default controls_overlay_keyboard controls_overlay_dismissed controls_overlay_rebound PROPERTIES
TIMEOUT 120
FAIL_REGULAR_EXPRESSION "headless: FAIL")
add_test(NAME controls_overlay_rebound${_th}
COMMAND virtualjaguar --headless ${_thargs} --frames 5 --data-dir "${VJ_CTL_REBOUND}" --expect-controls-overlay shown --print-controls "${VJ_ROMS}/yarc.j64")
set_tests_properties(controls_overlay_rebound${_th} PROPERTIES
PASS_REGULAR_EXPRESSION " B: M\n")
set_tests_properties(controls_overlay_default${_th} controls_overlay_keyboard${_th} controls_overlay_dismissed${_th}
controls_overlay_rebound${_th} PROPERTIES TIMEOUT 120)
if(_th)
set_tests_properties(controls_overlay_default${_th} controls_overlay_keyboard${_th} controls_overlay_dismissed${_th}
controls_overlay_rebound${_th} PROPERTIES LABELS emu_thread
FAIL_REGULAR_EXPRESSION "headless: FAIL|owner violation|ThreadSanitizer")
else()
set_tests_properties(controls_overlay_default controls_overlay_keyboard controls_overlay_dismissed
controls_overlay_rebound PROPERTIES FAIL_REGULAR_EXPRESSION "headless: FAIL")
endif()
endforeach()
+16 -9
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
"""Community cheat store end to end (#1071): the real app, headless, real HTTP over loopback.
usage: cheat_community_smoke.py VIRTUALJAGUAR SITE_CHEATS_DIR ROM CRC32 WORKDIR
usage: cheat_community_smoke.py VIRTUALJAGUAR SITE_CHEATS_DIR ROM CRC32 WORKDIR [on]
The optional sixth argument "on" runs every app launch with --emu-thread on (the _threaded twin); an owner violation or a
ThreadSanitizer report in any launch fails the run.
1. Serves SITE_CHEATS_DIR on 127.0.0.1 and runs `virtualjaguar --headless --cheats-community-install ROM` with
cheats.community.url pointing at it: the pack must land in <data>/cheats/community/packs/<CRC>.cht with its sidecar,
@@ -24,16 +26,21 @@ class Quiet(http.server.SimpleHTTPRequestHandler):
pass
def run(exe, data, rom, url):
def run(exe, data, rom, url, threaded=False):
data.mkdir(parents=True, exist_ok=True)
(data / "config.ini").write_text("cheats.community.url=%s\nlibrary.art=1\n" % url)
p = subprocess.run([exe, "--headless", "--frames", "30", "--allow-blank", "--data-dir", str(data),
"--cheats-community-install", rom], capture_output=True, text=True, timeout=120)
return p.returncode, p.stdout + p.stderr
cmd = [exe, "--headless"] + (["--emu-thread", "on"] if threaded else []) + [
"--frames", "30", "--allow-blank", "--data-dir", str(data), "--cheats-community-install", rom]
p = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
out = p.stdout + p.stderr
if "owner violation" in out or "ThreadSanitizer" in out:
print("FAIL threading error (rc=%d)\n%s" % (p.returncode, out)); sys.exit(1)
return p.returncode, out
def main():
exe, site, rom, crc, work = sys.argv[1:6]
threaded = len(sys.argv) > 6 and sys.argv[6] == "on"
site, work = pathlib.Path(site), pathlib.Path(work)
shutil.rmtree(work, ignore_errors=True)
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), functools.partial(Quiet, directory=str(site)))
@@ -41,7 +48,7 @@ def main():
threading.Thread(target=srv.serve_forever, daemon=True).start()
data = work / "data"
rc, out = run(exe, data, rom, url)
rc, out = run(exe, data, rom, url, threaded)
pack = data / "cheats/community/packs" / (crc + ".cht")
if rc != 0 or "COMMUNITY pack %s installed" % crc not in out:
print("FAIL install (rc=%d)\n%s" % (rc, out)); return 1
@@ -52,18 +59,18 @@ def main():
with pack.open("ab") as f: # a hand edit: the file no longer matches its sidecar (EditedLocally)
f.write(b"# my tweak\n")
rc, out = run(exe, data, rom, url)
rc, out = run(exe, data, rom, url, threaded)
if rc != 0 or "COMMUNITY pack %s installed" % crc not in out:
print("FAIL reinstall over a locally edited pack (rc=%d)\n%s" % (rc, out)); return 1
if pack.read_bytes() != (site / "packs" / (crc + ".cht")).read_bytes():
print("FAIL the locally edited pack was not replaced"); return 1
srv.shutdown(); srv.server_close() # from here on nothing answers
rc, out = run(exe, data, rom, url)
rc, out = run(exe, data, rom, url, threaded)
if rc != 0 or "COMMUNITY pack %s installed" % crc not in out:
print("FAIL offline reuse of the installed pack (rc=%d)\n%s" % (rc, out)); return 1
rc, out = run(exe, work / "fresh", rom, url)
rc, out = run(exe, work / "fresh", rom, url, threaded)
if rc == 0 or (work / "fresh/cheats/community/packs" / (crc + ".cht")).exists():
print("FAIL a fresh data dir with no server must not install (rc=%d)\n%s" % (rc, out)); return 1
print("ok")
+13 -6
View File
@@ -8,6 +8,7 @@
// two app processes on localhost, one Host and one Join through
// the command-line twin of the window; both must report the link UP.
// Does not use UDP broadcast: the client is given the address.
// vj_netplay_test pair-shared EXE ROM [THREAD] two copies on ONE data folder (instance.h); THREAD = on runs both with the emulation thread
// vj_netplay_test control EXE ROM [THREAD] the same with nobody to talk to: must FAIL (so the check can fail).
// vj_netplay_test discovery EXE ROM a host and a client that must FIND it by LAN discovery and join
// it. Exit 77 (ctest: skipped) when this machine does not deliver
@@ -652,7 +653,7 @@ static int runPair(const std::string& exe, const std::string& rom, const std::st
// Two copies of the app on ONE data folder. The test takes instance 1 itself, so both children are
// secondaries (2 and 3): they must coexist, link up, and leave the shared files alone.
static int runPairShared(const std::string& exe, const std::string& rom) {
static int runPairShared(const std::string& exe, const std::string& rom, const std::string& thread = "off") {
namespace fs = std::filesystem;
const int port = pickFreePort();
const int disc = pickFreePort(port);
@@ -674,10 +675,16 @@ static int runPairShared(const std::string& exe, const std::string& rom) {
const std::string p = std::to_string(port);
const std::string d = dir.string();
Child host = spawn(appArgs(exe, rom, {"--data-dir", d, "--netplay", "host", "--netplay-port", p, "--netplay-expect-link", "30"}));
std::vector<std::string> hostX = {"--data-dir", d, "--netplay", "host", "--netplay-port", p, "--netplay-expect-link", "30"};
std::vector<std::string> joinX = {"--data-dir", d, "--netplay", "join", "--netplay-addr", "127.0.0.1",
"--netplay-port", p, "--netplay-expect-link", "30"};
if (thread == "on") {
hostX.insert(hostX.end(), {"--emu-thread", "on"});
joinX.insert(joinX.end(), {"--emu-thread", "on"});
}
Child host = spawn(appArgs(exe, rom, hostX));
SDL_Delay(300);
Child join = spawn(appArgs(exe, rom, {"--data-dir", d, "--netplay", "join", "--netplay-addr", "127.0.0.1",
"--netplay-port", p, "--netplay-expect-link", "30"}));
Child join = spawn(appArgs(exe, rom, joinX));
bool finished = waitBoth(host, join, 90);
reap(host);
reap(join);
@@ -771,7 +778,7 @@ static int runDiscovery(const std::string& exe, const std::string& rom) {
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: vj_netplay_test unit ROM | pair EXE ROM [DEVICE [HOST JOIN]] | control EXE ROM [THREAD] | discovery EXE ROM\n");
std::fprintf(stderr, "usage: vj_netplay_test unit ROM | pair EXE ROM [DEVICE [HOST JOIN]] | pair-shared EXE ROM [THREAD] | control EXE ROM [THREAD] | discovery EXE ROM\n");
return 2;
}
netInit();
@@ -785,7 +792,7 @@ int main(int argc, char** argv) {
} else if (mode == "pair" && argc >= 4) {
rc = runPair(argv[2], argv[3], argc >= 5 ? argv[4] : "jaglink", argc >= 6 ? argv[5] : "off", argc >= 7 ? argv[6] : "off");
} else if (mode == "pair-shared" && argc >= 4) {
rc = runPairShared(argv[2], argv[3]);
rc = runPairShared(argv[2], argv[3], argc >= 5 ? argv[4] : "off");
} else if (mode == "control" && argc >= 4) {
rc = runControl(argv[2], argv[3], argc >= 5 ? argv[4] : "off");
} else if (mode == "discovery" && argc >= 4) {
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
quit_during_call.py -- a quit (SIGTERM) that arrives while the UI thread waits in a slow EmuThread::call (#967, #1039).
VJ_TEST_SLOW_CALL_MS stalls the load call() on the emulation thread (as smoke_busy_threaded does). Once the app has
logged "[emu] busy" the UI thread is parked in that call; the script sends SIGTERM then. The headless signal handler
only sets the quit flag, so the call must finish on its own, the frame loops must be skipped, and the app must leave
cleanly: exit 0, the "quit requested" line, no owner violation, and neither 3 s warning (a thread that did not stop,
a call that was not answered).
usage: quit_during_call.py <virtualjaguar> <rom>
"""
import os
import signal
import subprocess
import sys
import threading
import time
SLOW_MS = 2000
def main():
exe, rom = sys.argv[1:3]
env = dict(os.environ, VJ_TEST_SLOW_CALL_MS=str(SLOW_MS))
p = subprocess.Popen([exe, "--headless", "--emu-thread", "on", "--frames", "30", rom], env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
lines = []
busy = threading.Event()
def reader():
for line in p.stdout:
lines.append(line)
if "[emu] busy" in line:
busy.set()
t = threading.Thread(target=reader, daemon=True)
t.start()
try:
if not busy.wait(30):
print("FAIL the app never logged the busy state\n" + "".join(lines))
return 1
t0 = time.monotonic()
p.send_signal(signal.SIGTERM) # the UI thread is inside the call right now
try:
rc = p.wait(timeout=20)
except subprocess.TimeoutExpired:
print("FAIL the app did not exit within 20 s of SIGTERM\n" + "".join(lines))
return 1
t.join(5)
out = "".join(lines)
if rc != 0:
print("FAIL exit code %d, expected 0\n%s" % (rc, out)); return 1
for bad in ("owner violation", "ThreadSanitizer", "did not stop within 3 s", "did not answer within 3 s"):
if bad in out:
print("FAIL the log says %r\n%s" % (bad, out)); return 1
if "headless: quit requested, exiting cleanly" not in out:
print("FAIL no \"quit requested\" line (the run ended some other way)\n" + out); return 1
print("PASS: quit during a slow call exits 0 (%.1f s after the signal)" % (time.monotonic() - t0))
return 0
finally:
if p.poll() is None:
p.kill()
if __name__ == "__main__":
sys.exit(main())