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,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+ require "gemba/headless"
7
+
8
+ class TestLogging < Minitest::Test
9
+ def setup
10
+ @dir = Dir.mktmpdir("gemba-logs-test")
11
+ @logger = Gemba::SessionLogger.new(dir: @dir, level: :info)
12
+ end
13
+
14
+ def teardown
15
+ Gemba.logger = nil
16
+ FileUtils.rm_rf(@dir)
17
+ end
18
+
19
+ # -- lazy file creation --
20
+
21
+ def test_no_file_before_first_log
22
+ assert_empty Dir.glob(File.join(@dir, "*.log"))
23
+ end
24
+
25
+ def test_file_created_on_first_log
26
+ @logger.log(:info) { "hello" }
27
+ logs = Dir.glob(File.join(@dir, "*.log"))
28
+ assert_equal 1, logs.length
29
+ end
30
+
31
+ def test_file_named_by_date
32
+ @logger.log(:info) { "hello" }
33
+ logs = Dir.glob(File.join(@dir, "*.log"))
34
+ assert_match(/gemba-\d{4}-\d{2}-\d{2}\.log/, File.basename(logs.first))
35
+ end
36
+
37
+ # -- level filtering --
38
+
39
+ def test_filters_below_level
40
+ @logger.log(:debug) { "should not appear" }
41
+ assert_empty Dir.glob(File.join(@dir, "*.log")),
42
+ "Debug message should not create log file at info level"
43
+ end
44
+
45
+ def test_allows_at_level
46
+ @logger.log(:info) { "visible" }
47
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
48
+ assert_includes content, "visible"
49
+ end
50
+
51
+ def test_allows_above_level
52
+ @logger.log(:error) { "bad thing" }
53
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
54
+ assert_includes content, "bad thing"
55
+ end
56
+
57
+ def test_debug_level_allows_debug
58
+ logger = Gemba::SessionLogger.new(dir: @dir, level: :debug)
59
+ logger.log(:debug) { "debug msg" }
60
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
61
+ assert_includes content, "debug msg"
62
+ end
63
+
64
+ # -- log format --
65
+
66
+ def test_log_format
67
+ @logger.log(:info) { "test message" }
68
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
69
+ assert_match(/\d{2}:\d{2}:\d{2}\.\d{3} \[INFO\] test message/, content)
70
+ end
71
+
72
+ # -- auto-prune --
73
+
74
+ def test_prune_keeps_max_files
75
+ # Create 30 fake log files
76
+ 30.times do |i|
77
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "old")
78
+ end
79
+
80
+ # New logger prunes on init
81
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
82
+
83
+ remaining = Dir.glob(File.join(@dir, "gemba-*.log"))
84
+ assert_equal Gemba::SessionLogger::MAX_LOG_FILES, remaining.length
85
+ end
86
+
87
+ def test_prune_keeps_newest
88
+ 30.times do |i|
89
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "old")
90
+ end
91
+
92
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
93
+
94
+ remaining = Dir.glob(File.join(@dir, "gemba-*.log")).sort
95
+ # Should keep the last 25 (days 06-30)
96
+ assert_equal "gemba-2026-01-06.log", File.basename(remaining.first)
97
+ assert_equal "gemba-2026-01-30.log", File.basename(remaining.last)
98
+ end
99
+
100
+ def test_prune_no_op_when_under_limit
101
+ 3.times do |i|
102
+ File.write(File.join(@dir, "gemba-2026-01-#{format('%02d', i + 1)}.log"), "ok")
103
+ end
104
+
105
+ Gemba::SessionLogger.new(dir: @dir, level: :info)
106
+ assert_equal 3, Dir.glob(File.join(@dir, "gemba-*.log")).length
107
+ end
108
+
109
+ # -- module interface --
110
+
111
+ def test_gemba_log_module_method
112
+ Gemba.logger = Gemba::SessionLogger.new(dir: @dir, level: :info)
113
+ Gemba.log(:info) { "module test" }
114
+ content = File.read(Dir.glob(File.join(@dir, "*.log")).first)
115
+ assert_includes content, "module test"
116
+ end
117
+
118
+ def test_gemba_logger_setter
119
+ custom = Gemba::SessionLogger.new(dir: @dir, level: :warn)
120
+ Gemba.logger = custom
121
+ assert_same custom, Gemba.logger
122
+ end
123
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "gemba/achievements"
6
+ require_relative "support/fake_core"
7
+
8
+ class TestNullBackend < Minitest::Test
9
+ def setup
10
+ @b = Gemba::Achievements::NullBackend.new
11
+ end
12
+
13
+ def test_not_enabled
14
+ refute @b.enabled?
15
+ end
16
+
17
+ def test_not_authenticated
18
+ refute @b.authenticated?
19
+ end
20
+
21
+ def test_achievement_list_empty
22
+ assert_equal [], @b.achievement_list
23
+ end
24
+
25
+ def test_counts_zero
26
+ assert_equal 0, @b.earned_count
27
+ assert_equal 0, @b.total_count
28
+ end
29
+
30
+ def test_do_frame_is_noop
31
+ assert_nil @b.do_frame(FakeCore.new)
32
+ end
33
+
34
+ def test_login_noop
35
+ assert_nil @b.login_with_token(username: 'u', token: 't')
36
+ refute @b.authenticated?
37
+ end
38
+
39
+ def test_logout_noop
40
+ assert_nil @b.logout
41
+ end
42
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+
6
+ class TestOfflineBackend < Minitest::Test
7
+ ROM = "test/fixtures/test.gba"
8
+
9
+ def setup
10
+ skip "test.gba fixture not found" unless File.exist?(ROM)
11
+ @unlocked = []
12
+ @backend = Gemba::Achievements::OfflineBackend.new
13
+ @backend.on_unlock { |ach| @unlocked << ach }
14
+ end
15
+
16
+ def test_always_authenticated
17
+ assert @backend.authenticated?
18
+ end
19
+
20
+ def test_enabled
21
+ assert @backend.enabled?
22
+ end
23
+
24
+ def test_login_and_logout_are_noops
25
+ @backend.login_with_password(username: "anyone", password: "anything")
26
+ assert @backend.authenticated?
27
+ @backend.logout
28
+ assert @backend.authenticated?
29
+ end
30
+
31
+ def test_on_load_achievement_fires_during_load_game
32
+ Gemba::HeadlessPlayer.open(ROM) do |player|
33
+ @backend.load_game(player.core)
34
+ assert_equal 1, @unlocked.size
35
+ assert_equal "gembatest_loaded", @unlocked.first.id
36
+ assert_equal "Ready to Play", @unlocked.first.title
37
+ assert @unlocked.first.earned?
38
+ end
39
+ end
40
+
41
+ def test_achievement_list_shows_earned_after_load
42
+ Gemba::HeadlessPlayer.open(ROM) do |player|
43
+ @backend.load_game(player.core)
44
+ list = @backend.achievement_list
45
+ assert_equal 1, list.size
46
+ assert list.first.earned?
47
+ end
48
+ end
49
+
50
+ def test_counts
51
+ Gemba::HeadlessPlayer.open(ROM) do |player|
52
+ assert_equal 0, @backend.total_count
53
+ @backend.load_game(player.core)
54
+ assert_equal 1, @backend.total_count
55
+ assert_equal 1, @backend.earned_count
56
+ end
57
+ end
58
+
59
+ def test_unload_game_clears_state
60
+ Gemba::HeadlessPlayer.open(ROM) do |player|
61
+ @backend.load_game(player.core)
62
+ assert_equal 1, @backend.earned_count
63
+ @backend.unload_game
64
+ assert_equal 0, @backend.total_count
65
+ assert_equal 0, @backend.earned_count
66
+ end
67
+ end
68
+
69
+ def test_unknown_rom_has_no_achievements
70
+ Gemba::HeadlessPlayer.open(ROM) do |player|
71
+ custom = Gemba::Achievements::OfflineBackend.new(db: {})
72
+ custom.load_game(player.core)
73
+ assert_equal 0, custom.total_count
74
+ assert_empty @unlocked
75
+ end
76
+ end
77
+
78
+ def test_store_adds_definitions
79
+ Gemba::HeadlessPlayer.open(ROM) do |player|
80
+ custom = Gemba::Achievements::OfflineBackend.new(db: {})
81
+ custom.on_unlock { |a| @unlocked << a }
82
+ custom.store(player.core.checksum, [
83
+ { id: "extra", title: "Extra", description: "desc",
84
+ points: 5, trigger: :on_load }
85
+ ])
86
+ custom.load_game(player.core)
87
+ assert_equal 1, @unlocked.size
88
+ assert_equal "extra", @unlocked.first.id
89
+ end
90
+ end
91
+
92
+ def test_memory_achievement_fires_on_rising_edge
93
+ addr = 0x02000000
94
+ Gemba::HeadlessPlayer.open(ROM) do |player|
95
+ backend = Gemba::Achievements::OfflineBackend.new(db: {
96
+ player.core.checksum => [
97
+ { id: "mem_test", title: "Mem", description: "d", points: 2,
98
+ trigger: :memory,
99
+ condition: ->(mem) { mem.call(addr) == 0x01 } }
100
+ ]
101
+ })
102
+ backend.on_unlock { |a| @unlocked << a }
103
+ backend.load_game(player.core)
104
+
105
+ # EWRAM starts zeroed — condition false
106
+ player.step(1)
107
+ backend.do_frame(player.core)
108
+ assert_empty @unlocked
109
+
110
+ # Write 0x01 to EWRAM — but we can't poke real memory from Ruby,
111
+ # so verify do_frame doesn't crash and condition stays unevaluated
112
+ backend.do_frame(player.core)
113
+ assert_empty @unlocked
114
+ end
115
+ end
116
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require_relative "../lib/gemba/overlay_renderer"
4
+ require "gemba/headless"
5
5
 
6
6
  class TestOverlayRenderer < Minitest::Test
7
7
  class MockTexture
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+
6
+ class TestPlatform < Minitest::Test
7
+ # -- Factory ---------------------------------------------------------------
8
+
9
+ def test_for_gba
10
+ core = MockCore.new("GBA")
11
+ platform = Gemba::Platform.for(core)
12
+ assert_instance_of Gemba::Platform::GBA, platform
13
+ end
14
+
15
+ def test_for_gb
16
+ core = MockCore.new("GB")
17
+ platform = Gemba::Platform.for(core)
18
+ assert_instance_of Gemba::Platform::GB, platform
19
+ end
20
+
21
+ def test_for_gbc
22
+ core = MockCore.new("GBC")
23
+ platform = Gemba::Platform.for(core)
24
+ assert_instance_of Gemba::Platform::GBC, platform
25
+ end
26
+
27
+ def test_for_unknown_defaults_to_gb
28
+ core = MockCore.new("Unknown")
29
+ platform = Gemba::Platform.for(core)
30
+ assert_instance_of Gemba::Platform::GB, platform
31
+ end
32
+
33
+ def test_default_is_gba
34
+ platform = Gemba::Platform.default
35
+ assert_instance_of Gemba::Platform::GBA, platform
36
+ end
37
+
38
+ # -- GBA -------------------------------------------------------------------
39
+
40
+ def test_gba_resolution
41
+ p = Gemba::Platform::GBA.new
42
+ assert_equal 240, p.width
43
+ assert_equal 160, p.height
44
+ end
45
+
46
+ def test_gba_fps
47
+ p = Gemba::Platform::GBA.new
48
+ assert_in_delta 59.7272, p.fps, 0.001
49
+ end
50
+
51
+ def test_gba_fps_fraction
52
+ num, den = Gemba::Platform::GBA.new.fps_fraction
53
+ assert_in_delta 59.7272, num.to_f / den, 0.001
54
+ end
55
+
56
+ def test_gba_aspect
57
+ assert_equal [3, 2], Gemba::Platform::GBA.new.aspect
58
+ end
59
+
60
+ def test_gba_name
61
+ assert_equal "Game Boy Advance", Gemba::Platform::GBA.new.name
62
+ assert_equal "GBA", Gemba::Platform::GBA.new.short_name
63
+ end
64
+
65
+ def test_gba_buttons_include_lr
66
+ buttons = Gemba::Platform::GBA.new.buttons
67
+ assert_includes buttons, :l
68
+ assert_includes buttons, :r
69
+ assert_equal 10, buttons.size
70
+ end
71
+
72
+ def test_gba_thumb_size
73
+ assert_equal [120, 80], Gemba::Platform::GBA.new.thumb_size
74
+ end
75
+
76
+ # -- GB --------------------------------------------------------------------
77
+
78
+ def test_gb_resolution
79
+ p = Gemba::Platform::GB.new
80
+ assert_equal 160, p.width
81
+ assert_equal 144, p.height
82
+ end
83
+
84
+ def test_gb_fps
85
+ assert_in_delta 59.7275, Gemba::Platform::GB.new.fps, 0.001
86
+ end
87
+
88
+ def test_gb_fps_fraction
89
+ num, den = Gemba::Platform::GB.new.fps_fraction
90
+ assert_in_delta 59.7275, num.to_f / den, 0.001
91
+ end
92
+
93
+ def test_gb_aspect
94
+ assert_equal [10, 9], Gemba::Platform::GB.new.aspect
95
+ end
96
+
97
+ def test_gb_name
98
+ assert_equal "Game Boy", Gemba::Platform::GB.new.name
99
+ assert_equal "GB", Gemba::Platform::GB.new.short_name
100
+ end
101
+
102
+ def test_gb_buttons_no_lr
103
+ buttons = Gemba::Platform::GB.new.buttons
104
+ refute_includes buttons, :l
105
+ refute_includes buttons, :r
106
+ assert_equal 8, buttons.size
107
+ end
108
+
109
+ def test_gb_thumb_size
110
+ assert_equal [80, 72], Gemba::Platform::GB.new.thumb_size
111
+ end
112
+
113
+ # -- GBC -------------------------------------------------------------------
114
+
115
+ def test_gbc_resolution_same_as_gb
116
+ p = Gemba::Platform::GBC.new
117
+ assert_equal 160, p.width
118
+ assert_equal 144, p.height
119
+ end
120
+
121
+ def test_gbc_name_differs_from_gb
122
+ assert_equal "Game Boy Color", Gemba::Platform::GBC.new.name
123
+ assert_equal "GBC", Gemba::Platform::GBC.new.short_name
124
+ end
125
+
126
+ def test_gbc_buttons_no_lr
127
+ buttons = Gemba::Platform::GBC.new.buttons
128
+ refute_includes buttons, :l
129
+ refute_includes buttons, :r
130
+ end
131
+
132
+ # -- Equality --------------------------------------------------------------
133
+
134
+ def test_same_platform_equal
135
+ assert_equal Gemba::Platform::GBA.new, Gemba::Platform::GBA.new
136
+ assert_equal Gemba::Platform::GB.new, Gemba::Platform::GB.new
137
+ assert_equal Gemba::Platform::GBC.new, Gemba::Platform::GBC.new
138
+ end
139
+
140
+ def test_different_platforms_not_equal
141
+ refute_equal Gemba::Platform::GBA.new, Gemba::Platform::GB.new
142
+ refute_equal Gemba::Platform::GBA.new, Gemba::Platform::GBC.new
143
+ refute_equal Gemba::Platform::GB.new, Gemba::Platform::GBC.new
144
+ end
145
+
146
+ private
147
+
148
+ MockCore = Struct.new(:platform)
149
+ end