Test plans¶
A test plan is a declarative list of CLI invocations and the results each one should produce. Click Extra runs the plan against any command or binary as separate subprocesses, checking exit codes and output. It is the black-box, subprocess-level complement to CliRunner, which drives a CLI in-process: a test plan never imports the target, so it works just as well against a compiled binary, a shell command, or a CLI written in another language.
Important
Parsing a plan from YAML needs the optional pyyaml dependency. Install it with the yaml extra:
{code-block} shell-session $ pip install click-extra[yaml]
The engine itself (building CLITestCase objects and running them) has no such requirement: only parse_test_plan does.
Writing a plan¶
A plan is a YAML list. Each entry is one case: the parameters to append to the command, plus the expectations to check. A case with no expectation only asserts that the command ran.
- cli_parameters: --version
exit_code: 0
- cli_parameters: forecast --city paris
stdout_contains: Sunny
timeout: 5
- cli_parameters: --help
stdout_regex_matches:
- Usage:.+
skip_platforms:
- windows
The directives map one-to-one onto CLITestCase fields:
cli_parameters: arguments appended to the command (a string is split, a list is used as-is).exit_code: the expected process exit code.stdout_contains/stderr_contains: substrings that must appear.stdout_regex_matches/stderr_regex_matches: regexes that must each match somewhere.stdout_regex_fullmatch/stderr_regex_fullmatch: a regex that must fully match, line by line.strip_ansi: strip ANSI escapes before matching.timeout: seconds before the case fails as a timeout.skip_platforms/only_platforms:extra_platformsidentifiers (linux,macos,windows, group IDs) controlling where the case runs.
Running from the command line¶
The click-extra test-plan subcommand runs a plan against a target. Point it at a command on the PATH, a command line, or a path to a binary:
$ click-extra test-plan --command weather --plan-file plan.yaml
Running 3 test cases across 7 workers (os.cpu_count()=8).
Test plan results - Total: 3, Skipped: 0, Failed: 0
Cases run in parallel by default, one fewer than the available logical CPUs (see --jobs). Pass --jobs max to use every core, or --jobs 1 for sequential execution, which lets --exit-on-error stop on the first failure. On an interactive terminal a spinner reports progress; it is silent in pipes and CI logs, and --no-progress turns it off.
Configuring the plan¶
Rather than passing --plan-file every time, a project can declare its plan once under [tool.click-extra.test-plan], and click-extra test-plan picks it up when no plan is given on the command line:
[tool.click-extra.test-plan]
file = "tests/cli-test-plan.yaml" # default; a path to the YAML plan
# inline = "- cli_parameters: --version" # or embed the plan directly
# timeout = 30 # default per-case timeout in seconds
The resolution precedence is: --plan-file/--plan-envvar, then [tool.click-extra.test-plan] inline, then its file, then a built-in default plan that exercises --version and --help. The config maps onto the TestPlanConfig schema (wrapped by ClickExtraConfig).
Running from Python¶
parse_test_plan() turns YAML into cases, and run_test_plan() runs them, returning a Counter of total, skipped, and failed:
from click_extra import parse_test_plan, run_test_plan
cases = list(parse_test_plan(open("plan.yaml").read()))
counter = run_test_plan("weather", cases, jobs=4)
if counter["failed"]:
raise SystemExit(1)
Build cases directly when a plan is computed rather than read from YAML (this path needs no yaml extra):
from click_extra import CLITestCase, run_test_plan
cases = [
CLITestCase(cli_parameters="--version", exit_code=0),
CLITestCase(cli_parameters="forecast --city lyon", stdout_contains="Cloudy"),
]
run_test_plan("weather", cases)
click_extra.test_plan API¶
classDiagram
Exception <|-- SkippedTest
Declarative, black-box CLI test plans.
A test plan is a list of CLITestCase invocations: each runs a target
command (a name, a command line, or a path to a binary) once with extra
parameters, then checks its exit code and stdout/stderr against literal,
substring, or regex expectations. Cases carry their own platform skip/only
rules, so one plan runs across operating systems unchanged.
Plans are usually written as YAML and loaded with parse_test_plan(),
which needs the optional click-extra[yaml] extra. run_test_plan()
drives a list of cases against a target, parallelized per the resolved
--jobs count (see click_extra.execution.run_jobs()) and reporting
live progress through a click_extra.spinner.Spinner.
This is the black-box, subprocess-level complement to
click_extra.testing.CliRunner, which drives a CLI in-process.
- exception click_extra.test_plan.SkippedTest[source]¶
Bases:
ExceptionRaised when a test case should be skipped.
- class click_extra.test_plan.CLITestCase(cli_parameters=<factory>, skip_platforms=<factory>, only_platforms=<factory>, timeout=None, exit_code=None, strip_ansi=False, output_contains=<factory>, stdout_contains=<factory>, stderr_contains=<factory>, output_regex_matches=<factory>, stdout_regex_matches=<factory>, stderr_regex_matches=<factory>, output_regex_fullmatch=None, stdout_regex_fullmatch=None, stderr_regex_fullmatch=None, execution_trace=None)[source]¶
Bases:
objectA single CLI test case: how to invoke the command and what to expect.
Each case runs the command-under-test once with cli_parameters appended, then checks the captured result against the expectation directives below. A case with no expectation only asserts the command ran (plus exit_code, if set).
- cli_parameters: tuple[str, ...] | str¶
Arguments and options appended to the command-under-test.
A plain string is split into arguments (on spaces on Windows, with shlex elsewhere); a list or tuple is used as-is.
- skip_platforms: Trait | Group | str | None | Iterable[Trait | Group | str | None | Iterable[_TNestedReferences]]¶
Platforms (or platform-group IDs) on which to skip this case.
Accepts extra_platforms identifiers such as linux, macos, windows, in any case, mixed freely with group IDs.
- only_platforms: Trait | Group | str | None | Iterable[Trait | Group | str | None | Iterable[_TNestedReferences]]¶
Restrict this case to these platforms; skip it everywhere else.
The mirror image of skip_platforms, using the same identifiers.
- timeout: float | str | None = None¶
Seconds before the command is killed and the case fails as a timeout.
Falls back to the command’s –timeout default, then to no limit.
- output_contains: tuple[str, ...] | str¶
Reserved: combined stdout/stderr matching is not implemented yet.
Setting any output_* directive raises at runtime; use the stdout_* and stderr_* variants instead.
- stdout_regex_matches: tuple[Pattern | str, ...] | str¶
Regexes that must each match somewhere in stdout (searched, re.DOTALL).
- stderr_regex_matches: tuple[Pattern | str, ...] | str¶
Regexes that must each match somewhere in stderr (searched, re.DOTALL).
- stdout_regex_fullmatch: Pattern | str | None = None¶
Regex that must fully match stdout, line by line.
- stderr_regex_fullmatch: Pattern | str | None = None¶
Regex that must fully match stderr, line by line.
- execution_trace: str | None = None¶
Rendering of the command execution and its output.
Populated after the case runs, for inspection on failure; not a directive you set in a test plan.
- run_cli_test(command, additional_skip_platforms, default_timeout)[source]¶
Run a CLI command and check its output against the test case.
The provided command can be either:
a path to a binary or script to execute;
a command name to be searched in the PATH,
a command line with arguments to be parsed and executed by the shell.
`{todo} Add support for environment variables. ``{todo} Add support for proper mixed <stdout>/<stderr> stream as a single, intertwined output. `
- class click_extra.test_plan.TestPlanConfig(file='./tests/cli-test-plan.yaml', inline=None, timeout=None)[source]¶
Bases:
objectConfig schema for a project’s test plan, read from
[tool.<cli>.test-plan].The
test-planCLI command resolves its cases from this config when no plan is given on the command line. Map it onto an app’s config section with a field carryingmetadata={"click_extra.config_path": "test-plan"}.
- class click_extra.test_plan.ClickExtraConfig(test_plan=<factory>)[source]¶
Bases:
objectSchema for the
[tool.click-extra]configuration section.Currently carries only the
test-plansub-table, letting a project pointclick-extra test-planat its own plan without repeating it on the command line. It is theconfig_schemaof thetest-planCLI command.- test_plan: TestPlanConfig¶
The
[tool.click-extra.test-plan]sub-table (file/inline/timeout).
- click_extra.test_plan.run_test_plan(command, cases, *, jobs=1, select_test=None, skip_platform=None, timeout=None, exit_on_error=False, show_trace_on_error=True, stats=True, show_progress=True)[source]¶
Run a list of test cases against a target command and tally the results.
Cases are parallelized per
jobs(seeclick_extra.execution.run_jobs()): at one worker they run sequentially and lazily, soexit_on_errorcan stop before the rest start; otherwise they run in a thread pool and every case runs to completion. Either way outcomes are tallied in submission order. On an interactive terminal aclick_extra.spinner.Spinnerreports progress unlessshow_progressis false.- Parameters:
command (
Path|str) – The target to test: a command name, a command line, or a path to a binary or script.cases (
Sequence[CLITestCase]) – The test cases to run.jobs (
int) – Number of parallel workers;1runs sequentially.select_test (
Sequence[int] |None) – 1-based case numbers to run; others are skipped.skip_platform (
Trait|Group|str|None|Iterable[Trait|Group|str|None|Iterable[Trait|Group|str|None|Iterable[Trait|Group|str|None|Iterable[_TNestedReferences]]]]) – Extra platforms (or group IDs) to skip every case on.timeout (
float|None) – Default per-case timeout in seconds when a case sets none.exit_on_error (
bool) – Stop at the first failure (sequential runs only).show_trace_on_error (
bool) – Echo the execution trace of each failed case.stats (
bool) – Echo a one-line worker summary up front and a result tally.show_progress (
bool) – Allow the progress spinner on an interactive terminal.
- Return type:
- Returns:
A
collections.Counterwithtotal,skipped, andfailedkeys. A non-zerofailedcount signals the caller to exit with an error.