- perl
- dnspython
- pytest-xdist (for parallel execution)
+- python-jinja2 (for tests which use jinja templates)
Individual system tests might also require additional dependencies. If those
are missing, the affected tests will be skipped and should produce a message
- `tests_*.py`: These python files are picked up by pytest as modules. If they
contain any test functions, they're added to the test suite.
-- `setup.sh`: This sets up the preconditions for the tests. Although optional,
- virtually all tests will require such a file to set up the ports they should
- use for the test.
+- `*.j2`: These jinja2 templates can be used for configuration files or any
+ other files which require certain variables filled in, e.g. ports from the
+ environment variables. During test setup, the pytest runner will automatically
+ fill those in and strip the filename extension .j2, e.g. `ns1/named.conf.j2`
+ becomes `ns1/named.conf`. When using advanced templating to conditionally
+ include/omit entire sections or when filling in custom variables used for the
+ test, ensure the templates always include the defaults. If you don't need the
+ file to be auto-templated during test setup, use `.j2.manual` instead and then
+ no defaults are needed.
+
+- `setup.sh`: This sets up the preconditions for the tests.
- `tests.sh`: Any shell-based tests are located within this file. Runs the
actual tests.
--- /dev/null
+#!/usr/bin/python3
+
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from pathlib import Path
+from typing import Any, Dict, Optional, Union
+
+import pytest
+
+from .log import debug
+from .vars import ALL
+
+
+class TemplateEngine:
+ """
+ Engine for rendering jinja2 templates in system test directories.
+ """
+
+ def __init__(self, directory: Union[str, Path], env_vars=ALL):
+ """
+ Initialize the template engine for `directory`, optionally overriding
+ the `env_vars` that will be used when rendering the templates (defaults
+ to the environment variables set by the pytest runner).
+ """
+ self.directory = Path(directory)
+ self._j2env = None
+ self.env_vars = dict(env_vars)
+
+ @property
+ def j2env(self):
+ """
+ Jinja2 engine that is initialized when first requested. In case the
+ jinja2 package in unavailable, the current test will be skipped.
+ """
+ if self._j2env is None:
+ try:
+ import jinja2 # pylint: disable=import-outside-toplevel
+ except ImportError:
+ pytest.skip("jinja2 not found")
+
+ loader = jinja2.FileSystemLoader(str(self.directory))
+ return jinja2.Environment(
+ loader=loader,
+ undefined=jinja2.StrictUndefined,
+ variable_start_string="@",
+ variable_end_string="@",
+ )
+ return self._j2env
+
+ def render(
+ self,
+ output: str,
+ data: Optional[Dict[str, Any]] = None,
+ template: Optional[str] = None,
+ ) -> None:
+ """
+ Render `output` file from jinja `template` and fill in the `data`. The
+ `template` defaults to *.j2.manual or *.j2 file. The environment
+ variables which the engine was initialized with are also filled in. In
+ case of a variable name clash, `data` has precedence.
+ """
+ if template is None:
+ template = f"{output}.j2.manual"
+ if not Path(template).is_file():
+ template = f"{output}.j2"
+ if not Path(template).is_file():
+ raise RuntimeError('No jinja2 template found for "{output}"')
+
+ if data is None:
+ data = self.env_vars
+ else:
+ data = {**self.env_vars, **data}
+
+ debug("rendering template `%s` to file `%s`", template, output)
+ stream = self.j2env.get_template(template).stream(data)
+ stream.dump(output, encoding="utf-8")
+
+ def render_auto(self):
+ """
+ Render all *.j2 templates with default values and write the output to
+ files without the .j2 extensions.
+ """
+ templates = [
+ str(filepath.relative_to(self.directory))
+ for filepath in self.directory.rglob("*.j2")
+ ]
+ for template in templates:
+ self.render(template[:-3])