Skip to content
Open
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
31 changes: 31 additions & 0 deletions CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ cfengine build --hub myhub --non-interactive
cfengine deploy --hub myhub
```

## Testing policy with `cfengine test`

Runs `test_*.cf` files (bundles that contain their own `assert:` section) in disposable Docker containers and reports a pytest-style pass/fail summary.

```bash
# Auto-discover and run tests/test_*.cf
cfengine test

# Run specific files or directories instead
cfengine test tests/test_foo.cf
cfengine test tests/

# Inside a cfbs project: builds first, then runs each test file at the end of
# the project's real promises.cf (with that test's fixture applied), so the
# tests see the state a full agent run leaves behind. A fixture
# (tests/fixtures/test_<name>.json) is merged into the project's def.json
cfengine test

# Test against a custom base image (e.g. a different distro), bypassing the build cache
cfengine test --dockerfile ./my-ubuntu.Dockerfile --rebuild

# Inside a cfbs project: run only the test bundles, from a minimal generated
# policy. Faster, but the project's policy then only ran once during setup,
# without any fixture, and every test sees that snapshot
cfengine test --test-only

# Outside a cfbs project: test against a built masterfiles directory, e.g. to
# use the standard library (m(), insert_lines(), ...) of that version
cfengine test --masterfiles ./out/masterfiles
```

## Running the CFEngine agent

```bash
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ cfengine = "cfengine_cli.main:main"
license-files = [] # Workaround bug in setuptools https://github.com/astral-sh/uv/issues/9513

[tool.setuptools.package-data]
cfengine_cli = ["*.json", "docker/test-agent/Dockerfile"] # syntax-description.json, cfengine test's container
cfengine_cli = ["*.json", "docker/test-agent/Dockerfile", "docker/test-agent/assert-module/*"] # syntax-description.json, cfengine test's container (+ assert promise declaration it COPYs and lints with)

[tool.pyright]
include = ["src"]
Expand Down
50 changes: 44 additions & 6 deletions src/cfengine_cli/cfengine_wrapper/cfengine_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
)

from cfengine_cli.utils import UserError
from cfengine_cli.container import run_in_container
from cfengine_cli.container import (
run_in_container,
run_files_in_container,
discover_test_files,
discover_module_files,
)
from cfengine_cli.cfengine_wrapper.cfengine_objects import (
Executable,
ensure_default_agent_flags,
Expand Down Expand Up @@ -320,12 +325,45 @@ def deploy(
return error


def test() -> int:
rc = build_command()
if rc != 0:
return rc
def test(
files: list[str] | None = None,
masterfiles_dir: str | None = None,
dockerfile: str | None = None,
rebuild: bool = False,
test_only: bool = False,
) -> int:
"""Runs the tests, after container.prepare_test_masterfiles (and lint)
-- see there for masterfiles_dir."""

if files:
# Module files are libraries whether tests are auto-discovered or
# named explicitly, so a test sees the same bundles either way.
return run_files_in_container(
discover_module_files() + files,
masterfiles_dir=masterfiles_dir,
dockerfile=dockerfile,
rebuild=rebuild,
test_only=test_only,
)

discovered = discover_test_files()
if discovered:
return run_files_in_container(
discover_module_files() + discovered,
masterfiles_dir=masterfiles_dir,
dockerfile=dockerfile,
rebuild=rebuild,
test_only=test_only,
)

if masterfiles_dir is not None:
return run_in_container(masterfiles_dir, dockerfile=dockerfile, rebuild=rebuild)

return run_in_container("out/masterfiles")
raise UserError(
"Nothing to test here -- no cfbs.json project and no test_*.cf files "
"found. Add a test_*.cf file, or run `cfengine init` to start a cfbs "
"project."
)


def show(target: list[str] | None = None) -> int:
Expand Down
20 changes: 17 additions & 3 deletions src/cfengine_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
init_policy_set,
init_promise_type,
)
from cfengine_cli.container import (
assert_module_declaration_path,
prepare_test_masterfiles,
)
from cf_remote.paths import cf_remote_dir
from cfengine_cli.cfengine_wrapper.cfengine_commands import test as _cfengine_test
from pydantic import ValidationError
Expand Down Expand Up @@ -51,15 +55,25 @@ def lint(files, strict, syntax_path) -> int:
return errors


def test(files, strict) -> int:
errors = _lint(files, strict, None)
def test(
files, strict, dockerfile=None, rebuild=False, test_only=False, masterfiles=None
) -> int:
# Adds the assert module so linting passes
lint_targets = (list(files) if files else ["."]) + [
assert_module_declaration_path()
]

errors = _lint(lint_targets, strict, None)
if errors != 0:
plural = "error" if errors == 1 else "errors"
print(f"Lint failed, {errors} {plural} in total. Skipping build/deploy/run.")
return errors

print("Lint passed, no errors found.")
return _cfengine_test()
rc, masterfiles_dir = prepare_test_masterfiles(masterfiles)
if rc != 0:
return rc
return _cfengine_test(files, masterfiles_dir, dockerfile, rebuild, test_only)


def dev(subcommand, args) -> int:
Expand Down
Loading
Loading