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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Click-to-show tooltip service for Tk widgets.
|
|
5
|
+
#
|
|
6
|
+
# Labels registered with {#register} get an underlined font (like HTML
|
|
7
|
+
# <abbr>). Clicking them shows a tooltip popup below the label. Only one
|
|
8
|
+
# tooltip is visible at a time; it auto-dismisses after {#dismiss_ms}
|
|
9
|
+
# unless the mouse hovers over the tip or its label.
|
|
10
|
+
#
|
|
11
|
+
# The tooltip is rendered as a frame inside the parent window (not a
|
|
12
|
+
# toplevel), so it draws as a true rectangle on all platforms.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# tips = TipService.new(app, parent: '.settings')
|
|
16
|
+
# tips.register('.settings.nb.video.lbl_color', 'Adjusts GBA LCD colors')
|
|
17
|
+
class TipService
|
|
18
|
+
DEFAULT_DISMISS_MS = 4000
|
|
19
|
+
|
|
20
|
+
# Tooltip colors (pale yellow with gray border, dark text)
|
|
21
|
+
TIP_BG = '#FFFFEE'
|
|
22
|
+
TIP_FG = '#333333'
|
|
23
|
+
TIP_BORDER = '#999999'
|
|
24
|
+
|
|
25
|
+
# @param app [Teek::App]
|
|
26
|
+
# @param parent [String] parent Toplevel path (for unique tooltip path)
|
|
27
|
+
# @param dismiss_ms [Integer] auto-dismiss delay in milliseconds
|
|
28
|
+
def initialize(app, parent: '.', dismiss_ms: DEFAULT_DISMISS_MS)
|
|
29
|
+
@app = app
|
|
30
|
+
@parent = parent
|
|
31
|
+
@tip_path = parent == '.' ? '.__tip' : "#{parent}.__tip"
|
|
32
|
+
@dismiss_ms = dismiss_ms
|
|
33
|
+
@target = nil
|
|
34
|
+
@timer = nil
|
|
35
|
+
@click_guard = false
|
|
36
|
+
|
|
37
|
+
# Underlined font for registered labels
|
|
38
|
+
@font_name = "__tip_font_#{parent.tr('.', '_')}"
|
|
39
|
+
@app.tcl_eval("catch {font create #{@font_name} {*}[font actual TkDefaultFont] -underline 1}")
|
|
40
|
+
|
|
41
|
+
# Click anywhere in the parent window dismisses the tooltip,
|
|
42
|
+
# unless the click was on a registered label (guard prevents that).
|
|
43
|
+
@app.command(:bind, @parent, '<Button-1>', proc {
|
|
44
|
+
if @click_guard
|
|
45
|
+
@click_guard = false
|
|
46
|
+
elsif showing?
|
|
47
|
+
hide
|
|
48
|
+
end
|
|
49
|
+
})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Integer] auto-dismiss delay in milliseconds
|
|
53
|
+
attr_accessor :dismiss_ms
|
|
54
|
+
|
|
55
|
+
# @return [String, nil] widget path of the currently showing tip's label
|
|
56
|
+
attr_reader :target
|
|
57
|
+
|
|
58
|
+
# Register a widget for click-to-show tooltip.
|
|
59
|
+
# @param widget_path [String] Tk widget path (typically a label)
|
|
60
|
+
# @param text [String] tooltip text (may contain \n for line breaks)
|
|
61
|
+
def register(widget_path, text)
|
|
62
|
+
@app.command(widget_path, 'configure', font: @font_name)
|
|
63
|
+
@app.command(:bind, widget_path, '<Button-1>', proc { toggle(widget_path, text) })
|
|
64
|
+
@app.command(:bind, widget_path, '<Enter>', proc { cancel_dismiss })
|
|
65
|
+
@app.command(:bind, widget_path, '<Leave>', proc { schedule_dismiss })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Show a tooltip below the given widget. Hides any existing tip first.
|
|
69
|
+
# @param widget_path [String]
|
|
70
|
+
# @param text [String]
|
|
71
|
+
def show(widget_path, text)
|
|
72
|
+
hide
|
|
73
|
+
|
|
74
|
+
@target = widget_path
|
|
75
|
+
|
|
76
|
+
# Position relative to the parent toplevel
|
|
77
|
+
lx, ly, _lw, lh = @app.interp.window_geometry(widget_path)
|
|
78
|
+
px, py, _pw, _ph = @app.interp.window_geometry(@parent)
|
|
79
|
+
rel_x = lx - px
|
|
80
|
+
rel_y = ly - py + lh + 4
|
|
81
|
+
|
|
82
|
+
# Border frame (1px border effect via padding)
|
|
83
|
+
@app.command(:frame, @tip_path, background: TIP_BORDER, borderwidth: 0)
|
|
84
|
+
|
|
85
|
+
@app.command(:label, "#{@tip_path}.l",
|
|
86
|
+
text: text, background: TIP_BG, foreground: TIP_FG,
|
|
87
|
+
padx: 8, pady: 6, justify: :left)
|
|
88
|
+
@app.command(:pack, "#{@tip_path}.l", padx: 1, pady: 1)
|
|
89
|
+
|
|
90
|
+
@app.command(:place, @tip_path, x: rel_x, y: rel_y)
|
|
91
|
+
@app.command(:raise, @tip_path)
|
|
92
|
+
|
|
93
|
+
# Pause auto-dismiss while hovering the tooltip itself
|
|
94
|
+
@app.command(:bind, @tip_path, '<Enter>', proc { cancel_dismiss })
|
|
95
|
+
@app.command(:bind, @tip_path, '<Leave>', proc { schedule_dismiss })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Hide the current tooltip.
|
|
99
|
+
def hide
|
|
100
|
+
cancel_dismiss
|
|
101
|
+
@target = nil
|
|
102
|
+
@app.tcl_eval("catch {destroy #{@tip_path}}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Boolean] true if a tooltip is currently visible
|
|
106
|
+
def showing?
|
|
107
|
+
!!@target
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def toggle(widget_path, text)
|
|
113
|
+
@click_guard = true
|
|
114
|
+
if @target == widget_path
|
|
115
|
+
hide
|
|
116
|
+
else
|
|
117
|
+
show(widget_path, text)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def schedule_dismiss
|
|
122
|
+
cancel_dismiss
|
|
123
|
+
@timer = @app.after(@dismiss_ms) { hide }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cancel_dismiss
|
|
127
|
+
if @timer
|
|
128
|
+
@app.command(:after, :cancel, @timer)
|
|
129
|
+
@timer = nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemba
|
|
4
|
+
# Renders timed notification toasts at the bottom of the game viewport.
|
|
5
|
+
#
|
|
6
|
+
# One toast at a time; showing a new one replaces the old. The background
|
|
7
|
+
# is a pre-rendered anti-aliased rounded rectangle (generated in C).
|
|
8
|
+
#
|
|
9
|
+
# All SDL2 objects (renderer, font, textures) are injected or created
|
|
10
|
+
# internally, so the class can be tested with lightweight mocks.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# toast = ToastOverlay.new(renderer: vp.renderer, font: font, duration: 1.5)
|
|
14
|
+
# toast.show("State saved to slot 1")
|
|
15
|
+
# # inside render loop:
|
|
16
|
+
# toast.draw(r, dest_rect)
|
|
17
|
+
class ToastOverlay
|
|
18
|
+
PAD_X = 14
|
|
19
|
+
PAD_Y = 8
|
|
20
|
+
RADIUS = 8
|
|
21
|
+
|
|
22
|
+
# @param renderer [Teek::SDL2::Renderer] creates background textures
|
|
23
|
+
# @param font [Teek::SDL2::Font] renders toast text
|
|
24
|
+
# @param duration [Float] default display time in seconds
|
|
25
|
+
# @param bg_fn [#call] generates ARGB pixel data for the rounded-rect
|
|
26
|
+
# background; signature: bg_fn.call(w, h, radius) → String.
|
|
27
|
+
# Defaults to the C-implemented {Gemba.toast_background}.
|
|
28
|
+
def initialize(renderer:, font:, duration: 1.5, bg_fn: Gemba.method(:toast_background))
|
|
29
|
+
@renderer = renderer
|
|
30
|
+
@font = font
|
|
31
|
+
@duration = duration
|
|
32
|
+
@bg_fn = bg_fn
|
|
33
|
+
@crop_h = compute_crop_h(font)
|
|
34
|
+
@bg_tex = nil
|
|
35
|
+
@text_tex = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Float] default display duration in seconds
|
|
39
|
+
attr_accessor :duration
|
|
40
|
+
|
|
41
|
+
# Whether a toast is currently visible.
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def visible?
|
|
44
|
+
!!@bg_tex
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Display a toast message. Replaces any existing toast.
|
|
48
|
+
#
|
|
49
|
+
# @param message [String]
|
|
50
|
+
# @param duration [Float, nil] seconds; nil uses the default
|
|
51
|
+
# @param permanent [Boolean] stays until {#destroy} is called
|
|
52
|
+
def show(message, duration: nil, permanent: false)
|
|
53
|
+
destroy
|
|
54
|
+
|
|
55
|
+
@text_tex = @font.render_text(message, 255, 255, 255)
|
|
56
|
+
tw = @text_tex.width
|
|
57
|
+
th = @crop_h || @text_tex.height
|
|
58
|
+
|
|
59
|
+
box_w = tw + PAD_X * 2
|
|
60
|
+
box_h = th + PAD_Y * 2
|
|
61
|
+
|
|
62
|
+
bg_pixels = @bg_fn.call(box_w, box_h, RADIUS)
|
|
63
|
+
@bg_tex = @renderer.create_texture(box_w, box_h, :streaming)
|
|
64
|
+
@bg_tex.update(bg_pixels)
|
|
65
|
+
@bg_tex.blend_mode = :blend
|
|
66
|
+
|
|
67
|
+
@box_w = box_w
|
|
68
|
+
@box_h = box_h
|
|
69
|
+
@text_w = tw
|
|
70
|
+
@text_h = th
|
|
71
|
+
@permanent = permanent
|
|
72
|
+
@expires = permanent ? nil : Process.clock_gettime(Process::CLOCK_MONOTONIC) + (duration || @duration)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Draw the toast centered at the bottom of the game area.
|
|
76
|
+
#
|
|
77
|
+
# @param r [Teek::SDL2::Renderer]
|
|
78
|
+
# @param dest [Array(Integer,Integer,Integer,Integer), nil] game area rect
|
|
79
|
+
def draw(r, dest)
|
|
80
|
+
return unless @bg_tex
|
|
81
|
+
unless @permanent
|
|
82
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
83
|
+
if now >= @expires
|
|
84
|
+
destroy
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Position: bottom-center of game area, 12px from bottom
|
|
90
|
+
if dest
|
|
91
|
+
cx = dest[0] + dest[2] / 2
|
|
92
|
+
by = dest[1] + dest[3] - 12 - @box_h
|
|
93
|
+
else
|
|
94
|
+
out_w, out_h = r.output_size
|
|
95
|
+
cx = out_w / 2
|
|
96
|
+
by = out_h - 12 - @box_h
|
|
97
|
+
end
|
|
98
|
+
bx = cx - @box_w / 2
|
|
99
|
+
|
|
100
|
+
# Background (pre-rendered with AA rounded corners)
|
|
101
|
+
r.copy(@bg_tex, nil, [bx, by, @box_w, @box_h])
|
|
102
|
+
# White text centered in the box
|
|
103
|
+
tx = bx + (@box_w - @text_w) / 2
|
|
104
|
+
ty = by + (@box_h - @text_h) / 2
|
|
105
|
+
r.copy(@text_tex, [0, 0, @text_w, @text_h],
|
|
106
|
+
[tx, ty, @text_w, @text_h])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Remove the current toast and free textures.
|
|
110
|
+
def destroy
|
|
111
|
+
@bg_tex&.destroy
|
|
112
|
+
@bg_tex = nil
|
|
113
|
+
@text_tex&.destroy
|
|
114
|
+
@text_tex = nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Crop height: ascent + partial descender. Excludes the very bottom
|
|
120
|
+
# rows where TTF anti-alias residue causes visible white-line artifacts.
|
|
121
|
+
def compute_crop_h(font)
|
|
122
|
+
return nil unless font
|
|
123
|
+
ascent = font.ascent
|
|
124
|
+
full_h = font.measure('p')[1]
|
|
125
|
+
[ascent + (full_h - ascent) / 2, full_h - 1].min
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/gemba.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "teek"
|
|
4
|
+
require "teek/sdl2"
|
|
5
|
+
require_relative "gemba/runtime"
|
|
6
|
+
require_relative "gemba/child_window"
|
|
7
|
+
require_relative "gemba/tip_service"
|
|
8
|
+
require_relative "gemba/settings_window"
|
|
9
|
+
require_relative "gemba/rom_info_window"
|
|
10
|
+
require_relative "gemba/save_state_picker"
|
|
11
|
+
require_relative "gemba/save_state_manager"
|
|
12
|
+
require_relative "gemba/toast_overlay"
|
|
13
|
+
require_relative "gemba/overlay_renderer"
|
|
14
|
+
require_relative "gemba/input_mappings"
|
|
15
|
+
require_relative "gemba/hotkey_map"
|
|
16
|
+
require_relative "gemba/recorder"
|
|
17
|
+
require_relative "gemba/player"
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Screenshot comparison helpers for SDL2 visual regression testing.
|
|
4
|
+
#
|
|
5
|
+
# Uses Renderer#read_pixels to capture GPU framebuffer output and
|
|
6
|
+
# ImageMagick to convert raw RGBA to PNG and compare against blessed baselines.
|
|
7
|
+
#
|
|
8
|
+
# Directory layout:
|
|
9
|
+
# screenshots/blessed/{platform}/ — committed gold images
|
|
10
|
+
# screenshots/unverified/{platform}/ — generated during test (gitignored)
|
|
11
|
+
# screenshots/diffs/{platform}/ — diff images on failure (gitignored)
|
|
12
|
+
|
|
13
|
+
require 'fileutils'
|
|
14
|
+
require 'open3'
|
|
15
|
+
require 'teek/platform'
|
|
16
|
+
|
|
17
|
+
module ScreenshotHelper
|
|
18
|
+
SCREENSHOTS_ROOT = File.expand_path('../screenshots', __dir__)
|
|
19
|
+
|
|
20
|
+
PLATFORM = Teek.platform.to_s
|
|
21
|
+
|
|
22
|
+
# Default pixel difference threshold for ImageMagick compare (AE metric).
|
|
23
|
+
# GPU drivers may produce minor anti-aliasing variations across runs.
|
|
24
|
+
THRESHOLD = Integer(ENV.fetch('SCREENSHOT_THRESHOLD', 100))
|
|
25
|
+
|
|
26
|
+
def self.blessed_dir
|
|
27
|
+
File.join(SCREENSHOTS_ROOT, 'blessed', PLATFORM)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.unverified_dir
|
|
31
|
+
File.join(SCREENSHOTS_ROOT, 'unverified', PLATFORM)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.diffs_dir
|
|
35
|
+
File.join(SCREENSHOTS_ROOT, 'diffs', PLATFORM)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.setup_dirs
|
|
39
|
+
FileUtils.mkdir_p(blessed_dir)
|
|
40
|
+
FileUtils.mkdir_p(unverified_dir)
|
|
41
|
+
FileUtils.mkdir_p(diffs_dir)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if ImageMagick is available.
|
|
45
|
+
def self.imagemagick?
|
|
46
|
+
return @imagemagick if defined?(@imagemagick)
|
|
47
|
+
_, _, status = Open3.capture3('magick', '-version')
|
|
48
|
+
@imagemagick = status.success?
|
|
49
|
+
rescue Errno::ENOENT
|
|
50
|
+
@imagemagick = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Save raw RGBA pixels as PNG via ImageMagick.
|
|
54
|
+
def self.save_png(pixels, width, height, path)
|
|
55
|
+
cmd = ['magick', '-size', "#{width}x#{height}", '-depth', '8', 'rgba:-', path]
|
|
56
|
+
IO.popen(cmd, 'wb') { |io| io.write(pixels) }
|
|
57
|
+
raise "magick convert failed for #{path}" unless $?.success?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compare two PNGs with ImageMagick compare (AE metric).
|
|
61
|
+
# Returns [passed, pixel_diff, output].
|
|
62
|
+
def self.compare(expected, actual, diff_output)
|
|
63
|
+
cmd = ['magick', 'compare', '-metric', 'AE', expected, actual, diff_output]
|
|
64
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
65
|
+
output = stdout + stderr
|
|
66
|
+
|
|
67
|
+
pixel_diff = output[/(\d+)/]&.to_i
|
|
68
|
+
passed = pixel_diff ? pixel_diff <= THRESHOLD : status.success?
|
|
69
|
+
|
|
70
|
+
[passed, pixel_diff, output.strip]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Assert that the current renderer output matches the blessed screenshot.
|
|
74
|
+
#
|
|
75
|
+
# Captures pixels from the renderer, saves to unverified/{platform}/{name}.png,
|
|
76
|
+
# then compares against blessed/{platform}/{name}.png.
|
|
77
|
+
#
|
|
78
|
+
# assert_sdl2_screenshot(renderer, "red_rect")
|
|
79
|
+
#
|
|
80
|
+
def assert_sdl2_screenshot(renderer, name, message: nil)
|
|
81
|
+
ScreenshotHelper.setup_dirs
|
|
82
|
+
|
|
83
|
+
unless ScreenshotHelper.imagemagick?
|
|
84
|
+
skip "ImageMagick not installed — skipping screenshot comparison"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
w, h = renderer.output_size
|
|
88
|
+
pixels = renderer.read_pixels
|
|
89
|
+
|
|
90
|
+
unverified = File.join(ScreenshotHelper.unverified_dir, "#{name}.png")
|
|
91
|
+
ScreenshotHelper.save_png(pixels, w, h, unverified)
|
|
92
|
+
|
|
93
|
+
blessed = File.join(ScreenshotHelper.blessed_dir, "#{name}.png")
|
|
94
|
+
|
|
95
|
+
unless File.exist?(blessed)
|
|
96
|
+
flunk "No blessed screenshot for '#{name}'. " \
|
|
97
|
+
"Inspect #{unverified} and run: rake screenshots:bless"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
diff = File.join(ScreenshotHelper.diffs_dir, "#{name}_diff.png")
|
|
101
|
+
passed, pixel_diff, output = ScreenshotHelper.compare(blessed, unverified, diff)
|
|
102
|
+
|
|
103
|
+
if passed
|
|
104
|
+
FileUtils.rm_f(diff)
|
|
105
|
+
else
|
|
106
|
+
msg = message || "Screenshot '#{name}' differs by #{pixel_diff} pixels (threshold: #{ScreenshotHelper::THRESHOLD})"
|
|
107
|
+
msg += "\n Blessed: #{blessed}"
|
|
108
|
+
msg += "\n Unverified: #{unverified}"
|
|
109
|
+
msg += "\n Diff: #{diff}"
|
|
110
|
+
flunk msg
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared SimpleCov configuration for all test contexts:
|
|
4
|
+
# - Main test process
|
|
5
|
+
# - Teek::TestWorker subprocess
|
|
6
|
+
# - Collation (Rakefile)
|
|
7
|
+
# - Subprocess preamble (tk_test_helper.rb)
|
|
8
|
+
|
|
9
|
+
module SimpleCovConfig
|
|
10
|
+
PROJECT_ROOT = File.expand_path('../..', __dir__)
|
|
11
|
+
|
|
12
|
+
FILTERS = [
|
|
13
|
+
'/test/',
|
|
14
|
+
%r{^/ext/},
|
|
15
|
+
'lib/teek/method_coverage_service.rb',
|
|
16
|
+
'lib/teek/background_none.rb',
|
|
17
|
+
'lib/teek/demo_support.rb',
|
|
18
|
+
'lib/teek/version.rb',
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def self.apply_filters(simplecov_context)
|
|
22
|
+
FILTERS.each { |f| simplecov_context.add_filter(f) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.apply_groups(simplecov_context)
|
|
26
|
+
simplecov_context.add_group 'Core', 'lib/teek.rb'
|
|
27
|
+
simplecov_context.add_group 'SDL2', 'lib/teek/sdl2'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Generate add_filter code lines from FILTERS array (for subprocess preamble)
|
|
31
|
+
def self.filters_as_code
|
|
32
|
+
FILTERS.map do |f|
|
|
33
|
+
case f
|
|
34
|
+
when Regexp then "add_filter #{f.inspect}"
|
|
35
|
+
when String then "add_filter '#{f}'"
|
|
36
|
+
end
|
|
37
|
+
end.join("\n ")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Ruby code string for subprocess SimpleCov setup
|
|
41
|
+
def self.subprocess_preamble(project_root: PROJECT_ROOT)
|
|
42
|
+
<<~RUBY
|
|
43
|
+
if ENV['COVERAGE']
|
|
44
|
+
require 'simplecov'
|
|
45
|
+
|
|
46
|
+
coverage_name = ENV['COVERAGE_NAME'] || 'default'
|
|
47
|
+
SimpleCov.coverage_dir "#{project_root}/coverage/results/\#{coverage_name}_sub_\#{Process.pid}"
|
|
48
|
+
SimpleCov.command_name "subprocess:\#{Process.pid}"
|
|
49
|
+
SimpleCov.print_error_status = false
|
|
50
|
+
SimpleCov.formatter SimpleCov::Formatter::SimpleFormatter
|
|
51
|
+
|
|
52
|
+
SimpleCov.start do
|
|
53
|
+
#{filters_as_code}
|
|
54
|
+
track_files "#{project_root}/lib/**/*.rb"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
end
|