From: Ahmad Fatoum <a.fatoum@barebox.org>
To: barebox@lists.infradead.org
Cc: "Claude Opus 4.6 (1M context)" <noreply@anthropic.com>,
Ahmad Fatoum <a.fatoum@barebox.org>
Subject: [PATCH 7/7] test: add framebuffer screenshot testing via QMP screendump
Date: Mon, 13 Apr 2026 09:44:50 +0200 [thread overview]
Message-ID: <20260413074522.1410710-8-a.fatoum@barebox.org> (raw)
In-Reply-To: <20260413074522.1410710-1-a.fatoum@barebox.org>
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@anthropic.com>
Signed-off-by: Ahmad Fatoum <a.fatoum@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
+
+
+@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
next prev parent reply other threads:[~2026-04-13 7:46 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-13 7:44 [PATCH 0/7] test: Add framebuffer test Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 1/7] ARM: multi_v8_defconfig: enable QEMU ramfb driver Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 2/7] test: enable VirtIO keyboard Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 3/7] commands: fbtest: add flush for single pattern Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 4/7] test: conftest: don't call .startswith on int Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 5/7] test: conftest: set -display none when non-interactive Ahmad Fatoum
2026-04-13 7:44 ` [PATCH 6/7] test: conftest: add qemu feature Ahmad Fatoum
2026-04-13 7:44 ` Ahmad Fatoum [this message]
2026-04-22 8:01 ` [PATCH 0/7] test: Add framebuffer test Sascha Hauer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260413074522.1410710-8-a.fatoum@barebox.org \
--to=a.fatoum@barebox.org \
--cc=barebox@lists.infradead.org \
--cc=noreply@anthropic.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox