mail archive of the barebox mailing list
 help / color / mirror / Atom feed
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




  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