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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/decode"
6
+
7
+ class TestCLIDecode < Minitest::Test
8
+ Decode = Gemba::CLI::Commands::Decode
9
+ # -- parse --
10
+
11
+ def test_grec_file
12
+ opts = dec(["recording.grec"]).parse
13
+ assert_equal "recording.grec", opts[:grec]
14
+ end
15
+
16
+ def test_output
17
+ opts = dec(["-o", "out.mkv", "recording.grec"]).parse
18
+ assert_equal "out.mkv", opts[:output]
19
+ end
20
+
21
+ def test_codecs
22
+ opts = dec(["--video-codec", "libx265", "--audio-codec", "opus", "r.grec"]).parse
23
+ assert_equal "libx265", opts[:video_codec]
24
+ assert_equal "opus", opts[:audio_codec]
25
+ end
26
+
27
+ def test_stats
28
+ opts = dec(["--stats", "recording.grec"]).parse
29
+ assert opts[:stats]
30
+ assert_equal "recording.grec", opts[:grec]
31
+ end
32
+
33
+ def test_list
34
+ opts = dec(["--list"]).parse
35
+ assert opts[:list]
36
+ end
37
+
38
+ def test_list_short
39
+ opts = dec(["-l"]).parse
40
+ assert opts[:list]
41
+ end
42
+
43
+ def test_no_progress
44
+ opts = dec(["--no-progress", "r.grec"]).parse
45
+ assert_equal false, opts[:progress]
46
+ end
47
+
48
+ def test_help
49
+ opts = dec(["--help"]).parse
50
+ assert opts[:help]
51
+ end
52
+
53
+ # -- dry_run via CLI.run dispatch --
54
+
55
+ def test_dry_run_no_args_lists
56
+ result = Gemba::CLI.run(["decode"], dry_run: true)
57
+ assert_equal :decode_list, result[:command]
58
+ end
59
+
60
+ def test_dry_run
61
+ result = Gemba::CLI.run(["decode", "recording.grec"], dry_run: true)
62
+ assert_equal :decode, result[:command]
63
+ assert_equal "recording.grec", result[:grec]
64
+ refute result[:stats]
65
+ end
66
+
67
+ def test_dry_run_stats
68
+ result = Gemba::CLI.run(["decode", "--stats", "recording.grec"], dry_run: true)
69
+ assert_equal :decode_stats, result[:command]
70
+ assert result[:stats]
71
+ assert_equal "recording.grec", result[:grec]
72
+ end
73
+
74
+ def test_dry_run_with_codecs
75
+ result = Gemba::CLI.run(["decode", "--video-codec", "libx265", "--audio-codec", "opus", "r.grec"], dry_run: true)
76
+ assert_equal :decode, result[:command]
77
+ assert_equal "libx265", result[:video_codec]
78
+ assert_equal "opus", result[:audio_codec]
79
+ end
80
+
81
+ def test_dry_run_list
82
+ result = Gemba::CLI.run(["decode", "--list"], dry_run: true)
83
+ assert_equal :decode_list, result[:command]
84
+ end
85
+
86
+ def test_dry_run_help
87
+ result = Gemba::CLI.run(["decode", "--help"], dry_run: true)
88
+ assert_equal :decode, result[:command]
89
+ assert result[:help]
90
+ end
91
+
92
+ private
93
+
94
+ def dec(argv)
95
+ Decode.new(argv)
96
+ end
97
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/patch"
6
+
7
+ class TestCLIPatch < Minitest::Test
8
+ Patch = Gemba::CLI::Commands::Patch
9
+
10
+ # -- parse --
11
+
12
+ def test_rom_and_patch_args
13
+ opts = pat(["game.gba", "fix.ips"]).parse
14
+ assert_equal File.expand_path("game.gba"), opts[:rom]
15
+ assert_equal File.expand_path("fix.ips"), opts[:patch]
16
+ end
17
+
18
+ def test_output_flag
19
+ opts = pat(["-o", "/tmp/out.gba", "game.gba", "fix.ips"]).parse
20
+ assert_equal File.expand_path("/tmp/out.gba"), opts[:output]
21
+ end
22
+
23
+ def test_help
24
+ assert pat(["--help"]).parse[:help]
25
+ end
26
+
27
+ def test_missing_args_returns_error
28
+ result = pat([]).call
29
+ assert_equal :patch, result[:command]
30
+ assert_equal :missing_args, result[:error]
31
+ end
32
+
33
+ # -- dry_run via CLI.run dispatch --
34
+
35
+ def test_dry_run
36
+ result = Gemba::CLI.run(["patch", "game.gba", "fix.ips"], dry_run: true)
37
+ assert_equal :patch, result[:command]
38
+ assert_equal File.expand_path("game.gba"), result[:rom]
39
+ assert_equal File.expand_path("fix.ips"), result[:patch]
40
+ end
41
+
42
+ def test_dry_run_default_out_path
43
+ result = Gemba::CLI.run(["patch", "game.gba", "fix.ips"], dry_run: true)
44
+ assert_match(/-patched\.gba\z/, result[:out])
45
+ end
46
+
47
+ def test_dry_run_help
48
+ result = Gemba::CLI.run(["patch", "--help"], dry_run: true)
49
+ assert_equal :patch, result[:command]
50
+ assert result[:help]
51
+ end
52
+
53
+ private
54
+
55
+ def pat(argv)
56
+ Patch.new(argv)
57
+ end
58
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/play"
6
+
7
+ class TestCLIPlay < Minitest::Test
8
+ Play = Gemba::CLI::Commands::Play
9
+ # -- ROM argument --
10
+
11
+ def test_rom_path
12
+ assert_equal File.expand_path("game.gba"), play(["game.gba"]).parse[:rom]
13
+ end
14
+
15
+ def test_rom_path_expands_tilde
16
+ assert_equal File.join(Dir.home, "game.gba"), play(["~/game.gba"]).parse[:rom]
17
+ end
18
+
19
+ def test_no_rom
20
+ assert_nil play([]).parse[:rom]
21
+ end
22
+
23
+ # -- flags --
24
+
25
+ def test_scale
26
+ assert_equal 2, play(["--scale", "2"]).parse[:scale]
27
+ end
28
+
29
+ def test_scale_short
30
+ assert_equal 3, play(["-s", "3"]).parse[:scale]
31
+ end
32
+
33
+ def test_scale_clamps_high
34
+ assert_equal 4, play(["--scale", "10"]).parse[:scale]
35
+ end
36
+
37
+ def test_scale_clamps_low
38
+ assert_equal 1, play(["--scale", "0"]).parse[:scale]
39
+ end
40
+
41
+ def test_volume
42
+ assert_equal 50, play(["--volume", "50"]).parse[:volume]
43
+ end
44
+
45
+ def test_volume_short
46
+ assert_equal 75, play(["-v", "75"]).parse[:volume]
47
+ end
48
+
49
+ def test_volume_clamps
50
+ assert_equal 100, play(["--volume", "200"]).parse[:volume]
51
+ end
52
+
53
+ def test_mute
54
+ assert play(["--mute"]).parse[:mute]
55
+ end
56
+
57
+ def test_mute_short
58
+ assert play(["-m"]).parse[:mute]
59
+ end
60
+
61
+ def test_no_sound
62
+ assert_equal false, play(["--no-sound"]).parse[:sound]
63
+ end
64
+
65
+ def test_fullscreen
66
+ assert play(["--fullscreen"]).parse[:fullscreen]
67
+ end
68
+
69
+ def test_fullscreen_short
70
+ assert play(["-f"]).parse[:fullscreen]
71
+ end
72
+
73
+ def test_show_fps
74
+ assert play(["--show-fps"]).parse[:show_fps]
75
+ end
76
+
77
+ def test_turbo_speed
78
+ assert_equal 3, play(["--turbo-speed", "3"]).parse[:turbo_speed]
79
+ end
80
+
81
+ def test_turbo_speed_clamps
82
+ assert_equal 4, play(["--turbo-speed", "99"]).parse[:turbo_speed]
83
+ end
84
+
85
+ def test_locale
86
+ assert_equal "ja", play(["--locale", "ja"]).parse[:locale]
87
+ end
88
+
89
+ def test_help_flag
90
+ assert play(["--help"]).parse[:help]
91
+ end
92
+
93
+ def test_flags_with_rom
94
+ opts = play(["-s", "2", "--mute", "pokemon.gba"]).parse
95
+ assert_equal 2, opts[:scale]
96
+ assert opts[:mute]
97
+ assert_equal File.expand_path("pokemon.gba"), opts[:rom]
98
+ end
99
+
100
+ def test_rom_before_flags
101
+ opts = play(["game.gba", "--scale", "4"]).parse
102
+ assert_equal File.expand_path("game.gba"), opts[:rom]
103
+ assert_equal 4, opts[:scale]
104
+ end
105
+
106
+ def test_parser_present
107
+ assert_kind_of OptionParser, play([]).parse[:parser]
108
+ end
109
+
110
+ def test_help_output_includes_banner
111
+ assert_includes play([]).parse[:parser].to_s, "Usage: gemba"
112
+ end
113
+
114
+ # -- apply --
115
+
116
+ class MockConfig
117
+ attr_accessor :scale, :volume, :muted, :show_fps, :turbo_speed, :locale
118
+
119
+ def initialize
120
+ @scale = 3
121
+ @volume = 100
122
+ @muted = false
123
+ @show_fps = false
124
+ @turbo_speed = 0
125
+ @locale = 'auto'
126
+ end
127
+ end
128
+
129
+ def test_apply_overrides_config
130
+ config = MockConfig.new
131
+ play([]).apply(config, { scale: 2, volume: 50, mute: true, show_fps: true })
132
+ assert_equal 2, config.scale
133
+ assert_equal 50, config.volume
134
+ assert config.muted
135
+ assert config.show_fps
136
+ end
137
+
138
+ def test_apply_skips_unset_options
139
+ config = MockConfig.new
140
+ play([]).apply(config, {})
141
+ assert_equal 3, config.scale
142
+ assert_equal 100, config.volume
143
+ refute config.muted
144
+ end
145
+
146
+ def test_apply_locale
147
+ config = MockConfig.new
148
+ play([]).apply(config, { locale: 'ja' })
149
+ assert_equal 'ja', config.locale
150
+ end
151
+
152
+ def test_apply_turbo_speed
153
+ config = MockConfig.new
154
+ play([]).apply(config, { turbo_speed: 3 })
155
+ assert_equal 3, config.turbo_speed
156
+ end
157
+
158
+ # -- dry_run via CLI.run dispatch --
159
+
160
+ def test_dry_run_no_args
161
+ result = Gemba::CLI.run([], dry_run: true)
162
+ assert_equal :play, result[:command]
163
+ assert_nil result[:rom]
164
+ assert_equal true, result[:sound]
165
+ end
166
+
167
+ def test_dry_run_with_rom
168
+ result = Gemba::CLI.run(["game.gba"], dry_run: true)
169
+ assert_equal :play, result[:command]
170
+ assert_equal File.expand_path("game.gba"), result[:rom]
171
+ end
172
+
173
+ def test_dry_run_explicit_play
174
+ result = Gemba::CLI.run(["play", "-s", "2", "game.gba"], dry_run: true)
175
+ assert_equal :play, result[:command]
176
+ assert_equal File.expand_path("game.gba"), result[:rom]
177
+ assert_equal 2, result[:options][:scale]
178
+ end
179
+
180
+ def test_dry_run_no_sound
181
+ result = Gemba::CLI.run(["--no-sound", "game.gba"], dry_run: true)
182
+ assert_equal :play, result[:command]
183
+ assert_equal false, result[:sound]
184
+ end
185
+
186
+ def test_dry_run_fullscreen
187
+ result = Gemba::CLI.run(["-f", "game.gba"], dry_run: true)
188
+ assert_equal :play, result[:command]
189
+ assert_equal true, result[:fullscreen]
190
+ end
191
+
192
+ def test_dry_run_help
193
+ result = Gemba::CLI.run(["play", "--help"], dry_run: true)
194
+ assert_equal :play, result[:command]
195
+ assert result[:help]
196
+ end
197
+
198
+ # -- error cases --
199
+
200
+ def test_unknown_flag_raises
201
+ assert_raises(OptionParser::InvalidOption) { play(["--bogus"]).parse }
202
+ end
203
+
204
+ def test_missing_scale_value_raises
205
+ assert_raises(OptionParser::MissingArgument) { play(["--scale"]).parse }
206
+ end
207
+
208
+ private
209
+
210
+ def play(argv)
211
+ Play.new(argv)
212
+ end
213
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "gemba/headless"
6
+ require "gemba/cli/commands/retro_achievements"
7
+ require_relative "support/fake_requester"
8
+
9
+ # Tests for the `gemba ra` CLI subcommand.
10
+ #
11
+ # All tests use:
12
+ # - FakeRequester so no real HTTP calls are made
13
+ # - A temp Config so real credentials on disk are never read or written
14
+ class TestCLIRa < Minitest::Test
15
+ RA = Gemba::CLI::Commands::RetroAchievements
16
+
17
+ def setup
18
+ @tmpdir = Dir.mktmpdir("gemba_cli_ra_test")
19
+ @config = Gemba::Config.new(path: File.join(@tmpdir, "settings.json"))
20
+ @req = FakeRequester.new
21
+ end
22
+
23
+ def teardown
24
+ FileUtils.rm_rf(@tmpdir)
25
+ end
26
+
27
+ def run_ra(argv, dry_run: false)
28
+ RA.new(argv, dry_run: dry_run, config: @config, requester: @req).call
29
+ end
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # parse / dry_run
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def test_parse_login_returns_subcommand_and_username
36
+ result = run_ra(["login", "--username", "bob"], dry_run: true)
37
+ assert_equal :ra_login, result[:command]
38
+ assert_equal :login, result[:subcommand]
39
+ assert_equal "bob", result[:username]
40
+ end
41
+
42
+ def test_parse_login_captures_password
43
+ result = run_ra(["login", "--username", "bob", "--password", "s3cr3t"], dry_run: true)
44
+ assert_equal "s3cr3t", result[:password]
45
+ end
46
+
47
+ def test_parse_verify_returns_subcommand
48
+ result = run_ra(["verify"], dry_run: true)
49
+ assert_equal :ra_verify, result[:command]
50
+ end
51
+
52
+ def test_parse_logout_returns_subcommand
53
+ result = run_ra(["logout"], dry_run: true)
54
+ assert_equal :ra_logout, result[:command]
55
+ end
56
+
57
+ def test_parse_achievements_captures_rom_and_json
58
+ result = run_ra(["achievements", "--rom", "/tmp/game.gba", "--json"], dry_run: true)
59
+ assert_equal :ra_achievements, result[:command]
60
+ assert_equal File.expand_path("/tmp/game.gba"), result[:rom]
61
+ assert result[:json]
62
+ end
63
+
64
+ def test_parse_no_subcommand_returns_help
65
+ result = run_ra([], dry_run: true)
66
+ assert_equal :ra, result[:command]
67
+ assert result[:help]
68
+ end
69
+
70
+ def test_parse_unknown_subcommand_returns_help
71
+ result = run_ra(["frobnicate"], dry_run: true)
72
+ assert result[:help]
73
+ end
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # ra login
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def test_login_success_saves_credentials
80
+ @req.stub(r: "login2", body: { "Success" => true, "Token" => "tok123" })
81
+
82
+ out, = capture_io do
83
+ run_ra(["login", "--username", "bob", "--password", "pass"])
84
+ end
85
+
86
+ assert_match(/logged in as bob/i, out)
87
+ assert_equal "bob", @config.ra_username
88
+ assert_equal "tok123", @config.ra_token
89
+ assert @config.ra_enabled?
90
+
91
+ # Verify persisted to disk
92
+ reloaded = Gemba::Config.new(path: @config.instance_variable_get(:@path))
93
+ assert_equal "bob", reloaded.ra_username
94
+ assert_equal "tok123", reloaded.ra_token
95
+ end
96
+
97
+ def test_login_failure_prints_error_and_exits
98
+ @req.stub(r: "login2", body: { "Success" => false, "Error" => "Bad password" }, ok: false)
99
+
100
+ assert_raises(SystemExit) do
101
+ capture_io { run_ra(["login", "--username", "bob", "--password", "wrong"]) }
102
+ end
103
+
104
+ assert_empty @config.ra_username, "credentials must not be saved on failure"
105
+ end
106
+
107
+ def test_login_missing_username_exits
108
+ assert_raises(SystemExit) do
109
+ capture_io { run_ra(["login", "--password", "pass"]) }
110
+ end
111
+ end
112
+
113
+ def test_login_posts_to_login2
114
+ @req.stub(r: "login2", body: { "Success" => true, "Token" => "t" })
115
+ capture_io { run_ra(["login", "--username", "bob", "--password", "p"]) }
116
+ assert @req.requested?("login2")
117
+ assert_equal "bob", @req.requests_for("login2").first[:u]
118
+ end
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # ra verify
122
+ # ---------------------------------------------------------------------------
123
+
124
+ def test_verify_success_prints_ok
125
+ @config.ra_username = "bob"
126
+ @config.ra_token = "tok"
127
+ @req.stub(r: "login2", body: { "Success" => true })
128
+
129
+ out, = capture_io { run_ra(["verify"]) }
130
+ assert_match(/token valid for bob/i, out)
131
+ end
132
+
133
+ def test_verify_failure_exits
134
+ @config.ra_username = "bob"
135
+ @config.ra_token = "bad"
136
+ @req.stub(r: "login2", body: { "Success" => false, "Error" => "Token invalid" }, ok: false)
137
+
138
+ assert_raises(SystemExit) do
139
+ capture_io { run_ra(["verify"]) }
140
+ end
141
+ end
142
+
143
+ def test_verify_not_logged_in_exits
144
+ assert_raises(SystemExit) do
145
+ capture_io { run_ra(["verify"]) }
146
+ end
147
+ end
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # ra logout
151
+ # ---------------------------------------------------------------------------
152
+
153
+ def test_logout_clears_credentials
154
+ @config.ra_username = "bob"
155
+ @config.ra_token = "tok"
156
+ @config.ra_enabled = true
157
+ @config.save!
158
+
159
+ out, = capture_io { run_ra(["logout"]) }
160
+
161
+ assert_match(/logged out/i, out)
162
+ assert_empty @config.ra_username
163
+ assert_empty @config.ra_token
164
+ refute @config.ra_enabled?
165
+
166
+ reloaded = Gemba::Config.new(path: @config.instance_variable_get(:@path))
167
+ assert_empty reloaded.ra_username
168
+ assert_empty reloaded.ra_token
169
+ end
170
+
171
+ def test_logout_makes_no_http_requests
172
+ capture_io { run_ra(["logout"]) }
173
+ assert_empty @req.requests
174
+ end
175
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/record"
6
+
7
+ class TestCLIRecord < Minitest::Test
8
+ Record = Gemba::CLI::Commands::Record
9
+ # -- parse --
10
+
11
+ def test_frames_and_rom
12
+ opts = rec(["--frames", "100", "game.gba"]).parse
13
+ assert_equal 100, opts[:frames]
14
+ assert_equal File.expand_path("game.gba"), opts[:rom]
15
+ end
16
+
17
+ def test_output
18
+ opts = rec(["-o", "out.grec", "--frames", "10", "game.gba"]).parse
19
+ assert_equal "out.grec", opts[:output]
20
+ end
21
+
22
+ def test_compression
23
+ opts = rec(["-c", "6", "--frames", "10", "game.gba"]).parse
24
+ assert_equal 6, opts[:compression]
25
+ end
26
+
27
+ def test_compression_clamps
28
+ opts = rec(["--compression", "99", "--frames", "10", "game.gba"]).parse
29
+ assert_equal 9, opts[:compression]
30
+ end
31
+
32
+ def test_progress_flag
33
+ opts = rec(["--progress", "--frames", "10", "game.gba"]).parse
34
+ assert opts[:progress]
35
+ end
36
+
37
+ def test_help
38
+ opts = rec(["--help"]).parse
39
+ assert opts[:help]
40
+ end
41
+
42
+ # -- dry_run via CLI.run dispatch --
43
+
44
+ def test_dry_run
45
+ result = Gemba::CLI.run(["record", "--frames", "100", "game.gba"], dry_run: true)
46
+ assert_equal :record, result[:command]
47
+ assert_equal File.expand_path("game.gba"), result[:rom]
48
+ assert_equal 100, result[:frames]
49
+ end
50
+
51
+ def test_dry_run_with_output
52
+ result = Gemba::CLI.run(["record", "--frames", "50", "-o", "out.grec", "game.gba"], dry_run: true)
53
+ assert_equal :record, result[:command]
54
+ assert_equal "out.grec", result[:output]
55
+ assert_equal 50, result[:frames]
56
+ end
57
+
58
+ def test_dry_run_help
59
+ result = Gemba::CLI.run(["record", "--help"], dry_run: true)
60
+ assert_equal :record, result[:command]
61
+ assert result[:help]
62
+ end
63
+
64
+ private
65
+
66
+ def rec(argv)
67
+ Record.new(argv)
68
+ end
69
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/replay"
6
+
7
+ class TestCLIReplay < Minitest::Test
8
+ Replay = Gemba::CLI::Commands::Replay
9
+ # -- parse --
10
+
11
+ def test_gir_file
12
+ opts = rep(["session.gir"]).parse
13
+ assert_equal "session.gir", opts[:gir]
14
+ end
15
+
16
+ def test_headless
17
+ opts = rep(["--headless", "session.gir"]).parse
18
+ assert opts[:headless]
19
+ end
20
+
21
+ def test_list
22
+ opts = rep(["--list"]).parse
23
+ assert opts[:list]
24
+ end
25
+
26
+ def test_fullscreen
27
+ opts = rep(["-f", "session.gir"]).parse
28
+ assert opts[:fullscreen]
29
+ end
30
+
31
+ def test_no_sound
32
+ opts = rep(["--no-sound", "session.gir"]).parse
33
+ assert_equal false, opts[:sound]
34
+ end
35
+
36
+ def test_help
37
+ opts = rep(["--help"]).parse
38
+ assert opts[:help]
39
+ end
40
+
41
+ # -- dry_run via CLI.run dispatch --
42
+
43
+ def test_dry_run
44
+ result = Gemba::CLI.run(["replay", "session.gir"], dry_run: true)
45
+ assert_equal :replay, result[:command]
46
+ assert_equal File.expand_path("session.gir"), result[:gir]
47
+ assert_equal true, result[:sound]
48
+ end
49
+
50
+ def test_dry_run_headless
51
+ result = Gemba::CLI.run(["replay", "--headless", "session.gir"], dry_run: true)
52
+ assert_equal :replay_headless, result[:command]
53
+ assert result[:headless]
54
+ end
55
+
56
+ def test_dry_run_list
57
+ result = Gemba::CLI.run(["replay", "--list"], dry_run: true)
58
+ assert_equal :replay_list, result[:command]
59
+ end
60
+
61
+ def test_dry_run_help
62
+ result = Gemba::CLI.run(["replay", "--help"], dry_run: true)
63
+ assert_equal :replay, result[:command]
64
+ assert result[:help]
65
+ end
66
+
67
+ private
68
+
69
+ def rep(argv)
70
+ Replay.new(argv)
71
+ end
72
+ end