Skip to content

Commit adcd549

Browse files
committed
feat: render common steps into every test case
Add a common-steps directory whose files are rendered into every generated test case, in addition to the test's own steps. This lets shared steps - such as a teardown that removes the product CRs before the namespace is deleted - live in a single place instead of being copied into each test. The directory defaults to '<template_dir>/commons' (overridable via --common_dir) and is skipped if absent, so this is a no-op for repositories that do not opt in. Common files are templated identically to regular steps (.j2/.jinja2 rendered, others copied; NAMESPACE and lookup available) and are rendered after the test's own files.
1 parent f8256bb commit adcd549

5 files changed

Lines changed: 133 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

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

916
- Update registry references to oci ([#36]).

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ rm -rf tests/_work && beku
6161
cd tests/_work && kubectl kuttl test
6262
```
6363

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

6677
## Release a new version

src/beku/kuttl.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,26 +140,43 @@ def tid(self) -> str:
140140
else:
141141
return name
142142

143-
def expand(self, template_dir: str, target_dir: str, namespace: str) -> None:
144-
"""Expand test case This will create the target folder, copy files and render render templates."""
143+
def expand(self, template_dir: str, target_dir: str, namespace: str, common_dir: Optional[str] = None) -> None:
144+
"""Expand test case. This will create the target folder, copy files and render templates.
145+
146+
The test's own steps (from ``template_dir/<name>``) are rendered first. If ``common_dir`` is
147+
given and exists, its files are then rendered into the SAME test folder, so shared steps (e.g.
148+
a teardown) can live in a single place instead of being copied into every test. Common files
149+
are rendered after the test's own files, so a common file wins on a name collision; use
150+
non-colliding names (e.g. a high step number like ``99-teardown.yaml``).
151+
"""
145152
logging.info("Expanding test case id [%s]", self.tid)
146-
td_root = path.join(template_dir, self.name)
147153
tc_root = path.join(target_dir, self.name, self.tid)
148154
_mkdir_ignore_exists(tc_root)
149-
test_env = Environment(loader=FileSystemLoader(path.join(template_dir, self.name)), trim_blocks=True)
150-
test_env.globals["lookup"] = ansible_lookup
151-
test_env.globals["NAMESPACE"] = determine_namespace(self.tid, namespace)
155+
namespace = determine_namespace(self.tid, namespace)
156+
self._render_dir(path.join(template_dir, self.name), tc_root, namespace)
157+
if common_dir:
158+
if path.isdir(common_dir):
159+
logging.debug("Rendering common steps from [%s] into [%s]", common_dir, tc_root)
160+
self._render_dir(common_dir, tc_root, namespace)
161+
else:
162+
logging.warning("Common steps directory [%s] does not exist, skipping", common_dir)
163+
164+
def _render_dir(self, source_root: str, tc_root: str, namespace: str) -> None:
165+
"""Render/copy every file under ``source_root`` into ``tc_root``, preserving sub-directories."""
166+
env = Environment(loader=FileSystemLoader(source_root), trim_blocks=True)
167+
env.globals["lookup"] = ansible_lookup
168+
env.globals["NAMESPACE"] = namespace
152169
sub_level: int = 0
153-
for root, dirs, files in walk(td_root):
170+
for root, dirs, files in walk(source_root):
154171
sub_level += 1
155172
if sub_level == 8:
156173
# Sanity check
157174
raise ValueError("Maximum recursive level (8) reached.")
158175
for dir_name in dirs:
159-
_mkdir_ignore_exists(path.join(tc_root, root[len(td_root) + 1 :], dir_name))
176+
_mkdir_ignore_exists(path.join(tc_root, root[len(source_root) + 1 :], dir_name))
160177
for file_name in files:
161178
test_source = make_test_source_with_context(
162-
file_name, root, path.join(tc_root, root[len(td_root) + 1 :]), test_env, self.values
179+
file_name, root, path.join(tc_root, root[len(source_root) + 1 :]), env, self.values
163180
)
164181
test_source.build_destination()
165182

@@ -336,6 +353,7 @@ def expand(
336353
output_dir: str,
337354
kuttl_tests: str,
338355
namespace: str,
356+
common_dir: Optional[str] = None,
339357
) -> int:
340358
"""Expand test suite."""
341359
try:
@@ -344,7 +362,7 @@ def expand(
344362
_mkdir_ignore_exists(output_dir)
345363
_expand_kuttl_tests(ets.test_cases, output_dir, kuttl_tests)
346364
for test_case in ets.test_cases:
347-
test_case.expand(template_dir, output_dir, namespace)
365+
test_case.expand(template_dir, output_dir, namespace, common_dir)
348366
except StopIteration as exc:
349367
raise ValueError(f"Cannot expand test suite [{suite}] because cannot find it in [{kuttl_tests}]") from exc
350368
return 0

src/beku/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ def parse_cli_args() -> Namespace:
6060
default="tests/kuttl-test.yaml.jinja2",
6161
)
6262

63+
parser.add_argument(
64+
"-c",
65+
"--common_dir",
66+
help="Folder with common test step templates/files that are rendered into EVERY generated "
67+
"test case (in addition to the test's own steps). Lets shared steps such as a teardown live "
68+
"in a single place instead of being copied into each test. Defaults to a 'commons' folder "
69+
"next to the test templates (i.e. <template_dir>/commons); skipped if that folder does not "
70+
"exist, so this is a no-op unless you create it.",
71+
type=str,
72+
required=False,
73+
default=None,
74+
)
75+
6376
parser.add_argument(
6477
"-s",
6578
"--suite",
@@ -94,11 +107,16 @@ def main() -> int:
94107
rmtree(path=cli_args.output_dir, ignore_errors=True)
95108
# Compatibility warning: add 'tests' to output_dir
96109
output_dir = path.join(cli_args.output_dir, "tests")
110+
# Default the common steps directory to a 'commons' folder next to the test templates. It is
111+
# rendered into every test case if present, and silently skipped otherwise (so this stays a no-op
112+
# for repositories that don't opt in by creating it).
113+
common_dir = cli_args.common_dir if cli_args.common_dir is not None else path.join(cli_args.template_dir, "commons")
97114
return expand(
98115
cli_args.suite,
99116
effective_test_suites,
100117
cli_args.template_dir,
101118
output_dir,
102119
cli_args.kuttl_test,
103120
cli_args.namespace,
121+
common_dir,
104122
)

src/beku/test/test_common_steps.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Tests for the common-steps feature: files in a common directory are rendered into every test case."""
2+
3+
import tempfile
4+
import unittest
5+
from os import makedirs, path
6+
7+
from beku.kuttl import TestCase
8+
9+
10+
def _write(file_path: str, content: str) -> None:
11+
makedirs(path.dirname(file_path), exist_ok=True)
12+
with open(file_path, encoding="utf8", mode="w") as stream:
13+
stream.write(content)
14+
15+
16+
class TestCommonSteps(unittest.TestCase):
17+
def test_common_dir_rendered_into_test_case(self):
18+
with tempfile.TemporaryDirectory() as tmp:
19+
template_dir = path.join(tmp, "templates")
20+
common_dir = path.join(tmp, "commons")
21+
target_dir = path.join(tmp, "_work")
22+
23+
# A test with its own step, and a plain + templated common file.
24+
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")
25+
_write(path.join(common_dir, "99-teardown.yaml"), "teardown\n")
26+
_write(path.join(common_dir, "50-note.txt.j2"), "ns={{ NAMESPACE }}\n")
27+
28+
TestCase(name="mytest", values={}).expand(template_dir, target_dir, "kuttl-fixed", common_dir)
29+
30+
tc_dir = path.join(target_dir, "mytest", "mytest")
31+
# the test's own step is present
32+
self.assertTrue(path.isfile(path.join(tc_dir, "00-install.yaml")))
33+
# the common plain file is present
34+
self.assertTrue(path.isfile(path.join(tc_dir, "99-teardown.yaml")))
35+
# the common template is rendered (suffix stripped, NAMESPACE substituted)
36+
rendered = path.join(tc_dir, "50-note.txt")
37+
self.assertTrue(path.isfile(rendered))
38+
with open(rendered, encoding="utf8") as stream:
39+
self.assertIn("ns=kuttl-fixed", stream.read())
40+
41+
def test_missing_common_dir_is_skipped(self):
42+
with tempfile.TemporaryDirectory() as tmp:
43+
template_dir = path.join(tmp, "templates")
44+
target_dir = path.join(tmp, "_work")
45+
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")
46+
47+
# Non-existent common dir must not raise and must not add anything.
48+
TestCase(name="mytest", values={}).expand(
49+
template_dir, target_dir, "kuttl-fixed", path.join(tmp, "does-not-exist")
50+
)
51+
52+
tc_dir = path.join(target_dir, "mytest", "mytest")
53+
self.assertTrue(path.isfile(path.join(tc_dir, "00-install.yaml")))
54+
self.assertFalse(path.isfile(path.join(tc_dir, "99-teardown.yaml")))
55+
56+
def test_no_common_dir_argument_is_backwards_compatible(self):
57+
with tempfile.TemporaryDirectory() as tmp:
58+
template_dir = path.join(tmp, "templates")
59+
target_dir = path.join(tmp, "_work")
60+
_write(path.join(template_dir, "mytest", "00-install.yaml"), "step\n")
61+
62+
# Old call signature (no common_dir) keeps working.
63+
TestCase(name="mytest", values={}).expand(template_dir, target_dir, "kuttl-fixed")
64+
65+
self.assertTrue(path.isfile(path.join(target_dir, "mytest", "mytest", "00-install.yaml")))
66+
67+
68+
if __name__ == "__main__":
69+
unittest.main()

0 commit comments

Comments
 (0)