]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Migrate digdelv dig formatting checks to pytest
authorMartin Basti <mbasti@isc.org>
Wed, 22 Jul 2026 14:08:43 +0000 (14:08 +0000)
committerMartin Basti <mbasti@isc.org>
Wed, 29 Jul 2026 11:52:25 +0000 (13:52 +0200)
Move the dig output-formatting and command-line handling checks from
tests.sh into a new tests_dig.py module: +short, +split/+sp,
+unknownformat, reverse lookups, the rrcomments/multi matrix,
+header-only, +ttlunits, +expandaaaa, +bufsize, query-time units and
+yaml timestamp precision, +nocrypto, class CHAOS, query-name error
handling and the UPDATE-opcode rejection.  Repeated checks are
parametrized and the duplicated "+short +rrcomments" check is folded
into a single test.

The shared check_ttl_range helper mirrors the original awk check: it
scans the output line by line, accepts the class tokens IN and CLASS1
(the RFC 3597 unknown-format spelling), and compares the RR type
token textually and the TTL numerically.

Assisted-by: Claude:claude-fable-5
bin/tests/system/digdelv/common.py
bin/tests/system/digdelv/conftest.py [new file with mode: 0644]
bin/tests/system/digdelv/tests.sh
bin/tests/system/digdelv/tests_dig.py [new file with mode: 0644]

index dc1bb200f8dcc1e01bff8e7ee6f5f7df04eaf191..696cd2674be1d53d38925226bbc1abe9f3fda8f7 100644 (file)
@@ -25,3 +25,22 @@ ARTIFACTS = [
     "ns2/example.db",
     "ns2/example.tld.db",
 ]
+
+
+def check_ttl_range(text, rrtype, max_ttl):
+    """Check that a record of the given RR type and class IN (or its
+    unknown-format spelling CLASS1) is present with a TTL not exceeding
+    max_ttl."""
+    for line in text.splitlines():
+        fields = line.split()
+        if len(fields) < 4:
+            continue
+        if fields[2] not in ("IN", "CLASS1") or fields[3] != rrtype:
+            continue
+        try:
+            ttl = int(fields[1])
+        except ValueError:
+            continue
+        if ttl <= max_ttl:
+            return True
+    return False
diff --git a/bin/tests/system/digdelv/conftest.py b/bin/tests/system/digdelv/conftest.py
new file mode 100644 (file)
index 0000000..530000f
--- /dev/null
@@ -0,0 +1,41 @@
+# 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.
+
+"""
+Fixtures shared by the digdelv test modules.
+"""
+
+from dataclasses import dataclass
+
+import os
+
+import pytest
+
+
+@dataclass
+class Zsk:
+    keyid: str
+    keydata: str
+    rrcomment: str
+
+
+@pytest.fixture(name="zsk")
+def zsk_fixture():
+    """Key id and rdata of the ZSK generated for the example zone."""
+    with open("ns2/keyid", encoding="utf-8") as keyid_file:
+        keyid = keyid_file.read().strip()
+    with open("ns2/keydata", encoding="utf-8") as keydata_file:
+        keydata = keydata_file.read().strip()
+    return Zsk(
+        keyid=keyid,
+        keydata=keydata,
+        rrcomment=f"; ZSK; alg = {os.environ['DEFAULT_ALGORITHM']} ; key id = {keyid}",
+    )
index 1d3c0c8514567fa7633abcaf3df575b6e72ff48e..da3cd6b106ead9c93c3d9a62bd49d1e0a284ec9d 100644 (file)
@@ -74,172 +74,6 @@ HAS_PYYAML=0
 $PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1
 
 if [ -x "$DIG" ]; then
-  n=$((n + 1))
-  echo_i "check dig handles UPDATE response ($n)"
-  ret=0
-  dig_with_opts @10.53.0.6 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1
-  grep "Opcode mismatch" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig short form works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +short a a.example >dig.out.test$n || ret=1
-  test "$(wc -l <dig.out.test$n)" -eq 1 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig split width works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +split=4 -t sshfp foo.example >dig.out.test$n || ret=1
-  grep " 9ABC DEF6 7890 " <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "SSHFP" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +unknownformat works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +unknownformat a a.example >dig.out.test$n || ret=1
-  grep "CLASS1[        ][      ]*TYPE1[        ][      ]*\\\\# 4 0A000001" <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "TYPE1" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig with reverse lookup works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 -x 127.0.0.1 >dig.out.test$n 2>&1 || ret=1
-  # doesn't matter if has answer
-  grep -i "127\\.in-addr\\.arpa\\." <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "SOA" 86400 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig -x rejects deeply nested input cleanly ($n)"
-  ret=0
-  longinput="$(printf '1.%.0s' $(seq 1 6400))1"
-  # Pre-fix: SIGSEGV (139) or ASan abort (134) from unbounded recursion in
-  # reverse_octets() on the dots. Post-fix: structured rejection via
-  # DNS_R_NAMETOOLONG -> "Invalid IP address" -> exit 1.
-  rc=0
-  dig_with_opts -x "$longinput" >dig.out.test$n 2>&1 || rc=$?
-  [ $rc -ge 128 ] && ret=1
-  grep "Invalid IP address" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig over TCP works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 a a.example >dig.out.test$n || ret=1
-  grep "10\\.0\\.0\\.1$" <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "A" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +multi +norrcomments works for DNSKEY (when default is rrcomments)($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +multi +norrcomments -t DNSKEY example >dig.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" dig.out.test$n >/dev/null && ret=1
-  check_ttl_range dig.out.test$n "DNSKEY" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +multi +norrcomments works for SOA (when default is rrcomments)($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +multi +norrcomments -t SOA example >dig.out.test$n || ret=1
-  grep "; serial" dig.out.test$n >/dev/null && ret=1
-  check_ttl_range dig.out.test$n "SOA" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +rrcomments works for DNSKEY($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +rrcomments DNSKEY example >dig.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "DNSKEY" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +short +rrcomments works for DNSKEY ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +short +nosplit works($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +short +nosplit DNSKEY example >dig.out.test$n || ret=1
-  grep "$NOSPLIT" <dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +short +rrcomments works($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1
-  grep -q "$KEYDATA  ; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID\$" <dig.out.test$n || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig multi flag is local($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +nomulti example +nomulti >dig.out.nn.$n || ret=1
-  dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +multi example +nomulti >dig.out.mn.$n || ret=1
-  dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +nomulti example +multi >dig.out.nm.$n || ret=1
-  dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +multi example +multi >dig.out.mm.$n || ret=1
-  lcnn=$(wc -l <dig.out.nn.$n)
-  lcmn=$(wc -l <dig.out.mn.$n)
-  lcnm=$(wc -l <dig.out.nm.$n)
-  lcmm=$(wc -l <dig.out.mm.$n)
-  test "$lcmm" -ge "$lcnm" || ret=1
-  test "$lcmm" -ge "$lcmn" || ret=1
-  test "$lcnm" -ge "$lcnn" || ret=1
-  test "$lcmn" -ge "$lcnn" || ret=1
-  check_ttl_range dig.out.nn.$n "DNSKEY" 300 || ret=1
-  check_ttl_range dig.out.mn.$n "DNSKEY" 300 || ret=1
-  check_ttl_range dig.out.nm.$n "DNSKEY" 300 || ret=1
-  check_ttl_range dig.out.mm.$n "DNSKEY" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +noheader-only works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +noheader-only A example >dig.out.test$n || ret=1
-  grep "Got answer:" <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "SOA" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +short +rrcomments works($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1
-  grep -q "$KEYDATA  ; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID\$" <dig.out.test$n || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig +header-only works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +header-only example >dig.out.test$n || ret=1
-  grep "^;; flags: qr rd; QUERY: 0, ANSWER: 0," <dig.out.test$n >/dev/null || ret=1
-  grep "^;; QUESTION SECTION:" <dig.out.test$n >/dev/null && ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "checking dig +coflag works ($n)"
   ret=0
@@ -281,15 +115,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "checking dig +header-only works (with class and type set) ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +header-only -c IN -t A example >dig.out.test$n || ret=1
-  grep "^;; flags: qr rd; QUERY: 0, ANSWER: 0," <dig.out.test$n >/dev/null || ret=1
-  grep "^;; QUESTION SECTION:" <dig.out.test$n >/dev/null && ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "checking dig +zflag works, and that BIND properly ignores it ($n)"
   ret=0
@@ -309,34 +134,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "checking dig +ttlunits works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.2 +ttlunits A weeks.example >dig.out.test$n || ret=1
-  grep "^weeks.example.                3w" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +ttlunits A days.example >dig.out.test$n || ret=1
-  grep "^days.example.         3d" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +ttlunits A hours.example >dig.out.test$n || ret=1
-  grep "^hours.example.                3h" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +ttlunits A minutes.example >dig.out.test$n || ret=1
-  grep "^minutes.example.      45m" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +ttlunits A seconds.example >dig.out.test$n || ret=1
-  grep "^seconds.example.      45s" <dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig respects precedence of options with +ttlunits ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.2 +ttlunits +nottlid A weeks.example >dig.out.test$n || ret=1
-  grep "^weeks.example.                IN" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +nottlid +ttlunits A weeks.example >dig.out.test$n || ret=1
-  grep "^weeks.example.                3w" <dig.out.test$n >/dev/null || ret=1
-  dig_with_opts +tcp @10.53.0.2 +nottlid +nottlunits A weeks.example >dig.out.test$n || ret=1
-  grep "^weeks.example.                1814400" <dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "checking dig preserves origin on TCP retries ($n)"
   ret=0
@@ -533,23 +330,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "checking dig +sp works as an abbreviated form of split ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +sp=4 -t sshfp foo.example >dig.out.test$n || ret=1
-  grep " 9ABC DEF6 7890 " <dig.out.test$n >/dev/null || ret=1
-  check_ttl_range dig.out.test$n "SSHFP" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking dig -c works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 -c CHAOS -t txt version.bind >dig.out.test$n || ret=1
-  grep "version.bind.          0       CH      TXT" <dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "checking dig +ednsopt with option number ($n)"
   ret=0
@@ -649,14 +429,6 @@ if [ -x "$DIG" ]; then
     status=$((status + ret))
   fi
 
-  n=$((n + 1))
-  echo_i "checking that dig warns about .local queries ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 local soa >dig.out.test$n 2>&1 || ret=1
-  grep ";; WARNING: .local is reserved for Multicast DNS" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "check that dig processes +ednsopt=key-tag and FORMERR is returned ($n)"
   ret=0
@@ -1038,28 +810,6 @@ if [ -x "$DIG" ]; then
     status=$((status + ret))
   fi
 
-  n=$((n + 1))
-  echo_i "check that dig gracefully handles bad escape in domain name ($n)"
-  ret=0
-  digstatus=0
-  dig_with_opts @10.53.0.3 '\0.' >dig.out.test$n 2>&1 || digstatus=$?
-  echo digstatus=$digstatus >>dig.out.test$n
-  test $digstatus -eq 10 || ret=1
-  grep REQUIRE dig.out.test$n >/dev/null && ret=1
-  grep "is not a legal name (bad escape)" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig -q -m works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 -q -m >dig.out.test$n 2>&1
-  pat='^;-m\..*IN.*A$'
-  grep "$pat" dig.out.test$n >/dev/null || ret=1
-  grep "Dump of all outstanding memory allocations" dig.out.test$n >/dev/null && ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   n=$((n + 1))
   echo_i "checking exit code for a retry upon TCP EOF (immediate -> immediate) ($n)"
   ret=0
@@ -1139,40 +889,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "check that dig +expandaaaa works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +expandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1
-  grep "ns2.example.*fd92:7065:0b8e:ffff:0000:0000:0000:0002" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig +noexpandaaaa works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +noexpandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1
-  grep "ns2.example.*fd92:7065:b8e:ffff::2" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig default for +[no]expandaaa (+noexpandaaaa) works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 AAAA ns2.example >dig.out.test$n 2>&1 || ret=1
-  grep "ns2.example.*fd92:7065:b8e:ffff::2" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-
-  echo_i "check that dig +short +expandaaaa works ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +short +expandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1
-  pat='^fd92:7065:0b8e:ffff:0000:0000:0000:0002$'
-  grep "$pat" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   if [ $HAS_PYYAML -ne 0 ]; then
     n=$((n + 1))
     echo_i "check dig +yaml ANY output ($n)"
@@ -1242,59 +958,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "check that dig +bufsize=0 just sets the buffer size to 0 ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 a.example +bufsize=0 +qr >dig.out.test$n 2>&1 || ret=1
-  grep "EDNS:" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig +bufsize restores default bufsize ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 a.example +bufsize=0 +bufsize +qr >dig.out.test$n 2>&1 || ret=1
-  lines=$(grep "EDNS:.* udp:" dig.out.test$n | wc -l)
-  lines1232=$(grep "EDNS:.* udp: 1232" dig.out.test$n | wc -l)
-  test $lines -eq 2 || ret=1
-  test $lines1232 -eq 2 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig without -u displays 'Query time' in millseconds ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep ';; Query time: [0-9][0-9]* msec' dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig -u displays 'Query time' in microseconds ($n)"
-  ret=0
-  dig_with_opts -u @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep ';; Query time: [0-9][0-9]* usec' dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig +yaml without -u displays timestamps in milliseconds ($n)"
-  ret=0
-  dig_with_opts +yaml @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep 'query_time: !!timestamp ....-..-..T..:..:..\....Z' dig.out.test$n >/dev/null || ret=1
-  grep 'response_time: !!timestamp ....-..-..T..:..:..\....Z' dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that dig -u +yaml displays timestamps in microseconds ($n)"
-  ret=0
-  dig_with_opts -u +yaml @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep 'query_time: !!timestamp ....-..-..T..:..:..\.......Z' dig.out.test$n >/dev/null || ret=1
-  grep 'response_time: !!timestamp ....-..-..T..:..:..\.......Z' dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   # See [GL #3020] for more information
   n=$((n + 1))
   echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)"
@@ -1444,18 +1107,6 @@ if [ -x "$DIG" ]; then
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
-  n=$((n + 1))
-  echo_i "check dig's +nocrypto flag ($n)"
-  ret=0
-  dig_with_opts +dnssec +norec +nocrypto DNSKEY . @10.53.0.1 >dig.out.dnskey.test$n || ret=1
-  grep -E "256 [0-9]+ $DEFAULT_ALGORITHM_NUMBER \\[key id = [1-9][0-9]*]" dig.out.dnskey.test$n >/dev/null || ret=1
-  grep -E "RRSIG.* \\[omitted]" dig.out.dnskey.test$n >/dev/null || ret=1
-  dig_with_opts +norec +nocrypto DS example \
-    @10.53.0.1 >dig.out.ds.test$n || ret=1
-  grep -E "DS.* [0-9]+ [12] \[omitted]" dig.out.ds.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
   # See GL#5609
   n=$((n + 1))
   echo_i "check dig with a IPv4 source address and a server with both IPv4 and IPv6 addresses doesn't crash ($n)"
diff --git a/bin/tests/system/digdelv/tests_dig.py b/bin/tests/system/digdelv/tests_dig.py
new file mode 100644 (file)
index 0000000..6b89877
--- /dev/null
@@ -0,0 +1,311 @@
+# 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.
+
+"""
+Tests for the dig tool.
+"""
+
+from re import compile as Re
+
+import os
+import re
+
+import pytest
+
+from digdelv.common import ARTIFACTS, check_ttl_range
+from isctest.util import param
+
+import isctest
+
+pytestmark = [
+    pytest.mark.skipif(
+        not os.access(os.environ.get("DIG", ""), os.X_OK),
+        reason="dig executable not available",
+    ),
+    pytest.mark.extra_artifacts(ARTIFACTS),
+]
+
+
+@pytest.fixture(name="dig")
+def dig_fixture(named_port):
+    return isctest.run.EnvCmd("DIG", f"-p {named_port}")
+
+
+def test_update_response(dig, ans6):
+    """Check that dig rejects a response with the UPDATE opcode."""
+    result = dig(
+        f"@{ans6.ip} +tries=1 +timeout=1 cname foo.bar", raise_on_exception=False
+    )
+    assert result.rc != 0
+    assert "Opcode mismatch" in result.out
+
+
+def test_short(dig, ns3):
+    """Check that dig +short returns a single-line answer."""
+    result = dig(f"@{ns3.ip} +short a a.example")
+    assert len(result.out.splitlines()) == 1
+
+
+@pytest.mark.parametrize("option", ["+split=4", "+sp=4"])
+def test_split_width(dig, ns3, option):
+    """Check that dig +split (and its +sp abbreviation) splits hex data
+    into fields of the requested width."""
+    result = dig(f"@{ns3.ip} {option} -t sshfp foo.example")
+    assert " 9ABC DEF6 7890 " in result.out
+    assert check_ttl_range(result.out, "SSHFP", 300)
+
+
+def test_unknownformat(dig, ns3):
+    """Check that dig +unknownformat prints RFC 3597 format."""
+    result = dig(f"@{ns3.ip} +unknownformat a a.example")
+    assert Re(r"CLASS1\s+TYPE1\s+\\# 4 0A000001") in result.out
+    assert check_ttl_range(result.out, "TYPE1", 300)
+
+
+def test_reverse_lookup(dig, ns3):
+    """Check that dig -x works."""
+    result = dig(f"@{ns3.ip} -x 127.0.0.1")
+    # doesn't matter if has answer
+    assert Re(r"127\.in-addr\.arpa\.", re.IGNORECASE) in result.out
+    assert check_ttl_range(result.out, "SOA", 86400)
+
+
+def test_reverse_lookup_deeply_nested(dig):
+    """Check that dig -x rejects deeply nested input cleanly instead of
+    crashing on unbounded recursion in reverse_octets()."""
+    longinput = "1." * 6400 + "1"
+    result = dig(f"-x {longinput}", raise_on_exception=False)
+    assert result.rc == 1
+    assert "Invalid IP address" in result.err
+
+
+def test_tcp(dig, ns3):
+    """Check that dig over TCP works."""
+    result = dig(f"+tcp @{ns3.ip} a a.example")
+    assert Re(r"10\.0\.0\.1$") in result.out
+    assert check_ttl_range(result.out, "A", 300)
+
+
+@pytest.mark.parametrize(
+    "args,expect_rrcomment",
+    [
+        param("+multi +norrcomments -t DNSKEY example", False, id="multi-norrcomments"),
+        param("+rrcomments DNSKEY example", True, id="rrcomments"),
+        param("+short +rrcomments DNSKEY example", True, id="short-rrcomments"),
+    ],
+)
+def test_dnskey_rrcomments(dig, ns3, zsk, args, expect_rrcomment):
+    """Check that +[no]rrcomments controls the DNSKEY comment
+    (the default is rrcomments, even with +multi)."""
+    result = dig(f"+tcp @{ns3.ip} {args}")
+    assert (zsk.rrcomment in result.out) == expect_rrcomment
+    if "+short" not in args:
+        assert check_ttl_range(result.out, "DNSKEY", 300)
+
+
+def test_soa_norrcomments(dig, ns3):
+    """Check that +multi +norrcomments suppresses the SOA field comments."""
+    result = dig(f"+tcp @{ns3.ip} +multi +norrcomments -t SOA example")
+    assert "; serial" not in result.out
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+def test_short_nosplit(dig, ns3, zsk):
+    """Check that dig +short +nosplit does not split the key data."""
+    result = dig(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example")
+    assert zsk.keydata.replace(" ", "") in result.out
+
+
+def test_short_rrcomments_line(dig, ns3, zsk):
+    """Check the exact dig +short +rrcomments output line."""
+    result = dig(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example")
+    expected = re.escape(f"{zsk.keydata}  {zsk.rrcomment}")
+    assert Re(expected + "$") in result.out
+
+
+def test_multi_flag_is_local(dig, ns3):
+    """Check that +[no]multi applies to a single lookup only."""
+    lines = {}
+    for flags in [
+        ("nomulti", "nomulti"),
+        ("multi", "nomulti"),
+        ("nomulti", "multi"),
+        ("multi", "multi"),
+    ]:
+        first, second = flags
+        result = dig(f"+tcp @{ns3.ip} -t DNSKEY example +{first} example +{second}")
+        assert check_ttl_range(result.out, "DNSKEY", 300)
+        lines[flags] = len(result.out.splitlines())
+    assert lines[("multi", "multi")] >= lines[("nomulti", "multi")]
+    assert lines[("multi", "multi")] >= lines[("multi", "nomulti")]
+    assert lines[("nomulti", "multi")] >= lines[("nomulti", "nomulti")]
+    assert lines[("multi", "nomulti")] >= lines[("nomulti", "nomulti")]
+
+
+def test_noheader_only(dig, ns3):
+    """Check that dig +noheader-only sends a full query."""
+    result = dig(f"+tcp @{ns3.ip} +noheader-only A example")
+    assert "Got answer:" in result.out
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+@pytest.mark.parametrize(
+    "class_type",
+    [
+        param("", id="default"),
+        param("-c IN -t A", id="with-class-and-type"),
+    ],
+)
+def test_header_only(dig, ns3, class_type):
+    """Check that dig +header-only sends a query without a question."""
+    result = dig(f"+tcp @{ns3.ip} +header-only {class_type} example")
+    assert Re(r"^;; flags: qr rd; QUERY: 0, ANSWER: 0,") in result.out
+    assert Re(r"^;; QUESTION SECTION:") not in result.out
+
+
+@pytest.mark.parametrize(
+    "qname,ttl",
+    [
+        param("weeks", "3w"),
+        param("days", "3d"),
+        param("hours", "3h"),
+        param("minutes", "45m"),
+        param("seconds", "45s"),
+    ],
+)
+def test_ttl_units(dig, ns2, qname, ttl):
+    """Check that dig +ttlunits prints TTLs in time units."""
+    result = dig(f"+tcp @{ns2.ip} +ttlunits A {qname}.example")
+    assert Re(rf"^{qname}\.example\.\s+{ttl}\s") in result.out
+
+
+@pytest.mark.parametrize(
+    "options,field",
+    [
+        param("+ttlunits +nottlid", "IN", id="nottlid-wins"),
+        param("+nottlid +ttlunits", "3w", id="ttlunits-wins"),
+        param("+nottlid +nottlunits", "1814400", id="plain-seconds"),
+    ],
+)
+def test_ttl_units_precedence(dig, ns2, options, field):
+    """Check that the last of the +ttlid/+ttlunits options wins."""
+    result = dig(f"+tcp @{ns2.ip} {options} A weeks.example")
+    assert Re(rf"^weeks\.example\.\s+{re.escape(field)}\s") in result.out
+
+
+def test_class_chaos(dig, ns3):
+    """Check that dig -c CHAOS works."""
+    result = dig(f"@{ns3.ip} -c CHAOS -t txt version.bind")
+    assert "version.bind.\t\t0\tCH\tTXT" in result.out
+
+
+def test_bad_escape(dig, ns3):
+    """Check that dig gracefully rejects a bad escape in the domain name."""
+    result = dig(rf"@{ns3.ip} \0.", raise_on_exception=False)
+    assert result.rc == 10
+    assert "REQUIRE" not in result.err
+    assert "is not a legal name (bad escape)" in result.err
+
+
+def test_q_m(dig, ns3):
+    """Check that -q -m treats -m as a query name, not as the memory
+    debugging flag."""
+    result = dig(f"@{ns3.ip} -q -m", raise_on_exception=False)
+    assert Re(r"^;-m\..*IN.*A$") in result.out
+    assert "Dump of all outstanding memory allocations" not in result.out
+
+
+@pytest.mark.parametrize(
+    "options,pattern",
+    [
+        param(
+            "+expandaaaa",
+            r"ns2\.example.*fd92:7065:0b8e:ffff:0000:0000:0000:0002",
+            id="expandaaaa",
+        ),
+        param(
+            "+noexpandaaaa", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="noexpandaaaa"
+        ),
+        param("", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="default"),
+        param(
+            "+short +expandaaaa",
+            r"^fd92:7065:0b8e:ffff:0000:0000:0000:0002$",
+            id="short-expandaaaa",
+        ),
+    ],
+)
+def test_expandaaaa(dig, ns3, options, pattern):
+    """Check that +[no]expandaaaa controls AAAA address formatting
+    (the default is +noexpandaaaa)."""
+    result = dig(f"@{ns3.ip} {options} AAAA ns2.example")
+    assert Re(pattern) in result.out
+
+
+def test_bufsize_zero(dig, ns3):
+    """Check that +bufsize=0 just sets the advertised buffer size to 0
+    instead of disabling EDNS."""
+    result = dig(f"@{ns3.ip} a.example +bufsize=0 +qr")
+    assert "EDNS:" in result.out
+
+
+def test_bufsize_restores_default(dig, ns3):
+    """Check that a later +bufsize restores the default buffer size."""
+    result = dig(f"@{ns3.ip} a.example +bufsize=0 +bufsize +qr")
+    assert len(result.out.grep(Re(r"EDNS:.* udp:"))) == 2
+    assert len(result.out.grep(Re(r"EDNS:.* udp: 1232"))) == 2
+
+
+@pytest.mark.parametrize(
+    "options,unit",
+    [
+        param("", "msec", id="msec"),
+        param("-u", "usec", id="usec"),
+    ],
+)
+def test_query_time_units(dig, ns3, options, unit):
+    """Check that Query time is in milliseconds, or in microseconds
+    with -u."""
+    result = dig(f"{options} @{ns3.ip} a.example")
+    assert Re(rf";; Query time: \d+ {unit}") in result.out
+
+
+@pytest.mark.parametrize(
+    "options,digits",
+    [
+        param("+yaml", 3, id="msec"),
+        param("-u +yaml", 6, id="usec"),
+    ],
+)
+def test_yaml_timestamp_precision(dig, ns3, options, digits):
+    """Check that +yaml timestamps have millisecond precision, or
+    microsecond precision with -u."""
+    result = dig(f"{options} @{ns3.ip} a.example")
+    for field in ("query_time", "response_time"):
+        pattern = (
+            rf"{field}: !!timestamp \d{{4}}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{{{digits}}}Z"
+        )
+        assert Re(pattern) in result.out
+
+
+def test_local_reserved_warning(dig, ns3):
+    """Check that dig warns about .local queries."""
+    result = dig(f"@{ns3.ip} local soa")
+    assert ";; WARNING: .local is reserved for Multicast DNS" in result.out
+
+
+def test_nocrypto(dig, ns1):
+    """Check that +nocrypto omits the key and signature data."""
+    alg_num = os.environ["DEFAULT_ALGORITHM_NUMBER"]
+    result = dig(f"+dnssec +norec +nocrypto DNSKEY . @{ns1.ip}")
+    assert Re(rf"256 \d+ {alg_num} \[key id = [1-9]\d*]") in result.out
+    assert Re(r"RRSIG.* \[omitted]") in result.out
+    result = dig(f"+norec +nocrypto DS example @{ns1.ip}")
+    assert Re(r"DS.* \d+ [12] \[omitted]") in result.out