gemba 0.1.1 → 0.2.1
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 +190 -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 +511 -0
- data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
- data/lib/gemba/achievements/retro_achievements/ping_worker.rb +28 -0
- data/lib/gemba/achievements/retro_achievements/unlock_retry_worker.rb +35 -0
- data/lib/gemba/achievements.rb +19 -0
- data/lib/gemba/achievements_window.rb +556 -0
- data/lib/gemba/app_controller.rb +1036 -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 +154 -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 +1084 -0
- data/lib/gemba/event_bus.rb +48 -0
- data/lib/gemba/frame_stack.rb +70 -0
- data/lib/gemba/game_index.rb +84 -0
- data/lib/gemba/game_picker_frame.rb +309 -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/list_picker_frame.rb +271 -0
- data/lib/gemba/locales/en.yml +109 -5
- data/lib/gemba/locales/ja.yml +109 -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 +119 -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 +377 -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 +92 -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 +78 -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 +3 -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_list_picker_frame.rb +391 -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_ra_backend_unlock_retry.rb +123 -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 +383 -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 +221 -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 +277 -10
- data/lib/gemba/input_mappings.rb +0 -214
- data/lib/gemba/player.rb +0 -1525
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
# Bootstrap Zeitwerk autoloading without Tk/SDL2.
|
|
8
|
+
require_relative "../lib/gemba/headless"
|
|
9
|
+
|
|
10
|
+
class TestRomPatcher < Minitest::Test
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Fixture helpers — build valid binary patch data in-memory
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# Build a minimal IPS patch that applies the given records.
|
|
17
|
+
# records: [{offset:, data:}] or [{offset:, rle_count:, rle_val:}]
|
|
18
|
+
def build_ips(records)
|
|
19
|
+
io = StringIO.new.tap { |s| s.binmode }
|
|
20
|
+
io.write("PATCH")
|
|
21
|
+
records.each do |rec|
|
|
22
|
+
off = rec[:offset]
|
|
23
|
+
io.write([off >> 16, (off >> 8) & 0xFF, off & 0xFF].pack("CCC"))
|
|
24
|
+
if rec[:rle_count]
|
|
25
|
+
io.write([0, rec[:rle_count]].pack("nn"))
|
|
26
|
+
io.write([rec[:rle_val]].pack("C"))
|
|
27
|
+
else
|
|
28
|
+
io.write([rec[:data].bytesize].pack("n"))
|
|
29
|
+
io.write(rec[:data].b)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
io.write("EOF")
|
|
33
|
+
io.string.b
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Encode a BPS varint (byuu's additive-shift encoding).
|
|
37
|
+
def bps_varint(n)
|
|
38
|
+
out = "".b
|
|
39
|
+
loop do
|
|
40
|
+
x = n & 0x7f
|
|
41
|
+
n >>= 7
|
|
42
|
+
if n == 0
|
|
43
|
+
out << (0x80 | x).chr
|
|
44
|
+
break
|
|
45
|
+
end
|
|
46
|
+
out << x.chr
|
|
47
|
+
n -= 1
|
|
48
|
+
end
|
|
49
|
+
out
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build a BPS patch using only TargetRead records (writes literal target data).
|
|
53
|
+
# Simplest valid BPS: ignores source entirely, just emits target bytes.
|
|
54
|
+
def build_bps(source, target)
|
|
55
|
+
source = source.b
|
|
56
|
+
target = target.b
|
|
57
|
+
body = StringIO.new.tap { |s| s.binmode }
|
|
58
|
+
body.write("BPS1")
|
|
59
|
+
body.write(bps_varint(source.bytesize))
|
|
60
|
+
body.write(bps_varint(target.bytesize))
|
|
61
|
+
body.write(bps_varint(0)) # metadata_size = 0
|
|
62
|
+
# One TargetRead record covering the entire target
|
|
63
|
+
word = ((target.bytesize - 1) << 2) | 1
|
|
64
|
+
body.write(bps_varint(word))
|
|
65
|
+
body.write(target)
|
|
66
|
+
payload = body.string.b
|
|
67
|
+
src_crc = Zlib.crc32(source)
|
|
68
|
+
tgt_crc = Zlib.crc32(target)
|
|
69
|
+
patch_crc = Zlib.crc32(payload)
|
|
70
|
+
payload + [src_crc, tgt_crc, patch_crc].pack("VVV")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Encode a UPS varint (additive-shift encoding with continuation bias).
|
|
74
|
+
def ups_varint(n)
|
|
75
|
+
out = "".b
|
|
76
|
+
loop do
|
|
77
|
+
x = n & 0x7f
|
|
78
|
+
n >>= 7
|
|
79
|
+
if n == 0
|
|
80
|
+
out << (0x80 | x).chr
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
out << x.chr
|
|
84
|
+
n -= 1
|
|
85
|
+
end
|
|
86
|
+
out
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build a UPS patch from source → target.
|
|
90
|
+
def build_ups(source, target)
|
|
91
|
+
source = source.b
|
|
92
|
+
target = target.b
|
|
93
|
+
max_size = [source.bytesize, target.bytesize].max
|
|
94
|
+
|
|
95
|
+
# Collect diff hunks: each is {start:, xor_bytes:}
|
|
96
|
+
hunks = []
|
|
97
|
+
i = 0
|
|
98
|
+
while i < max_size
|
|
99
|
+
s = source.getbyte(i) || 0
|
|
100
|
+
t = target.getbyte(i) || 0
|
|
101
|
+
if s != t
|
|
102
|
+
hunk_start = i
|
|
103
|
+
xor_bytes = "".b
|
|
104
|
+
while i < max_size
|
|
105
|
+
s = source.getbyte(i) || 0
|
|
106
|
+
t = target.getbyte(i) || 0
|
|
107
|
+
break if s == t
|
|
108
|
+
xor_bytes << (s ^ t).chr
|
|
109
|
+
i += 1
|
|
110
|
+
end
|
|
111
|
+
hunks << { start: hunk_start, xor_bytes: xor_bytes }
|
|
112
|
+
else
|
|
113
|
+
i += 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Build body
|
|
118
|
+
body = StringIO.new.tap { |s| s.binmode }
|
|
119
|
+
body.write("UPS1")
|
|
120
|
+
body.write(ups_varint(source.bytesize))
|
|
121
|
+
body.write(ups_varint(target.bytesize))
|
|
122
|
+
|
|
123
|
+
pos = 0
|
|
124
|
+
hunks.each do |h|
|
|
125
|
+
skip = h[:start] - pos
|
|
126
|
+
body.write(ups_varint(skip))
|
|
127
|
+
body.write(h[:xor_bytes])
|
|
128
|
+
body.write("\x00")
|
|
129
|
+
pos = h[:start] + h[:xor_bytes].bytesize + 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
payload = body.string.b
|
|
133
|
+
src_crc = Zlib.crc32(source)
|
|
134
|
+
tgt_crc = Zlib.crc32(target)
|
|
135
|
+
patch_crc = Zlib.crc32(payload)
|
|
136
|
+
payload + [src_crc, tgt_crc, patch_crc].pack("VVV")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# A small fake ROM — 64 zero bytes, like a blank cartridge header area.
|
|
140
|
+
def blank_rom(size = 64)
|
|
141
|
+
"\x00".b * size
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# RomPatcher (dispatcher)
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def test_detect_format_ips
|
|
149
|
+
patch = "PATCH" + "EOF"
|
|
150
|
+
assert_equal :ips, Gemba::RomPatcher.detect_format(patch)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_detect_format_bps
|
|
154
|
+
patch = "BPS1\x00"
|
|
155
|
+
assert_equal :bps, Gemba::RomPatcher.detect_format(patch)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_detect_format_ups
|
|
159
|
+
patch = "UPS1\x00"
|
|
160
|
+
assert_equal :ups, Gemba::RomPatcher.detect_format(patch)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_detect_format_unknown
|
|
164
|
+
assert_nil Gemba::RomPatcher.detect_format("JUNK")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_safe_out_path_no_collision
|
|
168
|
+
path = "/tmp/nonexistent_gemba_test_#{Process.pid}.gba"
|
|
169
|
+
assert_equal path, Gemba::RomPatcher.safe_out_path(path)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_safe_out_path_collision
|
|
173
|
+
Dir.mktmpdir do |dir|
|
|
174
|
+
base = File.join(dir, "game.gba")
|
|
175
|
+
File.write(base, "x")
|
|
176
|
+
result = Gemba::RomPatcher.safe_out_path(base)
|
|
177
|
+
assert_equal File.join(dir, "game-(2).gba"), result
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_safe_out_path_multiple_collisions
|
|
182
|
+
Dir.mktmpdir do |dir|
|
|
183
|
+
File.write(File.join(dir, "game.gba"), "x")
|
|
184
|
+
File.write(File.join(dir, "game-(2).gba"), "x")
|
|
185
|
+
result = Gemba::RomPatcher.safe_out_path(File.join(dir, "game.gba"))
|
|
186
|
+
assert_equal File.join(dir, "game-(3).gba"), result
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_patch_dispatches_to_ips
|
|
191
|
+
Dir.mktmpdir do |dir|
|
|
192
|
+
rom_path = File.join(dir, "rom.gba")
|
|
193
|
+
patch_path = File.join(dir, "fix.ips")
|
|
194
|
+
out_path = File.join(dir, "rom-patched.gba")
|
|
195
|
+
|
|
196
|
+
source = blank_rom
|
|
197
|
+
File.binwrite(rom_path, source)
|
|
198
|
+
File.binwrite(patch_path, build_ips([{ offset: 0, data: "\xFF\xFE\xFD\xFC" }]))
|
|
199
|
+
|
|
200
|
+
Gemba::RomPatcher.patch(rom_path: rom_path, patch_path: patch_path, out_path: out_path)
|
|
201
|
+
result = File.binread(out_path)
|
|
202
|
+
assert_equal "\xFF".b, result[0, 1]
|
|
203
|
+
assert_equal "\xFE".b, result[1, 1]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def test_patch_raises_on_unknown_format
|
|
208
|
+
Dir.mktmpdir do |dir|
|
|
209
|
+
File.binwrite(File.join(dir, "rom.gba"), "X" * 16)
|
|
210
|
+
File.binwrite(File.join(dir, "bad.xyz"), "JUNK")
|
|
211
|
+
assert_raises(RuntimeError) do
|
|
212
|
+
Gemba::RomPatcher.patch(
|
|
213
|
+
rom_path: File.join(dir, "rom.gba"),
|
|
214
|
+
patch_path: File.join(dir, "bad.xyz"),
|
|
215
|
+
out_path: File.join(dir, "out.gba")
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# IPS
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def test_ips_overwrites_bytes_at_offset
|
|
226
|
+
source = blank_rom
|
|
227
|
+
patch = build_ips([{ offset: 4, data: "\xFF\xFE\xFD" }])
|
|
228
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
229
|
+
assert_equal "\x00".b * 4, result[0, 4], "bytes before offset unchanged"
|
|
230
|
+
assert_equal "\xFF\xFE\xFD".b, result[4, 3], "patch bytes applied"
|
|
231
|
+
assert_equal "\x00".b, result[7, 1], "bytes after patch unchanged"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_ips_rle_record_fills_region
|
|
235
|
+
source = blank_rom
|
|
236
|
+
patch = build_ips([{ offset: 8, rle_count: 4, rle_val: 0xAB }])
|
|
237
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
238
|
+
assert_equal "\xAB".b * 4, result[8, 4]
|
|
239
|
+
assert_equal "\x00".b, result[7, 1], "byte before RLE unchanged"
|
|
240
|
+
assert_equal "\x00".b, result[12, 1], "byte after RLE unchanged"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_ips_multiple_records
|
|
244
|
+
source = blank_rom
|
|
245
|
+
patch = build_ips([
|
|
246
|
+
{ offset: 0, data: "\x01\x02" },
|
|
247
|
+
{ offset: 10, data: "\x03\x04" },
|
|
248
|
+
])
|
|
249
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
250
|
+
assert_equal "\x01\x02".b, result[0, 2]
|
|
251
|
+
assert_equal "\x03\x04".b, result[10, 2]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_ips_extends_rom_if_patch_exceeds_size
|
|
255
|
+
source = "\x00".b * 4
|
|
256
|
+
patch = build_ips([{ offset: 8, data: "\xFF\xFF" }])
|
|
257
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
258
|
+
assert result.bytesize >= 10, "ROM extended to fit patch"
|
|
259
|
+
assert_equal "\xFF\xFF".b, result[8, 2]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_ips_empty_patch_returns_rom_unchanged
|
|
263
|
+
source = "HELLO".b
|
|
264
|
+
patch = build_ips([])
|
|
265
|
+
result = Gemba::RomPatcher::IPS.apply(source, patch)
|
|
266
|
+
assert_equal source, result
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# BPS
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def test_bps_target_read_produces_correct_output
|
|
274
|
+
source = blank_rom(8)
|
|
275
|
+
target = "\x11\x22\x33\x44\x55\x66\x77\x88".b
|
|
276
|
+
patch = build_bps(source, target)
|
|
277
|
+
result = Gemba::RomPatcher::BPS.apply(source, patch)
|
|
278
|
+
assert_equal target, result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_bps_crc_mismatch_raises
|
|
282
|
+
source = blank_rom(8)
|
|
283
|
+
target = "\xDE\xAD\xBE\xEF\x00\x00\x00\x00".b
|
|
284
|
+
patch = build_bps(source, target)
|
|
285
|
+
# Corrupt the source CRC (bytes -12..-9)
|
|
286
|
+
bad_patch = patch.dup.b
|
|
287
|
+
bad_patch[-12] = "\xFF".b
|
|
288
|
+
err = assert_raises(RuntimeError) { Gemba::RomPatcher::BPS.apply(source, bad_patch) }
|
|
289
|
+
assert_match(/CRC32/, err.message)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_bps_identical_source_and_target
|
|
293
|
+
source = "GEMBA".b
|
|
294
|
+
target = "GEMBA".b
|
|
295
|
+
patch = build_bps(source, target)
|
|
296
|
+
result = Gemba::RomPatcher::BPS.apply(source, patch)
|
|
297
|
+
assert_equal target, result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# UPS
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def test_ups_xors_differing_bytes
|
|
305
|
+
source = "\x00\x00\x00\x00".b
|
|
306
|
+
target = "\xFF\x00\xFF\x00".b
|
|
307
|
+
patch = build_ups(source, target)
|
|
308
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
309
|
+
assert_equal target, result
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_ups_multiple_hunks
|
|
313
|
+
source = "\x00" * 16
|
|
314
|
+
target = source.dup.b
|
|
315
|
+
target.setbyte(0, 0xAA)
|
|
316
|
+
target.setbyte(8, 0xBB)
|
|
317
|
+
target.setbyte(15, 0xCC)
|
|
318
|
+
patch = build_ups(source.b, target)
|
|
319
|
+
result = Gemba::RomPatcher::UPS.apply(source.b, patch)
|
|
320
|
+
assert_equal target, result
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def test_ups_crc_mismatch_raises
|
|
324
|
+
source = blank_rom(8)
|
|
325
|
+
target = "\xCA\xFE\xBA\xBE\x00\x00\x00\x00".b
|
|
326
|
+
patch = build_ups(source, target)
|
|
327
|
+
bad_patch = patch.dup.b
|
|
328
|
+
bad_patch[-12] = "\x00".b
|
|
329
|
+
bad_patch[-11] = "\x00".b
|
|
330
|
+
err = assert_raises(RuntimeError) { Gemba::RomPatcher::UPS.apply(source, bad_patch) }
|
|
331
|
+
assert_match(/CRC32/, err.message)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def test_ups_identical_source_and_target
|
|
335
|
+
source = "GEMBA\x00\x00\x00".b
|
|
336
|
+
target = source.dup
|
|
337
|
+
patch = build_ups(source, target)
|
|
338
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
339
|
+
assert_equal target, result
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def test_ups_pads_target_when_source_is_shorter
|
|
343
|
+
# target_size > source_size — result zero-pads to target_size
|
|
344
|
+
source = "\x01\x02".b
|
|
345
|
+
target = "\x01\x03\x00\x00".b # byte 1 differs; bytes 2-3 are 0 (matching padding)
|
|
346
|
+
patch = build_ups(source, target)
|
|
347
|
+
result = Gemba::RomPatcher::UPS.apply(source, patch)
|
|
348
|
+
assert_equal target, result
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# ZIP ROM input
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
def test_patch_with_zip_rom_produces_gba_output
|
|
356
|
+
require 'zip'
|
|
357
|
+
dir = Dir.mktmpdir
|
|
358
|
+
begin
|
|
359
|
+
# Build a tiny ROM and wrap it in a zip
|
|
360
|
+
rom_data = blank_rom
|
|
361
|
+
zip_path = File.join(dir, "game.zip")
|
|
362
|
+
patch_path = File.join(dir, "fix.ips")
|
|
363
|
+
out_path = File.join(dir, "game-patched.gba")
|
|
364
|
+
|
|
365
|
+
Zip::OutputStream.open(zip_path) do |zos|
|
|
366
|
+
zos.put_next_entry("game.gba")
|
|
367
|
+
zos.write(rom_data)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
File.binwrite(patch_path, build_ips([{ offset: 0, data: "\xFF\xFE" }]))
|
|
371
|
+
|
|
372
|
+
resolved = Gemba::RomResolver.resolve(zip_path)
|
|
373
|
+
Gemba::RomPatcher.patch(rom_path: resolved, patch_path: patch_path, out_path: out_path)
|
|
374
|
+
|
|
375
|
+
assert File.exist?(out_path), "expected output at #{out_path}"
|
|
376
|
+
assert_equal ".gba", File.extname(out_path)
|
|
377
|
+
assert_equal "\xFF".b, File.binread(out_path, 1)
|
|
378
|
+
ensure
|
|
379
|
+
# Windows may still hold the zip file handle until GC — ignore EACCES on cleanup
|
|
380
|
+
FileUtils.remove_entry(dir) rescue nil
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
@@ -5,40 +5,39 @@ require "gemba"
|
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "zip"
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class TestRomResolver < Minitest::Test
|
|
9
9
|
TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
|
|
10
10
|
|
|
11
11
|
def setup
|
|
12
|
-
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
13
12
|
@tmpdir = Dir.mktmpdir("rom_loader_test")
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def teardown
|
|
17
16
|
FileUtils.rm_rf(@tmpdir) if @tmpdir && File.directory?(@tmpdir)
|
|
18
|
-
Gemba::
|
|
17
|
+
Gemba::RomResolver.cleanup_temp
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
# -- resolve passthrough --
|
|
22
21
|
|
|
23
22
|
def test_resolve_gba_returns_path_unchanged
|
|
24
|
-
assert_equal TEST_ROM, Gemba::
|
|
23
|
+
assert_equal TEST_ROM, Gemba::RomResolver.resolve(TEST_ROM)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def test_resolve_gb_returns_path_unchanged
|
|
28
27
|
path = "/some/game.gb"
|
|
29
|
-
assert_equal path, Gemba::
|
|
28
|
+
assert_equal path, Gemba::RomResolver.resolve(path)
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def test_resolve_gbc_returns_path_unchanged
|
|
33
32
|
path = "/some/game.gbc"
|
|
34
|
-
assert_equal path, Gemba::
|
|
33
|
+
assert_equal path, Gemba::RomResolver.resolve(path)
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
# -- resolve from zip --
|
|
38
37
|
|
|
39
38
|
def test_resolve_zip_extracts_rom
|
|
40
39
|
zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
|
|
41
|
-
result = Gemba::
|
|
40
|
+
result = Gemba::RomResolver.resolve(zip_path)
|
|
42
41
|
|
|
43
42
|
assert File.exist?(result), "extracted ROM should exist"
|
|
44
43
|
assert_equal ".gba", File.extname(result).downcase
|
|
@@ -47,7 +46,7 @@ class TestRomLoader < Minitest::Test
|
|
|
47
46
|
|
|
48
47
|
def test_resolve_zip_loads_in_core
|
|
49
48
|
zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
|
|
50
|
-
rom_path = Gemba::
|
|
49
|
+
rom_path = Gemba::RomResolver.resolve(zip_path)
|
|
51
50
|
|
|
52
51
|
core = Gemba::Core.new(rom_path)
|
|
53
52
|
assert_equal "GEMBATEST", core.title
|
|
@@ -60,8 +59,8 @@ class TestRomLoader < Minitest::Test
|
|
|
60
59
|
def test_resolve_zip_no_rom_raises
|
|
61
60
|
zip_path = create_zip("empty.zip", "readme.txt" => "hello")
|
|
62
61
|
|
|
63
|
-
err = assert_raises(Gemba::
|
|
64
|
-
Gemba::
|
|
62
|
+
err = assert_raises(Gemba::RomResolver::NoRomInZip) do
|
|
63
|
+
Gemba::RomResolver.resolve(zip_path)
|
|
65
64
|
end
|
|
66
65
|
assert_includes err.message, "empty.zip"
|
|
67
66
|
end
|
|
@@ -72,15 +71,15 @@ class TestRomLoader < Minitest::Test
|
|
|
72
71
|
"game1.gba" => rom_data,
|
|
73
72
|
"game2.gba" => rom_data)
|
|
74
73
|
|
|
75
|
-
err = assert_raises(Gemba::
|
|
76
|
-
Gemba::
|
|
74
|
+
err = assert_raises(Gemba::RomResolver::MultipleRomsInZip) do
|
|
75
|
+
Gemba::RomResolver.resolve(zip_path)
|
|
77
76
|
end
|
|
78
77
|
assert_includes err.message, "multi.zip"
|
|
79
78
|
end
|
|
80
79
|
|
|
81
80
|
def test_resolve_unsupported_extension_raises
|
|
82
|
-
assert_raises(Gemba::
|
|
83
|
-
Gemba::
|
|
81
|
+
assert_raises(Gemba::RomResolver::UnsupportedFormat) do
|
|
82
|
+
Gemba::RomResolver.resolve("/some/file.rar")
|
|
84
83
|
end
|
|
85
84
|
end
|
|
86
85
|
|
|
@@ -88,8 +87,8 @@ class TestRomLoader < Minitest::Test
|
|
|
88
87
|
corrupt = File.join(@tmpdir, "corrupt.zip")
|
|
89
88
|
File.binwrite(corrupt, "this is not a zip file")
|
|
90
89
|
|
|
91
|
-
assert_raises(Gemba::
|
|
92
|
-
Gemba::
|
|
90
|
+
assert_raises(Gemba::RomResolver::ZipReadError) do
|
|
91
|
+
Gemba::RomResolver.resolve(corrupt)
|
|
93
92
|
end
|
|
94
93
|
end
|
|
95
94
|
|
|
@@ -102,32 +101,32 @@ class TestRomLoader < Minitest::Test
|
|
|
102
101
|
zos.write(File.binread(TEST_ROM))
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
assert_raises(Gemba::
|
|
106
|
-
Gemba::
|
|
104
|
+
assert_raises(Gemba::RomResolver::NoRomInZip) do
|
|
105
|
+
Gemba::RomResolver.resolve(zip_path)
|
|
107
106
|
end
|
|
108
107
|
end
|
|
109
108
|
|
|
110
109
|
# -- constants --
|
|
111
110
|
|
|
112
111
|
def test_rom_extensions
|
|
113
|
-
assert_includes Gemba::
|
|
114
|
-
assert_includes Gemba::
|
|
115
|
-
assert_includes Gemba::
|
|
112
|
+
assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gba"
|
|
113
|
+
assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gb"
|
|
114
|
+
assert_includes Gemba::RomResolver::ROM_EXTENSIONS, ".gbc"
|
|
116
115
|
end
|
|
117
116
|
|
|
118
117
|
def test_supported_extensions_includes_zip
|
|
119
|
-
assert_includes Gemba::
|
|
118
|
+
assert_includes Gemba::RomResolver::SUPPORTED_EXTENSIONS, ".zip"
|
|
120
119
|
end
|
|
121
120
|
|
|
122
121
|
# -- cleanup --
|
|
123
122
|
|
|
124
123
|
def test_cleanup_temp_removes_directory
|
|
125
124
|
zip_path = create_zip("game.zip", "game.gba" => File.binread(TEST_ROM))
|
|
126
|
-
Gemba::
|
|
127
|
-
assert File.directory?(Gemba::
|
|
125
|
+
Gemba::RomResolver.resolve(zip_path)
|
|
126
|
+
assert File.directory?(Gemba::RomResolver.tmp_dir)
|
|
128
127
|
|
|
129
|
-
Gemba::
|
|
130
|
-
refute File.directory?(Gemba::
|
|
128
|
+
Gemba::RomResolver.cleanup_temp
|
|
129
|
+
refute File.directory?(Gemba::RomResolver.tmp_dir)
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
private
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
require "minitest/autorun"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "json"
|
|
6
|
-
|
|
7
|
-
require_relative "../lib/gemba/locale"
|
|
8
|
-
require_relative "../lib/gemba/save_state_manager"
|
|
6
|
+
require "gemba/headless"
|
|
9
7
|
|
|
10
8
|
class TestSaveStateManager < Minitest::Test
|
|
11
9
|
# Recording mock for the mGBA Core.
|
|
@@ -92,7 +90,7 @@ class TestSaveStateManager < Minitest::Test
|
|
|
92
90
|
end
|
|
93
91
|
|
|
94
92
|
def new_manager(core: @core, config: @config, app: @app)
|
|
95
|
-
mgr = Gemba::SaveStateManager.new(core: core, config: config, app: app)
|
|
93
|
+
mgr = Gemba::SaveStateManager.new(core: core, config: config, app: app, platform: Gemba::Platform.default)
|
|
96
94
|
mgr.state_dir = mgr.state_dir_for_rom(core)
|
|
97
95
|
mgr
|
|
98
96
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "shared/tk_test_helper"
|
|
5
|
+
|
|
6
|
+
class TestSettingsAudioTab < Minitest::Test
|
|
7
|
+
include TeekTestHelper
|
|
8
|
+
|
|
9
|
+
# -- Volume slider ------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
def test_volume_defaults_to_100
|
|
12
|
+
assert_tk_app("volume defaults to 100") do
|
|
13
|
+
require "gemba/headless"
|
|
14
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
15
|
+
sw.show
|
|
16
|
+
app.update
|
|
17
|
+
|
|
18
|
+
assert_equal '100', app.get_variable(Gemba::Settings::AudioTab::VAR_VOLUME)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_dragging_volume_to_50_fires_callback
|
|
23
|
+
assert_tk_app("dragging volume to 50 fires on_volume_change") do
|
|
24
|
+
require "gemba/headless"
|
|
25
|
+
received = nil
|
|
26
|
+
Gemba.bus = Gemba::EventBus.new
|
|
27
|
+
Gemba.bus.on(:volume_changed) { |v| received = v }
|
|
28
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
29
|
+
sw.show
|
|
30
|
+
app.update
|
|
31
|
+
|
|
32
|
+
# Simulate user dragging volume slider to 50
|
|
33
|
+
app.command(Gemba::Settings::AudioTab::VOLUME_SCALE, 'set', 50)
|
|
34
|
+
app.update
|
|
35
|
+
|
|
36
|
+
assert_in_delta 0.5, received, 0.01
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_volume_at_zero
|
|
41
|
+
assert_tk_app("volume at zero") do
|
|
42
|
+
require "gemba/headless"
|
|
43
|
+
received = nil
|
|
44
|
+
Gemba.bus = Gemba::EventBus.new
|
|
45
|
+
Gemba.bus.on(:volume_changed) { |v| received = v }
|
|
46
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
47
|
+
sw.show
|
|
48
|
+
app.update
|
|
49
|
+
|
|
50
|
+
app.command(Gemba::Settings::AudioTab::VOLUME_SCALE, 'set', 0)
|
|
51
|
+
app.update
|
|
52
|
+
|
|
53
|
+
assert_in_delta 0.0, received, 0.01
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -- Mute checkbox ------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def test_mute_defaults_to_off
|
|
60
|
+
assert_tk_app("mute defaults to off") do
|
|
61
|
+
require "gemba/headless"
|
|
62
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
63
|
+
sw.show
|
|
64
|
+
app.update
|
|
65
|
+
|
|
66
|
+
assert_equal '0', app.get_variable(Gemba::Settings::AudioTab::VAR_MUTE)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_clicking_mute_fires_callback
|
|
71
|
+
assert_tk_app("clicking mute fires on_mute_change") do
|
|
72
|
+
require "gemba/headless"
|
|
73
|
+
received = nil
|
|
74
|
+
Gemba.bus = Gemba::EventBus.new
|
|
75
|
+
Gemba.bus.on(:mute_changed) { |m| received = m }
|
|
76
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
77
|
+
sw.show
|
|
78
|
+
app.update
|
|
79
|
+
|
|
80
|
+
# Simulate user clicking the mute checkbox
|
|
81
|
+
app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
|
|
82
|
+
app.update
|
|
83
|
+
|
|
84
|
+
assert_equal true, received
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_clicking_mute_twice_unmutes
|
|
89
|
+
assert_tk_app("clicking mute twice unmutes") do
|
|
90
|
+
require "gemba/headless"
|
|
91
|
+
received = nil
|
|
92
|
+
Gemba.bus = Gemba::EventBus.new
|
|
93
|
+
Gemba.bus.on(:mute_changed) { |m| received = m }
|
|
94
|
+
sw = Gemba::SettingsWindow.new(app)
|
|
95
|
+
sw.show
|
|
96
|
+
app.update
|
|
97
|
+
|
|
98
|
+
app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
|
|
99
|
+
app.update
|
|
100
|
+
assert_equal true, received
|
|
101
|
+
|
|
102
|
+
app.command(Gemba::Settings::AudioTab::MUTE_CHECK, 'invoke')
|
|
103
|
+
app.update
|
|
104
|
+
assert_equal false, received
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|