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_core.rb
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
class TestMGBACore < Minitest::Test
|
|
9
|
+
# Generated by: ruby gemba/scripts/generate_test_rom.rb
|
|
10
|
+
TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
|
|
11
|
+
|
|
12
|
+
def setup
|
|
13
|
+
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
14
|
+
@core = Gemba::Core.new(TEST_ROM)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def teardown
|
|
18
|
+
@core&.destroy unless @core.nil? || @core.destroyed?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# -- Dimensions --------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def test_width
|
|
24
|
+
assert_equal 240, @core.width
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_height
|
|
28
|
+
assert_equal 160, @core.height
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# -- Title -------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def test_title
|
|
34
|
+
assert_equal "GEMBATEST", @core.title
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# -- ROM metadata ------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_game_code
|
|
40
|
+
code = @core.game_code
|
|
41
|
+
# mGBA prefixes with platform: "AGB-" for GBA
|
|
42
|
+
assert_equal "AGB-BGBE", code
|
|
43
|
+
refute_includes code, "\0", "game_code must not contain null bytes"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_maker_code
|
|
47
|
+
maker = @core.maker_code
|
|
48
|
+
assert_equal "01", maker
|
|
49
|
+
refute_includes maker, "\0", "maker_code must not contain null bytes"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_title_no_null_bytes
|
|
53
|
+
title = @core.title
|
|
54
|
+
assert_equal "GEMBATEST", title
|
|
55
|
+
refute_includes title, "\0", "title must not contain null bytes"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_checksum
|
|
59
|
+
crc = @core.checksum
|
|
60
|
+
assert_kind_of Integer, crc
|
|
61
|
+
assert crc > 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_platform
|
|
65
|
+
assert_equal "GBA", @core.platform
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_rom_size
|
|
69
|
+
size = @core.rom_size
|
|
70
|
+
assert_kind_of Integer, size
|
|
71
|
+
assert size > 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# -- Lifecycle ---------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def test_not_destroyed_initially
|
|
77
|
+
refute @core.destroyed?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_destroy
|
|
81
|
+
@core.destroy
|
|
82
|
+
assert @core.destroyed?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_double_destroy_is_safe
|
|
86
|
+
@core.destroy
|
|
87
|
+
@core.destroy
|
|
88
|
+
assert @core.destroyed?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_methods_raise_after_destroy
|
|
92
|
+
@core.destroy
|
|
93
|
+
assert_raises(RuntimeError) { @core.run_frame }
|
|
94
|
+
assert_raises(RuntimeError) { @core.video_buffer }
|
|
95
|
+
assert_raises(RuntimeError) { @core.video_buffer_argb }
|
|
96
|
+
assert_raises(RuntimeError) { @core.audio_buffer }
|
|
97
|
+
assert_raises(RuntimeError) { @core.set_keys(0) }
|
|
98
|
+
assert_raises(RuntimeError) { @core.width }
|
|
99
|
+
assert_raises(RuntimeError) { @core.height }
|
|
100
|
+
assert_raises(RuntimeError) { @core.title }
|
|
101
|
+
assert_raises(RuntimeError) { @core.game_code }
|
|
102
|
+
assert_raises(RuntimeError) { @core.maker_code }
|
|
103
|
+
assert_raises(RuntimeError) { @core.checksum }
|
|
104
|
+
assert_raises(RuntimeError) { @core.platform }
|
|
105
|
+
assert_raises(RuntimeError) { @core.rom_size }
|
|
106
|
+
assert_raises(RuntimeError) { @core.save_state_to_file("/tmp/x.ss") }
|
|
107
|
+
assert_raises(RuntimeError) { @core.load_state_from_file("/tmp/x.ss") }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# -- Frame emulation ---------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def test_run_frame
|
|
113
|
+
@core.run_frame
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_video_buffer_size
|
|
117
|
+
@core.run_frame
|
|
118
|
+
buf = @core.video_buffer
|
|
119
|
+
assert_kind_of String, buf
|
|
120
|
+
assert_equal 240 * 160 * 4, buf.bytesize
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_video_buffer_argb_size
|
|
124
|
+
@core.run_frame
|
|
125
|
+
buf = @core.video_buffer_argb
|
|
126
|
+
assert_kind_of String, buf
|
|
127
|
+
assert_equal 240 * 160 * 4, buf.bytesize
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_video_buffer_argb_swaps_r_and_b
|
|
131
|
+
@core.run_frame
|
|
132
|
+
raw = @core.video_buffer.unpack('V*')
|
|
133
|
+
argb = @core.video_buffer_argb.unpack('V*')
|
|
134
|
+
assert_equal raw.size, argb.size
|
|
135
|
+
# Verify R↔B swap + forced opaque alpha for first non-zero pixel
|
|
136
|
+
idx = raw.index { |px| px != 0 } || 0
|
|
137
|
+
px = raw[idx]
|
|
138
|
+
expected = 0xFF000000 |
|
|
139
|
+
((px & 0x000000FF) << 16) |
|
|
140
|
+
(px & 0x0000FF00) |
|
|
141
|
+
((px & 0x00FF0000) >> 16)
|
|
142
|
+
assert_equal expected, argb[idx]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_audio_buffer_is_stereo_int16
|
|
146
|
+
@core.run_frame
|
|
147
|
+
buf = @core.audio_buffer
|
|
148
|
+
assert_kind_of String, buf
|
|
149
|
+
assert_equal 0, buf.bytesize % 4, "should be stereo int16 (4 bytes per frame)"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_multiple_frames
|
|
153
|
+
10.times { @core.run_frame }
|
|
154
|
+
assert_equal 240 * 160 * 4, @core.video_buffer.bytesize
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# -- Input -------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def test_set_keys
|
|
160
|
+
@core.set_keys(Gemba::KEY_A | Gemba::KEY_START)
|
|
161
|
+
@core.run_frame
|
|
162
|
+
@core.set_keys(0)
|
|
163
|
+
@core.run_frame
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# -- Save files --------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def test_save_dir_creates_sav_file
|
|
169
|
+
Dir.mktmpdir("gemba-saves") do |dir|
|
|
170
|
+
core = Gemba::Core.new(TEST_ROM, dir)
|
|
171
|
+
core.run_frame
|
|
172
|
+
core.destroy
|
|
173
|
+
sav = Dir.glob(File.join(dir, "*.sav"))
|
|
174
|
+
assert_equal 1, sav.size, "Expected a .sav file in #{dir}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_save_dir_nil_uses_rom_directory
|
|
179
|
+
# When save_dir is nil, saves go alongside the ROM — just verify no crash
|
|
180
|
+
core = Gemba::Core.new(TEST_ROM)
|
|
181
|
+
core.run_frame
|
|
182
|
+
core.destroy
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# -- Save states -------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def test_save_state_to_file
|
|
188
|
+
@core.run_frame
|
|
189
|
+
Dir.mktmpdir("gemba-states") do |dir|
|
|
190
|
+
path = File.join(dir, "test.ss1")
|
|
191
|
+
assert @core.save_state_to_file(path), "save_state_to_file should return true"
|
|
192
|
+
assert File.exist?(path), "State file should exist"
|
|
193
|
+
assert File.size(path) > 0, "State file should not be empty"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_load_state_from_file
|
|
198
|
+
# Run several frames to reach a known state
|
|
199
|
+
10.times { @core.run_frame }
|
|
200
|
+
|
|
201
|
+
Dir.mktmpdir("gemba-states") do |dir|
|
|
202
|
+
path = File.join(dir, "test.ss1")
|
|
203
|
+
|
|
204
|
+
# Save state at frame 10
|
|
205
|
+
assert @core.save_state_to_file(path)
|
|
206
|
+
|
|
207
|
+
# Run more frames to advance past the saved state
|
|
208
|
+
10.times { @core.run_frame }
|
|
209
|
+
|
|
210
|
+
# Load state — should succeed and restore core to a valid state
|
|
211
|
+
assert @core.load_state_from_file(path), "load_state_from_file should return true"
|
|
212
|
+
|
|
213
|
+
# Core should be functional after loading: run a frame without crashing
|
|
214
|
+
@core.run_frame
|
|
215
|
+
buf = @core.video_buffer
|
|
216
|
+
assert_equal 240 * 160 * 4, buf.bytesize, "Video buffer should be valid after state load"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_load_state_nonexistent_returns_false
|
|
221
|
+
result = @core.load_state_from_file("/no/such/state.ss1")
|
|
222
|
+
refute result, "load_state_from_file should return false for missing file"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def test_save_state_raises_on_bad_path
|
|
226
|
+
@core.run_frame
|
|
227
|
+
assert_raises(RuntimeError) { @core.save_state_to_file("/no/such/dir/state.ss1") }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_save_state_round_trip_preserves_state
|
|
231
|
+
# Run to frame 10, save, continue to frame 20, load, run 1 frame.
|
|
232
|
+
# If state was truly restored, the frame after load should match
|
|
233
|
+
# frame 11 from a fresh run.
|
|
234
|
+
10.times { @core.run_frame }
|
|
235
|
+
|
|
236
|
+
Dir.mktmpdir("gemba-states") do |dir|
|
|
237
|
+
path = File.join(dir, "test.ss1")
|
|
238
|
+
assert @core.save_state_to_file(path)
|
|
239
|
+
|
|
240
|
+
# Capture frame 11 from saved state
|
|
241
|
+
@core.run_frame
|
|
242
|
+
buf_frame_11 = @core.video_buffer
|
|
243
|
+
|
|
244
|
+
# Reload state (back to frame 10)
|
|
245
|
+
assert @core.load_state_from_file(path)
|
|
246
|
+
|
|
247
|
+
# Run 1 frame from restored state (should be frame 11 again)
|
|
248
|
+
@core.run_frame
|
|
249
|
+
buf_restored_11 = @core.video_buffer
|
|
250
|
+
|
|
251
|
+
assert_equal buf_frame_11, buf_restored_11,
|
|
252
|
+
"Frame after load should match frame after save point"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# -- Color correction --------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
def test_color_correction_defaults_to_false
|
|
259
|
+
refute @core.color_correction?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_color_correction_toggle
|
|
263
|
+
@core.color_correction = true
|
|
264
|
+
assert @core.color_correction?
|
|
265
|
+
@core.color_correction = false
|
|
266
|
+
refute @core.color_correction?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def test_color_correction_modifies_argb_output
|
|
270
|
+
@core.run_frame
|
|
271
|
+
buf_normal = @core.video_buffer_argb
|
|
272
|
+
|
|
273
|
+
@core.color_correction = true
|
|
274
|
+
buf_corrected = @core.video_buffer_argb
|
|
275
|
+
|
|
276
|
+
# The corrected buffer should differ (color values are transformed)
|
|
277
|
+
refute_equal buf_normal, buf_corrected,
|
|
278
|
+
"Color-corrected ARGB output should differ from uncorrected"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# -- Frame blending -----------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def test_frame_blending_defaults_to_false
|
|
284
|
+
refute @core.frame_blending?
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_frame_blending_toggle
|
|
288
|
+
@core.frame_blending = true
|
|
289
|
+
assert @core.frame_blending?
|
|
290
|
+
@core.frame_blending = false
|
|
291
|
+
refute @core.frame_blending?
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def test_frame_blending_modifies_argb_output
|
|
295
|
+
@core.run_frame
|
|
296
|
+
buf_normal = @core.video_buffer_argb
|
|
297
|
+
|
|
298
|
+
@core.frame_blending = true
|
|
299
|
+
# First call with blending on blends current frame with zeroed prev_frame
|
|
300
|
+
buf_blended = @core.video_buffer_argb
|
|
301
|
+
|
|
302
|
+
refute_equal buf_normal, buf_blended,
|
|
303
|
+
"Frame-blended ARGB output should differ from unblended"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_frame_blending_stabilizes_on_static_content
|
|
307
|
+
@core.frame_blending = true
|
|
308
|
+
@core.run_frame
|
|
309
|
+
@core.video_buffer_argb # prime prev_frame buffer
|
|
310
|
+
@core.run_frame
|
|
311
|
+
buf_a = @core.video_buffer_argb
|
|
312
|
+
@core.run_frame
|
|
313
|
+
buf_b = @core.video_buffer_argb
|
|
314
|
+
|
|
315
|
+
# Static content (test ROM draws nothing) should produce identical blended
|
|
316
|
+
# output once prev_frame is primed
|
|
317
|
+
assert_equal buf_a, buf_b,
|
|
318
|
+
"Frame blending on static content should stabilize"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# -- Rewind ----------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def test_rewind_init_and_count
|
|
324
|
+
@core.rewind_init(3)
|
|
325
|
+
assert_equal 0, @core.rewind_count
|
|
326
|
+
@core.run_frame
|
|
327
|
+
@core.rewind_push
|
|
328
|
+
assert_equal 1, @core.rewind_count
|
|
329
|
+
@core.run_frame
|
|
330
|
+
@core.rewind_push
|
|
331
|
+
assert_equal 2, @core.rewind_count
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def test_rewind_pop_restores_state
|
|
335
|
+
@core.rewind_init(5)
|
|
336
|
+
10.times { @core.run_frame }
|
|
337
|
+
|
|
338
|
+
# Save snapshot at frame 10
|
|
339
|
+
@core.rewind_push
|
|
340
|
+
@core.run_frame
|
|
341
|
+
buf_11 = @core.video_buffer
|
|
342
|
+
|
|
343
|
+
# Advance to frame 20
|
|
344
|
+
9.times { @core.run_frame }
|
|
345
|
+
|
|
346
|
+
# Rewind to frame 10
|
|
347
|
+
assert_equal true, @core.rewind_pop
|
|
348
|
+
|
|
349
|
+
# Run one frame from restored state → should match frame 11
|
|
350
|
+
@core.run_frame
|
|
351
|
+
buf_restored = @core.video_buffer
|
|
352
|
+
assert_equal buf_11, buf_restored
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def test_rewind_pop_empty_returns_false
|
|
356
|
+
@core.rewind_init(3)
|
|
357
|
+
assert_equal false, @core.rewind_pop
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def test_rewind_pop_without_init_returns_false
|
|
361
|
+
assert_equal false, @core.rewind_pop
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def test_rewind_circular_buffer
|
|
365
|
+
@core.rewind_init(3)
|
|
366
|
+
5.times do
|
|
367
|
+
@core.run_frame
|
|
368
|
+
@core.rewind_push
|
|
369
|
+
end
|
|
370
|
+
# Ring buffer capacity is 3, so count stays at 3
|
|
371
|
+
assert_equal 3, @core.rewind_count
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_rewind_deinit_clears_buffer
|
|
375
|
+
@core.rewind_init(3)
|
|
376
|
+
@core.run_frame
|
|
377
|
+
@core.rewind_push
|
|
378
|
+
@core.rewind_deinit
|
|
379
|
+
assert_equal 0, @core.rewind_count
|
|
380
|
+
assert_equal false, @core.rewind_pop
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def test_rewind_init_raises_on_invalid_capacity
|
|
384
|
+
assert_raises(ArgumentError) { @core.rewind_init(0) }
|
|
385
|
+
assert_raises(ArgumentError) { @core.rewind_init(-1) }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# -- Error handling ----------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def test_nonexistent_file
|
|
391
|
+
assert_raises(ArgumentError) { Gemba::Core.new("/no/such/game.gba") }
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def test_invalid_extension
|
|
395
|
+
Tempfile.create(["bad", ".txt"]) do |f|
|
|
396
|
+
f.write("not a rom")
|
|
397
|
+
f.flush
|
|
398
|
+
assert_raises(ArgumentError) { Gemba::Core.new(f.path) }
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba"
|
|
5
|
+
require_relative "../lib/gemba/config"
|
|
6
|
+
require_relative "../lib/gemba/input_mappings"
|
|
7
|
+
require_relative "support/input_mocks"
|
|
8
|
+
|
|
9
|
+
class TestGamepadMap < Minitest::Test
|
|
10
|
+
def setup
|
|
11
|
+
@config = MockInputConfig.new
|
|
12
|
+
@map = Gemba::GamepadMap.new(@config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_default_labels
|
|
16
|
+
labels = @map.labels
|
|
17
|
+
assert_equal 'a', labels[:a]
|
|
18
|
+
assert_equal 'b', labels[:b]
|
|
19
|
+
assert_equal 'start', labels[:start]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_set_remap
|
|
23
|
+
@map.set(:a, :y)
|
|
24
|
+
assert_equal 'y', @map.labels[:a]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_set_removes_old_binding
|
|
28
|
+
@map.set(:a, :y)
|
|
29
|
+
refute @map.labels.values.include?('a')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_reset
|
|
33
|
+
@map.set(:a, :y)
|
|
34
|
+
@map.set_dead_zone(12000)
|
|
35
|
+
@map.reset!
|
|
36
|
+
assert_equal 'a', @map.labels[:a]
|
|
37
|
+
assert_equal Gemba::GamepadMap::DEFAULT_DEAD_ZONE, @map.dead_zone
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_mask_no_device
|
|
41
|
+
assert_equal 0, @map.mask
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_mask_with_device
|
|
45
|
+
gp = MockGamepad.new
|
|
46
|
+
@map.device = gp
|
|
47
|
+
gp.buttons_pressed.add(:a)
|
|
48
|
+
mask = @map.mask
|
|
49
|
+
assert_equal Gemba::KEY_A, mask & Gemba::KEY_A
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_mask_closed_device
|
|
53
|
+
gp = MockGamepad.new
|
|
54
|
+
@map.device = gp
|
|
55
|
+
gp.buttons_pressed.add(:a)
|
|
56
|
+
gp.close!
|
|
57
|
+
assert_equal 0, @map.mask
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_supports_deadzone
|
|
61
|
+
assert @map.supports_deadzone?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_set_dead_zone
|
|
65
|
+
@map.set_dead_zone(12000)
|
|
66
|
+
assert_equal 12000, @map.dead_zone
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_dead_zone_pct
|
|
70
|
+
@map.set_dead_zone(16384)
|
|
71
|
+
assert_equal 50, @map.dead_zone_pct
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_load_config
|
|
75
|
+
gp = MockGamepad.new
|
|
76
|
+
@map.device = gp
|
|
77
|
+
@map.load_config
|
|
78
|
+
assert_equal 'x', @map.labels[:a]
|
|
79
|
+
assert_equal 'y', @map.labels[:b]
|
|
80
|
+
assert_equal (15 / 100.0 * 32767).round, @map.dead_zone
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_load_config_no_device
|
|
84
|
+
@map.load_config
|
|
85
|
+
assert_equal 'a', @map.labels[:a]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_reload
|
|
89
|
+
gp = MockGamepad.new
|
|
90
|
+
@map.device = gp
|
|
91
|
+
@map.set(:a, :y)
|
|
92
|
+
@map.reload!
|
|
93
|
+
assert @config.calls.any? { |c| c[0] == :reload! }
|
|
94
|
+
assert_equal 'x', @map.labels[:a]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_save_to_config
|
|
98
|
+
gp = MockGamepad.new(guid: 'test-guid', name: 'Test Pad')
|
|
99
|
+
@map.device = gp
|
|
100
|
+
@map.save_to_config
|
|
101
|
+
gp_calls = @config.calls.select { |c| c[0] == :gamepad }
|
|
102
|
+
assert_equal 1, gp_calls.size
|
|
103
|
+
assert_equal 'test-guid', gp_calls[0][1]
|
|
104
|
+
|
|
105
|
+
dz_calls = @config.calls.select { |c| c[0] == :set_dead_zone }
|
|
106
|
+
assert_equal 1, dz_calls.size
|
|
107
|
+
|
|
108
|
+
set_calls = @config.calls.select { |c| c[0] == :set_mapping }
|
|
109
|
+
assert_equal 10, set_calls.size
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_save_to_config_no_device
|
|
113
|
+
@map.save_to_config
|
|
114
|
+
assert_empty @config.calls.select { |c| c[0] == :set_mapping }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require "gemba/headless"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class TestHeadlessPlayer < Minitest::Test
|
|
8
|
+
TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# -- Lifecycle ---------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def test_open_and_close
|
|
17
|
+
player = Gemba::HeadlessPlayer.new(TEST_ROM)
|
|
18
|
+
refute player.closed?
|
|
19
|
+
player.close
|
|
20
|
+
assert player.closed?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_double_close_is_safe
|
|
24
|
+
player = Gemba::HeadlessPlayer.new(TEST_ROM)
|
|
25
|
+
player.close
|
|
26
|
+
player.close # should not raise
|
|
27
|
+
assert player.closed?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_block_form
|
|
31
|
+
result = Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
32
|
+
refute player.closed?
|
|
33
|
+
:ok
|
|
34
|
+
end
|
|
35
|
+
assert_equal :ok, result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_block_form_closes_on_exception
|
|
39
|
+
assert_raises(RuntimeError) do
|
|
40
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
41
|
+
@ref = player
|
|
42
|
+
raise "boom"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
assert @ref.closed?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# -- Stepping ----------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def test_step_single_frame
|
|
51
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
52
|
+
player.step # should not raise
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_step_multiple_frames
|
|
57
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
58
|
+
player.step(60) # should not raise
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_step_after_close_raises
|
|
63
|
+
player = Gemba::HeadlessPlayer.new(TEST_ROM)
|
|
64
|
+
player.close
|
|
65
|
+
assert_raises(RuntimeError) { player.step }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# -- Input -------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def test_press_and_release
|
|
71
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
72
|
+
player.press(Gemba::KEY_A | Gemba::KEY_START)
|
|
73
|
+
player.step
|
|
74
|
+
player.release_all
|
|
75
|
+
player.step
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# -- Buffers -----------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def test_video_buffer_argb_size
|
|
82
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
83
|
+
player.step
|
|
84
|
+
buf = player.video_buffer_argb
|
|
85
|
+
assert_equal 240 * 160 * 4, buf.bytesize
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_audio_buffer_returns_data
|
|
90
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
91
|
+
player.step
|
|
92
|
+
buf = player.audio_buffer
|
|
93
|
+
assert_kind_of String, buf
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# -- Dimensions --------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def test_width_and_height
|
|
100
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
101
|
+
assert_equal 240, player.width
|
|
102
|
+
assert_equal 160, player.height
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# -- ROM metadata ------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def test_title
|
|
109
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
110
|
+
assert_equal "GEMBATEST", player.title
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_game_code
|
|
115
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
116
|
+
assert_equal "AGB-BGBE", player.game_code
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_maker_code
|
|
121
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
122
|
+
assert_equal "01", player.maker_code
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_checksum
|
|
127
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
128
|
+
assert_kind_of Integer, player.checksum
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_platform
|
|
133
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
134
|
+
assert_equal "GBA", player.platform
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_rom_size
|
|
139
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
140
|
+
assert_operator player.rom_size, :>, 0
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# -- Save states -------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def test_save_and_load_state
|
|
147
|
+
Dir.mktmpdir do |dir|
|
|
148
|
+
path = File.join(dir, "test.ss1")
|
|
149
|
+
|
|
150
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
151
|
+
player.step(10)
|
|
152
|
+
assert player.save_state(path)
|
|
153
|
+
assert File.exist?(path)
|
|
154
|
+
|
|
155
|
+
player.step(60)
|
|
156
|
+
assert player.load_state(path)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# -- Rewind ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def test_rewind_init_and_count
|
|
164
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
165
|
+
player.rewind_init(3)
|
|
166
|
+
assert_equal 0, player.rewind_count
|
|
167
|
+
|
|
168
|
+
player.step
|
|
169
|
+
player.rewind_push
|
|
170
|
+
assert_equal 1, player.rewind_count
|
|
171
|
+
|
|
172
|
+
player.step
|
|
173
|
+
player.rewind_push
|
|
174
|
+
assert_equal 2, player.rewind_count
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_rewind_pop
|
|
179
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
180
|
+
player.rewind_init(5)
|
|
181
|
+
player.step(10)
|
|
182
|
+
player.rewind_push
|
|
183
|
+
player.step(10)
|
|
184
|
+
assert player.rewind_pop
|
|
185
|
+
assert_equal 0, player.rewind_count
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_rewind_pop_empty_returns_false
|
|
190
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
191
|
+
player.rewind_init(3)
|
|
192
|
+
refute player.rewind_pop
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def test_rewind_deinit
|
|
197
|
+
Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
|
|
198
|
+
player.rewind_init(3)
|
|
199
|
+
player.step
|
|
200
|
+
player.rewind_push
|
|
201
|
+
player.rewind_deinit
|
|
202
|
+
assert_equal 0, player.rewind_count
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|