[PATCH 7/7] test: add framebuffer screenshot testing via QMP screendump

Ahmad Fatoum a.fatoum at barebox.org
Mon Apr 13 00:44:50 PDT 2026


Add screendump() and parse_ppm() helpers to capture and parse QEMU
framebuffer screenshots using the QMP screendump command. Use them in a
new test that draws a solid color with fbtest and verifies the pixels
match. The test auto-skips when no framebuffer is available (e.g. without
--graphics).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
Signed-off-by: Ahmad Fatoum <a.fatoum at barebox.org>
---
 test/py/helper.py      | 53 ++++++++++++++++++++++++++++++
 test/py/test_fbtest.py | 74 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 127 insertions(+)
 create mode 100644 test/py/test_fbtest.py

diff --git a/test/py/helper.py b/test/py/helper.py
index ab615280048f..2e3bc489d88a 100644
--- a/test/py/helper.py
+++ b/test/py/helper.py
@@ -6,6 +6,7 @@ import os
 import re
 import shlex
 import subprocess
+import tempfile
 
 
 def parse_config(lines):
@@ -200,6 +201,58 @@ def skip_disabled(config, *options):
             pytest.skip("skipping test due to disabled " + (",".join(undefined)) + " dependency")
 
 
+def parse_ppm(path):
+    """Parse a PPM P6 (binary) image file.
+
+    Returns (width, height, data) where data is bytes of RGB pixel values.
+    Pixel (x, y) starts at offset (y * width + x) * 3.
+    """
+    with open(path, 'rb') as f:
+        magic = f.readline().strip()
+        assert magic == b'P6', f"Expected P6, got {magic}"
+
+        line = f.readline()
+        while line.startswith(b'#'):
+            line = f.readline()
+
+        width, height = map(int, line.split())
+        maxval = int(f.readline().strip())
+        assert maxval == 255
+
+        data = f.read()
+        expected = width * height * 3
+        assert len(data) == expected, \
+            f"Expected {expected} bytes, got {len(data)}"
+
+    return width, height, data
+
+
+def screendump(qemu, path=None):
+    """Capture a QEMU framebuffer screenshot via QMP screendump.
+
+    Args:
+        qemu: A labgrid QEMUDriver instance.
+        path: Optional host path for the PPM file. If None, a temp file is used.
+
+    Returns:
+        (width, height, data) tuple from parse_ppm().
+    """
+    if qemu is None:
+        pytest.skip("screendump requires a QEMU target")
+
+    cleanup = path is None
+    if path is None:
+        fd, path = tempfile.mkstemp(suffix='.ppm')
+        os.close(fd)
+
+    try:
+        qemu.monitor_command('screendump', {'filename': path})
+        return parse_ppm(path)
+    finally:
+        if cleanup:
+            os.unlink(path)
+
+
 def ensure_debian_iso(env, destdir):
     """
     Extract Debian kernel and initrd from ISO into destdir.
diff --git a/test/py/test_fbtest.py b/test/py/test_fbtest.py
new file mode 100644
index 000000000000..58b70d7dc4ab
--- /dev/null
+++ b/test/py/test_fbtest.py
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+import hashlib
+import pytest
+from .helper import skip_disabled, screendump
+
+
+ at pytest.fixture(autouse=True)
+def check_fbtest_in_qemu(barebox, env, barebox_config):
+    skip_disabled(barebox_config, "CONFIG_CMD_FBTEST")
+
+    if 'qemu' not in env.get_target_features():
+        pytest.skip("fbtest tests only possible with QEMU")
+
+    _, _, ret = barebox.run("test -e /dev/fb0")
+    if ret != 0:
+        pytest.skip("no framebuffer device available")
+
+
+def assert_solid_color(data, width, height, color):
+    """Verify that sampled pixels in the lower half match the expected color."""
+    sample_points = [
+        (width // 2, height * 3 // 4),
+        (width // 4, height * 3 // 4),
+        (width * 3 // 4, height * 3 // 4),
+        (width // 2, height - 2),
+    ]
+
+    r_exp = (color >> 16) & 0xff
+    g_exp = (color >> 8) & 0xff
+    b_exp = color & 0xff
+
+    for x, y in sample_points:
+        off = (y * width + x) * 3
+        r, g, b = data[off], data[off + 1], data[off + 2]
+        assert abs(r - r_exp) < 10, f"pixel ({x},{y}): R={r}, expected {r_exp}"
+        assert abs(g - g_exp) < 10, f"pixel ({x},{y}): G={g}, expected {g_exp}"
+        assert abs(b - b_exp) < 10, f"pixel ({x},{y}): B={b}, expected {b_exp}"
+
+
+def test_fb_solid_color(barebox, barebox_config, strategy):
+    color = 0xff0000
+    barebox.run_check(f"fbtest -p solid -c {color:06x}")
+
+    width, height, data = screendump(strategy.qemu)
+    assert_solid_color(data, width, height, color)
+
+
+def screendump_hash(qemu):
+    """Capture a screenshot and return a hash of the pixel data."""
+    _, _, data = screendump(qemu)
+    return hashlib.sha256(data).hexdigest()
+
+
+def test_fb_patterns_distinct_and_stable(barebox, barebox_config, strategy):
+    patterns = ["solid", "geometry", "bars", "gradient"]
+
+    # Render each pattern twice and collect hashes
+    hashes = {p: [] for p in patterns}
+
+    for run in range(2):
+        for pattern in patterns:
+            barebox.run_check(f"fbtest -p {pattern} -c ffffff")
+            hashes[pattern].append(screendump_hash(strategy.qemu))
+
+    # Same pattern must produce the same output across runs
+    for pattern in patterns:
+        assert hashes[pattern][0] == hashes[pattern][1], \
+            f"pattern '{pattern}' produced different output across runs"
+
+    # Different patterns must produce different output
+    unique = set(hashes[p][0] for p in patterns)
+    assert len(unique) == len(patterns), \
+        f"expected {len(patterns)} distinct patterns, got {len(unique)}"
-- 
2.47.3




More information about the barebox mailing list