Skip to content

Commit 7b2450b

Browse files
committed
Reorganize modules.
1 parent b4d2a56 commit 7b2450b

File tree

8 files changed

+293
-291
lines changed

8 files changed

+293
-291
lines changed

CHANGELOG.md

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

55
## [Unreleased]
66

7+
- Install and run pylint and mypy.
8+
- Reorganize package modules.
9+
- Install GitHub linting actions.
10+
- Install pre-commit hooks.
11+
712
## [0.0.5] - 2023-01-23
813

914
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Fast Kuttl tests expander for Stackable integration tests.
2323
Update the version in:
2424

2525
* `pyptoject.toml`
26-
* `__init__.py`
26+
* `version.py`
2727
* `README.md`
2828

2929
Update the CHANGELOG.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ lint = ['pylint', 'mypy']
2121
dev = ['pre-commit']
2222

2323
[project.scripts]
24-
beku = "beku:main"
24+
beku = "beku.main:main"
2525

2626
[project.urls]
2727
"Homepage" = "https://github.com/stackabletech/beku.py"

src/beku/__init__.py

Lines changed: 1 addition & 283 deletions
Original file line numberDiff line numberDiff line change
@@ -1,283 +1 @@
1-
"""Expand kuttl tests from templates."""
2-
import logging
3-
import os
4-
import re
5-
import sys
6-
from argparse import ArgumentParser, Namespace
7-
from dataclasses import dataclass, field
8-
from itertools import product, chain
9-
from os import walk, path, makedirs
10-
from shutil import copy2
11-
from typing import Dict, List, TypeVar, Type, Tuple
12-
13-
from jinja2 import Environment, FileSystemLoader
14-
from yaml import safe_load
15-
16-
__version_info__ = (0, 0, '6-dev')
17-
__version__ = ".".join(map(str, __version_info__))
18-
19-
20-
def ansible_lookup(loc: str, what: str) -> str:
21-
"""
22-
Lookup an environment variable (what) and return it's contents if any.
23-
Simulates the Ansible `lookup()` function which is made available by Ansible Jinja templates.
24-
Raises an exception if `loc` is not `env`.
25-
"""
26-
if loc != 'env':
27-
raise ValueError("Can only lookup() in 'env'")
28-
result = ""
29-
try:
30-
result = os.environ[what]
31-
except KeyError:
32-
pass
33-
return result
34-
35-
36-
@dataclass
37-
class TestCase:
38-
"""Test case definition."""
39-
name: str
40-
values: Dict[str, str]
41-
tid: str = field(init=False)
42-
43-
def __post_init__(self):
44-
self.tid = "_".join(
45-
chain(
46-
[self.name],
47-
["-".join([x, self.values.get(x)])
48-
for x in self.values.keys()],
49-
)
50-
)
51-
52-
def expand(self, template_dir: str, target_dir: str) -> None:
53-
"""Expand test case."""
54-
logging.info("Expanding test case id [%s]", self.tid)
55-
td_root = path.join(template_dir, self.name)
56-
tc_root = path.join(target_dir, self.name, self.tid)
57-
_mkdir_ignore_exists(tc_root)
58-
test_env = Environment(
59-
loader=FileSystemLoader(path.join(template_dir, self.name)),
60-
trim_blocks=True
61-
)
62-
test_env.globals['lookup'] = ansible_lookup
63-
sub_level: int = 0
64-
for root, dirs, files in walk(td_root):
65-
sub_level += 1
66-
if sub_level == 5:
67-
# Sanity check
68-
raise ValueError("Maximum recursive level (5) reached.")
69-
for dir_name in dirs:
70-
_mkdir_ignore_exists(
71-
path.join(tc_root, root[len(td_root) + 1:], dir_name))
72-
for file_name in files:
73-
source = path.join(root, file_name)
74-
dest = ""
75-
f_mode = os.stat(source).st_mode
76-
if file_name.endswith(".j2"):
77-
logging.debug("Render template %s to %s", file_name, dest)
78-
dest = path.join(
79-
tc_root, root[len(td_root) + 1:], file_name[:-3:])
80-
self._expand_template(file_name, dest, test_env)
81-
else:
82-
dest = path.join(
83-
tc_root, root[len(td_root) + 1:], file_name)
84-
logging.debug("Copy file %s to %s", file_name, dest)
85-
copy2(source, dest)
86-
# restore file permissions (especially the executable bit is important here)
87-
logging.debug("Update file mode for %s", dest)
88-
os.chmod(dest, f_mode)
89-
90-
def _expand_template(self, template_file: str, dest: str, env: Environment) -> None:
91-
logging.debug("Expanding template %s", template_file)
92-
template = env.get_template(template_file)
93-
with open(dest, encoding="utf8", mode="w") as stream:
94-
print(
95-
template.render({"test_scenario": {"values": self.values}}), file=stream)
96-
97-
98-
@dataclass
99-
class TestDimension:
100-
"""Test dimension."""
101-
name: str
102-
values: List[str]
103-
104-
def expand(self) -> List[Tuple[str, str]]:
105-
"""Return a list of tuples in the form of (<dimension>, <value>)"""
106-
return [(self.name, v) for v in self.values]
107-
108-
109-
@dataclass
110-
class TestDefinition:
111-
"""Test case definition."""
112-
name: str
113-
dimensions: List[str]
114-
115-
116-
TTestSuite = TypeVar( # pylint: disable=invalid-name
117-
"TTestSuite", bound="TestSuite")
118-
119-
120-
@dataclass(frozen=True)
121-
class TestSuite:
122-
"""Test suite template."""
123-
source: str = field()
124-
test_cases: List[TestCase] = field(default_factory=list)
125-
126-
def __post_init__(self) -> None:
127-
with open(self.source, encoding="utf8") as stream:
128-
tin = safe_load(stream)
129-
dimensions = [
130-
TestDimension(d["name"], d["values"]) for d in tin["dimensions"]
131-
]
132-
test_def = [
133-
TestDefinition(t["name"], t["dimensions"]) for t in tin["tests"]
134-
]
135-
self.test_cases.extend(self._build(dimensions, test_def))
136-
137-
@classmethod
138-
def _build(
139-
cls: Type[TTestSuite], dims: List[TestDimension], tests: List[TestDefinition]
140-
) -> List[TestCase]:
141-
"""
142-
>>> TestSuite._build([TestDimension(name='trino', values=['234', '235'])],
143-
... [TestDefinition(name='smoke', dimensions=['trino'])])
144-
[TestCase(name='smoke', values={'trino': '234'}, name='smoke_trino-234'), \
145-
TestCase(name='smoke', values={'trino': '235'}, name='smoke_trino-235')]
146-
"""
147-
result = []
148-
for test in tests:
149-
used_dims = [d for d in dims if d.name in test.dimensions]
150-
expanded_test_dims: List[List[Tuple[str, str]]] = [
151-
d.expand() for d in used_dims
152-
]
153-
for tc_dim in product(*expanded_test_dims):
154-
result.append(TestCase(name=test.name, values=dict(tc_dim)))
155-
return result
156-
157-
def __repr__(self) -> str:
158-
return f"TestSuite(source={self.source})"
159-
160-
def expand(self, template_dir: str, output_dir: str, kuttl_tests: str) -> int:
161-
"""Expand test suite."""
162-
logging.info("Expanding test suite from %s", self.source)
163-
self._sanity_checks(template_dir, kuttl_tests)
164-
_mkdir_ignore_exists(output_dir)
165-
self._expand_kuttl_tests(output_dir, kuttl_tests)
166-
for test_case in self.test_cases:
167-
test_case.expand(template_dir, output_dir)
168-
return 0
169-
170-
def _sanity_checks(self, template_dir: str, kuttl_tests: str) -> None:
171-
for test_case in self.test_cases:
172-
td_root = path.join(template_dir, test_case.name)
173-
if not path.isdir(td_root):
174-
raise ValueError(
175-
f"Test definition directory not found [{td_root}]")
176-
if not path.isfile(kuttl_tests):
177-
raise ValueError(
178-
f"Kuttl test config template not found [{kuttl_tests}]")
179-
180-
def _expand_kuttl_tests(self, output_dir: str, kuttl_tests: str) -> None:
181-
env = Environment(loader=FileSystemLoader(path.dirname(kuttl_tests)))
182-
kt_base_name = path.basename(kuttl_tests)
183-
template = env.get_template(kt_base_name)
184-
kt_dest_name = re.sub(r"\.j(inja)?2$", "", kt_base_name)
185-
# Compatibility warning: Assume output_dir ends with 'tests' and remove
186-
# it from the destination file
187-
dest = path.join(path.dirname(output_dir), kt_dest_name)
188-
kuttl_vars = {
189-
"testinput": {
190-
"tests": [{"name": tn} for tn in {tc.name for tc in self.test_cases}]
191-
}
192-
}
193-
logging.debug("kuttl vars %s", kuttl_vars)
194-
with open(dest, encoding="utf8", mode="w") as stream:
195-
print(template.render(kuttl_vars), file=stream)
196-
197-
198-
def parse_cli_args() -> Namespace:
199-
"""Parse command line args."""
200-
parser = ArgumentParser(
201-
description="Kuttl test expander for the Stackable Data Platform")
202-
parser.add_argument(
203-
"-v",
204-
"--version",
205-
help="Display application version",
206-
action='version',
207-
version=f'%(prog)s {__version__}'
208-
)
209-
210-
parser.add_argument(
211-
"-i",
212-
"--test_definition",
213-
help="Test definition file.",
214-
type=str,
215-
required=False,
216-
default="tests/test-definition.yaml",
217-
)
218-
parser.add_argument(
219-
"-t",
220-
"--template_dir",
221-
help="Folder with test templates.",
222-
type=str,
223-
required=False,
224-
default="tests/templates/kuttl",
225-
)
226-
parser.add_argument(
227-
"-o",
228-
"--output_dir",
229-
help="Output folder for the expanded test cases.",
230-
type=str,
231-
required=False,
232-
default="tests/_work",
233-
)
234-
235-
parser.add_argument(
236-
"-l",
237-
"--log_level",
238-
help="Set log level.",
239-
type=str,
240-
required=False,
241-
choices=["debug", "info"],
242-
default="info",
243-
)
244-
245-
parser.add_argument(
246-
"-k",
247-
"--kuttl_test",
248-
help="Kuttl test suite definition file.",
249-
type=str,
250-
required=False,
251-
default="tests/kuttl-test.yaml.jinja2",
252-
)
253-
254-
return parser.parse_args()
255-
256-
257-
def _cli_log_level(cli_arg: str) -> int:
258-
if cli_arg == "debug":
259-
return logging.DEBUG
260-
return logging.INFO
261-
262-
263-
def _mkdir_ignore_exists(dir_name: str) -> None:
264-
try:
265-
logging.debug("Creating directory %s", dir_name)
266-
makedirs(dir_name)
267-
except FileExistsError:
268-
pass
269-
270-
271-
def main() -> int:
272-
"""Main"""
273-
cli_args = parse_cli_args()
274-
logging.basicConfig(
275-
encoding="utf-8", level=_cli_log_level(cli_args.log_level))
276-
test_suite = TestSuite(cli_args.test_definition)
277-
# Compatibility warning: add 'tests' to output_dir
278-
output_dir = path.join(cli_args.output_dir, "tests")
279-
return test_suite.expand(cli_args.template_dir, output_dir, cli_args.kuttl_test)
280-
281-
282-
if __name__ == "__main__":
283-
sys.exit(main())
1+
"""Test suite expander for Kuttl tests."""

src/beku/__main__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
"""
2-
Example usage:
3-
4-
5-
"""
1+
"""Package entry point."""
62
import sys
7-
from beku import main
3+
from .main import main
84

95
sys.exit(main())

0 commit comments

Comments
 (0)