04.02 — Cram tests (prysk)#
CLI tests in cram format: a
.tfile contains shell commands and their expected output. Tool:prysk(modern rewrite ofcram).
.t file#
Valid args produce a deterministic parsed config
$ touch driver
$ "$PYTHON" "$TESTDIR/_demo_cli.py" --browser firefox --driver-path driver --workers 3 --wait-timeout 20 --logger terminal
browser=firefox
driver_path=driver
headless=True
workers=3
wait_timeout=20
logger=terminal
only=()
exclude=()- First line: title (comment for the human).
- Blank line.
- Lines starting with
$: shell command run. - Following lines (indent 2): expected output.
Actual output differs → test fails.
_demo_cli.py#
# tests/cram/_demo_cli.py
"""Tiny CLI that pushes a SeleniumCliStoreSingleton and prints its content."""
from ocarina.opinionated.cli.selenium.cli_store_singleton import (
SeleniumCliStoreSingleton as CliStoreSingleton,
)
from ocarina.opinionated.cli.selenium.create_cli_store import (
create_selenium_auto_cli_store,
)
if __name__ == "__main__":
CliStoreSingleton().push(create_selenium_auto_cli_store())
for key in ("browser", "driver_path", "headless", "workers", "wait_timeout", "logger", "only", "exclude"):
print(f"{key}={CliStoreSingleton().get(key)}")→ Prints the store contents deterministically. Cram diffs the output line by line.
.t files#
| File | Verifies |
|---|---|
cli_happy_path.t | A valid combination produces the right output |
cli_defaults.t | Defaults are parsed correctly |
cli_help.t | --help lists the n flags |
cli_invalid_browser.t | --browser=banana raises cleanly |
cli_invalid_wait_timeout.t | --wait-timeout=0 raises (is_not_zero) |
cli_missing_driver_path.t | --browser=chrome without --driver-path raises |
cli_not_headless_flag.t | --not-headless inverts the default (stored as headless=False) |
cli_only_flag.t | --only id1 id2 parses a list |
cli_exclude_flag.t | --exclude id1 id2 parses a list |
cli_only_exclude_mutex.t | --only and --exclude together raises |
cli_help.t#
CLI --help lists every declared flag
Pipe through grep to stay resilient to unrelated wording changes in argparse's
help output. Sorted unique output → alphabetical by first-differing character.
$ "$PYTHON" "$TESTDIR/_demo_cli.py" --help 2>&1 | grep -o -- '--\(driver-path\|profile-path\|browser\|not-headless\|workers\|logger\|wait-timeout\|dont-force-delete-tmp-dirs\|only\|exclude\)' | sort -u
--browser
--dont-force-delete-tmp-dirs
--driver-path
--exclude
--logger
--not-headless
--only
--profile-path
--wait-timeout
--workersWe don’t test --help’s full output (it shifts with argparse versions), just that the listed flags appear at minimum. grep each flag, then sort -u. Missing flag → output differs, test fails.
We test what matters, nothing more.
The Playwright .t files#
The Playwright launcher has its own CLI (PlaywrightCliStoreSingleton), hence its own cram runner: _demo_pw_cli.py, which prints the Playwright store instead of the Selenium one.
# tests/cram/_demo_pw_cli.py
from ocarina.opinionated.cli.playwright.cli_store_singleton import (
PlaywrightCliStoreSingleton as CliStoreSingleton,
)
from ocarina.opinionated.cli.playwright.create_cli_store import create_playwright_cli_store
CliStoreSingleton().push(create_playwright_cli_store())
store = CliStoreSingleton()
for key in ("browser", "profile_path", "headless", "workers", "wait_timeout",
"logger", "video_dir", "trace_dir", "only", "exclude"):
print(f"{key}={store.get(key)}")Five dedicated .t files, mirroring the Selenium surface by dropping --driver-path (Playwright ships its own browsers directly) and adding --video-dir / --trace-dir:
| File | Checks |
|---|---|
pw_cli_defaults.t | Playwright defaults parsed (workers=5, wait_timeout=10, video_dir=None) |
pw_cli_help.t | --help lists the flags: no --driver-path, but --video-dir / --trace-dir |
pw_cli_invalid_browser.t | --browser=banana raises (only chromium/firefox/webkit are valid) |
pw_cli_invalid_wait_timeout.t | --wait-timeout=0 raises |
pw_cli_only_exclude_mutex.t | --only and --exclude together raises |
Same philosophy as the Selenium side: grep the flags, sort -u, never test argparse’s full output. The CLI is a user surface, so cram is the natural tool, whatever the backend.
Makefile#
.PHONY: cram-test
cram-test:
ifeq ($(OS),Windows_NT)
@echo "N A P O L E O N D I S A P P R O V E S W I N D O W S"
else
@echo "Running cram tests..."
PYTHON="$(CURDIR)/$(VENV_PYTHON)" "$(CURDIR)/$(VENV_BIN)/prysk" tests/cram/
endif- Skip on Windows. Prysk doesn’t have the same shell support on Windows; Napoleon told the author to skip cleanly instead of fighting it.
PYTHONinjected: Prysk substitutes$PYTHONin.tfiles with the venv Python’s path.prysk tests/cram/: runs every.tin the folder.
Why cram rather than pytest for the CLI#
| Cram | pytest |
|---|---|
| Test = shell command + expected output (1-1) | Test = Python code calling, capturing stdout/stderr, asserting |
| Readable by non-Python folks | Requires understanding Python |
| Naturally captures stderr too | capsys to configure |
| No mocks to set up | Often mocks |
| Readable failure (line-by-line diff) | More noise |
The CLI is a user interface, not Python code. Cram is the natural tool — the most appropriate “system test” here.
make test#
.PHONY: test
test: cram-test
@echo "Running tests..."
-pytest --alluredir=$(ALLURE_RESULTS) -vv --hypothesis-show-statistics
→ make test chains:
cram-test(the.ts).pytestwith Allure output +--hypothesis-show-statistics.
If cram-test fails, pytest doesn’t run (Makefile target dependency). The - before pytest lets execution continue even if pytest fails (so we still get the Allure report).