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