gemba 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +24 -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 +135 -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 -1515
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "webmock"
5
+ require "gemba/headless"
6
+
7
+ RA_URL = "https://retroachievements.org/dorequest.php" unless defined?(RA_URL)
8
+
9
+ class TestCLISyncRequester < Minitest::Test
10
+ include WebMock::API
11
+
12
+ Requester = Gemba::Achievements::RetroAchievements::CliSyncRequester
13
+
14
+ def setup
15
+ WebMock.enable!
16
+ @req = Requester.new
17
+ end
18
+
19
+ def teardown
20
+ WebMock.reset!
21
+ WebMock.disable!
22
+ end
23
+
24
+ def stub_ra(body:, status: 200)
25
+ WebMock.stub_request(:post, RA_URL)
26
+ .to_return(status: status, body: body, headers: { "Content-Type" => "application/json" })
27
+ end
28
+
29
+ # -- Result interface -------------------------------------------------------
30
+
31
+ def test_result_on_progress_fires_synchronously
32
+ result = Requester::Result.new(["data", true])
33
+ received = nil
34
+ result.on_progress { |v| received = v }
35
+ assert_equal ["data", true], received
36
+ end
37
+
38
+ def test_result_on_progress_returns_self_for_chaining
39
+ r = Requester::Result.new(nil)
40
+ assert_same r, r.on_progress {}
41
+ end
42
+
43
+ def test_result_on_done_returns_self
44
+ r = Requester::Result.new(nil)
45
+ assert_same r, r.on_done {}
46
+ end
47
+
48
+ # -- Successful HTTP call ---------------------------------------------------
49
+
50
+ def test_success_returns_parsed_json_and_true
51
+ stub_ra(body: JSON.generate("Success" => true, "Token" => "abc"))
52
+
53
+ received = nil
54
+ @req.call(nil, { r: "login2", u: "user", p: "pass" })
55
+ .on_progress { |v| received = v }
56
+
57
+ assert_equal true, received[1]
58
+ assert_equal true, received[0]["Success"]
59
+ assert_equal "abc", received[0]["Token"]
60
+ end
61
+
62
+ def test_posts_to_correct_url
63
+ stub_ra(body: JSON.generate("Success" => true))
64
+
65
+ @req.call(nil, { r: "gameid", m: "deadbeef" })
66
+ .on_progress {}
67
+
68
+ assert_requested :post, RA_URL
69
+ end
70
+
71
+ def test_sends_params_as_form_data
72
+ stub_ra(body: JSON.generate("GameID" => 42))
73
+
74
+ @req.call(nil, { r: "gameid", m: "abc123" })
75
+ .on_progress {}
76
+
77
+ assert_requested :post, RA_URL, body: "r=gameid&m=abc123"
78
+ end
79
+
80
+ def test_symbol_keys_stringified
81
+ stub_ra(body: JSON.generate("Success" => true))
82
+
83
+ @req.call(nil, { r: "ping", u: "user", t: "tok", g: 42, m: "Playing" })
84
+ .on_progress {}
85
+
86
+ assert_requested :post, RA_URL,
87
+ body: hash_including("r" => "ping", "u" => "user", "t" => "tok")
88
+ end
89
+
90
+ # -- HTTP error responses ---------------------------------------------------
91
+
92
+ def test_http_404_returns_nil_false
93
+ stub_ra(body: "Not Found", status: 404)
94
+
95
+ received = nil
96
+ @req.call(nil, { r: "gameid", m: "bad" })
97
+ .on_progress { |v| received = v }
98
+
99
+ assert_equal [nil, false], received
100
+ end
101
+
102
+ def test_http_500_returns_nil_false
103
+ stub_ra(body: "Internal Server Error", status: 500)
104
+
105
+ received = nil
106
+ @req.call(nil, { r: "patch", u: "u", t: "t", g: 1 })
107
+ .on_progress { |v| received = v }
108
+
109
+ assert_equal [nil, false], received
110
+ end
111
+
112
+ # -- Network errors ---------------------------------------------------------
113
+
114
+ def test_connection_error_returns_nil_false
115
+ WebMock.stub_request(:post, RA_URL).to_raise(Errno::ECONNREFUSED)
116
+
117
+ received = nil
118
+ _, stderr = capture_io do
119
+ @req.call(nil, { r: "login2" }).on_progress { |v| received = v }
120
+ end
121
+
122
+ assert_equal [nil, false], received
123
+ assert_match(/request error/i, stderr)
124
+ end
125
+
126
+ def test_timeout_returns_nil_false
127
+ WebMock.stub_request(:post, RA_URL).to_raise(Net::ReadTimeout)
128
+
129
+ received = nil
130
+ capture_io do
131
+ @req.call(nil, { r: "login2" }).on_progress { |v| received = v }
132
+ end
133
+
134
+ assert_equal [nil, false], received
135
+ end
136
+
137
+ # -- mode: and worker: kwargs are accepted but ignored ----------------------
138
+
139
+ def test_accepts_mode_kwarg
140
+ stub_ra(body: JSON.generate("ok" => true))
141
+ assert_silent do
142
+ @req.call(nil, { r: "ping" }, mode: :ractor).on_progress {}
143
+ end
144
+ end
145
+
146
+ def test_accepts_worker_kwarg
147
+ stub_ra(body: JSON.generate("ok" => true))
148
+ assert_silent do
149
+ @req.call(nil, { r: "ping" }, worker: Object.new).on_progress {}
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/cli/commands/version"
6
+
7
+ class TestCLIVersion < Minitest::Test
8
+ Version = Gemba::CLI::Commands::Version
9
+
10
+ def test_dry_run_returns_version
11
+ result = Version.new([], dry_run: true).call
12
+ assert_equal :version, result[:command]
13
+ assert_equal Gemba::VERSION, result[:version]
14
+ end
15
+
16
+ def test_prints_version
17
+ out = capture_io { Version.new([]).call }[0]
18
+ assert_includes out, "gemba"
19
+ assert_includes out, Gemba::VERSION
20
+ end
21
+
22
+ def test_dry_run_via_dispatch
23
+ result = Gemba::CLI.run(["version"], dry_run: true)
24
+ assert_equal :version, result[:command]
25
+ assert_equal Gemba::VERSION, result[:version]
26
+ end
27
+ end
data/test/test_config.rb CHANGED
@@ -3,8 +3,7 @@
3
3
  require "minitest/autorun"
4
4
  require "tmpdir"
5
5
  require "json"
6
- require_relative "../lib/gemba/config"
7
- require_relative "../lib/gemba/version"
6
+ require "gemba/headless"
8
7
 
9
8
  class TestMGBAConfig < Minitest::Test
10
9
  def setup
@@ -747,7 +746,7 @@ class TestMGBAConfig < Minitest::Test
747
746
 
748
747
  def test_per_game_settings_constant_keys
749
748
  expected = %w[scale pixel_filter integer_scale color_correction frame_blending
750
- volume muted turbo_speed quick_save_slot save_state_backup]
749
+ volume muted turbo_speed quick_save_slot save_state_backup ra_rich_presence]
751
750
  assert_equal expected.sort, Gemba::Config::PER_GAME_SETTINGS.keys.sort
752
751
  end
753
752
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "gemba/headless"
6
+
7
+ class TestConfigRA < Minitest::Test
8
+ def setup
9
+ @dir = Dir.mktmpdir("gemba-ra-test")
10
+ @path = File.join(@dir, "settings.json")
11
+ end
12
+
13
+ def teardown
14
+ FileUtils.rm_rf(@dir)
15
+ end
16
+
17
+ def new_config
18
+ Gemba::Config.new(path: @path)
19
+ end
20
+
21
+ def test_defaults
22
+ c = new_config
23
+ refute c.ra_enabled?
24
+ assert_equal '', c.ra_username
25
+ assert_equal '', c.ra_token
26
+ refute c.ra_hardcore?
27
+ end
28
+
29
+ def test_enabled_setter
30
+ c = new_config
31
+ c.ra_enabled = true
32
+ assert c.ra_enabled?
33
+ c.ra_enabled = false
34
+ refute c.ra_enabled?
35
+ end
36
+
37
+ def test_username_setter
38
+ c = new_config
39
+ c.ra_username = 'alice'
40
+ assert_equal 'alice', c.ra_username
41
+ end
42
+
43
+ def test_token_setter
44
+ c = new_config
45
+ c.ra_token = 'abc123'
46
+ assert_equal 'abc123', c.ra_token
47
+ end
48
+
49
+ def test_hardcore_setter
50
+ c = new_config
51
+ c.ra_hardcore = true
52
+ assert c.ra_hardcore?
53
+ end
54
+
55
+ def test_persistence_roundtrip
56
+ c = new_config
57
+ c.ra_enabled = true
58
+ c.ra_username = 'alice'
59
+ c.ra_token = 'secret'
60
+ c.ra_hardcore = true
61
+ c.save!
62
+
63
+ c2 = new_config
64
+ assert c2.ra_enabled?
65
+ assert_equal 'alice', c2.ra_username
66
+ assert_equal 'secret', c2.ra_token
67
+ assert c2.ra_hardcore?
68
+ end
69
+ end
data/test/test_core.rb CHANGED
@@ -10,7 +10,6 @@ class TestMGBACore < Minitest::Test
10
10
  TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
11
11
 
12
12
  def setup
13
- skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
14
13
  @core = Gemba::Core.new(TEST_ROM)
15
14
  end
16
15
 
@@ -385,6 +384,68 @@ class TestMGBACore < Minitest::Test
385
384
  assert_raises(ArgumentError) { @core.rewind_init(-1) }
386
385
  end
387
386
 
387
+ # -- GB / GBC ROM loading ----------------------------------------------------
388
+
389
+ GB_ROM = File.expand_path("fixtures/test.gb", __dir__)
390
+ GBC_ROM = File.expand_path("fixtures/test.gbc", __dir__)
391
+
392
+ def test_gb_rom_dimensions
393
+ core = Gemba::Core.new(GB_ROM)
394
+ assert_equal 160, core.width
395
+ assert_equal 144, core.height
396
+ assert_equal "GB", core.platform
397
+ core.destroy
398
+ end
399
+
400
+ def test_gb_rom_video_buffer_size
401
+ core = Gemba::Core.new(GB_ROM)
402
+ core.run_frame
403
+ buf = core.video_buffer_argb
404
+ assert_equal 160 * 144 * 4, buf.bytesize
405
+ core.destroy
406
+ end
407
+
408
+ def test_gb_rom_title
409
+ core = Gemba::Core.new(GB_ROM)
410
+ assert_equal "GEMBAGB", core.title
411
+ core.destroy
412
+ end
413
+
414
+ def test_gb_rom_runs_frames
415
+ core = Gemba::Core.new(GB_ROM)
416
+ 10.times { core.run_frame }
417
+ assert_equal 160 * 144 * 4, core.video_buffer.bytesize
418
+ core.destroy
419
+ end
420
+
421
+ def test_gbc_rom_dimensions
422
+ core = Gemba::Core.new(GBC_ROM)
423
+ assert_equal 160, core.width
424
+ assert_equal 144, core.height
425
+ core.destroy
426
+ end
427
+
428
+ def test_gbc_rom_video_buffer_size
429
+ core = Gemba::Core.new(GBC_ROM)
430
+ core.run_frame
431
+ buf = core.video_buffer_argb
432
+ assert_equal 160 * 144 * 4, buf.bytesize
433
+ core.destroy
434
+ end
435
+
436
+ def test_gbc_rom_title
437
+ core = Gemba::Core.new(GBC_ROM)
438
+ assert_equal "GEMBAGBC", core.title
439
+ core.destroy
440
+ end
441
+
442
+ def test_gbc_rom_runs_frames
443
+ core = Gemba::Core.new(GBC_ROM)
444
+ 10.times { core.run_frame }
445
+ assert_equal 160 * 144 * 4, core.video_buffer.bytesize
446
+ core.destroy
447
+ end
448
+
388
449
  # -- Error handling ----------------------------------------------------------
389
450
 
390
451
  def test_nonexistent_file
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+
6
+ class TestCredentialsPresenter < Minitest::Test
7
+ def setup
8
+ Gemba.bus = Gemba::EventBus.new
9
+ @config = Gemba::Config.new(path: nil) # in-memory defaults
10
+ end
11
+
12
+ def presenter(**overrides)
13
+ cfg = @config
14
+ cfg.ra_enabled = overrides.fetch(:enabled, false)
15
+ cfg.ra_username = overrides.fetch(:username, '')
16
+ cfg.ra_token = overrides.fetch(:token, '')
17
+ Gemba::Achievements::CredentialsPresenter.new(cfg)
18
+ end
19
+
20
+ # -- Disabled state ----------------------------------------------------------
21
+
22
+ def test_disabled_everything_off
23
+ p = presenter(enabled: false)
24
+ assert_equal :disabled, p.fields_state
25
+ assert_equal :disabled, p.login_button_state
26
+ assert_equal :disabled, p.verify_button_state
27
+ assert_equal :disabled, p.logout_button_state
28
+ assert_equal :disabled, p.reset_button_state
29
+ assert_equal :empty, p.feedback[:key]
30
+ end
31
+
32
+ # -- Enabled, not logged in --------------------------------------------------
33
+
34
+ def test_enabled_no_token_fields_editable
35
+ p = presenter(enabled: true)
36
+ assert_equal :normal, p.fields_state
37
+ end
38
+
39
+ def test_enabled_no_token_login_disabled_when_fields_empty
40
+ p = presenter(enabled: true)
41
+ assert_equal :disabled, p.login_button_state
42
+ assert_equal :disabled, p.verify_button_state
43
+ end
44
+
45
+ def test_enabled_login_enabled_when_fields_filled
46
+ p = presenter(enabled: true)
47
+ p.username = 'alice'
48
+ p.password = 'secret'
49
+ assert_equal :normal, p.login_button_state
50
+ assert_equal :disabled, p.verify_button_state # no token yet — verify stays disabled
51
+ end
52
+
53
+ def test_enabled_login_disabled_with_only_username
54
+ p = presenter(enabled: true)
55
+ p.username = 'alice'
56
+ assert_equal :disabled, p.login_button_state
57
+ end
58
+
59
+ def test_enabled_login_disabled_with_only_password
60
+ p = presenter(enabled: true)
61
+ p.password = 'secret'
62
+ assert_equal :disabled, p.login_button_state
63
+ end
64
+
65
+ def test_enabled_no_token_logout_and_reset_disabled
66
+ p = presenter(enabled: true)
67
+ assert_equal :disabled, p.logout_button_state
68
+ assert_equal :disabled, p.reset_button_state
69
+ end
70
+
71
+ def test_enabled_no_token_feedback_not_logged_in
72
+ p = presenter(enabled: true)
73
+ assert_equal :not_logged_in, p.feedback[:key]
74
+ end
75
+
76
+ # -- Enabled, logged in (token present) --------------------------------------
77
+
78
+ def test_logged_in_fields_disabled
79
+ p = presenter(enabled: true, username: 'alice', token: 'tok123')
80
+ assert_equal :readonly, p.fields_state
81
+ end
82
+
83
+ def test_logged_in_login_disabled
84
+ p = presenter(enabled: true, username: 'alice', token: 'tok123')
85
+ assert_equal :disabled, p.login_button_state
86
+ end
87
+
88
+ def test_logged_in_verify_enabled
89
+ p = presenter(enabled: true, username: 'alice', token: 'tok123')
90
+ assert_equal :normal, p.verify_button_state
91
+ end
92
+
93
+ def test_logged_in_logout_and_reset_enabled
94
+ p = presenter(enabled: true, username: 'alice', token: 'tok123')
95
+ assert_equal :normal, p.logout_button_state
96
+ assert_equal :normal, p.reset_button_state
97
+ end
98
+
99
+ def test_logged_in_feedback_shows_username
100
+ p = presenter(enabled: true, username: 'alice', token: 'tok123')
101
+ fb = p.feedback
102
+ assert_equal :logged_in_as, fb[:key]
103
+ assert_equal 'alice', fb[:username]
104
+ end
105
+
106
+ # -- State mutations ----------------------------------------------------------
107
+
108
+ def test_enabling_clears_disabled_state
109
+ p = presenter(enabled: false)
110
+ assert_equal :disabled, p.fields_state
111
+ p.enabled = true
112
+ assert_equal :normal, p.fields_state
113
+ end
114
+
115
+ def test_credentials_changed_emitted_on_mutations
116
+ p = presenter(enabled: false)
117
+ fired = 0
118
+ Gemba.bus.on(:credentials_changed) { fired += 1 }
119
+ p.enabled = true
120
+ p.username = 'alice'
121
+ p.password = 'pw'
122
+ assert_equal 3, fired
123
+ end
124
+
125
+ # -- Auth result handling (via :ra_auth_result bus events) -------------------
126
+
127
+ def auth(status, token: nil, message: nil)
128
+ Gemba.bus.emit(:ra_auth_result, status: status, token: token, message: message)
129
+ end
130
+
131
+ def test_login_success_stores_token_and_clears_password
132
+ p = presenter(enabled: true, username: 'alice')
133
+ p.password = 'pw'
134
+ auth(:ok, token: 'real_token')
135
+ assert_equal 'real_token', p.token
136
+ assert_equal '', p.password
137
+ assert p.logged_in?
138
+ end
139
+
140
+ def test_login_success_locks_fields
141
+ p = presenter(enabled: true, username: 'alice')
142
+ auth(:ok, token: 'tok')
143
+ assert_equal :readonly, p.fields_state
144
+ assert_equal :normal, p.logout_button_state
145
+ assert_equal :normal, p.verify_button_state
146
+ end
147
+
148
+ def test_login_error_sets_feedback
149
+ p = presenter(enabled: true, username: 'alice')
150
+ auth(:error, message: 'Bad credentials')
151
+ assert_equal :error, p.feedback[:key]
152
+ assert_equal 'Bad credentials', p.feedback[:message]
153
+ end
154
+
155
+ def test_login_error_does_not_affect_button_states
156
+ p = presenter(enabled: true, username: 'alice')
157
+ p.password = 'pw'
158
+ auth(:error, message: 'Bad credentials')
159
+ # Fields and login button should still be enabled
160
+ assert_equal :normal, p.fields_state
161
+ assert_equal :normal, p.login_button_state
162
+ end
163
+
164
+ def test_logout_clears_token_keeps_username
165
+ p = presenter(enabled: true, username: 'alice', token: 'tok')
166
+ auth(:logout)
167
+ assert_equal '', p.token
168
+ assert_equal 'alice', p.username # kept so user can re-enter password
169
+ refute p.logged_in?
170
+ end
171
+
172
+ def test_logout_re_enables_fields
173
+ p = presenter(enabled: true, username: 'alice', token: 'tok')
174
+ auth(:logout)
175
+ assert_equal :normal, p.fields_state
176
+ end
177
+
178
+ # -- Transient feedback -------------------------------------------------------
179
+
180
+ def test_transient_overrides_normal_feedback
181
+ p = presenter(enabled: true, username: 'alice', token: 'tok')
182
+ p.show_transient(:test_ok)
183
+ assert_equal :test_ok, p.feedback[:key]
184
+ end
185
+
186
+ def test_clear_transient_restores_normal_feedback
187
+ p = presenter(enabled: true, username: 'alice', token: 'tok')
188
+ p.show_transient(:test_ok)
189
+ p.clear_transient
190
+ assert_equal :logged_in_as, p.feedback[:key]
191
+ end
192
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+
6
+ class TestEventBus < Minitest::Test
7
+ def setup
8
+ @bus = Gemba::EventBus.new
9
+ end
10
+
11
+ def test_on_and_emit
12
+ received = nil
13
+ @bus.on(:ping) { |val| received = val }
14
+ @bus.emit(:ping, 42)
15
+ assert_equal 42, received
16
+ end
17
+
18
+ def test_emit_with_no_subscribers_is_noop
19
+ @bus.emit(:ghost, 1, 2, 3) # should not raise
20
+ end
21
+
22
+ def test_multiple_subscribers
23
+ results = []
24
+ @bus.on(:tick) { |v| results << "a:#{v}" }
25
+ @bus.on(:tick) { |v| results << "b:#{v}" }
26
+ @bus.emit(:tick, 7)
27
+ assert_equal ["a:7", "b:7"], results
28
+ end
29
+
30
+ def test_different_events_are_independent
31
+ a = nil
32
+ b = nil
33
+ @bus.on(:foo) { |v| a = v }
34
+ @bus.on(:bar) { |v| b = v }
35
+ @bus.emit(:foo, 1)
36
+ assert_equal 1, a
37
+ assert_nil b
38
+ end
39
+
40
+ def test_emit_multiple_args
41
+ received = nil
42
+ @bus.on(:multi) { |x, y| received = [x, y] }
43
+ @bus.emit(:multi, :a, :b)
44
+ assert_equal [:a, :b], received
45
+ end
46
+
47
+ def test_emit_with_kwargs
48
+ received = nil
49
+ @bus.on(:kw) { |name:, val:| received = { name: name, val: val } }
50
+ @bus.emit(:kw, name: "scale", val: 3)
51
+ assert_equal({ name: "scale", val: 3 }, received)
52
+ end
53
+
54
+ def test_off_removes_subscriber
55
+ received = []
56
+ block = @bus.on(:evt) { |v| received << v }
57
+ @bus.emit(:evt, 1)
58
+ @bus.off(:evt, block)
59
+ @bus.emit(:evt, 2)
60
+ assert_equal [1], received
61
+ end
62
+
63
+ def test_on_returns_block_for_later_off
64
+ block = @bus.on(:x) { }
65
+ assert_instance_of Proc, block
66
+ end
67
+
68
+ # -- Module-level accessor ------------------------------------------------
69
+
70
+ def test_gemba_bus_auto_creates
71
+ Gemba.bus = nil
72
+ bus = Gemba.bus
73
+ assert_instance_of Gemba::EventBus, bus
74
+ ensure
75
+ Gemba.bus = nil
76
+ end
77
+
78
+ def test_gemba_bus_setter
79
+ custom = Gemba::EventBus.new
80
+ Gemba.bus = custom
81
+ assert_same custom, Gemba.bus
82
+ ensure
83
+ Gemba.bus = nil
84
+ end
85
+
86
+ # -- BusEmitter mixin -----------------------------------------------------
87
+
88
+ def test_bus_emitter_emits_to_gemba_bus
89
+ Gemba.bus = @bus
90
+ klass = Class.new { include Gemba::BusEmitter; public :emit }
91
+ obj = klass.new
92
+
93
+ received = nil
94
+ @bus.on(:test_event) { |v| received = v }
95
+ obj.emit(:test_event, 99)
96
+ assert_equal 99, received
97
+ ensure
98
+ Gemba.bus = nil
99
+ end
100
+ end