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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Common test steps: files in a `commons` directory next to the test templates (i.e.
`<template_dir>/commons`), or an explicit `--common_dir`, are rendered into every generated test
case in addition to the test's own steps. This lets shared steps such as a teardown live in a single
place instead of being copied into each test. No-op unless the directory exists.

### Changed

- Update registry references to oci ([#36]).
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ rm -rf tests/_work && beku
cd tests/_work && kubectl kuttl test
```

### Common steps

Files placed in a `commons` directory next to the test templates (i.e.
`tests/templates/kuttl/commons`) are rendered into *every* generated test case, in addition to that
test's own steps. This lets shared steps — for example a teardown that deletes the product custom
resources before the namespace is removed — live in a single place instead of being copied into each
test. The directory is templated the same way as regular steps (`.j2`/`.jinja2` files are rendered,
others copied; `NAMESPACE` and `lookup` are available). It is skipped if it does not exist, and its
location can be overridden with `--common_dir`. Use step names that don't collide with a test's own
steps (e.g. a high number like `99-teardown.yaml`).

Also see the `examples` folder.

## Release a new version
Expand Down
38 changes: 28 additions & 10 deletions src/beku/kuttl.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,26 +140,43 @@ def tid(self) -> str:
else:
return name

def expand(self, template_dir: str, target_dir: str, namespace: str) -> None:
"""Expand test case This will create the target folder, copy files and render render templates."""
def expand(self, template_dir: str, target_dir: str, namespace: str, common_dir: Optional[str] = None) -> None:
"""Expand test case. This will create the target folder, copy files and render templates.

The test's own steps (from ``template_dir/<name>``) are rendered first. If ``common_dir`` is
given and exists, its files are then rendered into the SAME test folder, so shared steps (e.g.
a teardown) can live in a single place instead of being copied into every test. Common files
are rendered after the test's own files, so a common file wins on a name collision; use
non-colliding names (e.g. a high step number like ``99-teardown.yaml``).
"""
logging.info("Expanding test case id [%s]", self.tid)
td_root = path.join(template_dir, self.name)
tc_root = path.join(target_dir, self.name, self.tid)
_mkdir_ignore_exists(tc_root)
test_env = Environment(loader=FileSystemLoader(path.join(template_dir, self.name)), trim_blocks=True)
test_env.globals["lookup"] = ansible_lookup
test_env.globals["NAMESPACE"] = determine_namespace(self.tid, namespace)
namespace = determine_namespace(self.tid, namespace)
self._render_dir(path.join(template_dir, self.name), tc_root, namespace)
if common_dir:
if path.isdir(common_dir):
logging.debug("Rendering common steps from [%s] into [%s]", common_dir, tc_root)
self._render_dir(common_dir, tc_root, namespace)
else:
logging.warning("Common steps directory [%s] does not exist, skipping", common_dir)

def _render_dir(self, source_root: str, tc_root: str, namespace: str) -> None:
"""Render/copy every file under ``source_root`` into ``tc_root``, preserving sub-directories."""
env = Environment(loader=FileSystemLoader(source_root), trim_blocks=True)
env.globals["lookup"] = ansible_lookup
env.globals["NAMESPACE"] = namespace
sub_level: int = 0
for root, dirs, files in walk(td_root):
for root, dirs, files in walk(source_root):
sub_level += 1
if sub_level == 8:
# Sanity check
raise ValueError("Maximum recursive level (8) reached.")
for dir_name in dirs:
_mkdir_ignore_exists(path.join(tc_root, root[len(td_root) + 1 :], dir_name))
_mkdir_ignore_exists(path.join(tc_root, root[len(source_root) + 1 :], dir_name))
for file_name in files:
test_source = make_test_source_with_context(
file_name, root, path.join(tc_root, root[len(td_root) + 1 :]), test_env, self.values
file_name, root, path.join(tc_root, root[len(source_root) + 1 :]), env, self.values
)
test_source.build_destination()

Expand Down Expand Up @@ -336,6 +353,7 @@ def expand(
output_dir: str,
kuttl_tests: str,
namespace: str,
common_dir: Optional[str] = None,
) -> int:
"""Expand test suite."""
try:
Expand All @@ -344,7 +362,7 @@ def expand(
_mkdir_ignore_exists(output_dir)
_expand_kuttl_tests(ets.test_cases, output_dir, kuttl_tests)
for test_case in ets.test_cases:
test_case.expand(template_dir, output_dir, namespace)
test_case.expand(template_dir, output_dir, namespace, common_dir)
except StopIteration as exc:
raise ValueError(f"Cannot expand test suite [{suite}] because cannot find it in [{kuttl_tests}]") from exc
return 0
Expand Down
18 changes: 18 additions & 0 deletions src/beku/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ def parse_cli_args() -> Namespace:
default="tests/kuttl-test.yaml.jinja2",
)

parser.add_argument(
"-c",
"--common_dir",
help="Folder with common test step templates/files that are rendered into EVERY generated "
"test case (in addition to the test's own steps). Lets shared steps such as a teardown live "
"in a single place instead of being copied into each test. Defaults to a 'commons' folder "
"next to the test templates (i.e. <template_dir>/commons); skipped if that folder does not "
"exist, so this is a no-op unless you create it.",
type=str,
required=False,
default=None,
)

parser.add_argument(
"-s",
"--suite",
Expand Down Expand Up @@ -94,11 +107,16 @@ def main() -> int:
rmtree(path=cli_args.output_dir, ignore_errors=True)
# Compatibility warning: add 'tests' to output_dir
output_dir = path.join(cli_args.output_dir, "tests")
# Default the common steps directory to a 'commons' folder next to the test templates. It is
# rendered into every test case if present, and silently skipped otherwise (so this stays a no-op
# for repositories that don't opt in by creating it).
common_dir = cli_args.common_dir if cli_args.common_dir is not None else path.join(cli_args.template_dir, "commons")
return expand(
cli_args.suite,
effective_test_suites,
cli_args.template_dir,
output_dir,
cli_args.kuttl_test,
cli_args.namespace,
common_dir,
)
69 changes: 69 additions & 0 deletions src/beku/test/test_common_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for the common-steps feature: files in a common directory are rendered into every test case."""

import tempfile
import unittest
from os import makedirs, path

from beku.kuttl import TestCase


def _write(file_path: str, content: str) -> None:
makedirs(path.dirname(file_path), exist_ok=True)
with open(file_path, encoding="utf8", mode="w") as stream:
stream.write(content)


class TestCommonSteps(unittest.TestCase):
def test_common_dir_rendered_into_test_case(self):
with tempfile.TemporaryDirectory() as tmp:
template_dir = path.join(tmp, "templates")
common_dir = path.join(tmp, "commons")
target_dir = path.join(tmp, "_work")

# A test with its own step, and a plain + templated common file.
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")
_write(path.join(common_dir, "99-teardown.yaml"), "teardown\n")
_write(path.join(common_dir, "50-note.txt.j2"), "ns={{ NAMESPACE }}\n")

TestCase(name="mytest", values={}).expand(template_dir, target_dir, "kuttl-fixed", common_dir)

tc_dir = path.join(target_dir, "mytest", "mytest")
# the test's own step is present
self.assertTrue(path.isfile(path.join(tc_dir, "00-install.yaml")))
# the common plain file is present
self.assertTrue(path.isfile(path.join(tc_dir, "99-teardown.yaml")))
# the common template is rendered (suffix stripped, NAMESPACE substituted)
rendered = path.join(tc_dir, "50-note.txt")
self.assertTrue(path.isfile(rendered))
with open(rendered, encoding="utf8") as stream:
self.assertIn("ns=kuttl-fixed", stream.read())

def test_missing_common_dir_is_skipped(self):
with tempfile.TemporaryDirectory() as tmp:
template_dir = path.join(tmp, "templates")
target_dir = path.join(tmp, "_work")
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")

# Non-existent common dir must not raise and must not add anything.
TestCase(name="mytest", values={}).expand(
template_dir, target_dir, "kuttl-fixed", path.join(tmp, "does-not-exist")
)

tc_dir = path.join(target_dir, "mytest", "mytest")
self.assertTrue(path.isfile(path.join(tc_dir, "00-install.yaml")))
self.assertFalse(path.isfile(path.join(tc_dir, "99-teardown.yaml")))

def test_no_common_dir_argument_is_backwards_compatible(self):
with tempfile.TemporaryDirectory() as tmp:
template_dir = path.join(tmp, "templates")
target_dir = path.join(tmp, "_work")
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")

# Old call signature (no common_dir) keeps working.
TestCase(name="mytest", values={}).expand(template_dir, target_dir, "kuttl-fixed")

self.assertTrue(path.isfile(path.join(target_dir, "mytest", "mytest", "00-install.yaml")))


if __name__ == "__main__":
unittest.main()
Loading