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
|
@@ -49,6 +49,52 @@ module TeekTestHelper
|
|
|
49
49
|
# Default timeout for subprocess tests (can be overridden via TK_TEST_TIMEOUT env var)
|
|
50
50
|
DEFAULT_SUBPROCESS_TIMEOUT = 5
|
|
51
51
|
|
|
52
|
+
# Tcl bgerror capture preamble — prepended to subprocess code so that
|
|
53
|
+
# any unhandled Tcl background error is written to a log file instead
|
|
54
|
+
# of vanishing into the void. tk_subprocess reads the log after the
|
|
55
|
+
# subprocess exits and appends it to stderr.
|
|
56
|
+
BGERROR_PREAMBLE = <<~'RUBY'
|
|
57
|
+
require 'teek'
|
|
58
|
+
module BgerrorCapture
|
|
59
|
+
def initialize(*)
|
|
60
|
+
super
|
|
61
|
+
if (path = ENV['GEMBA_BGERROR_LOG'])
|
|
62
|
+
tcl_eval(%Q{
|
|
63
|
+
proc bgerror {msg} {
|
|
64
|
+
set fd [open {#{path}} a]
|
|
65
|
+
puts $fd "bgerror: $msg"
|
|
66
|
+
if {[info exists ::errorInfo]} {
|
|
67
|
+
puts $fd $::errorInfo
|
|
68
|
+
}
|
|
69
|
+
puts $fd "---"
|
|
70
|
+
close $fd
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Wrap after() to capture the full Ruby backtrace before Teek converts
|
|
77
|
+
# the exception to a Tcl error (at which point the backtrace is lost).
|
|
78
|
+
def after(ms, on_error: :raise, &block)
|
|
79
|
+
path = ENV['GEMBA_BGERROR_LOG']
|
|
80
|
+
return super unless path
|
|
81
|
+
super(ms, on_error: on_error) do
|
|
82
|
+
begin
|
|
83
|
+
block.call
|
|
84
|
+
rescue => e
|
|
85
|
+
File.open(path, 'a') do |f|
|
|
86
|
+
f.puts "Ruby exception in after(#{ms}ms): #{e.class}: #{e.message}"
|
|
87
|
+
f.puts e.backtrace.join("\n")
|
|
88
|
+
f.puts "---"
|
|
89
|
+
end
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
Teek::App.prepend(BgerrorCapture)
|
|
96
|
+
RUBY
|
|
97
|
+
|
|
52
98
|
def tk_subprocess(code, coverage: true, timeout: nil)
|
|
53
99
|
timeout ||= Integer(ENV['TK_TEST_TIMEOUT'] || DEFAULT_SUBPROCESS_TIMEOUT)
|
|
54
100
|
|
|
@@ -57,18 +103,31 @@ module TeekTestHelper
|
|
|
57
103
|
load_paths = $LOAD_PATH.select { |p| p.start_with?(project_root) }
|
|
58
104
|
load_path_args = load_paths.flat_map { |p| ["-I", p] }
|
|
59
105
|
|
|
60
|
-
#
|
|
61
|
-
|
|
106
|
+
# Temp file for capturing Tcl bgerror output
|
|
107
|
+
require 'tempfile'
|
|
108
|
+
bgerror_file = Tempfile.new(['bgerror', '.log'])
|
|
109
|
+
bgerror_path = bgerror_file.path
|
|
110
|
+
bgerror_file.close
|
|
111
|
+
|
|
112
|
+
# Prepend SimpleCov + bgerror capture
|
|
113
|
+
preamble = coverage ? "#{TeekTestHelper.simplecov_preamble}\n" : ""
|
|
114
|
+
full_code = "#{preamble}#{BGERROR_PREAMBLE}\n#{code}"
|
|
115
|
+
|
|
116
|
+
# Write code to temp file so backtraces show real line numbers
|
|
117
|
+
code_file = Tempfile.new(['tk_test', '.rb'])
|
|
118
|
+
code_file.write(full_code)
|
|
119
|
+
code_file.close
|
|
62
120
|
|
|
63
121
|
# Pass env vars to subprocess
|
|
64
122
|
env = {}
|
|
65
123
|
env['VISUAL'] = '1' if ENV['VISUAL']
|
|
66
124
|
env['COVERAGE'] = '1' if ENV['COVERAGE']
|
|
125
|
+
env['GEMBA_BGERROR_LOG'] = bgerror_path
|
|
67
126
|
|
|
68
127
|
# -rbundler/setup activates Bundler in the subprocess so path: gems
|
|
69
128
|
# (e.g. teek, teek-sdl2 from sibling repos) are on the load path.
|
|
70
129
|
stdin, stdout, stderr, wait_thr = Open3.popen3(
|
|
71
|
-
env, RbConfig.ruby, "-rbundler/setup", *load_path_args,
|
|
130
|
+
env, RbConfig.ruby, "-rbundler/setup", *load_path_args, code_file.path
|
|
72
131
|
)
|
|
73
132
|
stdin.close
|
|
74
133
|
|
|
@@ -84,6 +143,10 @@ module TeekTestHelper
|
|
|
84
143
|
end
|
|
85
144
|
end
|
|
86
145
|
|
|
146
|
+
# Read bgerror log before cleanup
|
|
147
|
+
bgerrors = File.read(bgerror_path).strip rescue ""
|
|
148
|
+
File.delete(bgerror_path) rescue nil
|
|
149
|
+
|
|
87
150
|
unless status
|
|
88
151
|
# Timed out - capture any output before killing so errors are visible
|
|
89
152
|
out = stdout.read_nonblock(64 * 1024) rescue ""
|
|
@@ -92,7 +155,10 @@ module TeekTestHelper
|
|
|
92
155
|
wait_thr.join
|
|
93
156
|
stdout.close
|
|
94
157
|
stderr.close
|
|
95
|
-
|
|
158
|
+
err = "Test timed out after #{timeout}s\n#{err}"
|
|
159
|
+
err = "#{err}\nTcl bgerror log:\n#{bgerrors}" unless bgerrors.empty?
|
|
160
|
+
code_file.unlink rescue nil
|
|
161
|
+
return [false, out, err, nil]
|
|
96
162
|
end
|
|
97
163
|
|
|
98
164
|
out = stdout.read
|
|
@@ -100,6 +166,10 @@ module TeekTestHelper
|
|
|
100
166
|
stdout.close
|
|
101
167
|
stderr.close
|
|
102
168
|
|
|
169
|
+
# Append bgerrors to stderr so they surface in test output
|
|
170
|
+
err = "#{err}\nTcl bgerror log:\n#{bgerrors}" unless bgerrors.empty?
|
|
171
|
+
code_file.unlink rescue nil
|
|
172
|
+
|
|
103
173
|
[status.success?, out, err, status]
|
|
104
174
|
end
|
|
105
175
|
|
|
@@ -283,6 +353,23 @@ module TeekTestHelper
|
|
|
283
353
|
# The test code has access to `app` (a Teek::App instance) and minitest assertions.
|
|
284
354
|
# Do NOT create your own TkRoot or call root.destroy - worker manages this.
|
|
285
355
|
#
|
|
356
|
+
# == event generate gotcha: mouse button events ==
|
|
357
|
+
#
|
|
358
|
+
# The TestWorker calls `app.hide` (wm withdraw .) after every test.
|
|
359
|
+
# `event generate <Button-N>` silently does nothing when the root window is
|
|
360
|
+
# withdrawn because widgets are not mapped/viewable without a visible ancestor.
|
|
361
|
+
# Always call `app.show` + `app.update` before generating mouse button events:
|
|
362
|
+
#
|
|
363
|
+
# picker.show
|
|
364
|
+
# app.show # ← required: deiconify root so widgets are viewable
|
|
365
|
+
# app.update # ← let Tk map all windows
|
|
366
|
+
# app.tcl_eval("event generate .widget <Button-3> -x 10 -y 10")
|
|
367
|
+
# app.update
|
|
368
|
+
#
|
|
369
|
+
# Key events additionally require focus:
|
|
370
|
+
# app.tcl_eval("focus -force .widget")
|
|
371
|
+
# app.update
|
|
372
|
+
#
|
|
286
373
|
# Example:
|
|
287
374
|
# def test_something
|
|
288
375
|
# assert_tk_app("should work") do
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minimal config stand-in for AchievementsWindow tests.
|
|
4
|
+
# The window only reads ra_unofficial? from config at build time.
|
|
5
|
+
FakeConfig = Struct.new(:ra_unofficial) {
|
|
6
|
+
def ra_unofficial? = ra_unofficial
|
|
7
|
+
} unless defined?(FakeConfig)
|
|
8
|
+
|
|
9
|
+
# Build a plain rom hash suitable for RomLibrary stubbing.
|
|
10
|
+
def make_rom_entry(id:, title:, platform: 'gba', game_code: 'AGB-TEST', md5: "#{id}abcd")
|
|
11
|
+
{ 'rom_id' => id, 'title' => title, 'platform' => platform,
|
|
12
|
+
'game_code' => game_code, 'path' => "/games/#{id}.gba", 'md5' => md5 }
|
|
13
|
+
end unless respond_to?(:make_rom_entry, true)
|
|
14
|
+
|
|
15
|
+
# Wrap an array of rom hashes in a minimal RomLibrary-compatible struct.
|
|
16
|
+
def make_rom_library(*roms)
|
|
17
|
+
Struct.new(:roms) { def all = roms }.new(roms)
|
|
18
|
+
end unless respond_to?(:make_rom_library, true)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Minimal Core stub — no mGBA dependency, just a programmable memory map.
|
|
4
|
+
# Used by achievement backend tests to simulate bus reads without a real ROM.
|
|
5
|
+
class FakeCore
|
|
6
|
+
def initialize
|
|
7
|
+
@mem = Hash.new(0)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Write a byte into the fake memory map.
|
|
11
|
+
# @param address [Integer] GBA address
|
|
12
|
+
# @param value [Integer] 0..255
|
|
13
|
+
def poke(address, value)
|
|
14
|
+
@mem[address] = value & 0xFF
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Reads back what was poked (or 0 for anything not explicitly written).
|
|
18
|
+
def bus_read8(address)
|
|
19
|
+
@mem[address]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroyed?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Test double for Gemba::RARuntime (the C extension).
|
|
4
|
+
#
|
|
5
|
+
# Mirrors the full RARuntime interface so RetroAchievements::Backend can be
|
|
6
|
+
# tested without a real rcheevos instance or ROM memory.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rt = FakeRARuntime.new
|
|
10
|
+
# rt.queue_triggers("101", "102") # do_frame will return these once
|
|
11
|
+
# rt.rp_message = "Playing Stage 1" # get_richpresence returns this
|
|
12
|
+
# rt.rp_activate_result = false # activate_richpresence returns false
|
|
13
|
+
class FakeRARuntime
|
|
14
|
+
attr_reader :activated, :deactivated, :cleared, :reset_count
|
|
15
|
+
attr_reader :rp_script
|
|
16
|
+
attr_accessor :rp_message, :rp_activate_result
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@activated = {} # id => memaddr
|
|
20
|
+
@deactivated = []
|
|
21
|
+
@trigger_queue = [] # Array<Array<String>> — one entry consumed per do_frame
|
|
22
|
+
@cleared = false
|
|
23
|
+
@reset_count = 0
|
|
24
|
+
@rp_script = nil
|
|
25
|
+
@rp_message = nil
|
|
26
|
+
@rp_activate_result = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Queue one frame's worth of triggered achievement IDs.
|
|
30
|
+
# Each call adds one "frame": the next do_frame call pops and returns it.
|
|
31
|
+
def queue_triggers(*ids)
|
|
32
|
+
@trigger_queue << ids.flatten.map(&:to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# -- RARuntime interface ----------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def activate(id, memaddr)
|
|
38
|
+
@activated[id.to_s] = memaddr.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deactivate(id)
|
|
42
|
+
@deactivated << id.to_s
|
|
43
|
+
@activated.delete(id.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset_all
|
|
47
|
+
@reset_count += 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear
|
|
51
|
+
@activated.clear
|
|
52
|
+
@deactivated.clear
|
|
53
|
+
@trigger_queue.clear
|
|
54
|
+
@cleared = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the next queued batch of triggered IDs, or [] if nothing queued.
|
|
58
|
+
def do_frame(_core)
|
|
59
|
+
@trigger_queue.shift || []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def count
|
|
63
|
+
@activated.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def activate_richpresence(script)
|
|
67
|
+
@rp_script = script
|
|
68
|
+
@rp_activate_result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def get_richpresence(_core)
|
|
72
|
+
@rp_message
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Synchronous test double for the RetroAchievements HTTP transport.
|
|
4
|
+
#
|
|
5
|
+
# Designed to match Teek::BackgroundWork's fluent interface so it can be
|
|
6
|
+
# injected without any branching in ra_request — the same code path runs
|
|
7
|
+
# in production and tests.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# req = FakeRequester.new
|
|
11
|
+
# req.stub(r: "login2", body: { "Success" => true, "Token" => "tok" })
|
|
12
|
+
# req.stub(r: "gameid", body: { "GameID" => 42 })
|
|
13
|
+
#
|
|
14
|
+
# backend = Backend.new(app: nil, runtime: FakeRARuntime.new, requester: req)
|
|
15
|
+
# backend.login_with_token(username: "u", token: "t")
|
|
16
|
+
# assert backend.authenticated?
|
|
17
|
+
# assert req.requested?("login2")
|
|
18
|
+
class FakeRequester
|
|
19
|
+
# Returned by call() — mirrors BackgroundWork's fluent on_progress/on_done chain.
|
|
20
|
+
# on_progress fires synchronously with the canned result so callers behave
|
|
21
|
+
# identically to the async production path without needing an event loop.
|
|
22
|
+
class Result
|
|
23
|
+
def initialize(value)
|
|
24
|
+
@value = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_progress(&block)
|
|
28
|
+
block.call(@value)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_done(&block)
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :requests
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
@stubs = {} # r_string => [json_or_nil, ok_bool]
|
|
41
|
+
@requests = [] # all params hashes, in call order
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Register a canned response for a given r= value.
|
|
45
|
+
def stub(r:, body: nil, ok: true)
|
|
46
|
+
@stubs[r.to_s] = [body, ok]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Called by ra_request with the same signature as Teek::BackgroundWork.new.
|
|
50
|
+
# Ignores the block (which contains real Net::HTTP code) and returns a
|
|
51
|
+
# Result that fires on_progress synchronously with the canned response.
|
|
52
|
+
def call(_app, params, mode: nil, **_opts, &_block)
|
|
53
|
+
@requests << params.dup
|
|
54
|
+
r = (params[:r] || params["r"]).to_s
|
|
55
|
+
result = @stubs.fetch(r, [nil, false])
|
|
56
|
+
Result.new(result)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# True if at least one request with the given r= value was made.
|
|
60
|
+
def requested?(r)
|
|
61
|
+
@requests.any? { |p| (p[:r] || p["r"]).to_s == r.to_s }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# All params hashes for requests with the given r= value.
|
|
65
|
+
def requests_for(r)
|
|
66
|
+
@requests.select { |p| (p[:r] || p["r"]).to_s == r.to_s }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -36,7 +36,7 @@ end
|
|
|
36
36
|
# Falls back to xdotool to force focus if polling alone doesn't work.
|
|
37
37
|
def poll_until_focused(player, timeout_ms: 2_000, &block)
|
|
38
38
|
app = player.app
|
|
39
|
-
renderer = player.viewport.renderer
|
|
39
|
+
renderer = player.frame.viewport.renderer
|
|
40
40
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_ms / 1000.0
|
|
41
41
|
tried_xdotool = false
|
|
42
42
|
check = proc do
|
|
@@ -60,14 +60,21 @@ def poll_until_focused(player, timeout_ms: 2_000, &block)
|
|
|
60
60
|
app.after(50, &check)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
# Polls every 50ms until the given condition block returns truthy, then
|
|
64
|
+
# yields the action block. Aborts with exit 1 if the deadline is exceeded.
|
|
65
|
+
#
|
|
66
|
+
# Example — wait for a menu entry to become enabled:
|
|
67
|
+
# poll_until(app, timeout_ms: 3_000,
|
|
68
|
+
# condition: -> { app.tcl_eval('...entrycget 0 -state').strip == 'normal' }) do
|
|
69
|
+
# app.command(...)
|
|
70
|
+
# end
|
|
71
|
+
def poll_until(app, timeout_ms: 5_000, condition:, label: "condition", &block)
|
|
65
72
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_ms / 1000.0
|
|
66
73
|
check = proc do
|
|
67
|
-
if
|
|
74
|
+
if condition.call
|
|
68
75
|
block.call
|
|
69
76
|
elsif Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
70
|
-
$stderr.puts "FAIL:
|
|
77
|
+
$stderr.puts "FAIL: #{label} not true within #{timeout_ms}ms"
|
|
71
78
|
exit 1
|
|
72
79
|
else
|
|
73
80
|
app.after(50, &check)
|
|
@@ -75,3 +82,11 @@ def poll_until_ready(player, timeout_ms: 5_000, &block)
|
|
|
75
82
|
end
|
|
76
83
|
app.after(50, &check)
|
|
77
84
|
end
|
|
85
|
+
|
|
86
|
+
def poll_until_ready(player, timeout_ms: 5_000, &block)
|
|
87
|
+
poll_until(player.app, timeout_ms: timeout_ms,
|
|
88
|
+
condition: -> { player.ready? },
|
|
89
|
+
label: "Player not ready") do
|
|
90
|
+
block.call
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba/headless"
|
|
5
|
+
require "gemba/achievements"
|
|
6
|
+
|
|
7
|
+
class TestAchievement < Minitest::Test
|
|
8
|
+
def test_unearned_by_default
|
|
9
|
+
ach = Gemba::Achievements::Achievement.new(
|
|
10
|
+
id: 'x', title: 'T', description: 'D', points: 10, earned_at: nil
|
|
11
|
+
)
|
|
12
|
+
refute ach.earned?
|
|
13
|
+
assert_nil ach.earned_at
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_earn_returns_copy_with_timestamp
|
|
17
|
+
ach = Gemba::Achievements::Achievement.new(
|
|
18
|
+
id: 'x', title: 'T', description: 'D', points: 10, earned_at: nil
|
|
19
|
+
)
|
|
20
|
+
earned = ach.earn
|
|
21
|
+
assert earned.earned?
|
|
22
|
+
assert_instance_of Time, earned.earned_at
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_earn_does_not_mutate_original
|
|
26
|
+
ach = Gemba::Achievements::Achievement.new(
|
|
27
|
+
id: 'x', title: 'T', description: 'D', points: 10, earned_at: nil
|
|
28
|
+
)
|
|
29
|
+
ach.earn
|
|
30
|
+
refute ach.earned?
|
|
31
|
+
end
|
|
32
|
+
end
|