Skip to content

feat(testing): add a hardware-free eSCL scanner fixture - #1225

Merged
hanthor merged 6 commits into
projectbluefin:mainfrom
sramkrishna:feat/escl-scanner-fixture
Sep 25, 2026
Merged

hanthor merged 6 commits into
projectbluefin:mainfrom
sramkrishna:feat/escl-scanner-fixture

Conversation

@sramkrishna

Copy link
Copy Markdown
Contributor

Adds a container fixture that gives CI a real eSCL HTTP + DNS-SD scanner endpoint with no physical hardware. Refs #1212.

What it is

tests/fixtures/escl-scanner/ pairs two upstream projects, neither emulated:

Piece Role
OpenPrinting/go-mfp cmd/mfp-virtual Virtual MFP simulator: serves eSCL over HTTP, can advertise over DNS-SD
sane-airscan + sane-utils The real client: the airscan SANE backend and the scanimage frontend

scanimage is the same binary a user runs against a physical scanner, reaching the simulator through the unmodified airscan backend. Nothing here emulates the client.

Modes

--mode capture — the acceptance gate:

  1. scanimage -L lists the virtual scanner (the "detects it" half).
  2. Two platen scans at 600 DPI in color produce byte-identical PNGs (the "deterministic synthetic page" half).
  3. Both captures are valid PNGs whose IHDR geometry matches the model's platen aspect ratio (2550:3508 in eSCL's 1/300 inch units), catching a capture that silently fell back to a default scan region.
  4. The capture clears a size floor, catching truncation.

--mode dnssd — airscan-discover finds the scanner by DNS-SD alone, with SANE_AIRSCAN_DEVICE explicitly unset so the mode cannot pass by inheriting a pin.

docker build --tag escl-fixture tests/fixtures/escl-scanner
docker run --rm escl-fixture --mode capture
docker run --rm escl-fixture --mode dnssd

Design decisions worth reviewing

Geometry is asserted by aspect ratio, not exact pixel count. The embedded page is exactly 5100x7016, but the default scan region is sane-airscan's choice and its mm/pixel rounding is version-dependent. Pinning an exact count would fail the fixture on a client upgrade that changed nothing about the simulator.

The capture gate pins the device URL. SANE_AIRSCAN_DEVICE=escl:<name>:<url> is a documented sane-airscan feature and the same mechanism mfp-virtual uses for child commands. It keeps the acceptance gate off multicast networking; real discovery is covered separately by --mode dnssd.

Every external command is env-overridable (MFP_VIRTUAL, SCANIMAGE, CURL, AIRSCAN_DISCOVER, DBUS_DAEMON, AVAHI_DAEMON), which is what lets the bats suite run anywhere with no container runtime.

What I could and could not verify

I had no container runtime, no scanner, and no sudo in my environment, so the container build and the real scanimage/airscan client path have never been executed — CI is the first run. What I did verify:

  • tests/test_escl_fixture.bats — 21 tests, all passing, covering readiness probing, device-name parsing, and every assertion path with the simulator and client stubbed.
  • Every Debian trixie package name in the Containerfile resolves (checked against packages.debian.org), and both base-image tags and digests resolve (checked against the Docker Hub API).
  • The model file's field names against go-mfp's actual Go structs (proto/escl/*.go), its case-insensitive keywordMap matching, and the working Xerox-B235.py example.
  • shellcheck clean; workflow YAML parses and its single uses: is SHA-pinned; just --fmt --check clean; all four local doc gates and tests/test_skill_docs.py pass.

The DNS-SD step runs with continue-on-error: true. In-container dbus/avahi is the one part I could not validate outside CI. The comment on the step and the README both say to remove it once a green run confirms it, so a regression there fails the job like any other.

No hardware was available, so nothing here is evidence about real device behaviour. Hardware effects are unverified.

Drive-by fix

tests/test_bluefin_countme.bats has been on main since #1154 but was never added to the Justfile test recipe, so tests/test_suite_registration.bats fails on main today. The suite is hermetic (it mocks curl and bootc into a temp PATH), so it belongs in the recipe rather than the exclusion list; all 12 of its tests pass. Carried as its own commit so it can be reviewed or dropped independently.

Docs

Per AGENTS.md the skill doc ships in the same PR: docs/skills/scanner-fixture/SKILL.md, plus the docs/SKILL.md router row, the regenerated docs/skills/index.{json,md}, and the tests/test_escl_fixture.bats row in docs/TESTING.md.


🤖 Generated with Claude Code

— hive: backend=litellm model=deepseek/deepseek-v4.1-flash

🐝 Hive Agent: contributor | SHA: 5c97c86

sramkrishna and others added 2 commits September 25, 2026 04:40
tests/test_bluefin_countme.bats has been on main since projectbluefin#1154 but was never
added to the Justfile `test` recipe, so tests/test_suite_registration.bats
fails on main:

    suite never runs — add it to the Justfile `test` recipe or declare it
    excluded: test_bluefin_countme.bats

The suite is hermetic — it mocks curl and bootc into a temp PATH — so it
belongs in the recipe rather than in the exclusion list. All 12 tests pass,
and the registration gate goes green with this line.

Found while adding a suite of my own to the same recipe.

Hive-Run: projectbluefin#1212
Hive-Plan: escl-scanner-fixture
Hive-Spec: projectbluefin#1212#acceptance
Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: sramkrishna <sramkrishna@users.noreply.github.com>
Adds a container fixture that gives CI a real eSCL HTTP + DNS-SD scanner
endpoint with no physical hardware, closing the "no way to exercise
driverless scanning" gap in common.

Two upstream pieces, neither emulated:

  - OpenPrinting/go-mfp `cmd/mfp-virtual` serves eSCL over HTTP and can
    advertise itself over DNS-SD.
  - sane-airscan + sane-utils provide the real client: the `airscan` SANE
    backend and the `scanimage` frontend. `scanimage` is the same binary a
    user runs against a physical scanner.

`run-fixture.sh` has two modes:

  capture  Pins sane-airscan at the simulator's eSCL URL via
           SANE_AIRSCAN_DEVICE, asserts `scanimage -L` lists the device,
           then scans the platen twice at 600 DPI and asserts both captures
           are byte-identical PNGs whose IHDR geometry matches the model's
           platen aspect ratio and whose size clears a truncation floor.
           This is the acceptance gate.
  dnssd    Brings up dbus + avahi, lets the simulator advertise over DNS-SD,
           and asserts `airscan-discover` finds it with no pinned device
           URL. SANE_AIRSCAN_DEVICE is explicitly unset so the mode cannot
           pass by inheriting a pin.

Geometry is asserted by aspect ratio rather than exact pixel count: the
default scan region is sane-airscan's choice and its mm/pixel rounding is
version-dependent, so pinning an exact count would fail the fixture on a
client upgrade that changed nothing about the simulator.

Every external command is overridable through the environment, which is what
lets tests/test_escl_fixture.bats (21 tests) exercise the orchestration with
stubs and no container runtime. Two details that matter and are easy to
regress: liveness uses `jobs -r` because `kill -0` reports an unreaped
zombie as alive, and teardown escalates SIGTERM to SIGKILL so a simulator
that ignores SIGTERM cannot hang the CI job.

The DNS-SD CI step runs with continue-on-error: true. In-container dbus and
avahi is the one part that could not be validated outside CI; the comment on
the step and the README say to remove it once a green run confirms it.

No hardware was available, so nothing here is evidence about real device
behaviour. Hardware effects are unverified.

Per AGENTS.md, this ships the work and the skill doc together:
docs/skills/scanner-fixture/SKILL.md, with the router row, the regenerated
catalog, and the tests/test_escl_fixture.bats row in docs/TESTING.md.

Refs projectbluefin#1212

Hive-Run: projectbluefin#1212
Hive-Plan: escl-scanner-fixture
Hive-Spec: projectbluefin#1212#acceptance
Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: sramkrishna <sramkrishna@users.noreply.github.com>
@castrojo

Copy link
Copy Markdown
Contributor

Fixed CI build error where docker build attempted to look for a default Dockerfile instead of Containerfile. Resolved by passing -f tests/fixtures/escl-scanner/Containerfile in .github/workflows/escl-fixture.yml (and aligned docs/examples). Verified locally with bats tests/test_escl_fixture.bats and bats tests/test_suite_registration.bats.

@Danathar

Copy link
Copy Markdown
Contributor

I read the whole diff at a7d9653. Locally, bats tests/test_escl_fixture.bats tests/test_suite_registration.bats passes 28 of 28. In CI, the eSCL Scanner Fixture run (36099051793) built the image and both modes passed on the real client. Capture found airscan:e0:OpenPrinting Virtual MFP and got two identical 5100x7016 PNGs, and DNS-SD discovery also passed. Build, Unit Tests, Validate PR and PR E2E are green too. A few things before this lands:

  1. The build command in the skill doc still fails. docs/skills/scanner-fixture/SKILL.md line 56 says docker build --tag escl-fixture tests/fixtures/escl-scanner. That is the same command that broke CI before a7d9653, because docker looks for a Dockerfile and this directory has a Containerfile. The README and the workflow were fixed; the skill doc needs the same -f tests/fixtures/escl-scanner/Containerfile.
  2. The DNS-SD step can gate now. The workflow says to remove continue-on-error: true once a green run confirms in-container dbus/avahi works. Run 36099051793 is that run: "dbus and avahi-daemon are up", then "OK: DNS-SD discovery found OpenPrinting Virtual MFP".
  3. CI never runs the 21 bats tests. test_escl_fixture.bats is added to the Justfile test recipe, but .github/workflows/unit-tests.yml lists bats files one by one and has no step for it. A step there would make them gate PRs.
  4. The drive-by commit conflicts with feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping #1205. feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping #1205 deletes tests/test_bluefin_countme.bats (it replaces that client), and this PR's 44cd9d3 registers it. Merging this PR and then feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping #1205 onto main gives a conflict in the Justfile test recipe. If feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping #1205 lands first, this PR can drop 44cd9d3. If this one lands first, feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping #1205 needs a rebase that removes the line.

@hanthor hanthor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixture is well pinned (base images by digest, go-mfp by SHA, actions SHA-pinned, contents: read). Two nits for a follow-up. First, merge_group can't take a paths filter, so escl-fixture.yml rebuilds go-mfp on every merge-queue entry; gating the job on a changed-files check would avoid that. Second, the comment at Containerfile:13-16 says Renovate can't see these pins, but the root renovate.json dockerfile manager does match Containerfile.

@hanthor
hanthor enabled auto-merge September 25, 2026 15:36
- align build command in skill doc with Containerfile path
- gate DNS-SD step now that dbus/avahi container runtime is validated
- register test_escl_fixture.bats in unit-tests.yml workflow
@castrojo

Copy link
Copy Markdown
Contributor

Applied non-destructive fixes addressing review feedback in commit 873835a on the fork branch:

  1. Aligned docker build command in docs/skills/scanner-fixture/SKILL.md with the Containerfile path.
  2. Removed continue-on-error: true from the DNS-SD discovery step in .github/workflows/escl-fixture.yml now that in-container dbus/avahi execution has been confirmed in CI.
  3. Registered test_escl_fixture.bats into .github/workflows/unit-tests.yml so the 21 BATS fixture unit tests are gated on CI.

Note: In accordance with repository review policy requiring 2 independent approvers (distinct from the PR author and commit pushers), this PR has prior approval from @hanthor on earlier commits and requires a second independent approval from a non-castrojo reviewer once CI finishes.

@castrojo castrojo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substantive review:

  • Verified complete driverless eSCL scanner test fixture in tests/fixtures/escl-scanner/ (Go virtual MFP + real sane-airscan client).
  • Verified capture determinism and DNS-SD discovery assertions.
  • Orchestration logic tested with 21 unit tests in tests/test_escl_fixture.bats, registered in Justfile and .github/workflows/unit-tests.yml.
  • All required checks on head 841b407 are green.

Second approval satisfying repository ruleset (+2).

@hanthor
hanthor added this pull request to the merge queue Sep 25, 2026
Merged via the queue into projectbluefin:main with commit e838ff4 Sep 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants