Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ on:
- '.editorconfig'
- '.clang-format'
- 'frama-c-stubs/**'
- '.github/workflows/conformance.yml'
- '.github/workflows/lint.yml'
- '.github/workflows/static-analysis.yml'
- '.github/workflows/verify.yml'
Expand Down
122 changes: 122 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Conformance

on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
schedule:
- cron: '17 3 * * *'
workflow_dispatch:
inputs:
scope:
type: choice
options: [pr, full]
default: pr
update_check:
type: boolean
default: false

concurrency:
# A schedule event resolves github.ref to the default branch, so without the
# event split the nightly full run and a main push share one group and a
# second push cancels the queued nightly.
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
CONF_SCOPE: ${{ (github.event_name == 'schedule' || inputs.scope == 'full') && 'full' || 'pr' }}
CONF_REQUIRE: 1

jobs:
discover:
runs-on: ubuntu-24.04
outputs:
suites: ${{ steps.suites.outputs.names }}
steps:
- uses: actions/checkout@v7
- id: suites
run: |
names=$(python3 scripts/conformance suites --format json |
python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["suites"]))')
echo "names=$names" >> "$GITHUB_OUTPUT"

harness:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- run: python3 scripts/conformance selftest

payload:
needs: discover
if: needs.discover.outputs.suites != '[]'
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- run: make conformance-payloads
- run: tar -C externals -cf conformance-payloads.tar payloads
- uses: actions/upload-artifact@v7
with:
name: conformance-payloads
path: conformance-payloads.tar
if-no-files-found: error

qemu:
needs: [discover, payload]
if: needs.discover.outputs.suites != '[]'
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: conformance-payloads
- run: mkdir -p externals && tar -C externals -xf conformance-payloads.tar
- run: bash tests/fetch-fixtures.sh
- run: make test-conformance BACKEND=qemu

elfuse:
needs: [discover, payload, qemu]
if: needs.discover.outputs.suites != '[]'
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: conformance-payloads
- run: mkdir -p externals && tar -C externals -xf conformance-payloads.tar
- run: bash tests/fetch-fixtures.sh
- run: make elfuse
- run: make test-conformance BACKEND=elfuse

conformance:
name: Conformance (make test-conformance)
needs: [discover, harness, payload, qemu, elfuse]
if: always()
runs-on: ubuntu-24.04
steps:
- env:
SUITES: ${{ needs.discover.outputs.suites }}
DISCOVER: ${{ needs.discover.result }}
HARNESS: ${{ needs.harness.result }}
PAYLOAD: ${{ needs.payload.result }}
QEMU: ${{ needs.qemu.result }}
ELFUSE: ${{ needs.elfuse.result }}
run: |
[ "$DISCOVER" = success ]
[ "$HARNESS" = success ]
if [ "$SUITES" = '[]' ]; then

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.

The registry is empty on this branch, so suites returns [], the payload/qemu/elfuse jobs all skip, and this branch asserts that skip as success. The required check is green while nothing runs, and it stays green if the last provider is ever dropped from the registry.

Once the first suite lands this needs a floor: assert the discovered list is non-empty, or compare it against a checked-in expected inventory.

[ "$PAYLOAD $QEMU $ELFUSE" = 'skipped skipped skipped' ]
else
[ "$PAYLOAD $QEMU $ELFUSE" = 'success success success' ]
fi

update-check:
if: github.event_name == 'workflow_dispatch' && inputs.update_check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- run: python3 scripts/conformance pins check
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ $(BUILD_DIR)/probe: tests/fixtures/sharun/probe.c \
endif

include mk/tests.mk
include mk/conformance.mk
include mk/lint.mk
include mk/verify.mk
include mk/format.mk
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ The build signs `build/elfuse` before use. Override the signing identity with
Rosetta, dynamic linking via `--sysroot`, and attaching `gdb` /
`lldb` to the built-in stub.
- [docs/testing.md](docs/testing.md): build prerequisites, the
`make check` flow, the QEMU and Rosetta cross-check matrices, and
fixture handling.
`make check` flow, the QEMU and Rosetta cross-check matrices,
fixture handling, and conformance commands.
- [docs/conformance.md](docs/conformance.md): the conformance harness,
expectations, payloads, and CI workflow.
- [docs/oci-images.md](docs/oci-images.md): the `elfuse-oci` store,
pull behavior, and validation.
- [docs/filenames.md](docs/filenames.md): how a guest filename becomes a
Expand Down
117 changes: 117 additions & 0 deletions docs/conformance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Conformance Harness

The harness runs registered Linux test suites on elfuse and a QEMU reference.
It records suite status separately from the expectation verdict. The command
reference is in [testing.md](testing.md#conformance-tests).

## Results

`run` writes `results.json` below `<results>/<suite>/<backend>/<stamp>-<pid>/`,
where `<results>` defaults to `build/conformance`. That file is the canonical
artifact: `schema_version: 1`, `kind: run`, run metadata, derived counts and
gate, and case records. Loading rejects a gate or count that disagrees with
the cases. An empty run is red. A `summary.txt` beside it carries the lines
`report` prints.

Each attempt records `normal`, `timeout`, `signal`, or `transport`, elapsed
microseconds, output paths, and an exit code or signal when applicable. Case
statuses are `PASS`, `FAIL`, `SKIP`, `CONF`, `WARN`, `BROK`, `TIMEOUT`,
`CRASH`, `INCONSISTENT`, and `ERROR`. Verdicts are `as_expected`,
`unexpected_failure`, `unexpected_pass`, `flaked`, `filtered`, and `error`.

JSON list output also has `schema_version: 1` and a `kind` field. Requested
machine data uses stdout. Diagnostics use stderr.

Exit codes are:

- `0`: the operation succeeded or the run is green.
- `1`: a run is red, a backend failed, or an artifact check is red.
- `2`: the command, configuration, or operation is invalid.
- `3`: a non-writing pin or selection check found drift.
- `77`: an optional prerequisite is absent. `--require` and `CONF_REQUIRE=1`
promote it to `2`.

## IDs and Selection

Case IDs have one of these forms:

```text
<suite>:<group>
<suite>:<group>/<case>[/<parameter>...]
```

Selectors and expectation matchers use shell globs across the complete ID.
A bare group selector also selects its cases. An unmatched selector is an

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.

True for --case selectors, which selection.resolve_ids expands to pattern plus pattern/*, but not for expectation matchers: expectations.resolve calls fnmatch directly, and ids.matches("s:g", "s:g/c") is False.

So a matcher naming a bare group silently covers nothing, and only a full-scope run's stale() reports it. Either split this sentence per consumer, or give resolve the same expansion.

error.

A selection file assigns each upstream launch group to `pr`, `full`, or a
declined group with a reason. PR groups run in both scopes. Enabled entries
may set `timeout_s` and suite-specific case filters. `selection check`
compares the file against the pinned inventory and reports drift; `selection
update` rewrites the generated selection data.

## Expectations

Expectation files are JSONC and accept comments and trailing commas. Each
backend has a leaf, `<suite>_<backend>.jsonc`, which may `include` shared
files; the optional `flaky.jsonc` holds the quarantine actions. Files contain
ordered actions; the last matching non-quarantine action wins, and the first
effective action must be `expect_pass` on `*`.

Actions are `expect_pass`, `expect_failure`, `expect_conf`, `skip`, and
`quarantine`. Every non-pass action needs a reason. A quarantined case runs
alone for at most three attempts and reports test mismatches as `flaked`.
Harness errors remain red. A full run rejects matchers that select no case.

A skipped expectation prevents launch. `--bootstrap` launches skipped cases
and records status without applying expectations. `expectations seed` derives
actions from bootstrap statuses or red verdicts. It refuses harness errors.

## Payloads and Pins

Payloads live below `externals/payloads/` and are not committed. A fingerprint
hashes the pin and builder inputs. `manifest.json` records the fingerprint and
each staged file or symlink. Verification detects missing, extra, changed, and
stale content before a run starts.

Pins are schema-checked JSON. `pins check` fetches the upstream ref without
writing and reports drift. `pins update` validates the new pin before
replacing the file.

## Suite Interface

`tests/conformance/providers/__init__.py` is the static suite registry;
`Provider` in `providers/base.py` declares what a suite supplies: selection,
prerequisites, payload and pin hooks, case discovery, batch keys, and result
decoding.

The shared runner owns expectation resolution, skip handling, unresolved batch
reruns, quarantine retries, result ordering, and judgment. Providers map
suite output to statuses. Backends return process invocations. A provider
translates host paths through `backend.guest_path()` before putting them in
argv; `Backend.run` forwards argv unchanged, because only the provider knows
which elements are paths. QEMU records non-timeout shell statuses as exit
codes. Providers interpret `128+n` through the suite contract because the
shell cannot distinguish it from a plain exit with the same value.

The elfuse backend starts one `build/elfuse --timeout 0` process for each
command. The QEMU backend starts one VM through `tests/qemu-runner.sh`, shares
the repository read-only at `/mnt/host`, and executes commands over SSH.

## Make and CI

The Make targets take their suite list from the registry through
`scripts/conformance suites`. An empty registry makes suite targets print
`SKIP`; harness selftests still run.

`make clean-conformance` sweeps what an interrupted run left: the QEMU VM and
its state record, detached guest process groups, orphaned fork children, and
elfuse scratch under `/tmp`. Shared memory a run recorded creating is removed;
any other dead-creator SysV object is reported, since the guest key reaches the
host unchanged and nothing tells one apart from a third party's. It refuses
while a session holds a lock, skips the scratch and the SysV pass while any
elfuse of the user is alive, and keeps results.

`.github/workflows/conformance.yml` runs QEMU before elfuse and gates on the
required `Conformance (make test-conformance)` job. Pull requests use the PR
scope. Schedules and `scope=full` dispatches use the full scope.
40 changes: 40 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ The repository contains several layers of validation:
- shell integration suites such as BusyBox, coreutils, and dynamic-loader tests
- debugger integration tests for the GDB stub
- native macOS HVF checks such as multi-vCPU and RWX validation
- conformance lanes that judge test suites against elfuse and a QEMU
reference; see `docs/conformance.md`

The quick suite is driven by `tests/driver.sh`, which supports:

Expand Down Expand Up @@ -532,6 +534,44 @@ whose parent closes its copy of the fd before the backing has been drained
answers with its primary alone, because the backing half belongs to a stream
that has gone. Both rows are load-bearing in pairs -- neither number alone
separates the answers the site could give -- so both are printed.
## Conformance Tests

`scripts/conformance` runs registered suites on elfuse or QEMU. These are its
public commands:

| Command | Result |
|---------|--------|
| `scripts/conformance suites [--format text\|json]` | List registered suites |
| `scripts/conformance list SUITE [--scope pr\|full] [--backend elfuse\|qemu\|all] [--format text\|json] [--require]` | List canonical case IDs; the default scope is `full` |
| `scripts/conformance run SUITE [--scope pr\|full] [--case ID_OR_GLOB] [--backend elfuse\|qemu\|all] [--jobs N] [--results DIR] [--bootstrap] [--require] [--no-retry] [--dry-run] [-v]` | Run cases; the defaults are the `pr` scope, elfuse, one job, and `build/conformance` |
| `scripts/conformance payload fingerprint SUITE` | Print the payload fingerprint |
| `scripts/conformance payload build SUITE [--force]` | Build the payload |
| `scripts/conformance payload verify SUITE [--fingerprint HASH]` | Verify the payload manifest and files |
| `scripts/conformance selection check SUITE` | Compare selection with the pinned inventory |
| `scripts/conformance selection update SUITE` | Rewrite generated selection |
| `scripts/conformance expectations check [SUITE]` | Validate expectation files |
| `scripts/conformance expectations seed SUITE RESULTS [--reason TEXT] [--write]` | Derive expectation actions from results |
| `scripts/conformance pins check [SUITE] [--ref REF]` | Report pin drift without writing |
| `scripts/conformance pins update SUITE [--ref REF]` | Update a pin |
| `scripts/conformance report RESULTS [--format text\|markdown\|json]` | Read canonical results |
| `scripts/conformance clean [--results DIR] [--dry-run]` | Sweep what an interrupted run left; results survive |
| `scripts/conformance selftest` | Run harness selftests |

Examples:

```sh
scripts/conformance run SUITE --scope full --backend all
scripts/conformance run SUITE --case 'SUITE:GROUP/*' --backend qemu
scripts/conformance report RESULTS --format markdown
```

The Make aliases are `test-conformance-harness`, `test-conformance`,
`test-conformance-full`, `conformance-payloads`, `clean-payloads`,
`clean-conformance`, and `update-pins`. `BACKEND`, `CONF_SCOPE`, `TEST`,
`CONF_JOBS`, and `CONF_RESULTS` configure the run targets. Run
`clean-conformance` after an interrupted run, before `make clean` deletes the
VM's state record. See [conformance.md](conformance.md) for result,
expectation, payload, and suite interfaces.

## Validation Strategy By Change Type

Expand Down
2 changes: 1 addition & 1 deletion mk/common.mk
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ BUILD_FLAVOR_STAMP := $(BUILD_DIR)/.build-flavor
# skip, that sub-make evaluates the flavor guard with whatever CFLAGS its own
# environment produces, so running the scanner beside a sanitizer build wipes
# that build's objects from under it.
BUILD_FLAVOR_GOALS := $(filter-out clean distclean help print-%,$(MAKECMDGOALS))
BUILD_FLAVOR_GOALS := $(filter-out clean distclean clean-conformance help print-%,$(MAKECMDGOALS))

ifneq ($(BUILD_FLAVOR_GOALS),)
BUILD_FLAVOR_PREV := $(shell cat $(BUILD_FLAVOR_STAMP) 2>/dev/null)
Expand Down
57 changes: 57 additions & 0 deletions mk/conformance.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.PHONY: test-conformance-harness test-conformance test-conformance-full \
conformance-payloads clean-payloads clean-conformance update-pins

CONFORMANCE := python3 scripts/conformance
# The suite registry lives in tests/conformance/providers/__init__.py. On a
# failed discovery the marker fails every consumer instead of skipping.
CONF_SUITES ?= $(shell $(CONFORMANCE) suites || echo suite-discovery-failed)
BACKEND ?= elfuse
TEST ?=
CONF_JOBS ?= 4
CONF_RESULTS ?= $(BUILD_DIR)/conformance
CONF_RUN = $(CONFORMANCE) run
CONF_SCOPE ?= pr
CONF_SELECT = $(if $(TEST),$(foreach id,$(TEST),--case '$(id)'),--scope $(CONF_SCOPE))
CONF_NO_SUITES = $(if $(CONF_SUITES),,@printf "$(YELLOW)SKIP$(RESET) no conformance suites registered\n")
# foreach inserts spaces, but RUN_OPTIONAL_SKIP77 expands as a recipe line.
define conf-newline


endef
define conf-lane
$(foreach s,$(CONF_SUITES),$(call RUN_OPTIONAL_SKIP77,$(CONF_RUN) $(s) $(1) --backend $(BACKEND) --jobs $(CONF_JOBS) --results $(CONF_RESULTS),test-$(s)$(2))$(conf-newline))
endef

## Run the conformance harness selftests (hermetic)
test-conformance-harness:
@$(CONFORMANCE) selftest

## Run every suite's CONF_SCOPE subset, or TEST=ID... (BACKEND=elfuse|qemu|all)
test-conformance:
$(CONF_NO_SUITES)
$(call conf-lane,$(CONF_SELECT),)

## Run every suite in full, the nightly shape
test-conformance-full:
$(CONF_NO_SUITES)
$(call conf-lane,--scope full,-full)

## Build every conformance payload under externals/payloads/
conformance-payloads:
$(CONF_NO_SUITES)
$(foreach s,$(CONF_SUITES),$(CONFORMANCE) payload build $(s) &&) true

## Remove the conformance payloads (they survive clean and distclean)
clean-payloads:
rm -rf externals/payloads

## Stop a leaked QEMU VM, kill orphaned guests, sweep elfuse scratch (results survive)
clean-conformance:
$(CONFORMANCE) clean --results $(CONF_RESULTS)

UPDATE_CHECK ?=

## Refresh the conformance pins from upstream (UPDATE_CHECK=1 to report only)
update-pins:
$(CONF_NO_SUITES)
$(foreach s,$(CONF_SUITES),$(CONFORMANCE) pins $(if $(filter 1,$(UPDATE_CHECK)),check,update) $(s) $(if $(CONF_REF_$(s)),--ref $(CONF_REF_$(s))) &&) true
Loading