gemba 0.1.0 → 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 +24 -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 +135 -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 -1515
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba/headless"
|
|
5
|
+
require_relative "support/fake_ra_runtime"
|
|
6
|
+
require_relative "support/fake_requester"
|
|
7
|
+
require_relative "support/fake_core"
|
|
8
|
+
|
|
9
|
+
PATCH_RESPONSE = {
|
|
10
|
+
"PatchData" => {
|
|
11
|
+
"RichPresencePatch" => "",
|
|
12
|
+
"Achievements" => [
|
|
13
|
+
{ "ID" => 101, "Title" => "First Blood", "Description" => "Get a kill",
|
|
14
|
+
"Points" => 5, "MemAddr" => "0=1", "Flags" => 3 },
|
|
15
|
+
{ "ID" => 102, "Title" => "Survivor", "Description" => "Survive 60s",
|
|
16
|
+
"Points" => 10, "MemAddr" => "1=1", "Flags" => 3 },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Tests for Gemba::Achievements::RetroAchievements::Backend.
|
|
22
|
+
#
|
|
23
|
+
# FakeRequester replaces BackgroundWork so all HTTP callbacks fire synchronously
|
|
24
|
+
# in-process — no Tk event loop, no subprocesses, no wait_until.
|
|
25
|
+
class TestRABackend < Minitest::Test
|
|
26
|
+
Backend = Gemba::Achievements::RetroAchievements::Backend
|
|
27
|
+
|
|
28
|
+
def setup
|
|
29
|
+
@rt = FakeRARuntime.new
|
|
30
|
+
@req = FakeRequester.new
|
|
31
|
+
@b = Backend.new(app: nil, runtime: @rt, requester: @req)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Authenticate @b via the real login_with_token path.
|
|
35
|
+
def login(username: "user", token: "tok")
|
|
36
|
+
@req.stub(r: "login2", body: { "Success" => true })
|
|
37
|
+
@b.login_with_token(username: username, token: token)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Drive the full gameid→patch→unlocks chain.
|
|
41
|
+
def load_game(earned_ids: [], patch: PATCH_RESPONSE)
|
|
42
|
+
@req.stub(r: "gameid", body: { "GameID" => 42 })
|
|
43
|
+
@req.stub(r: "patch", body: patch)
|
|
44
|
+
@req.stub(r: "unlocks", body: { "Success" => true, "UserUnlocks" => earned_ids })
|
|
45
|
+
Dir.mktmpdir do |dir|
|
|
46
|
+
rom = File.join(dir, "test.gba")
|
|
47
|
+
File.write(rom, "FAKEGBAROM")
|
|
48
|
+
@b.load_game(nil, rom, "deadbeef" * 4)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Initial state
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def test_not_authenticated_by_default
|
|
57
|
+
refute @b.authenticated?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_enabled
|
|
61
|
+
assert @b.enabled?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_achievement_list_empty_before_game_load
|
|
65
|
+
assert_empty @b.achievement_list
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_rich_presence_message_nil_initially
|
|
69
|
+
assert_nil @b.rich_presence_message
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Authentication
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def test_login_with_password_success
|
|
77
|
+
@req.stub(r: "login2", body: { "Success" => true, "Token" => "tok123" })
|
|
78
|
+
result = nil
|
|
79
|
+
@b.on_auth_change { |status, payload| result = [status, payload] }
|
|
80
|
+
@b.login_with_password(username: "user", password: "hunter2")
|
|
81
|
+
|
|
82
|
+
assert_equal :ok, result[0]
|
|
83
|
+
assert_equal "tok123", result[1]
|
|
84
|
+
assert @b.authenticated?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_login_with_password_failure
|
|
88
|
+
@req.stub(r: "login2", body: { "Success" => false, "Error" => "Invalid credentials" })
|
|
89
|
+
result = nil
|
|
90
|
+
@b.on_auth_change { |status, msg| result = [status, msg] }
|
|
91
|
+
@b.login_with_password(username: "user", password: "wrong")
|
|
92
|
+
|
|
93
|
+
assert_equal :error, result[0]
|
|
94
|
+
assert_match(/invalid credentials/i, result[1])
|
|
95
|
+
refute @b.authenticated?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_login_with_token_success
|
|
99
|
+
result = nil
|
|
100
|
+
@b.on_auth_change { |status, _| result = status }
|
|
101
|
+
login
|
|
102
|
+
assert_equal :ok, result
|
|
103
|
+
assert @b.authenticated?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_login_with_token_failure
|
|
107
|
+
@req.stub(r: "login2", body: { "Success" => false, "Error" => "Token invalid" })
|
|
108
|
+
result = nil
|
|
109
|
+
@b.on_auth_change { |status, msg| result = [status, msg] }
|
|
110
|
+
@b.login_with_token(username: "user", token: "bad")
|
|
111
|
+
|
|
112
|
+
assert_equal :error, result[0]
|
|
113
|
+
refute @b.authenticated?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_token_test_success
|
|
117
|
+
login
|
|
118
|
+
result = nil
|
|
119
|
+
@b.on_auth_change { |status, _| result = status }
|
|
120
|
+
@b.token_test
|
|
121
|
+
assert_equal :ok, result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_token_test_failure
|
|
125
|
+
login
|
|
126
|
+
@req.stub(r: "login2", body: { "Success" => false, "Error" => "Token invalid" })
|
|
127
|
+
result = nil
|
|
128
|
+
@b.on_auth_change { |status, _| result = status }
|
|
129
|
+
@b.token_test
|
|
130
|
+
assert_equal :error, result
|
|
131
|
+
refute @b.authenticated?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_logout_clears_auth_state
|
|
135
|
+
login
|
|
136
|
+
@b.logout
|
|
137
|
+
refute @b.authenticated?
|
|
138
|
+
assert_empty @b.achievement_list
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Game load chain
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def test_load_game_skipped_when_not_authenticated
|
|
146
|
+
load_game
|
|
147
|
+
assert_empty @b.achievement_list
|
|
148
|
+
refute @req.requested?("gameid"), "should not hit network when unauthenticated"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_load_game_populates_achievement_list
|
|
152
|
+
login
|
|
153
|
+
load_game
|
|
154
|
+
|
|
155
|
+
assert_equal 2, @b.total_count
|
|
156
|
+
assert_equal "101", @b.achievement_list[0].id
|
|
157
|
+
assert_equal "102", @b.achievement_list[1].id
|
|
158
|
+
assert_equal 2, @rt.count
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_load_game_marks_preearned_achievements
|
|
162
|
+
login
|
|
163
|
+
load_game(earned_ids: [101])
|
|
164
|
+
|
|
165
|
+
list = @b.achievement_list
|
|
166
|
+
assert list.find { |a| a.id == "101" }&.earned?, "101 should be earned"
|
|
167
|
+
refute list.find { |a| a.id == "102" }&.earned?, "102 should not be earned"
|
|
168
|
+
assert_includes @rt.deactivated, "101"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_load_game_aborts_when_game_id_zero
|
|
172
|
+
login
|
|
173
|
+
@req.stub(r: "gameid", body: { "GameID" => 0 })
|
|
174
|
+
Dir.mktmpdir do |dir|
|
|
175
|
+
rom = File.join(dir, "test.gba")
|
|
176
|
+
File.write(rom, "FAKE")
|
|
177
|
+
@b.load_game(nil, rom, "deadbeef" * 4)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
assert_empty @b.achievement_list
|
|
181
|
+
refute @req.requested?("patch"), "patch must not be requested when GameID is 0"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test_load_game_activates_rich_presence_script
|
|
185
|
+
rp_patch = PATCH_RESPONSE.merge(
|
|
186
|
+
"PatchData" => PATCH_RESPONSE["PatchData"].merge("RichPresencePatch" => "Display: Hello")
|
|
187
|
+
)
|
|
188
|
+
login
|
|
189
|
+
load_game(patch: rp_patch)
|
|
190
|
+
|
|
191
|
+
assert_equal "Display: Hello", @rt.rp_script
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_unload_game_clears_achievement_list
|
|
195
|
+
login
|
|
196
|
+
load_game
|
|
197
|
+
@b.unload_game
|
|
198
|
+
assert_empty @b.achievement_list
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_sync_unlocks_repopulates_list
|
|
202
|
+
login
|
|
203
|
+
load_game
|
|
204
|
+
|
|
205
|
+
# Re-stub for the sync re-fetch
|
|
206
|
+
@req.stub(r: "patch", body: PATCH_RESPONSE)
|
|
207
|
+
@req.stub(r: "unlocks", body: { "Success" => true, "UserUnlocks" => [102] })
|
|
208
|
+
@b.sync_unlocks
|
|
209
|
+
|
|
210
|
+
list = @b.achievement_list
|
|
211
|
+
refute list.find { |a| a.id == "101" }&.earned?
|
|
212
|
+
assert list.find { |a| a.id == "102" }&.earned?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# do_frame
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def test_do_frame_silent_before_game_loaded
|
|
220
|
+
unlocked = []
|
|
221
|
+
@b.on_unlock { |a| unlocked << a }
|
|
222
|
+
@b.do_frame(FakeCore.new)
|
|
223
|
+
assert_empty unlocked
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def test_do_frame_fires_unlock_and_submits_to_server
|
|
227
|
+
login
|
|
228
|
+
load_game
|
|
229
|
+
|
|
230
|
+
@req.stub(r: "awardachievement", body: { "Success" => true })
|
|
231
|
+
@rt.queue_triggers("101")
|
|
232
|
+
|
|
233
|
+
unlocked = []
|
|
234
|
+
@b.on_unlock { |a| unlocked << a }
|
|
235
|
+
@b.do_frame(FakeCore.new)
|
|
236
|
+
|
|
237
|
+
assert_equal 1, unlocked.size
|
|
238
|
+
assert_equal "101", unlocked.first.id
|
|
239
|
+
assert @req.requested?("awardachievement")
|
|
240
|
+
assert_equal "101", @req.requests_for("awardachievement").first[:a].to_s
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_do_frame_skips_already_earned_achievement
|
|
244
|
+
login
|
|
245
|
+
load_game(earned_ids: [101])
|
|
246
|
+
|
|
247
|
+
@rt.queue_triggers("101")
|
|
248
|
+
unlocked = []
|
|
249
|
+
@b.on_unlock { |a| unlocked << a }
|
|
250
|
+
@b.do_frame(FakeCore.new)
|
|
251
|
+
|
|
252
|
+
assert_empty unlocked, "already-earned achievement must not fire again"
|
|
253
|
+
refute @req.requested?("awardachievement")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_do_frame_rich_presence_fires_callback_when_enabled
|
|
257
|
+
login
|
|
258
|
+
load_game
|
|
259
|
+
|
|
260
|
+
@rt.rp_message = "Playing Stage 1"
|
|
261
|
+
@b.rich_presence_enabled = true
|
|
262
|
+
@b.instance_variable_set(:@rp_eval_frame, 239)
|
|
263
|
+
|
|
264
|
+
fired = []
|
|
265
|
+
@b.on_rich_presence_changed { |m| fired << m }
|
|
266
|
+
@b.do_frame(FakeCore.new)
|
|
267
|
+
|
|
268
|
+
assert_equal ["Playing Stage 1"], fired
|
|
269
|
+
assert_equal "Playing Stage 1", @b.rich_presence_message
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_do_frame_rich_presence_silent_when_disabled
|
|
273
|
+
login
|
|
274
|
+
load_game
|
|
275
|
+
|
|
276
|
+
@rt.rp_message = "Playing Stage 1"
|
|
277
|
+
@b.rich_presence_enabled = false
|
|
278
|
+
@b.instance_variable_set(:@rp_eval_frame, 239)
|
|
279
|
+
|
|
280
|
+
fired = []
|
|
281
|
+
@b.on_rich_presence_changed { |m| fired << m }
|
|
282
|
+
@b.do_frame(FakeCore.new)
|
|
283
|
+
|
|
284
|
+
assert_empty fired
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# FakeRARuntime self-tests
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
def test_fake_runtime_activate_deactivate
|
|
292
|
+
@rt.activate("101", "0=1")
|
|
293
|
+
@rt.activate("102", "1=1")
|
|
294
|
+
assert_equal 2, @rt.count
|
|
295
|
+
@rt.deactivate("101")
|
|
296
|
+
assert_equal 1, @rt.count
|
|
297
|
+
refute @rt.activated.key?("101")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def test_fake_runtime_clear_resets_state
|
|
301
|
+
@rt.activate("101", "0=1")
|
|
302
|
+
@rt.queue_triggers("101")
|
|
303
|
+
@rt.clear
|
|
304
|
+
assert_equal 0, @rt.count
|
|
305
|
+
assert_empty @rt.do_frame(nil)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def test_fake_runtime_queue_consumed_once
|
|
309
|
+
@rt.queue_triggers("101")
|
|
310
|
+
assert_equal ["101"], @rt.do_frame(nil)
|
|
311
|
+
assert_empty @rt.do_frame(nil)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba/headless"
|
|
5
|
+
require "gemba/achievements"
|
|
6
|
+
require_relative "support/fake_core"
|
|
7
|
+
|
|
8
|
+
# Tests that RetroAchievements::Backend never awards achievements before the
|
|
9
|
+
# server's earned list is known.
|
|
10
|
+
#
|
|
11
|
+
# The bug scenario:
|
|
12
|
+
# fetch_patch_data completes → runtime activated with 89 achievements
|
|
13
|
+
# emulator starts → do_frame fires → some conditions true at frame 0
|
|
14
|
+
# @earned is empty (r=unlocks still in flight) → all are re-awarded
|
|
15
|
+
#
|
|
16
|
+
# The fix: @achievements stays empty until fetch_unlocks completes.
|
|
17
|
+
# do_frame's `return if @achievements.empty?` guards the window by construction.
|
|
18
|
+
#
|
|
19
|
+
# These tests verify the fix through the public API (no instance_variable_set):
|
|
20
|
+
# if @achievements is empty, do_frame must be silent regardless of what the
|
|
21
|
+
# underlying runtime would report.
|
|
22
|
+
class TestRABackendUnlockGate < Minitest::Test
|
|
23
|
+
def setup
|
|
24
|
+
@backend = Gemba::Achievements::RetroAchievements::Backend.new(app: nil)
|
|
25
|
+
@unlocked = []
|
|
26
|
+
@backend.on_unlock { |ach| @unlocked << ach }
|
|
27
|
+
@core = FakeCore.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Before any game is loaded, @achievements is empty → do_frame is a no-op.
|
|
31
|
+
def test_do_frame_silent_before_game_loaded
|
|
32
|
+
@backend.do_frame(@core)
|
|
33
|
+
assert_empty @unlocked
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @achievements only becomes non-empty after fetch_unlocks completes (HTTP).
|
|
37
|
+
# Since we can't make real HTTP calls, verify the state via achievement_list
|
|
38
|
+
# and total_count — they reflect @achievements.
|
|
39
|
+
def test_achievements_empty_until_unlocks_arrive
|
|
40
|
+
assert_equal 0, @backend.total_count,
|
|
41
|
+
"achievement list must be empty before fetch_unlocks completes"
|
|
42
|
+
assert_empty @backend.achievement_list
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# do_frame with an empty achievement list never fires unlock callbacks.
|
|
46
|
+
# This is the structural guarantee — as long as @achievements is empty,
|
|
47
|
+
# no award can happen even if the C runtime were somehow active.
|
|
48
|
+
def test_do_frame_never_fires_when_achievement_list_empty
|
|
49
|
+
# Simulate being "mid-load": patch data fetched but unlocks not yet back.
|
|
50
|
+
# In the new design @achievements stays [] during this window.
|
|
51
|
+
5.times { @backend.do_frame(@core) }
|
|
52
|
+
assert_empty @unlocked,
|
|
53
|
+
"no unlock must fire during the patch→unlocks window"
|
|
54
|
+
assert_equal 0, @backend.earned_count
|
|
55
|
+
end
|
|
56
|
+
end
|
data/test/test_recorder.rb
CHANGED
|
@@ -7,9 +7,6 @@ require "tmpdir"
|
|
|
7
7
|
class TestRecorder < Minitest::Test
|
|
8
8
|
TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
|
|
9
9
|
|
|
10
|
-
def setup
|
|
11
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
12
|
-
end
|
|
13
10
|
|
|
14
11
|
def test_record_and_decode_round_trip
|
|
15
12
|
skip "ffmpeg not installed" unless ffmpeg_available?
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "shared/tk_test_helper"
|
|
5
|
+
|
|
6
|
+
class TestReplayPlayer < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
PONG_ROM = File.expand_path("fixtures/pong.gba", __dir__)
|
|
10
|
+
|
|
11
|
+
# Generate a short .gir fixture for all GUI tests.
|
|
12
|
+
# Uses HeadlessPlayer + InputRecorder to record 60 frames of pong.
|
|
13
|
+
def self.gir_fixture_dir
|
|
14
|
+
@gir_fixture_dir ||= begin
|
|
15
|
+
require "tmpdir"
|
|
16
|
+
dir = Dir.mktmpdir("gemba_replay_test")
|
|
17
|
+
at_exit { FileUtils.rm_rf(dir) }
|
|
18
|
+
|
|
19
|
+
require "gemba/headless"
|
|
20
|
+
require "gemba/headless"
|
|
21
|
+
|
|
22
|
+
gir_path = File.join(dir, "pong_test.gir")
|
|
23
|
+
Gemba::HeadlessPlayer.open(PONG_ROM) do |player|
|
|
24
|
+
player.step(10)
|
|
25
|
+
core = player.core
|
|
26
|
+
rec = Gemba::InputRecorder.new(gir_path, core: core, rom_path: PONG_ROM)
|
|
27
|
+
rec.start
|
|
28
|
+
60.times do |i|
|
|
29
|
+
mask = i < 30 ? Gemba::KEY_START : 0
|
|
30
|
+
rec.capture(mask)
|
|
31
|
+
core.set_keys(mask)
|
|
32
|
+
core.run_frame
|
|
33
|
+
end
|
|
34
|
+
rec.stop
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
dir
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gir_path
|
|
42
|
+
File.join(self.class.gir_fixture_dir, "pong_test.gir")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ReplayPlayer opens a window, plays frames, then exits cleanly.
|
|
46
|
+
def test_replay_exits_cleanly
|
|
47
|
+
code = <<~RUBY
|
|
48
|
+
require "gemba"
|
|
49
|
+
require "support/player_helpers"
|
|
50
|
+
|
|
51
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
52
|
+
app = rp.app
|
|
53
|
+
|
|
54
|
+
poll_until_ready(rp) { rp.running = false }
|
|
55
|
+
|
|
56
|
+
rp.run
|
|
57
|
+
RUBY
|
|
58
|
+
|
|
59
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
60
|
+
|
|
61
|
+
output = []
|
|
62
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
63
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
64
|
+
|
|
65
|
+
assert success, "ReplayPlayer should exit cleanly\n#{output.join("\n")}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# After replay ends (60 frames), player should pause on last frame.
|
|
69
|
+
def test_replay_pauses_on_end
|
|
70
|
+
code = <<~RUBY
|
|
71
|
+
require "gemba"
|
|
72
|
+
require "support/player_helpers"
|
|
73
|
+
|
|
74
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
75
|
+
app = rp.app
|
|
76
|
+
|
|
77
|
+
poll_until_ready(rp) do
|
|
78
|
+
check = proc do
|
|
79
|
+
if rp.replay_ended?
|
|
80
|
+
unless rp.paused?
|
|
81
|
+
$stderr.puts "FAIL: replay ended but not paused"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
rp.running = false
|
|
85
|
+
else
|
|
86
|
+
app.after(100, &check)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
app.after(100, &check)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
rp.run
|
|
93
|
+
RUBY
|
|
94
|
+
|
|
95
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
96
|
+
|
|
97
|
+
output = []
|
|
98
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
99
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
100
|
+
|
|
101
|
+
assert success, "ReplayPlayer should pause on replay end\n#{output.join("\n")}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fullscreen toggle (F11 twice) should not hang.
|
|
105
|
+
def test_fullscreen_toggle
|
|
106
|
+
code = <<~RUBY
|
|
107
|
+
require "gemba"
|
|
108
|
+
require "support/player_helpers"
|
|
109
|
+
|
|
110
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
111
|
+
app = rp.app
|
|
112
|
+
|
|
113
|
+
poll_until_ready(rp) do
|
|
114
|
+
vp = rp.viewport
|
|
115
|
+
frame = vp.frame.path
|
|
116
|
+
|
|
117
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
118
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F11')
|
|
119
|
+
app.update
|
|
120
|
+
|
|
121
|
+
app.after(50) do
|
|
122
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'F11')
|
|
123
|
+
app.update
|
|
124
|
+
app.after(50) { rp.running = false }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
rp.run
|
|
129
|
+
RUBY
|
|
130
|
+
|
|
131
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
132
|
+
|
|
133
|
+
output = []
|
|
134
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
135
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
136
|
+
|
|
137
|
+
assert success, "ReplayPlayer fullscreen toggle should not hang\n#{output.join("\n")}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Fast-forward toggle (Tab) should not hang.
|
|
141
|
+
def test_fast_forward_toggle
|
|
142
|
+
code = <<~RUBY
|
|
143
|
+
require "gemba"
|
|
144
|
+
require "support/player_helpers"
|
|
145
|
+
|
|
146
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
147
|
+
app = rp.app
|
|
148
|
+
|
|
149
|
+
poll_until_ready(rp) do
|
|
150
|
+
vp = rp.viewport
|
|
151
|
+
frame = vp.frame.path
|
|
152
|
+
|
|
153
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
154
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Tab')
|
|
155
|
+
app.update
|
|
156
|
+
|
|
157
|
+
app.after(200) do
|
|
158
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
159
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Tab')
|
|
160
|
+
app.update
|
|
161
|
+
app.after(50) { rp.running = false }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
rp.run
|
|
166
|
+
RUBY
|
|
167
|
+
|
|
168
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
169
|
+
|
|
170
|
+
output = []
|
|
171
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
172
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
173
|
+
|
|
174
|
+
assert success, "ReplayPlayer fast-forward toggle should not hang\n#{output.join("\n")}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Pause via public method, verify predicate.
|
|
178
|
+
def test_pause_via_method
|
|
179
|
+
code = <<~RUBY
|
|
180
|
+
require "gemba"
|
|
181
|
+
require "support/player_helpers"
|
|
182
|
+
|
|
183
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
184
|
+
app = rp.app
|
|
185
|
+
|
|
186
|
+
poll_until_ready(rp) do
|
|
187
|
+
app.after(100) do
|
|
188
|
+
rp.pause
|
|
189
|
+
unless rp.paused?
|
|
190
|
+
$stderr.puts "FAIL: pause method should set paused?"
|
|
191
|
+
exit 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
rp.resume
|
|
195
|
+
if rp.paused?
|
|
196
|
+
$stderr.puts "FAIL: resume should clear paused?"
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
rp.running = false
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
rp.run
|
|
205
|
+
RUBY
|
|
206
|
+
|
|
207
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
208
|
+
|
|
209
|
+
output = []
|
|
210
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
211
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
212
|
+
|
|
213
|
+
assert success, "ReplayPlayer pause/resume methods should work\n#{output.join("\n")}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Pressing P should toggle pause via hotkey.
|
|
217
|
+
def test_pause_hotkey
|
|
218
|
+
code = <<~RUBY
|
|
219
|
+
require "gemba"
|
|
220
|
+
require "support/player_helpers"
|
|
221
|
+
|
|
222
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
223
|
+
app = rp.app
|
|
224
|
+
|
|
225
|
+
poll_until_ready(rp) do
|
|
226
|
+
vp = rp.viewport
|
|
227
|
+
frame = vp.frame.path
|
|
228
|
+
|
|
229
|
+
app.after(200) do
|
|
230
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
231
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'p')
|
|
232
|
+
app.update
|
|
233
|
+
|
|
234
|
+
app.after(200) do
|
|
235
|
+
unless rp.paused?
|
|
236
|
+
$stderr.puts "FAIL: P should pause"
|
|
237
|
+
exit 1
|
|
238
|
+
end
|
|
239
|
+
rp.running = false
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
rp.run
|
|
245
|
+
RUBY
|
|
246
|
+
|
|
247
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
248
|
+
|
|
249
|
+
output = []
|
|
250
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
251
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
252
|
+
|
|
253
|
+
assert success, "ReplayPlayer P hotkey should toggle pause\n#{output.join("\n")}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Escape should exit (when not fullscreen).
|
|
257
|
+
def test_escape_exits
|
|
258
|
+
code = <<~RUBY
|
|
259
|
+
require "gemba"
|
|
260
|
+
require "support/player_helpers"
|
|
261
|
+
|
|
262
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
263
|
+
app = rp.app
|
|
264
|
+
|
|
265
|
+
poll_until_ready(rp) do
|
|
266
|
+
vp = rp.viewport
|
|
267
|
+
frame = vp.frame.path
|
|
268
|
+
|
|
269
|
+
app.tcl_eval("focus -force \#{frame}")
|
|
270
|
+
app.command(:event, 'generate', frame, '<KeyPress>', keysym: 'Escape')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
rp.run
|
|
274
|
+
RUBY
|
|
275
|
+
|
|
276
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
277
|
+
|
|
278
|
+
output = []
|
|
279
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
280
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
281
|
+
|
|
282
|
+
assert success, "ReplayPlayer Escape should exit\n#{output.join("\n")}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# frame_index should advance during replay.
|
|
286
|
+
def test_frame_index_advances
|
|
287
|
+
code = <<~RUBY
|
|
288
|
+
require "gemba"
|
|
289
|
+
require "support/player_helpers"
|
|
290
|
+
|
|
291
|
+
rp = Gemba::ReplayPlayer.new("#{gir_path}")
|
|
292
|
+
app = rp.app
|
|
293
|
+
|
|
294
|
+
poll_until_ready(rp) do
|
|
295
|
+
app.after(500) do
|
|
296
|
+
idx = rp.frame_index
|
|
297
|
+
if idx <= 0
|
|
298
|
+
$stderr.puts "FAIL: frame_index should advance, got \#{idx}"
|
|
299
|
+
exit 1
|
|
300
|
+
end
|
|
301
|
+
rp.running = false
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
rp.run
|
|
306
|
+
RUBY
|
|
307
|
+
|
|
308
|
+
success, stdout, stderr, _status = tk_subprocess(code, timeout: 10)
|
|
309
|
+
|
|
310
|
+
output = []
|
|
311
|
+
output << "STDOUT:\n#{stdout}" unless stdout.empty?
|
|
312
|
+
output << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
313
|
+
|
|
314
|
+
assert success, "ReplayPlayer frame_index should advance\n#{output.join("\n")}"
|
|
315
|
+
end
|
|
316
|
+
end
|