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.
Files changed (285) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_NOTICES +37 -2
  3. data/assets/placeholder_boxart.png +0 -0
  4. data/bin/gemba +2 -2
  5. data/ext/gemba/extconf.rb +23 -1
  6. data/ext/gemba/gemba_ext.c +436 -2
  7. data/ext/gemba/gemba_ext.h +2 -0
  8. data/gemba.gemspec +5 -3
  9. data/lib/gemba/achievements/achievement.rb +23 -0
  10. data/lib/gemba/achievements/backend.rb +186 -0
  11. data/lib/gemba/achievements/cache.rb +70 -0
  12. data/lib/gemba/achievements/credentials_presenter.rb +142 -0
  13. data/lib/gemba/achievements/fake_backend.rb +205 -0
  14. data/lib/gemba/achievements/null_backend.rb +11 -0
  15. data/lib/gemba/achievements/offline_backend.rb +168 -0
  16. data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
  19. data/lib/gemba/achievements.rb +19 -0
  20. data/lib/gemba/achievements_window.rb +556 -0
  21. data/lib/gemba/app_controller.rb +1015 -0
  22. data/lib/gemba/bios.rb +54 -0
  23. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  24. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  25. data/lib/gemba/boxart_fetcher.rb +79 -0
  26. data/lib/gemba/bus_emitter.rb +13 -0
  27. data/lib/gemba/child_window.rb +24 -1
  28. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  29. data/lib/gemba/cli/commands/decode.rb +154 -0
  30. data/lib/gemba/cli/commands/patch.rb +78 -0
  31. data/lib/gemba/cli/commands/play.rb +78 -0
  32. data/lib/gemba/cli/commands/record.rb +114 -0
  33. data/lib/gemba/cli/commands/replay.rb +161 -0
  34. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  35. data/lib/gemba/cli/commands/version.rb +22 -0
  36. data/lib/gemba/cli.rb +52 -364
  37. data/lib/gemba/config.rb +134 -1
  38. data/lib/gemba/data/gb_games.json +1 -0
  39. data/lib/gemba/data/gb_md5.json +1 -0
  40. data/lib/gemba/data/gba_games.json +1 -0
  41. data/lib/gemba/data/gba_md5.json +1 -0
  42. data/lib/gemba/data/gbc_games.json +1 -0
  43. data/lib/gemba/data/gbc_md5.json +1 -0
  44. data/lib/gemba/emulator_frame.rb +1060 -0
  45. data/lib/gemba/event_bus.rb +48 -0
  46. data/lib/gemba/frame_stack.rb +60 -0
  47. data/lib/gemba/game_index.rb +84 -0
  48. data/lib/gemba/game_picker_frame.rb +268 -0
  49. data/lib/gemba/gamepad_map.rb +103 -0
  50. data/lib/gemba/headless.rb +6 -5
  51. data/lib/gemba/headless_player.rb +33 -3
  52. data/lib/gemba/help_window.rb +61 -0
  53. data/lib/gemba/hotkey_map.rb +3 -1
  54. data/lib/gemba/input_recorder.rb +107 -0
  55. data/lib/gemba/input_replayer.rb +119 -0
  56. data/lib/gemba/keyboard_map.rb +90 -0
  57. data/lib/gemba/locales/en.yml +97 -5
  58. data/lib/gemba/locales/ja.yml +97 -5
  59. data/lib/gemba/main_window.rb +56 -0
  60. data/lib/gemba/modal_stack.rb +81 -0
  61. data/lib/gemba/patcher_window.rb +223 -0
  62. data/lib/gemba/platform/gb.rb +21 -0
  63. data/lib/gemba/platform/gba.rb +21 -0
  64. data/lib/gemba/platform/gbc.rb +23 -0
  65. data/lib/gemba/platform.rb +20 -0
  66. data/lib/gemba/platform_open.rb +19 -0
  67. data/lib/gemba/recorder.rb +4 -3
  68. data/lib/gemba/replay_player.rb +691 -0
  69. data/lib/gemba/rom_info.rb +57 -0
  70. data/lib/gemba/rom_info_window.rb +16 -3
  71. data/lib/gemba/rom_library.rb +106 -0
  72. data/lib/gemba/rom_overrides.rb +47 -0
  73. data/lib/gemba/rom_patcher/bps.rb +161 -0
  74. data/lib/gemba/rom_patcher/ips.rb +101 -0
  75. data/lib/gemba/rom_patcher/ups.rb +118 -0
  76. data/lib/gemba/rom_patcher.rb +109 -0
  77. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  78. data/lib/gemba/runtime.rb +59 -26
  79. data/lib/gemba/save_state_manager.rb +4 -7
  80. data/lib/gemba/save_state_picker.rb +17 -4
  81. data/lib/gemba/session_logger.rb +64 -0
  82. data/lib/gemba/settings/audio_tab.rb +77 -0
  83. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  84. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  85. data/lib/gemba/settings/paths.rb +11 -0
  86. data/lib/gemba/settings/recording_tab.rb +83 -0
  87. data/lib/gemba/settings/save_states_tab.rb +91 -0
  88. data/lib/gemba/settings/system_tab.rb +362 -0
  89. data/lib/gemba/settings/video_tab.rb +318 -0
  90. data/lib/gemba/settings_window.rb +162 -1036
  91. data/lib/gemba/version.rb +1 -1
  92. data/lib/gemba/virtual_keyboard.rb +19 -0
  93. data/lib/gemba.rb +2 -12
  94. data/test/achievements_window/test_bulk_sync.rb +218 -0
  95. data/test/achievements_window/test_bus_events.rb +125 -0
  96. data/test/achievements_window/test_close_confirmation.rb +201 -0
  97. data/test/achievements_window/test_initial_state.rb +164 -0
  98. data/test/achievements_window/test_sorting.rb +227 -0
  99. data/test/achievements_window/test_tree_rendering.rb +133 -0
  100. data/test/fixtures/fake_bios.bin +0 -0
  101. data/test/fixtures/pong.gba +0 -0
  102. data/test/fixtures/test.gb +0 -0
  103. data/test/fixtures/test.gbc +0 -0
  104. data/test/fixtures/test_quicksave.ss +0 -0
  105. data/test/screenshots/no_focus.png +0 -0
  106. data/test/shared/teek_test_worker.rb +17 -1
  107. data/test/shared/tk_test_helper.rb +91 -4
  108. data/test/support/achievements_window_helpers.rb +18 -0
  109. data/test/support/fake_core.rb +25 -0
  110. data/test/support/fake_ra_runtime.rb +74 -0
  111. data/test/support/fake_requester.rb +68 -0
  112. data/test/support/player_helpers.rb +20 -5
  113. data/test/test_achievement.rb +32 -0
  114. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  115. data/test/test_bios.rb +123 -0
  116. data/test/test_boxart_fetcher.rb +150 -0
  117. data/test/test_cli.rb +17 -265
  118. data/test/test_cli_config.rb +64 -0
  119. data/test/test_cli_decode.rb +97 -0
  120. data/test/test_cli_patch.rb +58 -0
  121. data/test/test_cli_play.rb +213 -0
  122. data/test/test_cli_ra.rb +175 -0
  123. data/test/test_cli_record.rb +69 -0
  124. data/test/test_cli_replay.rb +72 -0
  125. data/test/test_cli_sync_requester.rb +152 -0
  126. data/test/test_cli_version.rb +27 -0
  127. data/test/test_config.rb +2 -3
  128. data/test/test_config_ra.rb +69 -0
  129. data/test/test_core.rb +62 -1
  130. data/test/test_credentials_presenter.rb +192 -0
  131. data/test/test_event_bus.rb +100 -0
  132. data/test/test_fake_backend_achievements.rb +130 -0
  133. data/test/test_fake_backend_auth.rb +68 -0
  134. data/test/test_game_index.rb +77 -0
  135. data/test/test_game_picker_frame.rb +310 -0
  136. data/test/test_gamepad_map.rb +1 -3
  137. data/test/test_headless_player.rb +17 -3
  138. data/test/test_help_window.rb +82 -0
  139. data/test/test_hotkey_map.rb +22 -1
  140. data/test/test_input_recorder.rb +179 -0
  141. data/test/test_input_replay_determinism.rb +113 -0
  142. data/test/test_input_replayer.rb +162 -0
  143. data/test/test_keyboard_map.rb +1 -3
  144. data/test/test_libretro_backend.rb +41 -0
  145. data/test/test_locale.rb +1 -1
  146. data/test/test_logging.rb +123 -0
  147. data/test/test_null_backend.rb +42 -0
  148. data/test/test_offline_backend.rb +116 -0
  149. data/test/test_overlay_renderer.rb +1 -1
  150. data/test/test_platform.rb +149 -0
  151. data/test/test_ra_backend.rb +313 -0
  152. data/test/test_ra_backend_unlock_gate.rb +56 -0
  153. data/test/test_recorder.rb +0 -3
  154. data/test/test_replay_player.rb +316 -0
  155. data/test/test_rom_info.rb +149 -0
  156. data/test/test_rom_overrides.rb +86 -0
  157. data/test/test_rom_patcher.rb +382 -0
  158. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  159. data/test/test_save_state_manager.rb +2 -4
  160. data/test/test_settings_audio.rb +107 -0
  161. data/test/test_settings_hotkeys.rb +83 -66
  162. data/test/test_settings_recording.rb +49 -0
  163. data/test/test_settings_save_states.rb +97 -0
  164. data/test/test_settings_system.rb +133 -0
  165. data/test/test_settings_video.rb +450 -0
  166. data/test/test_settings_window.rb +76 -507
  167. data/test/test_tip_service.rb +6 -6
  168. data/test/test_toast_overlay.rb +1 -1
  169. data/test/test_virtual_events.rb +156 -0
  170. data/test/test_virtual_keyboard.rb +1 -1
  171. data/vendor/rcheevos/CHANGELOG.md +495 -0
  172. data/vendor/rcheevos/LICENSE +21 -0
  173. data/vendor/rcheevos/Package.swift +33 -0
  174. data/vendor/rcheevos/README.md +67 -0
  175. data/vendor/rcheevos/include/module.modulemap +70 -0
  176. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  177. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  178. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  179. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  180. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  181. data/vendor/rcheevos/include/rc_client.h +877 -0
  182. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  183. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  184. data/vendor/rcheevos/include/rc_error.h +59 -0
  185. data/vendor/rcheevos/include/rc_export.h +100 -0
  186. data/vendor/rcheevos/include/rc_hash.h +200 -0
  187. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  188. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  189. data/vendor/rcheevos/include/rc_util.h +51 -0
  190. data/vendor/rcheevos/include/rcheevos.h +8 -0
  191. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  192. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  193. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  194. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  197. data/vendor/rcheevos/src/rc_client.c +6941 -0
  198. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  199. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  200. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  201. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  202. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  203. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  204. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  205. data/vendor/rcheevos/src/rc_compat.c +251 -0
  206. data/vendor/rcheevos/src/rc_compat.h +121 -0
  207. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  208. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  209. data/vendor/rcheevos/src/rc_util.c +199 -0
  210. data/vendor/rcheevos/src/rc_version.c +11 -0
  211. data/vendor/rcheevos/src/rc_version.h +32 -0
  212. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  213. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  214. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  215. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  216. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  217. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  218. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  219. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  220. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  221. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  222. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  223. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  224. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  225. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  226. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  227. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  228. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  229. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  230. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  231. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  232. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  233. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  234. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  235. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  236. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  237. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  238. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  239. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  240. data/vendor/rcheevos/test/libretro.h +205 -0
  241. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  242. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  243. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  244. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  246. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  247. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  248. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  249. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  250. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  251. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  252. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  253. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  254. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  255. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  256. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  257. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  258. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  259. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  260. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  261. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  262. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  263. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  264. data/vendor/rcheevos/test/rhash/data.c +657 -0
  265. data/vendor/rcheevos/test/rhash/data.h +32 -0
  266. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  267. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  268. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  269. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  270. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  271. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  272. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  273. data/vendor/rcheevos/test/test.c +113 -0
  274. data/vendor/rcheevos/test/test_framework.h +205 -0
  275. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  276. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  277. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  278. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  279. data/vendor/rcheevos/test/test_types.natvis +9 -0
  280. data/vendor/rcheevos/validator/validator.c +658 -0
  281. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  282. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  283. metadata +274 -11
  284. data/lib/gemba/input_mappings.rb +0 -214
  285. data/lib/gemba/player.rb +0 -1525
data/test/test_bios.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+ require "gemba/headless"
7
+
8
+ class TestBios < Minitest::Test
9
+ FAKE_BIOS = File.expand_path("fixtures/fake_bios.bin", __dir__)
10
+
11
+ def setup
12
+ @dir = Dir.mktmpdir("gemba-bios-test")
13
+ end
14
+
15
+ def teardown
16
+ FileUtils.rm_rf(@dir)
17
+ end
18
+
19
+ # -- exists? / valid? -------------------------------------------------------
20
+
21
+ def test_exists_true_for_real_file
22
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
23
+ assert bios.exists?
24
+ end
25
+
26
+ def test_exists_false_for_missing_file
27
+ bios = Gemba::Bios.new(path: File.join(@dir, "no_such.bin"))
28
+ refute bios.exists?
29
+ end
30
+
31
+ def test_valid_for_correct_size
32
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
33
+ assert_equal Gemba::Bios::EXPECTED_SIZE, bios.size
34
+ assert bios.valid?
35
+ end
36
+
37
+ def test_invalid_for_wrong_size
38
+ path = File.join(@dir, "small.bin")
39
+ File.binwrite(path, "\x00" * 100)
40
+ bios = Gemba::Bios.new(path: path)
41
+ refute bios.valid?
42
+ end
43
+
44
+ def test_invalid_for_missing_file
45
+ bios = Gemba::Bios.new(path: File.join(@dir, "missing.bin"))
46
+ refute bios.valid?
47
+ end
48
+
49
+ # -- filename ---------------------------------------------------------------
50
+
51
+ def test_filename_returns_basename
52
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
53
+ assert_equal "fake_bios.bin", bios.filename
54
+ end
55
+
56
+ # -- checksum ---------------------------------------------------------------
57
+
58
+ def test_checksum_nil_for_invalid_file
59
+ bios = Gemba::Bios.new(path: File.join(@dir, "missing.bin"))
60
+ assert_nil bios.checksum
61
+ end
62
+
63
+ def test_checksum_returns_integer_for_valid_file
64
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
65
+ assert_kind_of Integer, bios.checksum
66
+ end
67
+
68
+ def test_checksum_is_memoized
69
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
70
+ c1 = bios.checksum
71
+ c2 = bios.checksum
72
+ assert_equal c1, c2
73
+ assert_same c1, c2 # same object, not just equal
74
+ end
75
+
76
+ # -- known? / official? / label ---------------------------------------------
77
+
78
+ def test_fake_bios_is_not_official
79
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
80
+ refute bios.official?
81
+ refute bios.ds_mode?
82
+ refute bios.known?
83
+ end
84
+
85
+ def test_label_unknown_for_fake_bios
86
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
87
+ assert_equal "Unknown BIOS", bios.label
88
+ end
89
+
90
+ # -- status_text ------------------------------------------------------------
91
+
92
+ def test_status_text_includes_size_for_valid_file
93
+ bios = Gemba::Bios.new(path: FAKE_BIOS)
94
+ assert_includes bios.status_text, "16384"
95
+ end
96
+
97
+ def test_status_text_not_found_for_missing_file
98
+ bios = Gemba::Bios.new(path: File.join(@dir, "gone.bin"))
99
+ assert_includes bios.status_text, "not found"
100
+ end
101
+
102
+ def test_status_text_invalid_size_for_wrong_size
103
+ path = File.join(@dir, "wrong.bin")
104
+ File.binwrite(path, "\x00" * 512)
105
+ bios = Gemba::Bios.new(path: path)
106
+ assert_includes bios.status_text, "Invalid size"
107
+ end
108
+
109
+ # -- from_config_name -------------------------------------------------------
110
+
111
+ def test_from_config_name_nil_returns_nil
112
+ assert_nil Gemba::Bios.from_config_name(nil)
113
+ end
114
+
115
+ def test_from_config_name_empty_returns_nil
116
+ assert_nil Gemba::Bios.from_config_name("")
117
+ end
118
+
119
+ def test_from_config_name_builds_path_under_bios_dir
120
+ bios = Gemba::Bios.from_config_name("gba_bios.bin")
121
+ assert_equal File.join(Gemba::Config.bios_dir, "gba_bios.bin"), bios.path
122
+ end
123
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+ require "tmpdir"
6
+ require "fileutils"
7
+ require "gemba/headless"
8
+ require "gemba/headless"
9
+
10
+ class TestBoxartFetcher < Minitest::Test
11
+ include TeekTestHelper
12
+
13
+ FAKE_PNG = "\x89PNG\r\n\x1a\n fake image data".b
14
+
15
+ # Minimal backend that returns a fixed URL for known codes
16
+ class StubBackend
17
+ def url_for(game_code)
18
+ case game_code
19
+ when "AGB-AXVE"
20
+ "https://example.com/boxart/pokemon_ruby.png"
21
+ when "AGB-BPEE"
22
+ "https://example.com/boxart/pokemon_emerald.png"
23
+ else
24
+ nil
25
+ end
26
+ end
27
+ end
28
+
29
+ def setup
30
+ @tmpdir = Dir.mktmpdir("boxart_test")
31
+ @backend = StubBackend.new
32
+ end
33
+
34
+ def teardown
35
+ FileUtils.rm_rf(@tmpdir)
36
+ end
37
+
38
+ def test_cached_path
39
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
40
+ expected = File.join(@tmpdir, "AGB-AXVE", "boxart.png")
41
+ assert_equal expected, fetcher.cached_path("AGB-AXVE")
42
+ end
43
+
44
+ def test_cached_returns_false_when_not_cached
45
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
46
+ refute fetcher.cached?("AGB-AXVE")
47
+ end
48
+
49
+ def test_cached_returns_true_when_file_exists
50
+ dir = File.join(@tmpdir, "AGB-AXVE")
51
+ FileUtils.mkdir_p(dir)
52
+ File.binwrite(File.join(dir, "boxart.png"), FAKE_PNG)
53
+
54
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
55
+ assert fetcher.cached?("AGB-AXVE")
56
+ end
57
+
58
+ def test_fetch_returns_cached_path_immediately_when_cached
59
+ dir = File.join(@tmpdir, "AGB-AXVE")
60
+ FileUtils.mkdir_p(dir)
61
+ cached = File.join(dir, "boxart.png")
62
+ File.binwrite(cached, FAKE_PNG)
63
+
64
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
65
+ result = nil
66
+ fetcher.fetch("AGB-AXVE") { |path| result = path }
67
+
68
+ assert_equal cached, result
69
+ end
70
+
71
+ def test_fetch_does_nothing_for_unknown_game_code
72
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
73
+ called = false
74
+ fetcher.fetch("AGB-ZZZZ") { |_| called = true }
75
+ refute called
76
+ end
77
+
78
+ def test_fetch_does_nothing_with_null_backend
79
+ null_fetcher = Gemba::BoxartFetcher.new(
80
+ app: nil, cache_dir: @tmpdir,
81
+ backend: Gemba::BoxartFetcher::NullBackend.new
82
+ )
83
+ called = false
84
+ null_fetcher.fetch("AGB-AXVE") { |_| called = true }
85
+ refute called
86
+ end
87
+
88
+ def test_fetch_does_nothing_without_block
89
+ fetcher = Gemba::BoxartFetcher.new(app: nil, cache_dir: @tmpdir, backend: @backend)
90
+ fetcher.fetch("AGB-AXVE")
91
+ end
92
+
93
+ def test_fetch_downloads_and_caches
94
+ assert_tk_app("boxart fetch downloads and caches") do
95
+ require "tmpdir"
96
+ require "webmock"
97
+ require "gemba/headless"
98
+ WebMock.enable!
99
+ WebMock.stub_request(:get, "https://example.com/boxart/pokemon_ruby.png")
100
+ .to_return(status: 200, body: "\x89PNG fake".b, headers: { "Content-Type" => "image/png" })
101
+
102
+ tmpdir = Dir.mktmpdir("boxart_test")
103
+ backend = Struct.new(:url) { def url_for(_) = url }.new("https://example.com/boxart/pokemon_ruby.png")
104
+ fetcher = Gemba::BoxartFetcher.new(app: app, cache_dir: tmpdir, backend: backend)
105
+ result = nil
106
+ done = false
107
+
108
+ fetcher.fetch("AGB-AXVE") do |path|
109
+ result = path
110
+ done = true
111
+ end
112
+
113
+ wait_until(timeout: 5.0) { done }
114
+
115
+ assert done, "Fetch did not complete"
116
+ assert_equal fetcher.cached_path("AGB-AXVE"), result
117
+ assert File.exist?(result), "Cached file should exist"
118
+ assert fetcher.cached?("AGB-AXVE")
119
+
120
+ FileUtils.rm_rf(tmpdir)
121
+ WebMock.disable!
122
+ end
123
+ end
124
+
125
+ def test_fetch_handles_404
126
+ assert_tk_app("boxart fetch handles 404 gracefully") do
127
+ require "tmpdir"
128
+ require "webmock"
129
+ require "gemba/headless"
130
+ WebMock.enable!
131
+ WebMock.stub_request(:get, "https://example.com/boxart/pokemon_ruby.png")
132
+ .to_return(status: 404, body: "Not Found")
133
+
134
+ tmpdir = Dir.mktmpdir("boxart_test")
135
+ backend = Struct.new(:url) { def url_for(_) = url }.new("https://example.com/boxart/pokemon_ruby.png")
136
+ fetcher = Gemba::BoxartFetcher.new(app: app, cache_dir: tmpdir, backend: backend)
137
+ called = false
138
+
139
+ fetcher.fetch("AGB-AXVE") { |_| called = true }
140
+
141
+ wait_until(timeout: 2.0) { false } # let background thread finish
142
+
143
+ refute called, "Callback should not fire on 404"
144
+ refute fetcher.cached?("AGB-AXVE")
145
+
146
+ FileUtils.rm_rf(tmpdir)
147
+ WebMock.disable!
148
+ end
149
+ end
150
+ end
data/test/test_cli.rb CHANGED
@@ -1,281 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require "gemba"
5
- require_relative "../lib/gemba/version"
6
- require_relative "../lib/gemba/cli"
4
+ require "gemba/headless"
7
5
 
8
6
  class TestCLI < Minitest::Test
9
- def parse(args)
10
- Gemba::CLI.parse(args)
11
- end
12
-
13
- # -- ROM argument --
14
-
15
- def test_rom_path
16
- opts = parse(["game.gba"])
17
- assert_equal File.expand_path("game.gba"), opts[:rom]
18
- end
19
-
20
- def test_rom_path_expands_tilde
21
- opts = parse(["~/game.gba"])
22
- assert_equal File.join(Dir.home, "game.gba"), opts[:rom]
23
- end
24
-
25
- def test_no_rom
26
- opts = parse([])
27
- assert_nil opts[:rom]
28
- end
29
-
30
- # -- player flags --
31
-
32
- def test_scale
33
- opts = parse(["--scale", "2"])
34
- assert_equal 2, opts[:scale]
35
- end
36
-
37
- def test_scale_short
38
- opts = parse(["-s", "3"])
39
- assert_equal 3, opts[:scale]
40
- end
41
-
42
- def test_scale_clamps_high
43
- opts = parse(["--scale", "10"])
44
- assert_equal 4, opts[:scale]
45
- end
46
-
47
- def test_scale_clamps_low
48
- opts = parse(["--scale", "0"])
49
- assert_equal 1, opts[:scale]
50
- end
51
-
52
- def test_volume
53
- opts = parse(["--volume", "50"])
54
- assert_equal 50, opts[:volume]
55
- end
56
-
57
- def test_volume_short
58
- opts = parse(["-v", "75"])
59
- assert_equal 75, opts[:volume]
60
- end
61
-
62
- def test_volume_clamps
63
- opts = parse(["--volume", "200"])
64
- assert_equal 100, opts[:volume]
65
- end
66
-
67
- def test_mute
68
- opts = parse(["--mute"])
69
- assert opts[:mute]
70
- end
71
-
72
- def test_mute_short
73
- opts = parse(["-m"])
74
- assert opts[:mute]
75
- end
76
-
77
- def test_no_sound
78
- opts = parse(["--no-sound"])
79
- assert_equal false, opts[:sound]
80
- end
81
-
82
- def test_fullscreen
83
- opts = parse(["--fullscreen"])
84
- assert opts[:fullscreen]
85
- end
86
-
87
- def test_fullscreen_short
88
- opts = parse(["-f"])
89
- assert opts[:fullscreen]
90
- end
91
-
92
- def test_show_fps
93
- opts = parse(["--show-fps"])
94
- assert opts[:show_fps]
95
- end
96
-
97
- def test_turbo_speed
98
- opts = parse(["--turbo-speed", "3"])
99
- assert_equal 3, opts[:turbo_speed]
100
- end
101
-
102
- def test_turbo_speed_clamps
103
- opts = parse(["--turbo-speed", "99"])
104
- assert_equal 4, opts[:turbo_speed]
105
- end
106
-
107
- def test_locale
108
- opts = parse(["--locale", "ja"])
109
- assert_equal "ja", opts[:locale]
110
- end
111
-
112
- def test_version_flag
113
- opts = parse(["--version"])
114
- assert opts[:version]
115
- end
116
-
117
- def test_help_flag
118
- opts = parse(["--help"])
119
- assert opts[:help]
120
- end
121
-
122
- # -- combinations --
123
-
124
- def test_flags_with_rom
125
- opts = parse(["-s", "2", "--mute", "pokemon.gba"])
126
- assert_equal 2, opts[:scale]
127
- assert opts[:mute]
128
- assert_equal File.expand_path("pokemon.gba"), opts[:rom]
129
- end
130
-
131
- def test_rom_before_flags
132
- opts = parse(["game.gba", "--scale", "4"])
133
- assert_equal File.expand_path("game.gba"), opts[:rom]
134
- assert_equal 4, opts[:scale]
135
- end
136
-
137
- # -- parser included for help output --
138
-
139
- def test_parser_present
140
- opts = parse([])
141
- assert_kind_of OptionParser, opts[:parser]
142
- end
143
-
144
- def test_help_output_includes_banner
145
- opts = parse([])
146
- help = opts[:parser].to_s
147
- assert_includes help, "Usage: gemba"
148
- assert_includes help, "GBA emulator"
149
- end
150
-
151
- def test_help_lists_subcommands
152
- opts = parse([])
153
- help = opts[:parser].to_s
154
- assert_includes help, "record"
155
- assert_includes help, "decode"
156
- assert_includes help, "info"
157
- end
158
-
159
- # -- apply --
160
-
161
- class MockConfig
162
- attr_accessor :scale, :volume, :muted, :show_fps, :turbo_speed, :locale
163
-
164
- def initialize
165
- @scale = 3
166
- @volume = 100
167
- @muted = false
168
- @show_fps = false
169
- @turbo_speed = 0
170
- @locale = 'auto'
171
- end
172
- end
173
-
174
- def test_apply_overrides_config
175
- config = MockConfig.new
176
- Gemba::CLI.apply(config, { scale: 2, volume: 50, mute: true, show_fps: true })
177
- assert_equal 2, config.scale
178
- assert_equal 50, config.volume
179
- assert config.muted
180
- assert config.show_fps
181
- end
182
-
183
- def test_apply_skips_unset_options
184
- config = MockConfig.new
185
- Gemba::CLI.apply(config, {})
186
- assert_equal 3, config.scale
187
- assert_equal 100, config.volume
188
- refute config.muted
189
- end
190
-
191
- def test_apply_locale
192
- config = MockConfig.new
193
- Gemba::CLI.apply(config, { locale: 'ja' })
194
- assert_equal 'ja', config.locale
195
- end
196
-
197
- def test_apply_turbo_speed
198
- config = MockConfig.new
199
- Gemba::CLI.apply(config, { turbo_speed: 3 })
200
- assert_equal 3, config.turbo_speed
201
- end
202
-
203
7
  # -- subcommand dispatch --
204
8
 
205
9
  def test_subcommands_constant
10
+ assert_includes Gemba::CLI::SUBCOMMANDS, 'play'
206
11
  assert_includes Gemba::CLI::SUBCOMMANDS, 'record'
207
12
  assert_includes Gemba::CLI::SUBCOMMANDS, 'decode'
208
- assert_includes Gemba::CLI::SUBCOMMANDS, 'info'
209
- end
210
-
211
- # -- record subcommand parsing --
212
-
213
- def test_parse_record_frames_and_rom
214
- opts = Gemba::CLI.parse_record(["--frames", "100", "game.gba"])
215
- assert_equal 100, opts[:frames]
216
- assert_equal File.expand_path("game.gba"), opts[:rom]
13
+ assert_includes Gemba::CLI::SUBCOMMANDS, 'replay'
14
+ assert_includes Gemba::CLI::SUBCOMMANDS, 'config'
15
+ assert_includes Gemba::CLI::SUBCOMMANDS, 'version'
16
+ refute_includes Gemba::CLI::SUBCOMMANDS, 'info'
217
17
  end
218
18
 
219
- def test_parse_record_output
220
- opts = Gemba::CLI.parse_record(["-o", "out.grec", "--frames", "10", "game.gba"])
221
- assert_equal "out.grec", opts[:output]
222
- end
223
-
224
- def test_parse_record_help
225
- opts = Gemba::CLI.parse_record(["--help"])
226
- assert opts[:help]
227
- end
228
-
229
- # -- decode subcommand parsing --
230
-
231
- def test_parse_decode_trec
232
- opts = Gemba::CLI.parse_decode(["recording.grec"])
233
- assert_equal "recording.grec", opts[:trec]
234
- end
235
-
236
- def test_parse_decode_output
237
- opts = Gemba::CLI.parse_decode(["-o", "out.mkv", "recording.grec"])
238
- assert_equal "out.mkv", opts[:output]
239
- end
240
-
241
- def test_parse_decode_codecs
242
- opts = Gemba::CLI.parse_decode(["--video-codec", "libx265", "--audio-codec", "opus", "r.grec"])
243
- assert_equal "libx265", opts[:video_codec]
244
- assert_equal "opus", opts[:audio_codec]
245
- end
246
-
247
- # -- misc --
248
-
249
- def test_reset_config_flag
250
- opts = parse(["--reset-config"])
251
- assert opts[:reset_config]
252
- end
253
-
254
- def test_yes_flag
255
- opts = parse(["-y"])
256
- assert opts[:yes]
257
- end
258
-
259
- def test_frames
260
- opts = parse(["--frames", "100", "game.gba"])
261
- assert_equal 100, opts[:frames]
262
- end
263
-
264
- def test_frames_without_value_raises
265
- assert_raises(OptionParser::MissingArgument) { parse(["--frames"]) }
266
- end
267
-
268
- def test_frames_without_rom_exits
269
- assert_raises(SystemExit) do
270
- Gemba::CLI.run(["--frames", "100"])
271
- end
272
- end
273
-
274
- def test_unknown_flag_raises
275
- assert_raises(OptionParser::InvalidOption) { parse(["--bogus"]) }
19
+ def test_main_help_lists_subcommands
20
+ help = Gemba::CLI.main_help
21
+ assert_includes help, "play"
22
+ assert_includes help, "record"
23
+ assert_includes help, "decode"
24
+ assert_includes help, "replay"
25
+ assert_includes help, "config"
26
+ assert_includes help, "version"
276
27
  end
277
28
 
278
- def test_missing_scale_value_raises
279
- assert_raises(OptionParser::MissingArgument) { parse(["--scale"]) }
29
+ def test_dry_run_main_help
30
+ result = Gemba::CLI.run(["--help"], dry_run: true)
31
+ assert_equal :help, result[:command]
280
32
  end
281
33
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/config_cmd"
6
+
7
+ class TestCLIConfig < Minitest::Test
8
+ ConfigCmd = Gemba::CLI::Commands::ConfigCmd
9
+
10
+ # -- parse --
11
+
12
+ def test_reset_flag
13
+ assert cfg(["--reset"]).parse[:reset]
14
+ end
15
+
16
+ def test_reset_with_yes
17
+ opts = cfg(["--reset", "-y"]).parse
18
+ assert opts[:reset]
19
+ assert opts[:yes]
20
+ end
21
+
22
+ def test_help
23
+ assert cfg(["--help"]).parse[:help]
24
+ end
25
+
26
+ def test_no_flags
27
+ opts = cfg([]).parse
28
+ refute opts[:reset]
29
+ refute opts[:help]
30
+ end
31
+
32
+ # -- dry_run via CLI.run dispatch --
33
+
34
+ def test_dry_run_show
35
+ result = Gemba::CLI.run(["config"], dry_run: true)
36
+ assert_equal :config_show, result[:command]
37
+ refute result[:reset]
38
+ end
39
+
40
+ def test_dry_run_reset
41
+ result = Gemba::CLI.run(["config", "--reset"], dry_run: true)
42
+ assert_equal :config_reset, result[:command]
43
+ assert result[:reset]
44
+ end
45
+
46
+ def test_dry_run_reset_yes
47
+ result = Gemba::CLI.run(["config", "--reset", "-y"], dry_run: true)
48
+ assert_equal :config_reset, result[:command]
49
+ assert result[:reset]
50
+ assert result[:yes]
51
+ end
52
+
53
+ def test_dry_run_help
54
+ result = Gemba::CLI.run(["config", "--help"], dry_run: true)
55
+ assert_equal :config, result[:command]
56
+ assert result[:help]
57
+ end
58
+
59
+ private
60
+
61
+ def cfg(argv)
62
+ ConfigCmd.new(argv)
63
+ end
64
+ end