Skip to content

Just Module Tests

Contract and resolution tests for the just module infrastructure. Run via just maint test contract test (or just maint test contract test -v -k compose for filtered output).

Test Files

  • test_spec.py — verifies WHAT recipes promise (contracts), not HOW they work
  • test_modules.py — verifies modules are REACHABLE (CLI resolution), not what they do
  • test_gen_spec.py — unit tests for the spec generator (tools/gen_spec.py)
  • conftest.pyJustfileFixture (from pytest-just) and parametrized fixtures
  • fixtures/spec.yaml — per-recipe expectations, generated via just maint test spec spec-gen
  • tools/gen_spec.py — standalone tool to bootstrap spec.yaml from the live dump

How spec.yaml drives tests

At import time, conftest.py reads fixtures/spec.yaml and builds three parametrized fixtures from it. Each fixture yields a different slice of the spec:

Fixture Yields Tests
spec_case (namepath, expectations_dict) Privacy, dependencies, parameters
spec_namepaths set[str] Spec/dump completeness (same recipes)
flag_case (namepath, param_name, attrs_dict) Parameter attributes match

At test time, each test receives:

  1. jf — the JustfileFixture (session-scoped, autouse), which flattens the live just --dump output into a flat {namepath: recipe_data} dict
  2. A parametrized value from one of the fixtures above

The test calls jf.is_private(), jf.dependencies(), jf.parameters(), etc. to check the live dump against the spec's declared expectations.

The spec owns the expectations, the dump owns the truth. The test asks: Does truth match expectation?

Spec format

Grouped by contract — recipes with identical properties (private, depends, params) share a single entry. Each group lists namepaths first, then contract keys alphabetically.

- namepaths:
  - docker::compose::down
  - docker::compose::restart
  - docker::compose::up
  depends:
  - _require-docker
  - _require-compose
  params:
    interactive:
      long: interactive
      short: i
      value: 'true'
    profile:
      long: profile
      short: p
    services: {}

How test_modules.py iterates (fixture-based parametrization)

test_modules.py tests CLI resolution — listing, cross-directory, invocation path equivalence. Unlike @pytest.mark.parametrize (which declares values at the test site), these tests use fixture-based parametrization: the fixture is declared in conftest.py with params=, and any test requesting it by name runs once per value automatically.

This means the iteration is invisible at the test site — a test like this runs 16+ times because module is a fixture with params=_jf.module_namepaths.

def test_module_lists(self, module: str) -> None:
    result = just("--list", module, "--unsorted")
    assert result.returncode == 0

Dump-driven fixtures (no hardcoded module names)

Fixture Yields Source
module each module path _jf.module_namepaths
top_module top-level modules (no ::) modules without :: in path
parent_child_module (parent, child) pairs modules where parent::child exists
module_recipe (module, recipe) pairs all non-common public recipes per module
call_var (module, var) pairs _just_call_* variables from dump

All data is discovered from the live just --dump at import time. Adding a new module or recipe automatically adds it to the test matrix — no test file changes needed.

Regenerating the spec

After adding or modifying recipes:

just maint test spec spec-gen    # overwrites fixtures/spec.yaml from the live dump
just maint test contract test        # verify everything passes

Upstream issues

Features that would simplify or improve this test infrastructure: