gemba 0.1.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.
- checksums.yaml +7 -0
- data/THIRD_PARTY_NOTICES +113 -0
- data/assets/JetBrainsMonoNL-Regular.ttf +0 -0
- data/assets/ark-pixel-12px-monospaced-ja.ttf +0 -0
- data/bin/gemba +14 -0
- data/ext/gemba/extconf.rb +185 -0
- data/ext/gemba/gemba_ext.c +1051 -0
- data/ext/gemba/gemba_ext.h +15 -0
- data/gemba.gemspec +38 -0
- data/lib/gemba/child_window.rb +62 -0
- data/lib/gemba/cli.rb +384 -0
- data/lib/gemba/config.rb +621 -0
- data/lib/gemba/core.rb +121 -0
- data/lib/gemba/headless.rb +12 -0
- data/lib/gemba/headless_player.rb +206 -0
- data/lib/gemba/hotkey_map.rb +202 -0
- data/lib/gemba/input_mappings.rb +214 -0
- data/lib/gemba/locale.rb +92 -0
- data/lib/gemba/locales/en.yml +157 -0
- data/lib/gemba/locales/ja.yml +157 -0
- data/lib/gemba/method_coverage_service.rb +265 -0
- data/lib/gemba/overlay_renderer.rb +109 -0
- data/lib/gemba/player.rb +1515 -0
- data/lib/gemba/recorder.rb +156 -0
- data/lib/gemba/recorder_decoder.rb +325 -0
- data/lib/gemba/rom_info_window.rb +346 -0
- data/lib/gemba/rom_loader.rb +100 -0
- data/lib/gemba/runtime.rb +39 -0
- data/lib/gemba/save_state_manager.rb +155 -0
- data/lib/gemba/save_state_picker.rb +199 -0
- data/lib/gemba/settings_window.rb +1173 -0
- data/lib/gemba/tip_service.rb +133 -0
- data/lib/gemba/toast_overlay.rb +128 -0
- data/lib/gemba/version.rb +5 -0
- data/lib/gemba.rb +17 -0
- data/test/fixtures/test.gba +0 -0
- data/test/fixtures/test.sav +0 -0
- data/test/shared/screenshot_helper.rb +113 -0
- data/test/shared/simplecov_config.rb +59 -0
- data/test/shared/teek_test_worker.rb +388 -0
- data/test/shared/tk_test_helper.rb +354 -0
- data/test/support/input_mocks.rb +61 -0
- data/test/support/player_helpers.rb +77 -0
- data/test/test_cli.rb +281 -0
- data/test/test_config.rb +897 -0
- data/test/test_core.rb +401 -0
- data/test/test_gamepad_map.rb +116 -0
- data/test/test_headless_player.rb +205 -0
- data/test/test_helper.rb +19 -0
- data/test/test_hotkey_map.rb +396 -0
- data/test/test_keyboard_map.rb +108 -0
- data/test/test_locale.rb +159 -0
- data/test/test_mgba.rb +26 -0
- data/test/test_overlay_renderer.rb +199 -0
- data/test/test_player.rb +903 -0
- data/test/test_recorder.rb +180 -0
- data/test/test_rom_loader.rb +149 -0
- data/test/test_save_state_manager.rb +289 -0
- data/test/test_settings_hotkeys.rb +434 -0
- data/test/test_settings_window.rb +1039 -0
- data/test/test_tip_service.rb +138 -0
- data/test/test_toast_overlay.rb +216 -0
- data/test/test_virtual_keyboard.rb +39 -0
- data/test/test_xor_delta.rb +61 -0
- metadata +234 -0
data/test/test_config.rb
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../lib/gemba/config"
|
|
7
|
+
require_relative "../lib/gemba/version"
|
|
8
|
+
|
|
9
|
+
class TestMGBAConfig < Minitest::Test
|
|
10
|
+
def setup
|
|
11
|
+
@dir = Dir.mktmpdir("gemba-test")
|
|
12
|
+
@path = File.join(@dir, "settings.json")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def teardown
|
|
16
|
+
FileUtils.rm_rf(@dir)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def new_config
|
|
20
|
+
Gemba::Config.new(path: @path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# -- Platform paths -------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def test_default_path_ends_with_settings_json
|
|
26
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
27
|
+
assert Gemba::Config.default_path.start_with?(ENV['GEMBA_CONFIG_DIR'])
|
|
28
|
+
else
|
|
29
|
+
assert Gemba::Config.default_path.end_with?("gemba/settings.json")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_config_dir_contains_app_name
|
|
34
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
35
|
+
assert_equal ENV['GEMBA_CONFIG_DIR'], Gemba::Config.config_dir
|
|
36
|
+
else
|
|
37
|
+
assert_includes Gemba::Config.config_dir, "gemba"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# -- Global defaults ------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def test_defaults_scale
|
|
44
|
+
assert_equal 3, new_config.scale
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_defaults_volume
|
|
48
|
+
assert_equal 100, new_config.volume
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_defaults_muted
|
|
52
|
+
refute new_config.muted?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# -- Global setters -------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def test_set_scale
|
|
58
|
+
c = new_config
|
|
59
|
+
c.scale = 2
|
|
60
|
+
assert_equal 2, c.scale
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_scale_clamps_low
|
|
64
|
+
c = new_config
|
|
65
|
+
c.scale = 0
|
|
66
|
+
assert_equal 1, c.scale
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_scale_clamps_high
|
|
70
|
+
c = new_config
|
|
71
|
+
c.scale = 10
|
|
72
|
+
assert_equal 4, c.scale
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_set_volume
|
|
76
|
+
c = new_config
|
|
77
|
+
c.volume = 75
|
|
78
|
+
assert_equal 75, c.volume
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_volume_clamps
|
|
82
|
+
c = new_config
|
|
83
|
+
c.volume = -5
|
|
84
|
+
assert_equal 0, c.volume
|
|
85
|
+
c.volume = 200
|
|
86
|
+
assert_equal 100, c.volume
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_set_muted
|
|
90
|
+
c = new_config
|
|
91
|
+
c.muted = true
|
|
92
|
+
assert c.muted?
|
|
93
|
+
c.muted = false
|
|
94
|
+
refute c.muted?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# -- Persistence ----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def test_save_creates_file
|
|
100
|
+
c = new_config
|
|
101
|
+
c.scale = 2
|
|
102
|
+
c.save!
|
|
103
|
+
assert File.exist?(@path)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_save_creates_directory
|
|
107
|
+
nested = File.join(@dir, "sub", "dir", "settings.json")
|
|
108
|
+
c = Gemba::Config.new(path: nested)
|
|
109
|
+
c.save!
|
|
110
|
+
assert File.exist?(nested)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_save_writes_metadata
|
|
114
|
+
c = new_config
|
|
115
|
+
c.save!
|
|
116
|
+
data = JSON.parse(File.read(@path))
|
|
117
|
+
assert_equal Gemba::VERSION, data["meta"]["gemba_version"]
|
|
118
|
+
assert data.key?("meta")
|
|
119
|
+
assert data["meta"].key?("saved_at")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_round_trip_global
|
|
123
|
+
c = new_config
|
|
124
|
+
c.scale = 2
|
|
125
|
+
c.volume = 42
|
|
126
|
+
c.muted = true
|
|
127
|
+
c.save!
|
|
128
|
+
|
|
129
|
+
c2 = Gemba::Config.new(path: @path)
|
|
130
|
+
assert_equal 2, c2.scale
|
|
131
|
+
assert_equal 42, c2.volume
|
|
132
|
+
assert c2.muted?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_round_trip_gamepad
|
|
136
|
+
guid = "030000007e0500000920000001800000"
|
|
137
|
+
c = new_config
|
|
138
|
+
c.gamepad(guid, name: "Switch Pro")
|
|
139
|
+
c.set_dead_zone(guid, 15)
|
|
140
|
+
c.set_mapping(guid, :a, :x)
|
|
141
|
+
c.save!
|
|
142
|
+
|
|
143
|
+
c2 = Gemba::Config.new(path: @path)
|
|
144
|
+
assert_equal 15, c2.dead_zone(guid)
|
|
145
|
+
assert_equal "x", c2.mappings(guid)["a"]
|
|
146
|
+
gp = c2.gamepad(guid)
|
|
147
|
+
assert_equal "Switch Pro", gp["name"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# -- Gamepad defaults -----------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def test_gamepad_defaults
|
|
153
|
+
guid = "abcd1234"
|
|
154
|
+
c = new_config
|
|
155
|
+
assert_equal 25, c.dead_zone(guid)
|
|
156
|
+
assert_equal "a", c.mappings(guid)["a"]
|
|
157
|
+
assert_equal "dpad_up", c.mappings(guid)["up"]
|
|
158
|
+
assert_equal "left_shoulder", c.mappings(guid)["l"]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_set_dead_zone_clamps
|
|
162
|
+
guid = "abcd"
|
|
163
|
+
c = new_config
|
|
164
|
+
c.set_dead_zone(guid, -5)
|
|
165
|
+
assert_equal 0, c.dead_zone(guid)
|
|
166
|
+
c.set_dead_zone(guid, 99)
|
|
167
|
+
assert_equal 50, c.dead_zone(guid)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_set_mapping_removes_duplicate
|
|
171
|
+
guid = "abcd"
|
|
172
|
+
c = new_config
|
|
173
|
+
# Default: a -> a, b -> b
|
|
174
|
+
c.set_mapping(guid, :a, :x)
|
|
175
|
+
m = c.mappings(guid)
|
|
176
|
+
assert_equal "x", m["a"]
|
|
177
|
+
# :x should not be mapped to anything else
|
|
178
|
+
assert_nil m.values.count("x") > 1 ? "dup" : nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_reset_gamepad
|
|
182
|
+
guid = "abcd"
|
|
183
|
+
c = new_config
|
|
184
|
+
c.set_dead_zone(guid, 10)
|
|
185
|
+
c.set_mapping(guid, :a, :y)
|
|
186
|
+
c.reset_gamepad(guid)
|
|
187
|
+
assert_equal 25, c.dead_zone(guid)
|
|
188
|
+
assert_equal "a", c.mappings(guid)["a"]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# -- Multiple gamepads ----------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def test_separate_guids
|
|
194
|
+
c = new_config
|
|
195
|
+
c.set_dead_zone("guid_a", 10)
|
|
196
|
+
c.set_dead_zone("guid_b", 40)
|
|
197
|
+
assert_equal 10, c.dead_zone("guid_a")
|
|
198
|
+
assert_equal 40, c.dead_zone("guid_b")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_mapping_change_on_one_guid_does_not_affect_other
|
|
202
|
+
c = new_config
|
|
203
|
+
c.set_mapping("guid_a", :a, :y)
|
|
204
|
+
assert_equal "a", c.mappings("guid_b")["a"]
|
|
205
|
+
assert_equal "y", c.mappings("guid_a")["a"]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# -- Keyboard config (sentinel GUID) -------------------------------------
|
|
209
|
+
|
|
210
|
+
def test_keyboard_guid_defaults_to_keysyms
|
|
211
|
+
c = new_config
|
|
212
|
+
m = c.mappings(Gemba::Config::KEYBOARD_GUID)
|
|
213
|
+
assert_equal "z", m["a"]
|
|
214
|
+
assert_equal "x", m["b"]
|
|
215
|
+
assert_equal "Up", m["up"]
|
|
216
|
+
assert_equal "Return", m["start"]
|
|
217
|
+
assert_equal "BackSpace", m["select"]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_keyboard_guid_dead_zone_is_zero
|
|
221
|
+
c = new_config
|
|
222
|
+
assert_equal 0, c.dead_zone(Gemba::Config::KEYBOARD_GUID)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def test_keyboard_does_not_get_gamepad_defaults
|
|
226
|
+
c = new_config
|
|
227
|
+
m = c.mappings(Gemba::Config::KEYBOARD_GUID)
|
|
228
|
+
# Should NOT have gamepad button names like 'dpad_up'
|
|
229
|
+
refute_includes m.values, "dpad_up"
|
|
230
|
+
refute_includes m.values, "left_shoulder"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_regular_guid_does_not_get_keyboard_defaults
|
|
234
|
+
c = new_config
|
|
235
|
+
m = c.mappings("some_real_guid")
|
|
236
|
+
# Should NOT have keysyms like 'z' or 'Return'
|
|
237
|
+
refute_includes m.values, "z"
|
|
238
|
+
refute_includes m.values, "Return"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_round_trip_keyboard
|
|
242
|
+
c = new_config
|
|
243
|
+
c.set_mapping(Gemba::Config::KEYBOARD_GUID, :a, "q")
|
|
244
|
+
c.save!
|
|
245
|
+
|
|
246
|
+
c2 = Gemba::Config.new(path: @path)
|
|
247
|
+
assert_equal "q", c2.mappings(Gemba::Config::KEYBOARD_GUID)["a"]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_reset_keyboard_restores_defaults
|
|
251
|
+
guid = Gemba::Config::KEYBOARD_GUID
|
|
252
|
+
c = new_config
|
|
253
|
+
c.set_mapping(guid, :a, "q")
|
|
254
|
+
c.reset_gamepad(guid)
|
|
255
|
+
assert_equal "z", c.mappings(guid)["a"]
|
|
256
|
+
assert_equal 0, c.dead_zone(guid)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# -- Turbo settings ------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def test_defaults_turbo_speed
|
|
262
|
+
assert_equal 2, new_config.turbo_speed
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def test_set_turbo_speed
|
|
266
|
+
c = new_config
|
|
267
|
+
c.turbo_speed = 4
|
|
268
|
+
assert_equal 4, c.turbo_speed
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_defaults_turbo_volume_pct
|
|
272
|
+
assert_equal 25, new_config.turbo_volume_pct
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_set_turbo_volume_pct
|
|
276
|
+
c = new_config
|
|
277
|
+
c.turbo_volume_pct = 50
|
|
278
|
+
assert_equal 50, c.turbo_volume_pct
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_turbo_volume_pct_clamps
|
|
282
|
+
c = new_config
|
|
283
|
+
c.turbo_volume_pct = -10
|
|
284
|
+
assert_equal 0, c.turbo_volume_pct
|
|
285
|
+
c.turbo_volume_pct = 200
|
|
286
|
+
assert_equal 100, c.turbo_volume_pct
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_round_trip_turbo
|
|
290
|
+
c = new_config
|
|
291
|
+
c.turbo_speed = 3
|
|
292
|
+
c.turbo_volume_pct = 40
|
|
293
|
+
c.save!
|
|
294
|
+
|
|
295
|
+
c2 = Gemba::Config.new(path: @path)
|
|
296
|
+
assert_equal 3, c2.turbo_speed
|
|
297
|
+
assert_equal 40, c2.turbo_volume_pct
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# -- Aspect ratio --------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
def test_defaults_keep_aspect_ratio
|
|
303
|
+
assert new_config.keep_aspect_ratio?
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_set_keep_aspect_ratio
|
|
307
|
+
c = new_config
|
|
308
|
+
c.keep_aspect_ratio = false
|
|
309
|
+
refute c.keep_aspect_ratio?
|
|
310
|
+
c.keep_aspect_ratio = true
|
|
311
|
+
assert c.keep_aspect_ratio?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def test_round_trip_keep_aspect_ratio
|
|
315
|
+
c = new_config
|
|
316
|
+
c.keep_aspect_ratio = false
|
|
317
|
+
c.save!
|
|
318
|
+
|
|
319
|
+
c2 = Gemba::Config.new(path: @path)
|
|
320
|
+
refute c2.keep_aspect_ratio?
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# -- Show FPS ------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
def test_defaults_show_fps
|
|
326
|
+
assert new_config.show_fps?
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_set_show_fps
|
|
330
|
+
c = new_config
|
|
331
|
+
c.show_fps = false
|
|
332
|
+
refute c.show_fps?
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def test_round_trip_show_fps
|
|
336
|
+
c = new_config
|
|
337
|
+
c.show_fps = false
|
|
338
|
+
c.save!
|
|
339
|
+
|
|
340
|
+
c2 = Gemba::Config.new(path: @path)
|
|
341
|
+
refute c2.show_fps?
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# -- Saves dir -----------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def test_defaults_saves_dir
|
|
347
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
348
|
+
assert new_config.saves_dir.end_with?("saves")
|
|
349
|
+
else
|
|
350
|
+
assert new_config.saves_dir.end_with?("gemba/saves")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def test_set_saves_dir
|
|
355
|
+
c = new_config
|
|
356
|
+
c.saves_dir = "/custom/saves"
|
|
357
|
+
assert_equal "/custom/saves", c.saves_dir
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def test_round_trip_saves_dir
|
|
361
|
+
c = new_config
|
|
362
|
+
c.saves_dir = "/my/saves"
|
|
363
|
+
c.save!
|
|
364
|
+
|
|
365
|
+
c2 = Gemba::Config.new(path: @path)
|
|
366
|
+
assert_equal "/my/saves", c2.saves_dir
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def test_default_saves_dir_class_method
|
|
370
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
371
|
+
assert Gemba::Config.default_saves_dir.start_with?(ENV['GEMBA_CONFIG_DIR'])
|
|
372
|
+
else
|
|
373
|
+
assert Gemba::Config.default_saves_dir.end_with?("gemba/saves")
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# -- Recent ROMs ---------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
def test_recent_roms_default_empty
|
|
380
|
+
assert_equal [], new_config.recent_roms
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def test_add_recent_rom
|
|
384
|
+
c = new_config
|
|
385
|
+
c.add_recent_rom("/roms/a.gba")
|
|
386
|
+
c.add_recent_rom("/roms/b.gba")
|
|
387
|
+
assert_equal ["/roms/b.gba", "/roms/a.gba"], c.recent_roms
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def test_add_recent_rom_deduplicates
|
|
391
|
+
c = new_config
|
|
392
|
+
c.add_recent_rom("/roms/a.gba")
|
|
393
|
+
c.add_recent_rom("/roms/b.gba")
|
|
394
|
+
c.add_recent_rom("/roms/a.gba")
|
|
395
|
+
assert_equal ["/roms/a.gba", "/roms/b.gba"], c.recent_roms
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def test_add_recent_rom_caps_at_max
|
|
399
|
+
c = new_config
|
|
400
|
+
7.times { |i| c.add_recent_rom("/roms/#{i}.gba") }
|
|
401
|
+
assert_equal Gemba::Config::MAX_RECENT_ROMS, c.recent_roms.size
|
|
402
|
+
assert_equal "/roms/6.gba", c.recent_roms.first
|
|
403
|
+
assert_equal "/roms/2.gba", c.recent_roms.last
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def test_remove_recent_rom
|
|
407
|
+
c = new_config
|
|
408
|
+
c.add_recent_rom("/roms/a.gba")
|
|
409
|
+
c.add_recent_rom("/roms/b.gba")
|
|
410
|
+
c.remove_recent_rom("/roms/a.gba")
|
|
411
|
+
assert_equal ["/roms/b.gba"], c.recent_roms
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_remove_recent_rom_noop_if_missing
|
|
415
|
+
c = new_config
|
|
416
|
+
c.add_recent_rom("/roms/a.gba")
|
|
417
|
+
c.remove_recent_rom("/roms/nope.gba")
|
|
418
|
+
assert_equal ["/roms/a.gba"], c.recent_roms
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def test_clear_recent_roms
|
|
422
|
+
c = new_config
|
|
423
|
+
c.add_recent_rom("/roms/a.gba")
|
|
424
|
+
c.add_recent_rom("/roms/b.gba")
|
|
425
|
+
c.clear_recent_roms
|
|
426
|
+
assert_equal [], c.recent_roms
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def test_round_trip_recent_roms
|
|
430
|
+
c = new_config
|
|
431
|
+
c.add_recent_rom("/roms/a.gba")
|
|
432
|
+
c.add_recent_rom("/roms/b.gba")
|
|
433
|
+
c.save!
|
|
434
|
+
|
|
435
|
+
c2 = Gemba::Config.new(path: @path)
|
|
436
|
+
assert_equal ["/roms/b.gba", "/roms/a.gba"], c2.recent_roms
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# -- States dir ----------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
def test_defaults_states_dir
|
|
442
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
443
|
+
assert new_config.states_dir.end_with?("states")
|
|
444
|
+
else
|
|
445
|
+
assert new_config.states_dir.end_with?("gemba/states")
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def test_set_states_dir
|
|
450
|
+
c = new_config
|
|
451
|
+
c.states_dir = "/custom/states"
|
|
452
|
+
assert_equal "/custom/states", c.states_dir
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def test_round_trip_states_dir
|
|
456
|
+
c = new_config
|
|
457
|
+
c.states_dir = "/my/states"
|
|
458
|
+
c.save!
|
|
459
|
+
|
|
460
|
+
c2 = Gemba::Config.new(path: @path)
|
|
461
|
+
assert_equal "/my/states", c2.states_dir
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def test_default_states_dir_class_method
|
|
465
|
+
if ENV['GEMBA_CONFIG_DIR']
|
|
466
|
+
assert Gemba::Config.default_states_dir.start_with?(ENV['GEMBA_CONFIG_DIR'])
|
|
467
|
+
else
|
|
468
|
+
assert Gemba::Config.default_states_dir.end_with?("gemba/states")
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# -- Save state debounce -------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def test_defaults_save_state_debounce
|
|
475
|
+
assert_in_delta 3.0, new_config.save_state_debounce, 0.01
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def test_set_save_state_debounce
|
|
479
|
+
c = new_config
|
|
480
|
+
c.save_state_debounce = 5.0
|
|
481
|
+
assert_in_delta 5.0, c.save_state_debounce, 0.01
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def test_save_state_debounce_clamps
|
|
485
|
+
c = new_config
|
|
486
|
+
c.save_state_debounce = -1.0
|
|
487
|
+
assert_in_delta 0.0, c.save_state_debounce, 0.01
|
|
488
|
+
c.save_state_debounce = 99.0
|
|
489
|
+
assert_in_delta 30.0, c.save_state_debounce, 0.01
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def test_round_trip_save_state_debounce
|
|
493
|
+
c = new_config
|
|
494
|
+
c.save_state_debounce = 1.5
|
|
495
|
+
c.save!
|
|
496
|
+
|
|
497
|
+
c2 = Gemba::Config.new(path: @path)
|
|
498
|
+
assert_in_delta 1.5, c2.save_state_debounce, 0.01
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# -- Quick save slot -----------------------------------------------------
|
|
502
|
+
|
|
503
|
+
def test_defaults_quick_save_slot
|
|
504
|
+
assert_equal 1, new_config.quick_save_slot
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def test_set_quick_save_slot
|
|
508
|
+
c = new_config
|
|
509
|
+
c.quick_save_slot = 5
|
|
510
|
+
assert_equal 5, c.quick_save_slot
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def test_quick_save_slot_clamps
|
|
514
|
+
c = new_config
|
|
515
|
+
c.quick_save_slot = 0
|
|
516
|
+
assert_equal 1, c.quick_save_slot
|
|
517
|
+
c.quick_save_slot = 99
|
|
518
|
+
assert_equal 10, c.quick_save_slot
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def test_round_trip_quick_save_slot
|
|
522
|
+
c = new_config
|
|
523
|
+
c.quick_save_slot = 7
|
|
524
|
+
c.save!
|
|
525
|
+
|
|
526
|
+
c2 = Gemba::Config.new(path: @path)
|
|
527
|
+
assert_equal 7, c2.quick_save_slot
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# -- Save state backup ---------------------------------------------------
|
|
531
|
+
|
|
532
|
+
def test_defaults_save_state_backup
|
|
533
|
+
assert new_config.save_state_backup?
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def test_set_save_state_backup
|
|
537
|
+
c = new_config
|
|
538
|
+
c.save_state_backup = false
|
|
539
|
+
refute c.save_state_backup?
|
|
540
|
+
c.save_state_backup = true
|
|
541
|
+
assert c.save_state_backup?
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def test_round_trip_save_state_backup
|
|
545
|
+
c = new_config
|
|
546
|
+
c.save_state_backup = false
|
|
547
|
+
c.save!
|
|
548
|
+
|
|
549
|
+
c2 = Gemba::Config.new(path: @path)
|
|
550
|
+
refute c2.save_state_backup?
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# -- Locale --------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
def test_defaults_locale
|
|
556
|
+
assert_equal 'auto', new_config.locale
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def test_set_locale
|
|
560
|
+
c = new_config
|
|
561
|
+
c.locale = 'ja'
|
|
562
|
+
assert_equal 'ja', c.locale
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def test_round_trip_locale
|
|
566
|
+
c = new_config
|
|
567
|
+
c.locale = 'ja'
|
|
568
|
+
c.save!
|
|
569
|
+
|
|
570
|
+
c2 = Gemba::Config.new(path: @path)
|
|
571
|
+
assert_equal 'ja', c2.locale
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# -- Per-game settings ---------------------------------------------------
|
|
575
|
+
|
|
576
|
+
def test_per_game_settings_default_false
|
|
577
|
+
refute new_config.per_game_settings?
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def test_set_per_game_settings
|
|
581
|
+
c = new_config
|
|
582
|
+
c.per_game_settings = true
|
|
583
|
+
assert c.per_game_settings?
|
|
584
|
+
c.per_game_settings = false
|
|
585
|
+
refute c.per_game_settings?
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def test_rom_id
|
|
589
|
+
assert_equal "AGB-BGBE-DEADBEEF",
|
|
590
|
+
Gemba::Config.rom_id("AGB-BGBE", 0xDEADBEEF)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def test_rom_id_sanitizes_special_chars
|
|
594
|
+
# Spaces and slashes get replaced; hyphens, dots, underscores are kept
|
|
595
|
+
assert_equal "AGB_BGBE-0000CAFE",
|
|
596
|
+
Gemba::Config.rom_id("AGB BGBE", 0xCAFE)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def test_game_config_path
|
|
600
|
+
path = Gemba::Config.game_config_path("AGB_BGBE-DEADBEEF")
|
|
601
|
+
assert path.end_with?("games/AGB_BGBE-DEADBEEF/settings.json")
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def test_activate_game_without_per_game_reads_global
|
|
605
|
+
c = new_config
|
|
606
|
+
c.scale = 2
|
|
607
|
+
c.activate_game("TESTROM-00000001")
|
|
608
|
+
assert_equal 2, c.scale # still reads from global
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def test_enable_per_game_seeds_game_data
|
|
612
|
+
c = new_config
|
|
613
|
+
c.scale = 2
|
|
614
|
+
c.activate_game("TESTROM-00000001")
|
|
615
|
+
c.enable_per_game
|
|
616
|
+
assert_equal 2, c.scale # seeded from global
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def test_per_game_write_goes_to_overlay
|
|
620
|
+
c = new_config
|
|
621
|
+
c.scale = 3
|
|
622
|
+
c.activate_game("TESTROM-00000001")
|
|
623
|
+
c.enable_per_game
|
|
624
|
+
c.scale = 1
|
|
625
|
+
assert_equal 1, c.scale # reads from overlay
|
|
626
|
+
# Disable — should revert to global value
|
|
627
|
+
c.disable_per_game
|
|
628
|
+
assert_equal 3, c.scale
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def test_global_key_bypasses_overlay
|
|
632
|
+
c = new_config
|
|
633
|
+
c.activate_game("TESTROM-00000001")
|
|
634
|
+
c.enable_per_game
|
|
635
|
+
c.keep_aspect_ratio = false
|
|
636
|
+
refute c.keep_aspect_ratio?
|
|
637
|
+
# This key is NOT in PER_GAME_KEYS, so it went to global
|
|
638
|
+
c.disable_per_game
|
|
639
|
+
refute c.keep_aspect_ratio? # still false (it was written to global)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def test_save_writes_both_files
|
|
643
|
+
c = new_config
|
|
644
|
+
c.activate_game("TESTROM-00000001")
|
|
645
|
+
c.enable_per_game
|
|
646
|
+
c.scale = 1
|
|
647
|
+
c.save!
|
|
648
|
+
|
|
649
|
+
assert File.exist?(@path), "Global config should exist"
|
|
650
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
651
|
+
assert File.exist?(game_path), "Game config should exist"
|
|
652
|
+
|
|
653
|
+
game_data = JSON.parse(File.read(game_path))
|
|
654
|
+
assert_equal 1, game_data['scale']
|
|
655
|
+
ensure
|
|
656
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
657
|
+
FileUtils.rm_rf(File.dirname(game_path)) if game_path
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def test_round_trip_per_game
|
|
661
|
+
c = new_config
|
|
662
|
+
c.activate_game("TESTROM-00000001")
|
|
663
|
+
c.enable_per_game
|
|
664
|
+
c.scale = 1
|
|
665
|
+
c.volume = 42
|
|
666
|
+
c.save!
|
|
667
|
+
|
|
668
|
+
c2 = Gemba::Config.new(path: @path)
|
|
669
|
+
c2.activate_game("TESTROM-00000001")
|
|
670
|
+
assert_equal 1, c2.scale
|
|
671
|
+
assert_equal 42, c2.volume
|
|
672
|
+
ensure
|
|
673
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
674
|
+
FileUtils.rm_rf(File.dirname(game_path)) if game_path
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def test_activate_different_rom_uses_global
|
|
678
|
+
c = new_config
|
|
679
|
+
c.scale = 3
|
|
680
|
+
c.activate_game("ROM_A-00000001")
|
|
681
|
+
c.enable_per_game
|
|
682
|
+
c.scale = 1
|
|
683
|
+
c.save!
|
|
684
|
+
|
|
685
|
+
# Switch to a different ROM — no game file exists
|
|
686
|
+
c.activate_game("ROM_B-00000002")
|
|
687
|
+
assert_equal 3, c.scale # falls through to global
|
|
688
|
+
ensure
|
|
689
|
+
%w[ROM_A-00000001 ROM_B-00000002].each do |id|
|
|
690
|
+
p = Gemba::Config.game_config_path(id)
|
|
691
|
+
FileUtils.rm_rf(File.dirname(p)) if p
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def test_disable_per_game_preserves_file
|
|
696
|
+
c = new_config
|
|
697
|
+
c.activate_game("TESTROM-00000001")
|
|
698
|
+
c.enable_per_game
|
|
699
|
+
c.scale = 1
|
|
700
|
+
c.save!
|
|
701
|
+
|
|
702
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
703
|
+
assert File.exist?(game_path)
|
|
704
|
+
|
|
705
|
+
c.disable_per_game
|
|
706
|
+
c.save!
|
|
707
|
+
assert File.exist?(game_path), "Game file should NOT be deleted on disable"
|
|
708
|
+
ensure
|
|
709
|
+
FileUtils.rm_rf(File.dirname(game_path)) if game_path
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def test_corrupt_game_file_falls_back_to_global
|
|
713
|
+
c = new_config
|
|
714
|
+
c.per_game_settings = true
|
|
715
|
+
c.scale = 3
|
|
716
|
+
c.save!
|
|
717
|
+
|
|
718
|
+
# Write corrupt game file
|
|
719
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
720
|
+
FileUtils.mkdir_p(File.dirname(game_path))
|
|
721
|
+
File.write(game_path, "NOT VALID JSON {{{")
|
|
722
|
+
|
|
723
|
+
c.activate_game("TESTROM-00000001")
|
|
724
|
+
assert_equal 3, c.scale # falls back to global
|
|
725
|
+
ensure
|
|
726
|
+
FileUtils.rm_rf(File.dirname(game_path)) if game_path
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def test_reload_with_active_game
|
|
730
|
+
c = new_config
|
|
731
|
+
c.activate_game("TESTROM-00000001")
|
|
732
|
+
c.enable_per_game
|
|
733
|
+
c.scale = 1
|
|
734
|
+
c.save!
|
|
735
|
+
|
|
736
|
+
# Externally modify the game file
|
|
737
|
+
game_path = Gemba::Config.game_config_path("TESTROM-00000001")
|
|
738
|
+
game_data = JSON.parse(File.read(game_path))
|
|
739
|
+
game_data['scale'] = 4
|
|
740
|
+
File.write(game_path, JSON.generate(game_data))
|
|
741
|
+
|
|
742
|
+
c.reload!
|
|
743
|
+
assert_equal 4, c.scale
|
|
744
|
+
ensure
|
|
745
|
+
FileUtils.rm_rf(File.dirname(game_path)) if game_path
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def test_per_game_settings_constant_keys
|
|
749
|
+
expected = %w[scale pixel_filter integer_scale color_correction frame_blending
|
|
750
|
+
volume muted turbo_speed quick_save_slot save_state_backup]
|
|
751
|
+
assert_equal expected.sort, Gemba::Config::PER_GAME_SETTINGS.keys.sort
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# -- Rewind settings ----------------------------------------------------
|
|
755
|
+
|
|
756
|
+
def test_defaults_rewind_enabled
|
|
757
|
+
assert new_config.rewind_enabled?
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def test_set_rewind_enabled
|
|
761
|
+
c = new_config
|
|
762
|
+
c.rewind_enabled = false
|
|
763
|
+
refute c.rewind_enabled?
|
|
764
|
+
c.rewind_enabled = true
|
|
765
|
+
assert c.rewind_enabled?
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def test_defaults_rewind_seconds
|
|
769
|
+
assert_equal 10, new_config.rewind_seconds
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def test_set_rewind_seconds
|
|
773
|
+
c = new_config
|
|
774
|
+
c.rewind_seconds = 30
|
|
775
|
+
assert_equal 30, c.rewind_seconds
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def test_rewind_seconds_clamps
|
|
779
|
+
c = new_config
|
|
780
|
+
c.rewind_seconds = 0
|
|
781
|
+
assert_equal 1, c.rewind_seconds
|
|
782
|
+
c.rewind_seconds = 100
|
|
783
|
+
assert_equal 60, c.rewind_seconds
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def test_round_trip_rewind
|
|
787
|
+
c = new_config
|
|
788
|
+
c.rewind_enabled = false
|
|
789
|
+
c.rewind_seconds = 20
|
|
790
|
+
c.save!
|
|
791
|
+
|
|
792
|
+
c2 = Gemba::Config.new(path: @path)
|
|
793
|
+
refute c2.rewind_enabled?
|
|
794
|
+
assert_equal 20, c2.rewind_seconds
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# -- Edge cases -----------------------------------------------------------
|
|
798
|
+
|
|
799
|
+
def test_corrupt_json_falls_back_to_defaults
|
|
800
|
+
File.write(@path, "NOT VALID JSON {{{")
|
|
801
|
+
c = Gemba::Config.new(path: @path)
|
|
802
|
+
assert_equal 3, c.scale
|
|
803
|
+
assert_equal 100, c.volume
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def test_missing_file_uses_defaults
|
|
807
|
+
c = Gemba::Config.new(path: File.join(@dir, "nope.json"))
|
|
808
|
+
assert_equal 3, c.scale
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def test_forward_compat_new_global_key
|
|
812
|
+
# Simulate an old config file that doesn't have 'muted'
|
|
813
|
+
data = { "global" => { "scale" => 2, "volume" => 80 }, "gamepads" => {} }
|
|
814
|
+
File.write(@path, JSON.generate(data))
|
|
815
|
+
c = Gemba::Config.new(path: @path)
|
|
816
|
+
assert_equal 2, c.scale
|
|
817
|
+
assert_equal 80, c.volume
|
|
818
|
+
refute c.muted? # filled in from defaults
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def test_reload
|
|
822
|
+
c = new_config
|
|
823
|
+
c.scale = 2
|
|
824
|
+
c.save!
|
|
825
|
+
|
|
826
|
+
# Externally modify the file
|
|
827
|
+
data = JSON.parse(File.read(@path))
|
|
828
|
+
data["global"]["scale"] = 4
|
|
829
|
+
File.write(@path, JSON.generate(data))
|
|
830
|
+
|
|
831
|
+
c.reload!
|
|
832
|
+
assert_equal 4, c.scale
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# -- reset! ---------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
def test_reset_deletes_settings_file
|
|
838
|
+
c = new_config
|
|
839
|
+
c.save!
|
|
840
|
+
assert File.exist?(@path)
|
|
841
|
+
|
|
842
|
+
deleted = Gemba::Config.reset!(path: @path)
|
|
843
|
+
assert_equal @path, deleted
|
|
844
|
+
refute File.exist?(@path)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def test_reset_returns_nil_when_no_file
|
|
848
|
+
refute File.exist?(@path)
|
|
849
|
+
assert_nil Gemba::Config.reset!(path: @path)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# -- Recording settings ---------------------------------------------------
|
|
853
|
+
|
|
854
|
+
def test_recording_compression_default
|
|
855
|
+
assert_equal 1, new_config.recording_compression
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def test_recording_compression_setter
|
|
859
|
+
c = new_config
|
|
860
|
+
c.recording_compression = 6
|
|
861
|
+
assert_equal 6, c.recording_compression
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def test_recording_compression_clamps
|
|
865
|
+
c = new_config
|
|
866
|
+
c.recording_compression = 0
|
|
867
|
+
assert_equal 1, c.recording_compression
|
|
868
|
+
c.recording_compression = 99
|
|
869
|
+
assert_equal 9, c.recording_compression
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def test_recordings_dir
|
|
873
|
+
dir = new_config.recordings_dir
|
|
874
|
+
assert dir.end_with?('recordings'), "Expected recordings dir to end with 'recordings', got: #{dir}"
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# -- Pause on focus loss ---------------------------------------------------
|
|
878
|
+
|
|
879
|
+
def test_pause_on_focus_loss_default
|
|
880
|
+
assert new_config.pause_on_focus_loss?
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def test_set_pause_on_focus_loss
|
|
884
|
+
c = new_config
|
|
885
|
+
c.pause_on_focus_loss = false
|
|
886
|
+
refute c.pause_on_focus_loss?
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def test_round_trip_pause_on_focus_loss
|
|
890
|
+
c = new_config
|
|
891
|
+
c.pause_on_focus_loss = false
|
|
892
|
+
c.save!
|
|
893
|
+
|
|
894
|
+
c2 = Gemba::Config.new(path: @path)
|
|
895
|
+
refute c2.pause_on_focus_loss?
|
|
896
|
+
end
|
|
897
|
+
end
|