Commit 13b87a1828 for bind
commit 13b87a182821f3a0476e80fbf74cba6e60c9923f
Author: Mark Andrews <marka@isc.org>
Date: Thu Oct 22 16:54:28 2026 +1100
Extend template.render to ensure the rendered file is newer
named skips reloading a zone file that is not newer than its last load,
and where file timestamps resolve whole seconds only (named on macOS),
a test rewriting the file within the same second hits that. If
newer=True then ensure that the rendered file is newer than the old
file using seconds as the timing resolution.
The logic lives in isctest.util.newer_mtime(), for tests that rewrite
files by other means than templates.
Assisted-by: Claude:claude-opus-5-5
diff --git a/bin/tests/system/isctest/template.py b/bin/tests/system/isctest/template.py
index 87652ed6d4..cb69714291 100644
--- a/bin/tests/system/isctest/template.py
+++ b/bin/tests/system/isctest/template.py
@@ -14,6 +14,7 @@ from pathlib import Path
from re import compile as Re
from typing import TYPE_CHECKING, Any
+import contextlib
import re
import jinja2
@@ -22,6 +23,7 @@ import jinja2.nodes
import jinja2.parser
from .log import debug
+from .util import newer_mtime
from .vars import ALL
if TYPE_CHECKING:
@@ -127,12 +129,15 @@ class TemplateEngine:
output: str,
data: dict[str, Any] | None = None,
template: str | None = None,
+ newer: bool = False,
) -> 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.
+ case of a variable name clash, `data` has precedence. With `newer`, an
+ existing `output` file gets a modification time in a later second, so
+ that named does not skip reloading it (see `util.newer_mtime()`).
"""
available = self.j2env.list_templates()
if template is None:
@@ -155,7 +160,8 @@ class TemplateEngine:
debug("rendering template `%s` to file `%s`", template, output)
stream = self.j2env.get_template(template).stream(data)
- stream.dump(output, encoding="utf-8")
+ with newer_mtime(output) if newer else contextlib.nullcontext():
+ stream.dump(output, encoding="utf-8")
def render_auto(self, data: dict[str, Any] | None = None):
"""
diff --git a/bin/tests/system/isctest/util.py b/bin/tests/system/isctest/util.py
index 91382ae06f..05ec2971ee 100644
--- a/bin/tests/system/isctest/util.py
+++ b/bin/tests/system/isctest/util.py
@@ -9,10 +9,17 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
+from collections.abc import Iterator
+
+import contextlib
+import os
+
import dns.rrset
import dns.zone
import pytest
+NS_PER_SEC = 1_000_000_000
+
def zone_contains(
zone: dns.zone.Zone, rrset: dns.rrset.RRset, compare_ttl=False
@@ -46,6 +53,31 @@ def file_contents_contain(file, substr):
return False
+@contextlib.contextmanager
+def newer_mtime(path: str | os.PathLike) -> Iterator[None]:
+ """
+ Make sure that `path`, rewritten inside the block, ends up with
+ a modification time in a later second than it had before.
+
+ named reloads a file only when its modification time is newer than the
+ last load, and where file timestamps resolve whole seconds only (macOS,
+ some file systems), a file rewritten within the same second is skipped as
+ "older than last load". A file that did not exist before is left as is.
+ """
+ try:
+ before: int | None = os.stat(path).st_mtime_ns
+ except FileNotFoundError:
+ before = None
+
+ yield
+
+ if before is None:
+ return
+ after = os.stat(path)
+ if after.st_mtime_ns // NS_PER_SEC <= before // NS_PER_SEC:
+ os.utime(path, ns=(after.st_atime_ns, before + NS_PER_SEC))
+
+
def param(*args, **kwargs):
if "id" not in kwargs:
kwargs["id"] = args[0] # use first argument as test ID