gemba 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/THIRD_PARTY_NOTICES +37 -2
- data/assets/placeholder_boxart.png +0 -0
- data/bin/gemba +2 -2
- data/ext/gemba/extconf.rb +23 -1
- data/ext/gemba/gemba_ext.c +436 -2
- data/ext/gemba/gemba_ext.h +2 -0
- data/gemba.gemspec +5 -3
- data/lib/gemba/achievements/achievement.rb +23 -0
- data/lib/gemba/achievements/backend.rb +186 -0
- data/lib/gemba/achievements/cache.rb +70 -0
- data/lib/gemba/achievements/credentials_presenter.rb +142 -0
- data/lib/gemba/achievements/fake_backend.rb +205 -0
- data/lib/gemba/achievements/null_backend.rb +11 -0
- data/lib/gemba/achievements/offline_backend.rb +168 -0
- data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1015 -0
- data/lib/gemba/bios.rb +54 -0
- data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
- data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
- data/lib/gemba/boxart_fetcher.rb +79 -0
- data/lib/gemba/bus_emitter.rb +13 -0
- data/lib/gemba/child_window.rb +24 -1
- data/lib/gemba/cli/commands/config_cmd.rb +83 -0
- data/lib/gemba/cli/commands/decode.rb +154 -0
- data/lib/gemba/cli/commands/patch.rb +78 -0
- data/lib/gemba/cli/commands/play.rb +78 -0
- data/lib/gemba/cli/commands/record.rb +114 -0
- data/lib/gemba/cli/commands/replay.rb +161 -0
- data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
- data/lib/gemba/cli/commands/version.rb +22 -0
- data/lib/gemba/cli.rb +52 -364
- data/lib/gemba/config.rb +134 -1
- data/lib/gemba/data/gb_games.json +1 -0
- data/lib/gemba/data/gb_md5.json +1 -0
- data/lib/gemba/data/gba_games.json +1 -0
- data/lib/gemba/data/gba_md5.json +1 -0
- data/lib/gemba/data/gbc_games.json +1 -0
- data/lib/gemba/data/gbc_md5.json +1 -0
- data/lib/gemba/emulator_frame.rb +1060 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +60 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +268 -0
- data/lib/gemba/gamepad_map.rb +103 -0
- data/lib/gemba/headless.rb +6 -5
- data/lib/gemba/headless_player.rb +33 -3
- data/lib/gemba/help_window.rb +61 -0
- data/lib/gemba/hotkey_map.rb +3 -1
- data/lib/gemba/input_recorder.rb +107 -0
- data/lib/gemba/input_replayer.rb +119 -0
- data/lib/gemba/keyboard_map.rb +90 -0
- data/lib/gemba/locales/en.yml +97 -5
- data/lib/gemba/locales/ja.yml +97 -5
- data/lib/gemba/main_window.rb +56 -0
- data/lib/gemba/modal_stack.rb +81 -0
- data/lib/gemba/patcher_window.rb +223 -0
- data/lib/gemba/platform/gb.rb +21 -0
- data/lib/gemba/platform/gba.rb +21 -0
- data/lib/gemba/platform/gbc.rb +23 -0
- data/lib/gemba/platform.rb +20 -0
- data/lib/gemba/platform_open.rb +19 -0
- data/lib/gemba/recorder.rb +4 -3
- data/lib/gemba/replay_player.rb +691 -0
- data/lib/gemba/rom_info.rb +57 -0
- data/lib/gemba/rom_info_window.rb +16 -3
- data/lib/gemba/rom_library.rb +106 -0
- data/lib/gemba/rom_overrides.rb +47 -0
- data/lib/gemba/rom_patcher/bps.rb +161 -0
- data/lib/gemba/rom_patcher/ips.rb +101 -0
- data/lib/gemba/rom_patcher/ups.rb +118 -0
- data/lib/gemba/rom_patcher.rb +109 -0
- data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
- data/lib/gemba/runtime.rb +59 -26
- data/lib/gemba/save_state_manager.rb +4 -7
- data/lib/gemba/save_state_picker.rb +17 -4
- data/lib/gemba/session_logger.rb +64 -0
- data/lib/gemba/settings/audio_tab.rb +77 -0
- data/lib/gemba/settings/gamepad_tab.rb +351 -0
- data/lib/gemba/settings/hotkeys_tab.rb +259 -0
- data/lib/gemba/settings/paths.rb +11 -0
- data/lib/gemba/settings/recording_tab.rb +83 -0
- data/lib/gemba/settings/save_states_tab.rb +91 -0
- data/lib/gemba/settings/system_tab.rb +362 -0
- data/lib/gemba/settings/video_tab.rb +318 -0
- data/lib/gemba/settings_window.rb +162 -1036
- data/lib/gemba/version.rb +1 -1
- data/lib/gemba/virtual_keyboard.rb +19 -0
- data/lib/gemba.rb +2 -12
- data/test/achievements_window/test_bulk_sync.rb +218 -0
- data/test/achievements_window/test_bus_events.rb +125 -0
- data/test/achievements_window/test_close_confirmation.rb +201 -0
- data/test/achievements_window/test_initial_state.rb +164 -0
- data/test/achievements_window/test_sorting.rb +227 -0
- data/test/achievements_window/test_tree_rendering.rb +133 -0
- data/test/fixtures/fake_bios.bin +0 -0
- data/test/fixtures/pong.gba +0 -0
- data/test/fixtures/test.gb +0 -0
- data/test/fixtures/test.gbc +0 -0
- data/test/fixtures/test_quicksave.ss +0 -0
- data/test/screenshots/no_focus.png +0 -0
- data/test/shared/teek_test_worker.rb +17 -1
- data/test/shared/tk_test_helper.rb +91 -4
- data/test/support/achievements_window_helpers.rb +18 -0
- data/test/support/fake_core.rb +25 -0
- data/test/support/fake_ra_runtime.rb +74 -0
- data/test/support/fake_requester.rb +68 -0
- data/test/support/player_helpers.rb +20 -5
- data/test/test_achievement.rb +32 -0
- data/test/{test_player.rb → test_app_controller.rb} +353 -85
- data/test/test_bios.rb +123 -0
- data/test/test_boxart_fetcher.rb +150 -0
- data/test/test_cli.rb +17 -265
- data/test/test_cli_config.rb +64 -0
- data/test/test_cli_decode.rb +97 -0
- data/test/test_cli_patch.rb +58 -0
- data/test/test_cli_play.rb +213 -0
- data/test/test_cli_ra.rb +175 -0
- data/test/test_cli_record.rb +69 -0
- data/test/test_cli_replay.rb +72 -0
- data/test/test_cli_sync_requester.rb +152 -0
- data/test/test_cli_version.rb +27 -0
- data/test/test_config.rb +2 -3
- data/test/test_config_ra.rb +69 -0
- data/test/test_core.rb +62 -1
- data/test/test_credentials_presenter.rb +192 -0
- data/test/test_event_bus.rb +100 -0
- data/test/test_fake_backend_achievements.rb +130 -0
- data/test/test_fake_backend_auth.rb +68 -0
- data/test/test_game_index.rb +77 -0
- data/test/test_game_picker_frame.rb +310 -0
- data/test/test_gamepad_map.rb +1 -3
- data/test/test_headless_player.rb +17 -3
- data/test/test_help_window.rb +82 -0
- data/test/test_hotkey_map.rb +22 -1
- data/test/test_input_recorder.rb +179 -0
- data/test/test_input_replay_determinism.rb +113 -0
- data/test/test_input_replayer.rb +162 -0
- data/test/test_keyboard_map.rb +1 -3
- data/test/test_libretro_backend.rb +41 -0
- data/test/test_locale.rb +1 -1
- data/test/test_logging.rb +123 -0
- data/test/test_null_backend.rb +42 -0
- data/test/test_offline_backend.rb +116 -0
- data/test/test_overlay_renderer.rb +1 -1
- data/test/test_platform.rb +149 -0
- data/test/test_ra_backend.rb +313 -0
- data/test/test_ra_backend_unlock_gate.rb +56 -0
- data/test/test_recorder.rb +0 -3
- data/test/test_replay_player.rb +316 -0
- data/test/test_rom_info.rb +149 -0
- data/test/test_rom_overrides.rb +86 -0
- data/test/test_rom_patcher.rb +382 -0
- data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
- data/test/test_save_state_manager.rb +2 -4
- data/test/test_settings_audio.rb +107 -0
- data/test/test_settings_hotkeys.rb +83 -66
- data/test/test_settings_recording.rb +49 -0
- data/test/test_settings_save_states.rb +97 -0
- data/test/test_settings_system.rb +133 -0
- data/test/test_settings_video.rb +450 -0
- data/test/test_settings_window.rb +76 -507
- data/test/test_tip_service.rb +6 -6
- data/test/test_toast_overlay.rb +1 -1
- data/test/test_virtual_events.rb +156 -0
- data/test/test_virtual_keyboard.rb +1 -1
- data/vendor/rcheevos/CHANGELOG.md +495 -0
- data/vendor/rcheevos/LICENSE +21 -0
- data/vendor/rcheevos/Package.swift +33 -0
- data/vendor/rcheevos/README.md +67 -0
- data/vendor/rcheevos/include/module.modulemap +70 -0
- data/vendor/rcheevos/include/rc_api_editor.h +296 -0
- data/vendor/rcheevos/include/rc_api_info.h +280 -0
- data/vendor/rcheevos/include/rc_api_request.h +77 -0
- data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
- data/vendor/rcheevos/include/rc_api_user.h +262 -0
- data/vendor/rcheevos/include/rc_client.h +877 -0
- data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
- data/vendor/rcheevos/include/rc_consoles.h +138 -0
- data/vendor/rcheevos/include/rc_error.h +59 -0
- data/vendor/rcheevos/include/rc_export.h +100 -0
- data/vendor/rcheevos/include/rc_hash.h +200 -0
- data/vendor/rcheevos/include/rc_runtime.h +148 -0
- data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
- data/vendor/rcheevos/include/rc_util.h +51 -0
- data/vendor/rcheevos/include/rcheevos.h +8 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
- data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
- data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
- data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
- data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
- data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
- data/vendor/rcheevos/src/rc_client.c +6941 -0
- data/vendor/rcheevos/src/rc_client_external.c +281 -0
- data/vendor/rcheevos/src/rc_client_external.h +177 -0
- data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
- data/vendor/rcheevos/src/rc_client_internal.h +409 -0
- data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
- data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
- data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
- data/vendor/rcheevos/src/rc_compat.c +251 -0
- data/vendor/rcheevos/src/rc_compat.h +121 -0
- data/vendor/rcheevos/src/rc_libretro.c +915 -0
- data/vendor/rcheevos/src/rc_libretro.h +98 -0
- data/vendor/rcheevos/src/rc_util.c +199 -0
- data/vendor/rcheevos/src/rc_version.c +11 -0
- data/vendor/rcheevos/src/rc_version.h +32 -0
- data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
- data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
- data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
- data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
- data/vendor/rcheevos/src/rcheevos/format.c +330 -0
- data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
- data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
- data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
- data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
- data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
- data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
- data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
- data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
- data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
- data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
- data/vendor/rcheevos/src/rcheevos/value.c +935 -0
- data/vendor/rcheevos/src/rhash/aes.c +480 -0
- data/vendor/rcheevos/src/rhash/aes.h +49 -0
- data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
- data/vendor/rcheevos/src/rhash/hash.c +1402 -0
- data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
- data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
- data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
- data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
- data/vendor/rcheevos/src/rhash/md5.c +382 -0
- data/vendor/rcheevos/src/rhash/md5.h +91 -0
- data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
- data/vendor/rcheevos/test/libretro.h +205 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
- data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
- data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
- data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
- data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
- data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
- data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
- data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
- data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
- data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
- data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
- data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
- data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
- data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
- data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
- data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
- data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
- data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
- data/vendor/rcheevos/test/rhash/data.c +657 -0
- data/vendor/rcheevos/test/rhash/data.h +32 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
- data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
- data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
- data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
- data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
- data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
- data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
- data/vendor/rcheevos/test/test.c +113 -0
- data/vendor/rcheevos/test/test_framework.h +205 -0
- data/vendor/rcheevos/test/test_rc_client.c +10509 -0
- data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
- data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
- data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
- data/vendor/rcheevos/test/test_types.natvis +9 -0
- data/vendor/rcheevos/validator/validator.c +658 -0
- data/vendor/rcheevos/validator/validator.vcxproj +152 -0
- data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
- metadata +274 -11
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -16,7 +16,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
16
16
|
require "gemba"
|
|
17
17
|
require "support/player_helpers"
|
|
18
18
|
|
|
19
|
-
player = Gemba::
|
|
19
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
20
|
+
player.disable_confirmations!
|
|
20
21
|
app = player.app
|
|
21
22
|
|
|
22
23
|
poll_until_ready(player) { player.running = false }
|
|
@@ -33,6 +34,122 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
33
34
|
assert success, "Player should exit cleanly with ROM loaded (no hang)\n#{output.join("\n")}"
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# Quit hotkey works without a ROM loaded (no viewport/SDL2).
|
|
38
|
+
def test_quit_hotkey_without_rom
|
|
39
|
+
code = <<~RUBY
|
|
40
|
+
require "gemba"
|
|
41
|
+
|
|
42
|
+
player = Gemba::AppController.new
|
|
43
|
+
player.disable_confirmations!
|
|
44
|
+
app = player.app
|
|
45
|
+
|
|
46
|
+
app.after(100) do
|
|
47
|
+
app.tcl_eval("focus -force .")
|
|
48
|
+
app.command(:event, 'generate', '.', '<KeyPress>', keysym: 'q')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
player.run
|
|
52
|
+
puts "PASS"
|
|
53
|
+
RUBY
|
|
54
|
+
|
|
55
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
56
|
+
|
|
57
|
+
output = []
|
|
58
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
59
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
60
|
+
|
|
61
|
+
assert success, "Quit hotkey should work without ROM loaded\n#{output.join("\n")}"
|
|
62
|
+
assert_includes stdout, "PASS"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# View > Game Library returns to picker after loading a ROM.
|
|
66
|
+
def test_view_menu_game_library_returns_to_picker
|
|
67
|
+
code = <<~'RUBY'.sub('ROM_PATH', TEST_ROM)
|
|
68
|
+
require "gemba"
|
|
69
|
+
require "support/player_helpers"
|
|
70
|
+
|
|
71
|
+
player = Gemba::AppController.new("ROM_PATH")
|
|
72
|
+
player.disable_confirmations!
|
|
73
|
+
app = player.app
|
|
74
|
+
|
|
75
|
+
poll_until(app, timeout_ms: 5_000,
|
|
76
|
+
condition: -> { app.tcl_eval('.menubar.view entrycget 0 -state').strip == 'normal' },
|
|
77
|
+
label: "View menu entry never became enabled") do
|
|
78
|
+
puts "BEFORE=#{player.current_view}"
|
|
79
|
+
app.tcl_eval('.menubar.view invoke 0')
|
|
80
|
+
app.after(100) do
|
|
81
|
+
puts "AFTER=#{player.current_view}"
|
|
82
|
+
player.running = false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
player.run
|
|
87
|
+
RUBY
|
|
88
|
+
|
|
89
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
90
|
+
|
|
91
|
+
output = []
|
|
92
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
93
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
94
|
+
|
|
95
|
+
assert success, "View > Game Library should return to picker\n#{output.join("\n")}"
|
|
96
|
+
assert_includes stdout, "BEFORE=emulator"
|
|
97
|
+
assert_includes stdout, "AFTER=picker"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# File > Quit menu item exits cleanly (invokes the actual menu command).
|
|
101
|
+
def test_file_menu_quit_without_rom
|
|
102
|
+
code = <<~RUBY
|
|
103
|
+
require "gemba"
|
|
104
|
+
|
|
105
|
+
player = Gemba::AppController.new
|
|
106
|
+
app = player.app
|
|
107
|
+
|
|
108
|
+
app.after(100) do
|
|
109
|
+
app.command('.menubar.file', 'invoke', 'last')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
player.run
|
|
113
|
+
puts "PASS"
|
|
114
|
+
RUBY
|
|
115
|
+
|
|
116
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
117
|
+
|
|
118
|
+
output = []
|
|
119
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
120
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
121
|
+
|
|
122
|
+
assert success, "File > Quit should exit cleanly\n#{output.join("\n")}"
|
|
123
|
+
assert_includes stdout, "PASS"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Escape key quits without a ROM loaded.
|
|
127
|
+
def test_escape_quits_without_rom
|
|
128
|
+
code = <<~RUBY
|
|
129
|
+
require "gemba"
|
|
130
|
+
|
|
131
|
+
player = Gemba::AppController.new
|
|
132
|
+
app = player.app
|
|
133
|
+
|
|
134
|
+
app.after(100) do
|
|
135
|
+
app.tcl_eval("focus -force .")
|
|
136
|
+
app.command(:event, 'generate', '.', '<KeyPress>', keysym: 'Escape')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
player.run
|
|
140
|
+
puts "PASS"
|
|
141
|
+
RUBY
|
|
142
|
+
|
|
143
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
144
|
+
|
|
145
|
+
output = []
|
|
146
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
147
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
148
|
+
|
|
149
|
+
assert success, "Escape should quit without ROM loaded\n#{output.join("\n")}"
|
|
150
|
+
assert_includes stdout, "PASS"
|
|
151
|
+
end
|
|
152
|
+
|
|
36
153
|
# Simulate a user pressing F11 twice (fullscreen on → off) then q to quit.
|
|
37
154
|
# Exercises the wm attributes fullscreen path end-to-end. If the toggle
|
|
38
155
|
# causes a hang or crash the subprocess will time out.
|
|
@@ -41,11 +158,12 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
41
158
|
require "gemba"
|
|
42
159
|
require "support/player_helpers"
|
|
43
160
|
|
|
44
|
-
player = Gemba::
|
|
161
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
162
|
+
player.disable_confirmations!
|
|
45
163
|
app = player.app
|
|
46
164
|
|
|
47
165
|
poll_until_ready(player) do
|
|
48
|
-
vp = player.viewport
|
|
166
|
+
vp = player.frame.viewport
|
|
49
167
|
frame = vp.frame.path
|
|
50
168
|
|
|
51
169
|
# User presses F11 → fullscreen on
|
|
@@ -58,8 +176,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
58
176
|
app.update
|
|
59
177
|
|
|
60
178
|
app.after(50) do
|
|
61
|
-
#
|
|
62
|
-
app.command(:event, 'generate',
|
|
179
|
+
# Trigger quit via virtual event — no focus dependency
|
|
180
|
+
app.command(:event, 'generate', '.', '<<Quit>>')
|
|
63
181
|
end
|
|
64
182
|
end
|
|
65
183
|
end
|
|
@@ -85,11 +203,12 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
85
203
|
require "gemba"
|
|
86
204
|
require "support/player_helpers"
|
|
87
205
|
|
|
88
|
-
player = Gemba::
|
|
206
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
207
|
+
player.disable_confirmations!
|
|
89
208
|
app = player.app
|
|
90
209
|
|
|
91
210
|
poll_until_ready(player) do
|
|
92
|
-
vp = player.viewport
|
|
211
|
+
vp = player.frame.viewport
|
|
93
212
|
frame = vp.frame.path
|
|
94
213
|
|
|
95
214
|
# User presses Tab → enable turbo (2x default)
|
|
@@ -97,8 +216,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
97
216
|
app.update
|
|
98
217
|
|
|
99
218
|
app.after(50) do
|
|
100
|
-
#
|
|
101
|
-
app.command(:event, 'generate',
|
|
219
|
+
# Trigger quit via virtual event — no focus dependency
|
|
220
|
+
app.command(:event, 'generate', '.', '<<Quit>>')
|
|
102
221
|
end
|
|
103
222
|
end
|
|
104
223
|
|
|
@@ -118,8 +237,6 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
118
237
|
# Verifies state file + screenshot are created, backup rotation works,
|
|
119
238
|
# and the core remains functional after load.
|
|
120
239
|
def test_quick_save_and_load_creates_files_and_restores_state
|
|
121
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
122
|
-
|
|
123
240
|
code = <<~RUBY
|
|
124
241
|
require "gemba"
|
|
125
242
|
require "tmpdir"
|
|
@@ -129,7 +246,7 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
129
246
|
# Use a temp dir for all config/states so we don't pollute the real one
|
|
130
247
|
states_dir = Dir.mktmpdir("gemba-states-test")
|
|
131
248
|
|
|
132
|
-
player = Gemba::
|
|
249
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
133
250
|
app = player.app
|
|
134
251
|
config = player.config
|
|
135
252
|
|
|
@@ -138,13 +255,12 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
138
255
|
config.save_state_debounce = 0.1
|
|
139
256
|
|
|
140
257
|
poll_until_ready(player) do
|
|
141
|
-
core = player.save_mgr.core
|
|
142
|
-
state_dir
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
app.command(:event, 'generate', frame_path, '<KeyPress>', keysym: 'F5')
|
|
258
|
+
core = player.frame.save_mgr.core
|
|
259
|
+
# Recompute state_dir after overriding config.states_dir
|
|
260
|
+
player.frame.save_mgr.state_dir = player.frame.save_mgr.state_dir_for_rom(core)
|
|
261
|
+
state_dir = player.frame.save_mgr.state_dir
|
|
262
|
+
# Quick save
|
|
263
|
+
app.command(:event, 'generate', '.', '<<QuickSave>>')
|
|
148
264
|
app.update
|
|
149
265
|
|
|
150
266
|
app.after(50) do
|
|
@@ -168,7 +284,7 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
168
284
|
|
|
169
285
|
# Save again to test backup rotation (after debounce)
|
|
170
286
|
app.after(50) do
|
|
171
|
-
app.command(:event, 'generate',
|
|
287
|
+
app.command(:event, 'generate', '.', '<<QuickSave>>')
|
|
172
288
|
app.update
|
|
173
289
|
|
|
174
290
|
app.after(50) do
|
|
@@ -185,8 +301,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
185
301
|
exit 1
|
|
186
302
|
end
|
|
187
303
|
|
|
188
|
-
# Quick load
|
|
189
|
-
app.command(:event, 'generate',
|
|
304
|
+
# Quick load
|
|
305
|
+
app.command(:event, 'generate', '.', '<<QuickLoad>>')
|
|
190
306
|
app.update
|
|
191
307
|
|
|
192
308
|
app.after(50) do
|
|
@@ -241,8 +357,6 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
241
357
|
|
|
242
358
|
# E2E: verify debounce blocks rapid-fire saves.
|
|
243
359
|
def test_quick_save_debounce_blocks_rapid_fire
|
|
244
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
245
|
-
|
|
246
360
|
code = <<~RUBY
|
|
247
361
|
require "gemba"
|
|
248
362
|
require "tmpdir"
|
|
@@ -251,7 +365,7 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
251
365
|
|
|
252
366
|
states_dir = Dir.mktmpdir("gemba-debounce-test")
|
|
253
367
|
|
|
254
|
-
player = Gemba::
|
|
368
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
255
369
|
app = player.app
|
|
256
370
|
config = player.config
|
|
257
371
|
|
|
@@ -259,22 +373,22 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
259
373
|
config.save_state_debounce = 5.0 # long debounce
|
|
260
374
|
|
|
261
375
|
poll_until_ready(player) do
|
|
262
|
-
|
|
263
|
-
|
|
376
|
+
# Recompute state_dir after overriding config.states_dir
|
|
377
|
+
player.frame.save_mgr.state_dir = player.frame.save_mgr.state_dir_for_rom(player.frame.save_mgr.core)
|
|
264
378
|
|
|
265
379
|
# First save should succeed
|
|
266
|
-
app.command(:event, 'generate',
|
|
380
|
+
app.command(:event, 'generate', '.', '<<QuickSave>>')
|
|
267
381
|
app.update
|
|
268
382
|
|
|
269
383
|
app.after(50) do
|
|
270
|
-
state_dir = player.save_mgr.state_dir
|
|
384
|
+
state_dir = player.frame.save_mgr.state_dir
|
|
271
385
|
ss_path = File.join(state_dir, "state1.ss")
|
|
272
386
|
|
|
273
387
|
first_exists = File.exist?(ss_path)
|
|
274
388
|
first_mtime = first_exists ? File.mtime(ss_path) : nil
|
|
275
389
|
|
|
276
390
|
# Immediate second save should be debounced (within 5s window)
|
|
277
|
-
app.command(:event, 'generate',
|
|
391
|
+
app.command(:event, 'generate', '.', '<<QuickSave>>')
|
|
278
392
|
app.update
|
|
279
393
|
|
|
280
394
|
app.after(50) do
|
|
@@ -313,8 +427,6 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
313
427
|
# E2E: open Settings via menu, navigate to Save States tab,
|
|
314
428
|
# change quick save slot from 1 → 10, click Save, verify persisted.
|
|
315
429
|
def test_settings_change_quick_slot_and_save
|
|
316
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
317
|
-
|
|
318
430
|
code = <<~RUBY
|
|
319
431
|
require "gemba"
|
|
320
432
|
require "tmpdir"
|
|
@@ -325,7 +437,7 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
325
437
|
config_dir = Dir.mktmpdir("gemba-settings-test")
|
|
326
438
|
config_path = File.join(config_dir, "settings.json")
|
|
327
439
|
|
|
328
|
-
player = Gemba::
|
|
440
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
329
441
|
app = player.app
|
|
330
442
|
config = player.config
|
|
331
443
|
|
|
@@ -397,11 +509,11 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
397
509
|
# -- Audio fade ramp (pure function, no Tk/SDL2 needed) --------------------
|
|
398
510
|
|
|
399
511
|
def test_fade_ramp_attenuates_first_samples
|
|
400
|
-
require "gemba/
|
|
512
|
+
require "gemba/headless"
|
|
401
513
|
# 10 stereo frames of max-amplitude int16
|
|
402
514
|
pcm = ([32767, 32767] * 10).pack('s*')
|
|
403
515
|
total = 10
|
|
404
|
-
result, remaining = Gemba::
|
|
516
|
+
result, remaining = Gemba::EmulatorFrame.apply_fade_ramp(pcm, total, total)
|
|
405
517
|
samples = result.unpack('s*')
|
|
406
518
|
|
|
407
519
|
# First stereo pair: gain = 1 - 10/10 = 0.0 → should be 0
|
|
@@ -416,17 +528,17 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
416
528
|
end
|
|
417
529
|
|
|
418
530
|
def test_fade_ramp_returns_remaining_when_pcm_shorter_than_fade
|
|
419
|
-
require "gemba/
|
|
531
|
+
require "gemba/headless"
|
|
420
532
|
# Only 2 stereo frames but fade wants 10
|
|
421
533
|
pcm = ([20000, 20000] * 2).pack('s*')
|
|
422
|
-
_result, remaining = Gemba::
|
|
534
|
+
_result, remaining = Gemba::EmulatorFrame.apply_fade_ramp(pcm, 10, 10)
|
|
423
535
|
assert_equal 8, remaining, "should have 8 fade samples remaining"
|
|
424
536
|
end
|
|
425
537
|
|
|
426
538
|
def test_fade_ramp_noop_when_remaining_zero
|
|
427
|
-
require "gemba/
|
|
539
|
+
require "gemba/headless"
|
|
428
540
|
pcm = ([10000, -10000] * 4).pack('s*')
|
|
429
|
-
result, remaining = Gemba::
|
|
541
|
+
result, remaining = Gemba::EmulatorFrame.apply_fade_ramp(pcm, 0, 10)
|
|
430
542
|
assert_equal pcm, result, "should not modify samples when remaining is 0"
|
|
431
543
|
assert_equal 0, remaining
|
|
432
544
|
end
|
|
@@ -436,8 +548,6 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
436
548
|
# closes Settings, opens picker via F6, tries Settings menu (blocked),
|
|
437
549
|
# closes picker. Checks window visibility via `wm state`.
|
|
438
550
|
def test_modal_child_blocks_concurrent_windows
|
|
439
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
440
|
-
|
|
441
551
|
code = <<~RUBY
|
|
442
552
|
require "gemba"
|
|
443
553
|
require "support/player_helpers"
|
|
@@ -445,11 +555,11 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
445
555
|
sw_top = Gemba::SettingsWindow::TOP
|
|
446
556
|
sp_top = Gemba::SaveStatePicker::TOP
|
|
447
557
|
|
|
448
|
-
player = Gemba::
|
|
558
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
449
559
|
app = player.app
|
|
450
560
|
|
|
451
561
|
poll_until_ready(player) do
|
|
452
|
-
vp = player.viewport
|
|
562
|
+
vp = player.frame.viewport
|
|
453
563
|
frame = vp.frame.path
|
|
454
564
|
|
|
455
565
|
# 1. Open Settings via menu (Settings > Video = index 0)
|
|
@@ -539,13 +649,11 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
539
649
|
# -- File drop (DND) --------------------------------------------------------
|
|
540
650
|
|
|
541
651
|
def test_drop_rom_file_loads_game
|
|
542
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
543
|
-
|
|
544
652
|
code = <<~RUBY
|
|
545
653
|
require "gemba"
|
|
546
654
|
require "support/player_helpers"
|
|
547
655
|
|
|
548
|
-
player = Gemba::
|
|
656
|
+
player = Gemba::AppController.new
|
|
549
657
|
app = player.app
|
|
550
658
|
|
|
551
659
|
# Stub tk_messageBox so it never blocks
|
|
@@ -557,7 +665,7 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
557
665
|
app.update
|
|
558
666
|
|
|
559
667
|
app.after(50) do
|
|
560
|
-
core = player.core
|
|
668
|
+
core = player.frame.core
|
|
561
669
|
if core && !core.destroyed?
|
|
562
670
|
$stdout.puts "TITLE=\#{core.title}"
|
|
563
671
|
else
|
|
@@ -581,13 +689,11 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
581
689
|
end
|
|
582
690
|
|
|
583
691
|
def test_drop_unsupported_file_shows_error
|
|
584
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
585
|
-
|
|
586
692
|
code = <<~RUBY
|
|
587
693
|
require "gemba"
|
|
588
694
|
require "support/player_helpers"
|
|
589
695
|
|
|
590
|
-
player = Gemba::
|
|
696
|
+
player = Gemba::AppController.new
|
|
591
697
|
app = player.app
|
|
592
698
|
|
|
593
699
|
# Capture tk_messageBox calls instead of blocking
|
|
@@ -624,13 +730,11 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
624
730
|
end
|
|
625
731
|
|
|
626
732
|
def test_drop_multiple_files_shows_error
|
|
627
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
628
|
-
|
|
629
733
|
code = <<~RUBY
|
|
630
734
|
require "gemba"
|
|
631
735
|
require "support/player_helpers"
|
|
632
736
|
|
|
633
|
-
player = Gemba::
|
|
737
|
+
player = Gemba::AppController.new
|
|
634
738
|
app = player.app
|
|
635
739
|
|
|
636
740
|
# Capture tk_messageBox calls instead of blocking
|
|
@@ -669,8 +773,6 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
669
773
|
# E2E: press F10 to start recording, run a few frames with the red dot
|
|
670
774
|
# indicator rendering, press F10 to stop, verify .grec file was created.
|
|
671
775
|
def test_recording_toggle_creates_trec_file
|
|
672
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
673
|
-
|
|
674
776
|
code = <<~RUBY
|
|
675
777
|
require "gemba"
|
|
676
778
|
require "tmpdir"
|
|
@@ -680,31 +782,26 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
680
782
|
rec_dir = Dir.mktmpdir("gemba-rec-test")
|
|
681
783
|
|
|
682
784
|
begin
|
|
683
|
-
player = Gemba::
|
|
785
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
684
786
|
app = player.app
|
|
685
787
|
config = player.config
|
|
686
788
|
config.recordings_dir = rec_dir
|
|
687
789
|
|
|
688
790
|
poll_until_ready(player) do
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
# Press F10 → start recording
|
|
693
|
-
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F10')
|
|
791
|
+
# Start recording via virtual event (no focus needed)
|
|
792
|
+
app.command(:event, 'generate', '.', '<<RecordToggle>>')
|
|
694
793
|
app.update
|
|
695
794
|
|
|
696
795
|
# Let a few frames render with the recording indicator (red dot)
|
|
697
796
|
app.after(50) do
|
|
698
|
-
unless player.recording?
|
|
797
|
+
unless player.frame.recording?
|
|
699
798
|
puts "FAIL: recording never started"
|
|
700
799
|
player.running = false
|
|
701
800
|
next
|
|
702
801
|
end
|
|
703
802
|
|
|
704
|
-
#
|
|
705
|
-
app.
|
|
706
|
-
# Press F10 → stop recording
|
|
707
|
-
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F10')
|
|
803
|
+
# Stop recording via virtual event
|
|
804
|
+
app.command(:event, 'generate', '.', '<<RecordToggle>>')
|
|
708
805
|
app.update
|
|
709
806
|
|
|
710
807
|
app.after(50) do
|
|
@@ -738,33 +835,76 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
738
835
|
assert_includes stdout, "PASS", "Expected .grec file to be created\n#{output.join("\n")}"
|
|
739
836
|
end
|
|
740
837
|
|
|
838
|
+
# -- Frame stack show/hide ---------------------------------------------------
|
|
839
|
+
|
|
840
|
+
# Loads a ROM, verifies the emulator viewport is visible (packed),
|
|
841
|
+
# then hides it and confirms it's gone. Catches missing show/hide on frames.
|
|
842
|
+
def test_emulator_frame_show_hide_round_trip
|
|
843
|
+
code = <<~RUBY
|
|
844
|
+
require "gemba"
|
|
845
|
+
require "support/player_helpers"
|
|
846
|
+
|
|
847
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
848
|
+
app = player.app
|
|
849
|
+
|
|
850
|
+
poll_until_ready(player) do
|
|
851
|
+
vp_path = player.frame.viewport.frame.path
|
|
852
|
+
|
|
853
|
+
# Viewport should be visible after ROM load
|
|
854
|
+
info = app.tcl_eval("pack info \#{vp_path}") rescue ""
|
|
855
|
+
abort "FAIL: viewport not packed after load" if info.empty?
|
|
856
|
+
|
|
857
|
+
# Hide the frame, viewport should disappear
|
|
858
|
+
player.frame.hide
|
|
859
|
+
info_after = app.tcl_eval("pack info \#{vp_path}") rescue ""
|
|
860
|
+
abort "FAIL: viewport still packed after hide" unless info_after.empty?
|
|
861
|
+
|
|
862
|
+
# Show it again
|
|
863
|
+
player.frame.show
|
|
864
|
+
info_restored = app.tcl_eval("pack info \#{vp_path}") rescue ""
|
|
865
|
+
abort "FAIL: viewport not packed after show" if info_restored.empty?
|
|
866
|
+
|
|
867
|
+
puts "PASS"
|
|
868
|
+
player.running = false
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
player.run
|
|
872
|
+
RUBY
|
|
873
|
+
|
|
874
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
875
|
+
|
|
876
|
+
output = []
|
|
877
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
878
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
879
|
+
|
|
880
|
+
assert success, "Frame show/hide round-trip failed\n#{output.join("\n")}"
|
|
881
|
+
assert_includes stdout, "PASS"
|
|
882
|
+
end
|
|
883
|
+
|
|
741
884
|
# -- Pause CPU optimization (thread_timer_ms) --------------------------------
|
|
742
885
|
|
|
743
886
|
def test_event_loop_constants
|
|
744
|
-
require "gemba/
|
|
745
|
-
assert_equal 1, Gemba::
|
|
746
|
-
assert_equal 50, Gemba::
|
|
887
|
+
require "gemba/headless"
|
|
888
|
+
assert_equal 1, Gemba::AppController::EVENT_LOOP_FAST_MS, "fast loop should be 1ms"
|
|
889
|
+
assert_equal 50, Gemba::AppController::EVENT_LOOP_IDLE_MS, "idle loop should be 50ms"
|
|
747
890
|
end
|
|
748
891
|
|
|
749
892
|
# E2E: verify thread_timer_ms switches between idle (50ms) and fast (1ms)
|
|
750
893
|
# when pausing and unpausing the emulator.
|
|
751
894
|
def test_pause_switches_event_loop_speed
|
|
752
|
-
skip "
|
|
753
|
-
# Flaky under xvfb: SDL2 INPUT_FOCUS is unreliable, causing focus_poll_tick
|
|
754
|
-
# to auto-pause and interfere with manual pause/unpause assertions.
|
|
755
|
-
skip "Flaky under xvfb (SDL2 INPUT_FOCUS unreliable)" if ENV['CI']
|
|
895
|
+
skip "Flaky under xvfb: SDL2 INPUT_FOCUS is unreliable"
|
|
756
896
|
|
|
757
897
|
code = <<~RUBY
|
|
758
898
|
require "gemba"
|
|
759
899
|
require "support/player_helpers"
|
|
760
900
|
|
|
761
|
-
player = Gemba::
|
|
901
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
762
902
|
app = player.app
|
|
763
903
|
|
|
764
904
|
poll_until_ready(player) do
|
|
765
905
|
# Wait for focus so focus_poll_tick won't interfere with pause/unpause
|
|
766
906
|
poll_until_focused(player) do
|
|
767
|
-
vp = player.viewport
|
|
907
|
+
vp = player.frame.viewport
|
|
768
908
|
frame = vp.frame.path
|
|
769
909
|
|
|
770
910
|
# Before pause: should be fast (1ms) since ROM is running
|
|
@@ -795,8 +935,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
795
935
|
unless ms_resumed == 1
|
|
796
936
|
xvfb_screenshot("pause_resume_fail")
|
|
797
937
|
$stderr.puts "FAIL: expected thread_timer_ms=1 after resume, got \#{ms_resumed}"
|
|
798
|
-
$stderr.puts "input_focus?=\#{player.viewport.renderer.input_focus?}"
|
|
799
|
-
$stderr.puts "paused=\#{player.
|
|
938
|
+
$stderr.puts "input_focus?=\#{player.frame.viewport.renderer.input_focus?}"
|
|
939
|
+
$stderr.puts "paused=\#{player.frame.paused?}"
|
|
800
940
|
exit 1
|
|
801
941
|
end
|
|
802
942
|
|
|
@@ -825,20 +965,17 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
825
965
|
# E2E: verify focus loss pauses emulation and focus regain resumes it.
|
|
826
966
|
# Uses thread_timer_ms as a proxy for paused state (50=idle/paused, 1=fast/running).
|
|
827
967
|
def test_pause_on_focus_loss
|
|
828
|
-
skip "
|
|
829
|
-
# Flaky under xvfb: SDL2 INPUT_FOCUS is unreliable, so the window may
|
|
830
|
-
# never report having focus — making focus-loss detection untestable.
|
|
831
|
-
skip "Flaky under xvfb (SDL2 INPUT_FOCUS unreliable)" if ENV['CI']
|
|
968
|
+
skip "Flaky under xvfb: SDL2 INPUT_FOCUS is unreliable"
|
|
832
969
|
|
|
833
970
|
code = <<~RUBY
|
|
834
971
|
require "gemba"
|
|
835
972
|
require "support/player_helpers"
|
|
836
973
|
|
|
837
|
-
player = Gemba::
|
|
974
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
838
975
|
app = player.app
|
|
839
976
|
|
|
840
977
|
poll_until_ready(player) do
|
|
841
|
-
renderer = player.viewport.renderer
|
|
978
|
+
renderer = player.frame.viewport.renderer
|
|
842
979
|
|
|
843
980
|
# Ensure the window has focus before testing focus *loss*
|
|
844
981
|
poll_until_focused(player) do
|
|
@@ -871,8 +1008,8 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
871
1008
|
# production (Tk's mainloop pumps Cocoa) but is untested in CI.
|
|
872
1009
|
renderer.show_window
|
|
873
1010
|
renderer.raise_window
|
|
874
|
-
app.command(:event, 'generate', player.viewport.frame.path, '<KeyPress>', keysym: 'p')
|
|
875
|
-
app.command(:event, 'generate', player.viewport.frame.path, '<KeyRelease>', keysym: 'p')
|
|
1011
|
+
app.command(:event, 'generate', player.frame.viewport.frame.path, '<KeyPress>', keysym: 'p')
|
|
1012
|
+
app.command(:event, 'generate', player.frame.viewport.frame.path, '<KeyRelease>', keysym: 'p')
|
|
876
1013
|
|
|
877
1014
|
app.after(100) do
|
|
878
1015
|
ms_regained = app.interp.thread_timer_ms
|
|
@@ -900,4 +1037,135 @@ class TestMGBAPlayer < Minitest::Test
|
|
|
900
1037
|
assert success, "Pause on focus loss test failed\n#{output.join("\n")}"
|
|
901
1038
|
assert_includes stdout, "PASS", "Expected PASS in output\n#{output.join("\n")}"
|
|
902
1039
|
end
|
|
1040
|
+
|
|
1041
|
+
# Regression: on Linux/Windows the window may not have focus at startup,
|
|
1042
|
+
# causing focus_poll_tick to immediately pause the emulator. Loading a ROM
|
|
1043
|
+
# should always start in a running (unpaused) state regardless of focus.
|
|
1044
|
+
def test_rom_does_not_start_paused
|
|
1045
|
+
code = <<~RUBY
|
|
1046
|
+
require "gemba"
|
|
1047
|
+
require "support/player_helpers"
|
|
1048
|
+
|
|
1049
|
+
player = Gemba::AppController.new("#{TEST_ROM}")
|
|
1050
|
+
app = player.app
|
|
1051
|
+
|
|
1052
|
+
poll_until_ready(player) do
|
|
1053
|
+
# Give focus poll a chance to fire (polls every 200ms)
|
|
1054
|
+
app.after(400) do
|
|
1055
|
+
ms = app.interp.thread_timer_ms
|
|
1056
|
+
paused = player.frame.paused?
|
|
1057
|
+
if paused
|
|
1058
|
+
$stderr.puts "FAIL: ROM started paused (thread_timer_ms=\#{ms})"
|
|
1059
|
+
exit 1
|
|
1060
|
+
end
|
|
1061
|
+
$stdout.puts "PASS"
|
|
1062
|
+
player.running = false
|
|
1063
|
+
end
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
player.run
|
|
1067
|
+
RUBY
|
|
1068
|
+
|
|
1069
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
1070
|
+
|
|
1071
|
+
output = []
|
|
1072
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
1073
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
1074
|
+
|
|
1075
|
+
assert success, "ROM started paused\n#{output.join("\n")}"
|
|
1076
|
+
assert_includes stdout, "PASS", "Expected PASS in output\n#{output.join("\n")}"
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
def test_question_hotkey_shows_help_window
|
|
1080
|
+
code = <<~RUBY
|
|
1081
|
+
require "gemba"
|
|
1082
|
+
|
|
1083
|
+
player = Gemba::AppController.new
|
|
1084
|
+
app = player.app
|
|
1085
|
+
|
|
1086
|
+
app.after(100) do
|
|
1087
|
+
app.command(:event, 'generate', '.', '<<ToggleHelpWindow>>')
|
|
1088
|
+
app.update
|
|
1089
|
+
state = app.tcl_eval("wm state .help_window")
|
|
1090
|
+
puts state == 'normal' ? "PASS" : "FAIL: help window not visible (state=\#{state})"
|
|
1091
|
+
player.running = false
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
player.run
|
|
1095
|
+
RUBY
|
|
1096
|
+
|
|
1097
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
1098
|
+
|
|
1099
|
+
output = []
|
|
1100
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
1101
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
1102
|
+
|
|
1103
|
+
assert success, "? hotkey should show help window\n#{output.join("\n")}"
|
|
1104
|
+
assert_includes stdout, "PASS", "Expected PASS in output\n#{output.join("\n")}"
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def test_question_hotkey_toggles_help_window
|
|
1108
|
+
code = <<~RUBY
|
|
1109
|
+
require "gemba"
|
|
1110
|
+
|
|
1111
|
+
player = Gemba::AppController.new
|
|
1112
|
+
app = player.app
|
|
1113
|
+
|
|
1114
|
+
app.after(100) do
|
|
1115
|
+
# First press — show
|
|
1116
|
+
app.command(:event, 'generate', '.', '<<ToggleHelpWindow>>')
|
|
1117
|
+
app.update
|
|
1118
|
+
# Second press — hide
|
|
1119
|
+
app.command(:event, 'generate', '.', '<<ToggleHelpWindow>>')
|
|
1120
|
+
app.update
|
|
1121
|
+
state = app.tcl_eval("wm state .help_window")
|
|
1122
|
+
puts state == 'withdrawn' ? "PASS" : "FAIL: help window still visible after second ? press (state=\#{state})"
|
|
1123
|
+
player.running = false
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
player.run
|
|
1127
|
+
RUBY
|
|
1128
|
+
|
|
1129
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
1130
|
+
|
|
1131
|
+
output = []
|
|
1132
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
1133
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
1134
|
+
|
|
1135
|
+
assert success, "second ? press should hide help window\n#{output.join("\n")}"
|
|
1136
|
+
assert_includes stdout, "PASS", "Expected PASS in output\n#{output.join("\n")}"
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
def test_help_window_hidden_in_fullscreen
|
|
1140
|
+
code = <<~RUBY
|
|
1141
|
+
require "gemba"
|
|
1142
|
+
|
|
1143
|
+
player = Gemba::AppController.new
|
|
1144
|
+
app = player.app
|
|
1145
|
+
|
|
1146
|
+
app.after(100) do
|
|
1147
|
+
# Go fullscreen via bus
|
|
1148
|
+
Gemba.bus.emit(:request_fullscreen)
|
|
1149
|
+
app.update
|
|
1150
|
+
# Toggle help — should be suppressed in fullscreen
|
|
1151
|
+
app.command(:event, 'generate', '.', '<<ToggleHelpWindow>>')
|
|
1152
|
+
app.update
|
|
1153
|
+
exists = app.tcl_eval("winfo exists .help_window")
|
|
1154
|
+
state = exists == '1' ? app.tcl_eval("wm state .help_window") : 'withdrawn'
|
|
1155
|
+
puts state != 'normal' ? "PASS" : "FAIL: help window visible in fullscreen"
|
|
1156
|
+
player.running = false
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
player.run
|
|
1160
|
+
RUBY
|
|
1161
|
+
|
|
1162
|
+
success, stdout, stderr, _status = tk_subprocess(code)
|
|
1163
|
+
|
|
1164
|
+
output = []
|
|
1165
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
1166
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
1167
|
+
|
|
1168
|
+
assert success, "? hotkey should be suppressed in fullscreen\n#{output.join("\n")}"
|
|
1169
|
+
assert_includes stdout, "PASS", "Expected PASS in output\n#{output.join("\n")}"
|
|
1170
|
+
end
|
|
903
1171
|
end
|