Commit c0e1272a75 for bind

commit c0e1272a756261a6881100a0c6d556e68f0011f6
Author: Martin Basti <mbasti@isc.org>
Date:   Fri Oct 30 19:33:05 2026 +0000

    Migrate dnstap checkconf and file output checks to pytest

    The traffic generation, the reopen/roll of the output files, the
    per-server frame counts and the dnstap-read format checks (text,
    YAML, hex) move to tests_dnstap_file.py. The large-answer.fstrm check
    needs no running server and joins tests_dnstap.py instead. The frame
    counts are compared as one Counter per server, so an unexpected
    frame type fails the test instead of slipping past the greps.

    The named-checkconf loops move to the checkconf system test, one
    test per configuration file. The files are renamed dnstap-bad-* and
    dnstap-good-* so the existing bad-*/good-* loops in checkconf/tests.sh
    skip them; the pytest versions carry the with_dnstap marker instead.

    ydump.py is dropped: PyYAML is a hard requirement of the system
    tests, so the YAML output is parsed directly instead of being
    skipped when the module is missing.

    The shell test keeps the unix-socket and roll checks; it still stops
    the other servers first because those checks rely on ns4 being
    unable to resolve anything.

    Assisted-by: Claude:claude-fable-5-1

diff --git a/bin/tests/system/dnstap/bad-fstrm-reopen-interval.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-reopen-interval.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-reopen-interval.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-reopen-interval.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-buffer-hint-max.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-buffer-hint-max.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-buffer-hint-max.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-buffer-hint-max.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-buffer-hint-min.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-buffer-hint-min.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-buffer-hint-min.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-buffer-hint-min.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-flush-timeout-max.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-flush-timeout-max.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-flush-timeout-max.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-flush-timeout-max.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-flush-timeout-min.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-flush-timeout-min.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-flush-timeout-min.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-flush-timeout-min.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-max.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-max.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-max.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-max.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-min.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-min.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-min.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-min.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-po2.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-po2.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-input-queue-size-po2.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-input-queue-size-po2.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-output-notify-threshold.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-notify-threshold.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-output-notify-threshold.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-notify-threshold.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-output-queue-size-max.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-queue-size-max.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-output-queue-size-max.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-queue-size-max.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-output-queue-size-min.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-queue-size-min.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-output-queue-size-min.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-output-queue-size-min.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-reopen-interval-max.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-reopen-interval-max.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-reopen-interval-max.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-reopen-interval-max.conf
diff --git a/bin/tests/system/dnstap/bad-fstrm-set-reopen-interval-min.conf b/bin/tests/system/checkconf/dnstap-bad-fstrm-set-reopen-interval-min.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-fstrm-set-reopen-interval-min.conf
rename to bin/tests/system/checkconf/dnstap-bad-fstrm-set-reopen-interval-min.conf
diff --git a/bin/tests/system/dnstap/bad-missing-dnstap-output-view.conf b/bin/tests/system/checkconf/dnstap-bad-missing-dnstap-output-view.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-missing-dnstap-output-view.conf
rename to bin/tests/system/checkconf/dnstap-bad-missing-dnstap-output-view.conf
diff --git a/bin/tests/system/dnstap/bad-missing-dnstap-output.conf b/bin/tests/system/checkconf/dnstap-bad-missing-dnstap-output.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-missing-dnstap-output.conf
rename to bin/tests/system/checkconf/dnstap-bad-missing-dnstap-output.conf
diff --git a/bin/tests/system/dnstap/bad-size-version.conf b/bin/tests/system/checkconf/dnstap-bad-size-version.conf
similarity index 100%
rename from bin/tests/system/dnstap/bad-size-version.conf
rename to bin/tests/system/checkconf/dnstap-bad-size-version.conf
diff --git a/bin/tests/system/dnstap/good-dnstap-in-options.conf b/bin/tests/system/checkconf/dnstap-good-dnstap-in-options.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-dnstap-in-options.conf
rename to bin/tests/system/checkconf/dnstap-good-dnstap-in-options.conf
diff --git a/bin/tests/system/dnstap/good-dnstap-in-view.conf b/bin/tests/system/checkconf/dnstap-good-dnstap-in-view.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-dnstap-in-view.conf
rename to bin/tests/system/checkconf/dnstap-good-dnstap-in-view.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-reopen-interval.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-reopen-interval.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-reopen-interval.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-reopen-interval.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-buffer-hint.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-buffer-hint.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-buffer-hint.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-buffer-hint.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-flush-timeout.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-flush-timeout.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-flush-timeout.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-flush-timeout.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-input-queue-size.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-input-queue-size.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-input-queue-size.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-input-queue-size.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-output-notify-threshold.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-output-notify-threshold.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-output-notify-threshold.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-output-notify-threshold.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-output-queue-model-mpsc.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-model-mpsc.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-output-queue-model-mpsc.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-model-mpsc.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-output-queue-model-spsc.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-model-spsc.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-output-queue-model-spsc.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-model-spsc.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-output-queue-size.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-size.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-output-queue-size.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-output-queue-size.conf
diff --git a/bin/tests/system/dnstap/good-fstrm-set-reopen-interval.conf b/bin/tests/system/checkconf/dnstap-good-fstrm-set-reopen-interval.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-fstrm-set-reopen-interval.conf
rename to bin/tests/system/checkconf/dnstap-good-fstrm-set-reopen-interval.conf
diff --git a/bin/tests/system/dnstap/good-size-unlimited.conf b/bin/tests/system/checkconf/dnstap-good-size-unlimited.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-size-unlimited.conf
rename to bin/tests/system/checkconf/dnstap-good-size-unlimited.conf
diff --git a/bin/tests/system/dnstap/good-size-version.conf b/bin/tests/system/checkconf/dnstap-good-size-version.conf
similarity index 100%
rename from bin/tests/system/dnstap/good-size-version.conf
rename to bin/tests/system/checkconf/dnstap-good-size-version.conf
diff --git a/bin/tests/system/checkconf/tests_checkconf.py b/bin/tests/system/checkconf/tests_checkconf.py
index 9df02c34d1..a35736cf73 100644
--- a/bin/tests/system/checkconf/tests_checkconf.py
+++ b/bin/tests/system/checkconf/tests_checkconf.py
@@ -9,9 +9,14 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

+from pathlib import Path
+
 import os

+import pytest
+
 import isctest
+import isctest.mark


 def test_checkconf_effective():
@@ -50,3 +55,24 @@ def test_checkconf_included_unexpectedeof():
         == cmd.proc.stdout
     )
     assert cmd.proc.returncode != 0
+
+
+def dnstap_confs(pattern):
+    # Collection happens before the test directory becomes the working
+    # directory, so look next to this file.
+    return sorted(path.name for path in Path(__file__).parent.glob(pattern))
+
+
+# The dnstap-*.conf files are kept out of the bad-*/good-* loops in tests.sh
+# because they only make sense when dnstap support is compiled in.
+@isctest.mark.with_dnstap
+@pytest.mark.parametrize("conf", dnstap_confs("dnstap-bad-*.conf"))
+def test_checkconf_dnstap_bad(conf):
+    cmd = isctest.run.cmd([os.environ["CHECKCONF"], conf], raise_on_exception=False)
+    assert cmd.proc.returncode == 1
+
+
+@isctest.mark.with_dnstap
+@pytest.mark.parametrize("conf", dnstap_confs("dnstap-good-*.conf"))
+def test_checkconf_dnstap_good(conf):
+    isctest.run.cmd([os.environ["CHECKCONF"], conf])
diff --git a/bin/tests/system/dnstap/common.py b/bin/tests/system/dnstap/common.py
new file mode 100644
index 0000000000..53fdbd2aa6
--- /dev/null
+++ b/bin/tests/system/dnstap/common.py
@@ -0,0 +1,63 @@
+# 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.
+
+"""
+Helpers shared by the dnstap system test modules.
+"""
+
+from collections import Counter
+
+import os
+
+import isctest
+
+ARTIFACTS = [
+    "dnstap.out*",
+    "fstrm_capture.out.*",
+    "ns*/dnstap.out*",
+    "ns2/example.db",
+    "ns2/example.db.jnl",
+]
+
+
+def dnstap_read(path: str, *opts: str) -> isctest.run.CmdResult:
+    """
+    Run dnstap-read on a capture file with optional extra options.
+    """
+    return isctest.run.cmd([os.environ["DNSTAPREAD"], *opts, path])
+
+
+def message_counts(path: str) -> Counter:
+    """
+    Count dnstap frames in a capture file by message type (AQ, CR, ...) and
+    by transport protocol (UDP, TCP).
+
+    Each line of dnstap-read output looks like:
+
+        22-Jun-2022 12:09:06.168 RQ 10.53.0.3:56306 -> 10.53.0.2:7523 UDP 54b example/IN/A
+    """
+    counts: Counter = Counter()
+    for line in dnstap_read(path).out.splitlines():
+        fields = line.split()
+        counts[fields[2]] += 1
+        for protocol in ("UDP", "TCP"):
+            if protocol in fields:
+                counts[protocol] += 1
+    return counts
+
+
+def check_counts(path: str, expected: dict[str, int]) -> None:
+    """
+    Assert the frame counts in a capture file match the expectation exactly.
+    Message types and protocols with no expected frames may be omitted from
+    `expected`.
+    """
+    assert message_counts(path) == Counter(expected)
diff --git a/bin/tests/system/dnstap/tests.sh b/bin/tests/system/dnstap/tests.sh
index e992e08c45..5b881690a7 100644
--- a/bin/tests/system/dnstap/tests.sh
+++ b/bin/tests/system/dnstap/tests.sh
@@ -44,34 +44,6 @@ check_count() {
   }
 }

-for bad in bad-*.conf; do
-  ret=0
-  echo_i "checking that named-checkconf detects error in $bad"
-  {
-    $CHECKCONF $bad >/dev/null 2>&1
-    rc=$?
-  } || true
-  if [ $rc != 1 ]; then
-    echo_i "failed"
-    ret=1
-  fi
-  status=$((status + ret))
-done
-
-for good in good-*.conf; do
-  ret=0
-  echo_i "checking that named-checkconf detects no error in $good"
-  {
-    $CHECKCONF $good >/dev/null 2>&1
-    rc=$?
-  } || true
-  if [ $rc != 0 ]; then
-    echo_i "failed"
-    ret=1
-  fi
-  status=$((status + ret))
-done
-
 echo_i "wait for servers to finish loading"
 ret=0
 wait_for_log 20 "all zones loaded" ns1/named.run || ret=1
@@ -82,23 +54,6 @@ wait_for_log 20 "all zones loaded" ns5/named.run || ret=1
 if [ $ret != 0 ]; then echo_i "failed"; fi
 status=$((status + ret))

-# both the 'a.example/A' lookup and the './NS' lookup to ns1
-# need to complete before reopening/rolling for the counts to
-# be correct.
-
-echo_i "prime cache"
-ret=0
-$DIG $DIGOPTS @10.53.0.3 a.example >dig.out || true
-wait_for_log 20 "(.): reset client" ns1/named.run || true
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-# check three different dnstap reopen/roll methods:
-# ns1: dnstap-reopen; ns2: dnstap -reopen; ns3: dnstap -roll
-mv ns1/dnstap.out ns1/dnstap.out.save
-mv ns2/dnstap.out ns2/dnstap.out.save
-mv ns5/dnstap.out ns5/dnstap.out.save
-
 if [ -n "$FSTRM_CAPTURE" ]; then
   ret=0
   echo_i "starting fstrm_capture"
@@ -110,392 +65,18 @@ if [ -n "$FSTRM_CAPTURE" ]; then
   status=$((status + ret))
 fi

-echo_i "reopen/roll capture streams"
-ret=0
-$RNDCCMD -s 10.53.0.1 dnstap-reopen | sed 's/^/ns1 /' | cat_i
-$RNDCCMD -s 10.53.0.2 dnstap -reopen | sed 's/^/ns2 /' | cat_i
-$RNDCCMD -s 10.53.0.3 dnstap -roll | sed 's/^/ns3 /' | cat_i
+echo_i "reopen capture stream"
 $RNDCCMD -s 10.53.0.4 dnstap -reopen | sed 's/^/ns4 /' | cat_i
-$RNDCCMD -s 10.53.0.5 dnstap -reopen | sed 's/^/ns5 /' | cat_i

-echo_i "send test traffic"
-ret=0
-$DIG $DIGOPTS @10.53.0.5 a.example >dig.out || ret=1
-
-# send an UPDATE to ns2
-$NSUPDATE <<-EOF
-server 10.53.0.2 ${PORT}
-zone example
-update add b.example 3600 in a 10.10.10.10
-send
-EOF
-
-# XXX: file output should be flushed once a second according
-# to the libfstrm source, but it doesn't seem to happen until
-# enough data has accumulated. to get all the output, we stop
-# the name servers, forcing a flush on shutdown. it would be
-# nice to find a better way to do this.
+# ns4 must not be able to reach any other server, so that only its
+# client-facing frames are written to the unix socket.
 $RNDCCMD -s 10.53.0.1 stop | sed 's/^/ns1 /' | cat_i
 $RNDCCMD -s 10.53.0.2 stop | sed 's/^/ns2 /' | cat_i
 $RNDCCMD -s 10.53.0.3 stop | sed 's/^/ns3 /' | cat_i
 $RNDCCMD -s 10.53.0.5 stop | sed 's/^/ns5 /' | cat_i

-sleep 1
-
-echo_i "checking initial message counts"
-
-udp1=$($DNSTAPREAD ns1/dnstap.out.save | grep "UDP " | wc -l)
-tcp1=$($DNSTAPREAD ns1/dnstap.out.save | grep "TCP " | wc -l)
-aq1=$($DNSTAPREAD ns1/dnstap.out.save | grep "AQ " | wc -l)
-ar1=$($DNSTAPREAD ns1/dnstap.out.save | grep "AR " | wc -l)
-cq1=$($DNSTAPREAD ns1/dnstap.out.save | grep "CQ " | wc -l)
-cr1=$($DNSTAPREAD ns1/dnstap.out.save | grep "CR " | wc -l)
-fq1=$($DNSTAPREAD ns1/dnstap.out.save | grep "FQ " | wc -l)
-fr1=$($DNSTAPREAD ns1/dnstap.out.save | grep "FR " | wc -l)
-rq1=$($DNSTAPREAD ns1/dnstap.out.save | grep "RQ " | wc -l)
-rr1=$($DNSTAPREAD ns1/dnstap.out.save | grep "RR " | wc -l)
-uq1=$($DNSTAPREAD ns1/dnstap.out.save | grep "UQ " | wc -l)
-ur1=$($DNSTAPREAD ns1/dnstap.out.save | grep "UR " | wc -l)
-
-udp2=$($DNSTAPREAD ns2/dnstap.out.save | grep "UDP " | wc -l)
-tcp2=$($DNSTAPREAD ns2/dnstap.out.save | grep "TCP " | wc -l)
-aq2=$($DNSTAPREAD ns2/dnstap.out.save | grep "AQ " | wc -l)
-ar2=$($DNSTAPREAD ns2/dnstap.out.save | grep "AR " | wc -l)
-cq2=$($DNSTAPREAD ns2/dnstap.out.save | grep "CQ " | wc -l)
-cr2=$($DNSTAPREAD ns2/dnstap.out.save | grep "CR " | wc -l)
-fq2=$($DNSTAPREAD ns2/dnstap.out.save | grep "FQ " | wc -l)
-fr2=$($DNSTAPREAD ns2/dnstap.out.save | grep "FR " | wc -l)
-rq2=$($DNSTAPREAD ns2/dnstap.out.save | grep "RQ " | wc -l)
-rr2=$($DNSTAPREAD ns2/dnstap.out.save | grep "RR " | wc -l)
-uq2=$($DNSTAPREAD ns2/dnstap.out.save | grep "UQ " | wc -l)
-ur2=$($DNSTAPREAD ns2/dnstap.out.save | grep "UR " | wc -l)
-
-mv ns3/dnstap.out.0 ns3/dnstap.out.save
-udp3=$($DNSTAPREAD ns3/dnstap.out.save | grep "UDP " | wc -l)
-tcp3=$($DNSTAPREAD ns3/dnstap.out.save | grep "TCP " | wc -l)
-aq3=$($DNSTAPREAD ns3/dnstap.out.save | grep "AQ " | wc -l)
-ar3=$($DNSTAPREAD ns3/dnstap.out.save | grep "AR " | wc -l)
-cq3=$($DNSTAPREAD ns3/dnstap.out.save | grep "CQ " | wc -l)
-cr3=$($DNSTAPREAD ns3/dnstap.out.save | grep "CR " | wc -l)
-fq3=$($DNSTAPREAD ns3/dnstap.out.save | grep "FQ " | wc -l)
-fr3=$($DNSTAPREAD ns3/dnstap.out.save | grep "FR " | wc -l)
-rq3=$($DNSTAPREAD ns3/dnstap.out.save | grep "RQ " | wc -l)
-rr3=$($DNSTAPREAD ns3/dnstap.out.save | grep "RR " | wc -l)
-uq3=$($DNSTAPREAD ns3/dnstap.out.save | grep "UQ " | wc -l)
-ur3=$($DNSTAPREAD ns3/dnstap.out.save | grep "UR " | wc -l)
-
-udp5=$($DNSTAPREAD ns5/dnstap.out.save | grep "UDP " | wc -l)
-tcp5=$($DNSTAPREAD ns5/dnstap.out.save | grep "TCP " | wc -l)
-aq5=$($DNSTAPREAD ns5/dnstap.out.save | grep "AQ " | wc -l)
-ar5=$($DNSTAPREAD ns5/dnstap.out.save | grep "AR " | wc -l)
-cq5=$($DNSTAPREAD ns5/dnstap.out.save | grep "CQ " | wc -l)
-cr5=$($DNSTAPREAD ns5/dnstap.out.save | grep "CR " | wc -l)
-fq5=$($DNSTAPREAD ns5/dnstap.out.save | grep "FQ " | wc -l)
-fr5=$($DNSTAPREAD ns5/dnstap.out.save | grep "FR " | wc -l)
-rq5=$($DNSTAPREAD ns5/dnstap.out.save | grep "RQ " | wc -l)
-rr5=$($DNSTAPREAD ns5/dnstap.out.save | grep "RR " | wc -l)
-uq5=$($DNSTAPREAD ns5/dnstap.out.save | grep "UQ " | wc -l)
-ur5=$($DNSTAPREAD ns5/dnstap.out.save | grep "UR " | wc -l)
-
-echo_i "checking UDP message counts"
-ret=0
-check_count ns1 $udp1 0
-check_count ns2 $udp2 2
-check_count ns3 $udp3 4
-check_count ns5 $udp5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking TCP message counts"
-ret=0
-check_count ns1 $tcp1 6
-check_count ns2 $tcp2 2
-check_count ns3 $tcp3 6
-check_count ns5 $tcp5 2
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking AUTH_QUERY message counts"
-ret=0
-check_count ns1 $aq1 3
-check_count ns2 $aq2 2
-check_count ns3 $aq3 1
-check_count ns5 $aq5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking AUTH_RESPONSE message counts"
-ret=0
-check_count ns1 $ar1 2
-check_count ns2 $ar2 1
-check_count ns3 $ar3 0
-check_count ns5 $ar5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking CLIENT_QUERY message counts"
-ret=0
-check_count ns1 $cq1 0
-check_count ns2 $cq2 0
-check_count ns3 $cq3 1
-check_count ns5 $cq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking CLIENT_RESPONSE message counts"
-ret=0
-check_count ns1 $cr1 1
-check_count ns2 $cr2 1
-check_count ns3 $cr3 2
-check_count ns5 $cr5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking RESOLVER_QUERY message counts"
-ret=0
-check_count ns1 $rq1 0
-check_count ns2 $rq2 0
-check_count ns3 $rq3 3
-check_count ns5 $rq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking RESOLVER_RESPONSE message counts"
-ret=0
-check_count ns1 $rr1 0
-check_count ns2 $rr2 0
-check_count ns3 $rr3 3
-check_count ns5 $rr5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking FORWARD_QUERY message counts"
-ret=0
-check_count ns1 $fq1 0
-check_count ns2 $fq2 0
-check_count ns3 $fq3 0
-check_count ns5 $fq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking FORWARD_RESPONSE message counts"
-ret=0
-check_count ns1 $fr1 0
-check_count ns2 $fr2 0
-check_count ns3 $fr3 0
-check_count ns5 $fr5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking UPDATE_QUERY message counts"
-ret=0
-check_count ns1 $uq1 0
-check_count ns2 $uq2 0
-check_count ns3 $uq3 0
-check_count ns5 $uq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking UPDATE_RESPONSE message counts"
-ret=0
-check_count ns1 $ur1 0
-check_count ns2 $ur2 0
-check_count ns3 $ur3 0
-check_count ns5 $ur5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking reopened message counts"
-
-udp1=$($DNSTAPREAD ns1/dnstap.out | grep "UDP " | wc -l)
-tcp1=$($DNSTAPREAD ns1/dnstap.out | grep "TCP " | wc -l)
-aq1=$($DNSTAPREAD ns1/dnstap.out | grep "AQ " | wc -l)
-ar1=$($DNSTAPREAD ns1/dnstap.out | grep "AR " | wc -l)
-cq1=$($DNSTAPREAD ns1/dnstap.out | grep "CQ " | wc -l)
-cr1=$($DNSTAPREAD ns1/dnstap.out | grep "CR " | wc -l)
-fq1=$($DNSTAPREAD ns1/dnstap.out | grep "FQ " | wc -l)
-fr1=$($DNSTAPREAD ns1/dnstap.out | grep "FR " | wc -l)
-rq1=$($DNSTAPREAD ns1/dnstap.out | grep "RQ " | wc -l)
-rr1=$($DNSTAPREAD ns1/dnstap.out | grep "RR " | wc -l)
-uq1=$($DNSTAPREAD ns1/dnstap.out | grep "UQ " | wc -l)
-ur1=$($DNSTAPREAD ns1/dnstap.out | grep "UR " | wc -l)
-
-udp2=$($DNSTAPREAD ns2/dnstap.out | grep "UDP " | wc -l)
-tcp2=$($DNSTAPREAD ns2/dnstap.out | grep "TCP " | wc -l)
-aq2=$($DNSTAPREAD ns2/dnstap.out | grep "AQ " | wc -l)
-ar2=$($DNSTAPREAD ns2/dnstap.out | grep "AR " | wc -l)
-cq2=$($DNSTAPREAD ns2/dnstap.out | grep "CQ " | wc -l)
-cr2=$($DNSTAPREAD ns2/dnstap.out | grep "CR " | wc -l)
-fq2=$($DNSTAPREAD ns2/dnstap.out | grep "FQ " | wc -l)
-fr2=$($DNSTAPREAD ns2/dnstap.out | grep "FR " | wc -l)
-rq2=$($DNSTAPREAD ns2/dnstap.out | grep "RQ " | wc -l)
-rr2=$($DNSTAPREAD ns2/dnstap.out | grep "RR " | wc -l)
-uq2=$($DNSTAPREAD ns2/dnstap.out | grep "UQ " | wc -l)
-ur2=$($DNSTAPREAD ns2/dnstap.out | grep "UR " | wc -l)
-
-udp3=$($DNSTAPREAD ns3/dnstap.out | grep "UDP " | wc -l)
-tcp3=$($DNSTAPREAD ns3/dnstap.out | grep "TCP " | wc -l)
-aq3=$($DNSTAPREAD ns3/dnstap.out | grep "AQ " | wc -l)
-ar3=$($DNSTAPREAD ns3/dnstap.out | grep "AR " | wc -l)
-cq3=$($DNSTAPREAD ns3/dnstap.out | grep "CQ " | wc -l)
-cr3=$($DNSTAPREAD ns3/dnstap.out | grep "CR " | wc -l)
-fq3=$($DNSTAPREAD ns3/dnstap.out | grep "FQ " | wc -l)
-fr3=$($DNSTAPREAD ns3/dnstap.out | grep "FR " | wc -l)
-rq3=$($DNSTAPREAD ns3/dnstap.out | grep "RQ " | wc -l)
-rr3=$($DNSTAPREAD ns3/dnstap.out | grep "RR " | wc -l)
-uq3=$($DNSTAPREAD ns3/dnstap.out | grep "UQ " | wc -l)
-ur3=$($DNSTAPREAD ns3/dnstap.out | grep "UR " | wc -l)
-
-udp5=$($DNSTAPREAD ns5/dnstap.out | grep "UDP " | wc -l)
-tcp5=$($DNSTAPREAD ns5/dnstap.out | grep "TCP " | wc -l)
-aq5=$($DNSTAPREAD ns5/dnstap.out | grep "AQ " | wc -l)
-ar5=$($DNSTAPREAD ns5/dnstap.out | grep "AR " | wc -l)
-cq5=$($DNSTAPREAD ns5/dnstap.out | grep "CQ " | wc -l)
-cr5=$($DNSTAPREAD ns5/dnstap.out | grep "CR " | wc -l)
-fq5=$($DNSTAPREAD ns5/dnstap.out | grep "FQ " | wc -l)
-fr5=$($DNSTAPREAD ns5/dnstap.out | grep "FR " | wc -l)
-rq5=$($DNSTAPREAD ns5/dnstap.out | grep "RQ " | wc -l)
-rr5=$($DNSTAPREAD ns5/dnstap.out | grep "RR " | wc -l)
-uq5=$($DNSTAPREAD ns5/dnstap.out | grep "UQ " | wc -l)
-ur5=$($DNSTAPREAD ns5/dnstap.out | grep "UR " | wc -l)
-
-echo_i "checking UDP message counts"
-ret=0
-check_count ns1 $udp1 0
-check_count ns2 $udp2 2
-check_count ns3 $udp3 2
-check_count ns5 $udp5 4
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking TCP message counts"
-ret=0
-check_count ns1 $tcp1 0
-check_count ns2 $tcp2 0
-check_count ns3 $tcp3 0
-check_count ns5 $tcp5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking AUTH_QUERY message counts"
-ret=0
-check_count ns1 $aq1 0
-check_count ns2 $aq2 0
-check_count ns3 $aq3 0
-check_count ns5 $aq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking AUTH_RESPONSE message counts"
-ret=0
-check_count ns1 $ar1 0
-check_count ns2 $ar2 0
-check_count ns3 $ar3 0
-check_count ns5 $ar5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking CLIENT_QUERY message counts"
-ret=0
-check_count ns1 $cq1 0
-check_count ns2 $cq2 0
-check_count ns3 $cq3 1
-check_count ns5 $cq5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking CLIENT_RESPONSE message counts"
-ret=0
-check_count ns1 $cr1 0
-check_count ns2 $cr2 0
-check_count ns3 $cr3 1
-check_count ns5 $cr5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking RESOLVER_QUERY message counts"
-ret=0
-check_count ns1 $rq1 0
-check_count ns2 $rq2 0
-check_count ns3 $rq3 0
-check_count ns5 $rq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking RESOLVER_RESPONSE message counts"
-ret=0
-check_count ns1 $rr1 0
-check_count ns2 $rr2 0
-check_count ns3 $rr3 0
-check_count ns5 $rr5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking FORWARD_QUERY message counts"
-ret=0
-check_count ns1 $fq1 0
-check_count ns2 $fq2 0
-check_count ns3 $fq3 0
-check_count ns5 $fq5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking FORWARD_RESPONSE message counts"
-ret=0
-check_count ns1 $fr1 0
-check_count ns2 $fr2 0
-check_count ns3 $fr3 0
-check_count ns5 $fr5 1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking UPDATE_QUERY message counts"
-ret=0
-check_count ns1 $uq1 0
-check_count ns2 $uq2 1
-check_count ns3 $uq3 0
-check_count ns5 $uq5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking UPDATE_RESPONSE message counts"
-ret=0
-check_count ns1 $ur1 0
-check_count ns2 $ur2 1
-check_count ns3 $ur3 0
-check_count ns5 $ur5 0
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-echo_i "checking whether destination UDP port is logged for client queries"
-ret=0
-$DNSTAPREAD ns3/dnstap.out.save | grep -Eq "CQ [0-9:.]+ -> 10.53.0.3:${PORT} UDP" || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-HAS_PYYAML=0
-$PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1
-
-if [ $HAS_PYYAML -ne 0 ]; then
-  echo_i "checking dnstap-read YAML output"
-  ret=0
-  {
-    $PYTHON ydump.py "$DNSTAPREAD" "ns3/dnstap.out.save" >ydump.out || ret=1
-  } | cat_i
-  if [ $ret != 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-fi
-
-echo_i "checking dnstap-read hex output"
-ret=0
-hex=$($DNSTAPREAD -x ns3/dnstap.out | tail -1)
-echo $hex | $WIREFORMAT >dnstap.hex
-grep 'status: NOERROR' dnstap.hex >/dev/null 2>&1 || ret=1
-grep 'ANSWER: 3, AUTHORITY: 0' dnstap.hex >/dev/null 2>&1 || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
 if [ -n "$FSTRM_CAPTURE" ]; then
+  ret=0
   $DIG $DIGOPTS @10.53.0.4 a.example >dig.out || ret=1

   # send an UPDATE to ns4
@@ -711,15 +292,6 @@ EOF
   status=$((status + ret))
 fi

-echo_i "checking large packet printing"
-ret=0
-# Expect one occurrence of "opcode: QUERY" below "reponse_message_data" and
-# another one below "response_message".
-lines=$($DNSTAPREAD -y large-answer.fstrm | grep -c "opcode: QUERY")
-[ $lines -eq 2 ] || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
 _test_dnstap_roll() (
   ip="$1"
   ns="$2"
diff --git a/bin/tests/system/dnstap/tests_dnstap.py b/bin/tests/system/dnstap/tests_dnstap.py
index f9eca104ef..0ec3754cd3 100644
--- a/bin/tests/system/dnstap/tests_dnstap.py
+++ b/bin/tests/system/dnstap/tests_dnstap.py
@@ -15,19 +15,16 @@ import re
 import dns.rcode
 import dns.rrset
 import pytest
+import yaml
+
+from dnstap.common import ARTIFACTS, dnstap_read

 import isctest
 import isctest.mark

 pytestmark = [
     isctest.mark.with_dnstap,
-    pytest.mark.extra_artifacts(
-        [
-            "dnstap.out.*",
-            "ns*/dnstap.out*",
-            "ns2/example.db",
-        ]
-    ),
+    pytest.mark.extra_artifacts(ARTIFACTS),
 ]


@@ -77,3 +74,15 @@ def test_dnstap_dispatch_socket_addresses(ns3):
     ), "{} out of {} inspected frames contain unexpected addresses:\n\n{}".format(
         len(bad_frames), inspected_frames, "\n".join(bad_frames)
     )
+
+
+def test_large_answer():
+    """
+    Check that a large DNS message is printed completely, both as the parsed
+    message and as the response_message_data text.
+    """
+    frames = list(yaml.safe_load_all(dnstap_read("large-answer.fstrm", "-y").out))
+    assert len(frames) == 1
+    message = frames[0]["message"]
+    assert message["response_message_data"]["opcode"] == "QUERY"
+    assert "opcode: QUERY" in message["response_message"]
diff --git a/bin/tests/system/dnstap/tests_dnstap_file.py b/bin/tests/system/dnstap/tests_dnstap_file.py
new file mode 100644
index 0000000000..ec39f376c9
--- /dev/null
+++ b/bin/tests/system/dnstap/tests_dnstap_file.py
@@ -0,0 +1,132 @@
+# 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.
+
+"""
+Check the dnstap file output: the frames logged for authoritative, recursive
+and forwarding traffic, the reopen/roll commands, and the dnstap-read formats.
+"""
+
+from re import compile as Re
+
+import os
+import re
+
+import dns.update
+import pytest
+import yaml
+
+from dnstap.common import ARTIFACTS, check_counts, dnstap_read
+from isctest.util import param
+
+import isctest
+import isctest.mark
+
+pytestmark = [
+    isctest.mark.with_dnstap,
+    pytest.mark.requires_zones_loaded("ns1", "ns2", "ns3", "ns4", "ns5"),
+    pytest.mark.extra_artifacts(ARTIFACTS),
+]
+
+
+def query(server, qname="a.example.", qtype="A"):
+    msg = isctest.query.create(qname, qtype)
+    return isctest.query.udp(msg, server.ip)
+
+
+@pytest.fixture(scope="module", autouse=True)
+def captures(ns1, ns2, ns3, ns5):
+    """
+    Generate the dnstap traffic and flush it to disk.
+
+    After this fixture runs, "nsX/dnstap.out.save" holds the frames logged
+    before the output files were reopened and "nsX/dnstap.out" the frames
+    logged afterwards.
+    """
+    # Both the 'a.example/A' lookup and the './NS' lookup to ns1 need to
+    # complete before reopening/rolling for the counts to be correct.
+    with ns1.watch_log_from_start() as watcher:
+        query(ns3)
+        watcher.wait_for_line("(.): reset client")
+
+    # Check three different dnstap reopen/roll methods:
+    for ns in (ns1, ns2, ns5):
+        os.rename(f"{ns}/dnstap.out", f"{ns}/dnstap.out.save")
+    ns1.rndc("dnstap-reopen")
+    ns2.rndc("dnstap -reopen")
+    ns3.rndc("dnstap -roll")
+    ns5.rndc("dnstap -reopen")
+
+    # Send test traffic.
+    query(ns5)
+    update = dns.update.Update("example")
+    update.add("b.example.", 3600, "A", "10.10.10.10")
+    ns2.nsupdate(update)
+
+    # File output should be flushed once a second according to the libfstrm
+    # source, but it doesn't seem to happen until enough data has
+    # accumulated. To get all the output, we stop the name servers, forcing
+    # a flush on shutdown. It would be nice to find a better way to do this.
+    for ns in (ns1, ns2, ns3, ns5):
+        ns.stop()
+
+    os.rename("ns3/dnstap.out.0", "ns3/dnstap.out.save")
+
+
+@pytest.mark.parametrize(
+    "ns, expected",
+    [
+        param("ns1", {"TCP": 6, "AQ": 3, "AR": 2, "CR": 1}),
+        param("ns2", {"UDP": 2, "TCP": 2, "AQ": 2, "AR": 1, "CR": 1}),
+        param("ns3", {"UDP": 4, "TCP": 6, "AQ": 1, "CQ": 1, "CR": 2, "RQ": 3, "RR": 3}),
+        param("ns5", {"TCP": 2, "AQ": 1, "CR": 1}),
+    ],
+)
+def test_initial_counts(ns, expected):
+    check_counts(f"{ns}/dnstap.out.save", expected)
+
+
+@pytest.mark.parametrize(
+    "ns, expected",
+    [
+        param("ns1", {}),
+        param("ns2", {"UDP": 2, "UQ": 1, "UR": 1}),
+        param("ns3", {"UDP": 2, "CQ": 1, "CR": 1}),
+        param("ns5", {"UDP": 4, "CQ": 1, "CR": 1, "FQ": 1, "FR": 1}),
+    ],
+)
+def test_reopened_counts(ns, expected):
+    check_counts(f"{ns}/dnstap.out", expected)
+
+
+def test_client_query_destination_port(ns3, named_port):
+    """
+    Check that the destination UDP port is logged for client queries.
+    """
+    pattern = Re(rf"CQ [0-9:.]+ -> {re.escape(ns3.ip)}:{named_port} UDP")
+    assert pattern in dnstap_read("ns3/dnstap.out.save").out
+
+
+def test_yaml_output():
+    frames = list(yaml.safe_load_all(dnstap_read("ns3/dnstap.out.save", "-y").out))
+    assert frames
+    for frame in frames:
+        assert frame["type"] == "MESSAGE"
+        assert frame["identity"] == "ns3"
+        assert frame["version"] == "xxx"
+
+
+def test_hex_output():
+    hexdump = dnstap_read("ns3/dnstap.out", "-x").out.splitlines()[-1]
+    result = isctest.run.cmd(
+        [os.environ["WIREFORMAT"]], input_text=hexdump.encode("ascii")
+    )
+    assert "status: NOERROR" in result.out
+    assert "ANSWER: 3, AUTHORITY: 0" in result.out
diff --git a/bin/tests/system/dnstap/tests_sh_dnstap.py b/bin/tests/system/dnstap/tests_sh_dnstap.py
index 4dd6ca13ea..df2936633c 100644
--- a/bin/tests/system/dnstap/tests_sh_dnstap.py
+++ b/bin/tests/system/dnstap/tests_sh_dnstap.py
@@ -18,11 +18,9 @@ pytestmark = [
     pytest.mark.extra_artifacts(
         [
             "dig.out*",
-            "dnstap.hex*",
             "dnstap.out*",
             "fstrm_capture.out.*",
             "nsupdate.out*",
-            "ydump.out*",
             "ns*/dnstap.out*",
             "ns2/example.db",
             "ns2/example.db.jnl",
diff --git a/bin/tests/system/dnstap/ydump.py b/bin/tests/system/dnstap/ydump.py
deleted file mode 100644
index f38e47044f..0000000000
--- a/bin/tests/system/dnstap/ydump.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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 sys
-
-try:
-    import yaml
-except (ModuleNotFoundError, ImportError):
-    print("No python yaml module, skipping")
-    sys.exit(1)
-
-import pprint
-import subprocess
-
-DNSTAP_READ = sys.argv[1]
-DATAFILE = sys.argv[2]
-ARGS = [DNSTAP_READ, "-y", DATAFILE]
-
-with subprocess.Popen(ARGS, stdout=subprocess.PIPE) as f:
-    for y in yaml.load_all(f.stdout, Loader=yaml.SafeLoader):
-        pprint.pprint(y)