]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Support jinja2 templates in pytest runner
authorNicki Křížek <nicki@isc.org>
Tue, 1 Oct 2024 12:45:36 +0000 (14:45 +0200)
committerNicki Křížek <nicki@isc.org>
Thu, 31 Oct 2024 13:01:12 +0000 (14:01 +0100)
Configuration files in system tests which require some variables (e.g.
port numbers) filled in during test setup, can now use jinja2 templates
when `jinja2` python package is available.

Any `*.j2` file found within the system test directory will be
automatically rendered with the environment variables into a file
without the `.j2` extension by the pytest runner. E.g.
`ns1/named.conf.j2` will become `ns1/named.conf` during test setup. To
avoid automatic rendering, use `.j2.manual` extension and render the
files manually at test time.

New `templates` pytest fixture has been added. Its `render()` function
can be used to render a template with custom test variables. This can be
useful to fill in different config options during the test. With
advanced jinja2 template syntax, it can also be used to include/omit
entire sections of the config file rather than using `named1.conf.in`,
`named2.conf.in` etc.

(cherry picked from commit 60e118c4fb7085030f47ef69e136fc6ca710b009)

bin/tests/system/README
bin/tests/system/conftest.py
bin/tests/system/isctest/__init__.py
bin/tests/system/isctest/template.py [new file with mode: 0644]

index c9a57189f677b6d35682f21a9a720f59d0681e75..14f99088dcaa626da7379f6f8393ebb2179972b4 100644 (file)
@@ -319,6 +319,16 @@ prereq.sh   Run at the beginning to determine whether the test can be run at
             if not present, the test is assumed to have all its prerequisites
             met.
 
+*.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    Run after prereq.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.
index d0f4168dc7d310b9ea92770ff7e78bdc87bb1b2f..42b1a11f334456f194fd674acf5a7c129be4b95e 100644 (file)
@@ -453,6 +453,11 @@ def system_test_dir(request, env, system_test_name):
             unlink(symlink_dst)
 
 
+@pytest.fixture(scope="module")
+def templates(system_test_dir: Path):
+    return isctest.template.TemplateEngine(system_test_dir)
+
+
 def _run_script(
     env,
     system_test_dir: Path,
@@ -531,6 +536,7 @@ def system_test(
     request,
     env: Dict[str, str],
     system_test_dir,
+    templates,
     shell,
     perl,
 ):
@@ -572,6 +578,7 @@ def system_test(
             pytest.skip("Prerequisites missing.")
 
     def setup_test():
+        templates.render_auto()
         try:
             shell(f"{system_test_dir}/setup.sh")
         except FileNotFoundError:
index 3c04d73a49ca9912da607b4a7a83b54f91451c9a..9c066ec8a1d2974e867900b91ef6a325d39bbf2c 100644 (file)
@@ -15,6 +15,7 @@ from . import query
 from . import name
 from . import rndc
 from . import run
+from . import template
 from . import log
 from . import hypothesis
 
diff --git a/bin/tests/system/isctest/template.py b/bin/tests/system/isctest/template.py
new file mode 100644 (file)
index 0000000..123d758
--- /dev/null
@@ -0,0 +1,100 @@
+#!/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.
+
+import os
+from pathlib import Path
+from typing import Any, Dict, Optional, Union
+
+import pytest
+
+from .log import debug
+
+
+class TemplateEngine:
+    """
+    Engine for rendering jinja2 templates in system test directories.
+    """
+
+    def __init__(self, directory: Union[str, Path], env_vars=None):
+        """
+        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
+        if env_vars is None:
+            self.env_vars = dict(os.environ)
+        else:
+            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])