]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Migrate digdelv delv checks to pytest
authorMartin Basti <mbasti@isc.org>
Mon, 27 Jul 2026 14:12:10 +0000 (14:12 +0000)
committerMartin Basti <mbasti@isc.org>
Wed, 29 Jul 2026 11:56:50 +0000 (13:56 +0200)
Move the delv checks from tests.sh into a new tests_delv.py module:
+short/+sh, +split/+sp, +unknownformat, the rrcomments/multi matrix,
+nosplit field counts, +cookie/+nocookie, class handling, reverse
lookups, -q -m, -t ANY, -4/-6 handling and address family mismatches,
the malformed query name exit code, key-style and DS-style trust
anchors, REFUSED while chasing DS records, and the +yaml output
structure for positive and negative responses.  check_ttl_range()
learns to look inside delv's commented records ("; name ttl class
type ..."), where its textual type comparison also matches delv's
\-ANY pseudo-type; the duplicated "-c CH" check is folded into one
test, and the module is skipped entirely when the delv executable is
not available.

Add __init__.py to avoid a module-name collision with
dnssec/tests_delv.py: without it, pytest imports the file as plain
tests_delv, which conflicts.

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

diff --git a/bin/tests/system/digdelv/__init__.py b/bin/tests/system/digdelv/__init__.py
new file mode 100644 (file)
index 0000000..3cb1eb1
--- /dev/null
@@ -0,0 +1,10 @@
+# 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.
index e98e25258d62482822b06d1bb34ab4847bcd8a6f..06c54bfb6919611174c39f141240190357c9c8a1 100644 (file)
@@ -44,9 +44,12 @@ def parse_yaml(text):
 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."""
+    max_ttl.  A leading ";" token is ignored so that delv's commented
+    records ("; name ttl class type ...") are checked too."""
     for line in text.splitlines():
         fields = line.split()
+        if fields and fields[0] == ";":
+            fields = fields[1:]
         if len(fields) < 4:
             continue
         if fields[2] not in ("IN", "CLASS1") or fields[3] != rrtype:
index 17c92096ac6836a2ffec274dca6306ccbd8b9912..7a181e1694e8b166440ba6de2ba83c87ba9d1dbf 100644 (file)
@@ -19,369 +19,12 @@ set -e
 status=0
 n=0
 
-# Check if response in file $1 has the correct TTL range.
-# The response record must have RRtype $2 and class IN (CLASS1).
-# Maximum TTL is given by $3.  This works in most cases where TTL is
-# the second word on the line.  TTL position can be adjusted with
-# setting the position $4, but that requires updating this function.
-check_ttl_range() {
-  file=$1
-  pos=$4
-
-  case "$pos" in
-    "3")
-      {
-        awk -v rrtype="$2" -v ttl="$3" '($4 == "IN" || $4 == "CLASS1" ) && $5 == rrtype { if ($3 <= ttl) { ok=1 } } END { exit(ok?0:1) }' <$file
-        result=$?
-      } || true
-      ;;
-    *)
-      {
-        awk -v rrtype="$2" -v ttl="$3" '($3 == "IN" || $3 == "CLASS1" ) && $4 == rrtype { if ($2 <= ttl) { ok=1 } } END { exit(ok?0:1) }' <$file
-        result=$?
-      } || true
-      ;;
-  esac
-
-  [ $result -eq 0 ] || echo_i "ttl check failed"
-  return $result
-}
-
 # use delv insecure mode by default, as we're mostly not testing dnssec
 delv_with_opts() {
   "$DELV" +noroot -p "$PORT" "$@"
 }
 
-KEYID="$(cat ns2/keyid)"
-KEYDATA="$(sed <ns2/keydata -e 's/+/[+]/g')"
-NOSPLIT="$(sed <ns2/keydata -e 's/+/[+]/g' -e 's/ //g')"
-
-HAS_PYYAML=0
-$PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1
-
 if [ -x "$DELV" ]; then
-  n=$((n + 1))
-  echo_i "checking delv short form works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +short a a.example >delv.out.test$n || ret=1
-  test "$(wc -l <delv.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 delv +cookie works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +cookie a a.example -d 10 >delv.out.test$n 2>&1 || ret=1
-  grep "; COOKIE:" delv.out.test$n >/dev/null || ret=1
-  grep "; answer not validated" delv.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 delv +nocookie works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +nocookie +strace a a.example -d 10 >delv.out.test$n 2>&1 || ret=1
-  grep "; COOKIE:" delv.out.test$n >/dev/null && ret=1
-  grep "; answer not validated" delv.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 delv split width works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +split=4 -t sshfp foo.example >delv.out.test$n || ret=1
-  grep " 9ABC DEF6 7890 " <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv +unknownformat works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +unknownformat a a.example >delv.out.test$n || ret=1
-  grep "CLASS1[        ][      ]*TYPE1[        ][      ]*\\\\# 4 0A000001" <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv -4 -6 ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -4 -6 A a.example >delv.out.test$n 2>&1 && ret=1
-  grep "only one of -4 and -6 allowed" <delv.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 delv exits cleanly on malformed query name ($n)"
-  ret=0
-  longlabel="$(printf 'a%.0s' $(seq 1 64))"
-  delv_with_opts @10.53.0.3 -t a "$longlabel.example.com" >delv.out.test$n 2>&1
-  rc=$?
-  # Pre-fix: SIGABRT (exit 134) from dns_client_detach(NULL) in run_resolve cleanup.
-  [ $rc -eq 134 ] && ret=1
-  grep "label too long" delv.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 delv with IPv6 on IPv4 does not work ($n)"
-  if testsock6 fd92:7065:b8e:ffff::3 2>/dev/null; then
-    ret=0
-    # following should fail because @IPv4 overrides earlier @IPv6 above
-    # and -6 forces IPv6 so this should fail, with a message
-    # "Use of IPv4 disabled by -6"
-    delv_with_opts @fd92:7065:b8e:ffff::3 @10.53.0.3 -6 -t txt foo.example >delv.out.test$n 2>&1 && ret=1
-    # it should have no results but error output
-    grep "testing" <delv.out.test$n >/dev/null && ret=1
-    grep "Use of IPv4 disabled by -6" delv.out.test$n >/dev/null || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  else
-    echo_i "IPv6 unavailable; skipping"
-  fi
-
-  n=$((n + 1))
-  echo_i "checking delv with IPv4 on IPv6 does not work ($n)"
-  if testsock6 fd92:7065:b8e:ffff::3 2>/dev/null; then
-    ret=0
-    # following should fail because @IPv6 overrides earlier @IPv4 above
-    # and -4 forces IPv4 so this should fail, with a message
-    # "Use of IPv6 disabled by -4"
-    delv_with_opts @10.53.0.3 @fd92:7065:b8e:ffff::3 -4 -t txt foo.example >delv.out.test$n 2>&1 && ret=1
-    # it should have no results but error output
-    grep "testing" delv.out.test$n >/dev/null && ret=1
-    grep "Use of IPv6 disabled by -4" delv.out.test$n >/dev/null || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  else
-    echo_i "IPv6 unavailable; skipping"
-  fi
-
-  n=$((n + 1))
-  echo_i "checking delv with reverse lookup works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -x 127.0.0.1 >delv.out.test$n 2>&1 || ret=1
-  # doesn't matter if has answer
-  grep -i "127\\.in-addr\\.arpa\\." <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.out.test$n '\\-ANY' 10800 3 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking delv over TCP works ($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 a a.example >delv.out.test$n || ret=1
-  grep "10\\.0\\.0\\.1$" <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv +multi +norrcomments works for DNSKEY (when default is rrcomments)($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +multi +norrcomments DNSKEY example >delv.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <delv.out.test$n >/dev/null && ret=1
-  check_ttl_range delv.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 delv +multi +norrcomments works for SOA (when default is rrcomments)($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +multi +norrcomments SOA example >delv.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <delv.out.test$n >/dev/null && ret=1
-  check_ttl_range delv.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 delv +rrcomments works for DNSKEY($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +rrcomments DNSKEY example >delv.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv +short +rrcomments works for DNSKEY ($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >delv.out.test$n || ret=1
-  grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <delv.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 delv +short +rrcomments works ($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >delv.out.test$n || ret=1
-  grep -q "$KEYDATA  ; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" <delv.out.test$n || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking delv +short +nosplit works ($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +short +nosplit DNSKEY example >delv.out.test$n || ret=1
-  grep -q "$NOSPLIT" <delv.out.test$n || ret=1
-  test "$(wc -l <delv.out.test$n)" -eq 1 || ret=1
-  test "$(awk '{print NF}' <delv.out.test$n)" -eq 14 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking delv +short +nosplit +norrcomments works ($n)"
-  ret=0
-  delv_with_opts +tcp @10.53.0.3 +short +nosplit +norrcomments DNSKEY example >delv.out.test$n || ret=1
-  grep -q "$NOSPLIT\$" <delv.out.test$n || ret=1
-  test "$(wc -l <delv.out.test$n)" -eq 1 || ret=1
-  test "$(awk '{print NF}' <delv.out.test$n)" -eq 4 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking delv +sp works as an abbriviated form of split ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +sp=4 -t sshfp foo.example >delv.out.test$n || ret=1
-  grep " 9ABC DEF6 7890 " <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv +sh works as an abbriviated form of short ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 +sh a a.example >delv.out.test$n || ret=1
-  test "$(wc -l <delv.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 delv -c IN works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -c IN -t a a.example >delv.out.test$n || ret=1
-  grep "a.example." <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv -c CH is ignored, and treated like IN ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -c CH -t a a.example >delv.out.test$n || ret=1
-  grep "a.example." <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.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 delv -c CH is ignored, and treated like IN ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -c CH -t a a.example >delv.out.test$n || ret=1
-  grep "a.example." <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.out.test$n "A" 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that delv -q -m works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -q -m >delv.out.test$n 2>&1 || ret=1
-  grep '^; -m\..*[0-9]*.*IN.*ANY.*;' delv.out.test$n >/dev/null || ret=1
-  grep "^add " delv.out.test$n >/dev/null && ret=1
-  grep "^del " delv.out.test$n >/dev/null && ret=1
-  check_ttl_range delv.out.test$n '\\-ANY' 300 3 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that delv -t ANY works ($n)"
-  ret=0
-  delv_with_opts @10.53.0.3 -t ANY example >delv.out.test$n 2>&1 || ret=1
-  grep "^example." <delv.out.test$n >/dev/null || ret=1
-  check_ttl_range delv.out.test$n NS 300 || ret=1
-  check_ttl_range delv.out.test$n SOA 300 || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "check that delv loads key-style trust anchors ($n)"
-  ret=0
-  delv_with_opts -a ns3/anchor.dnskey +root=example @10.53.0.3 -t DNSKEY example >delv.out.test$n 2>&1 || ret=1
-  grep "fully validated" delv.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 delv loads DS-style trust anchors ($n)"
-  ret=0
-  delv_with_opts -a ns3/anchor.ds +root=example @10.53.0.3 -t DNSKEY example >delv.out.test$n 2>&1 || ret=1
-  grep "fully validated" delv.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 delv +yaml ANY output ($n)"
-    ret=0
-    delv_with_opts +yaml @10.53.0.3 any ns2.example >delv.out.test$n || ret=1
-    $PYTHON yamlget.py delv.out.test$n status >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "success" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n query_name >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "ns2.example" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n records 0 answer_not_validated 0 >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    count=$(echo $value | wc -w)
-    [ ${count:-0} -eq 5 ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check delv +yaml NODATA output ($n)"
-    ret=0
-    delv_with_opts +yaml @10.53.0.3 type500 ns2.example >delv.out.test$n || ret=1
-    $PYTHON yamlget.py delv.out.test$n status >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "ncache nxrrset" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n query_name >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "ns2.example" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n records 0 negative_response_answer_not_validated 0 >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    count=$(echo $value | wc -w)
-    [ ${count:-0} -eq 5 ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check delv +yaml NXDOMAIN output ($n)"
-    ret=0
-    delv_with_opts +yaml @10.53.0.3 a this-does-not-exist.ns2.example >delv.out.test$n || ret=1
-    $PYTHON yamlget.py delv.out.test$n status >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "ncache nxdomain" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n query_name >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "this-does-not-exist.ns2.example" ] || ret=1
-    $PYTHON yamlget.py delv.out.test$n records 0 negative_response_answer_not_validated 0 >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    count=$(echo $value | wc -w)
-    [ ${count:-0} -eq 5 ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that delv handles REFUSED when chasing DS records ($n)"
-  ret=0
-  delv_with_opts @10.53.0.2 +root xxx.example.tld A >delv.out.test$n 2>&1 || ret=1
-  grep ";; resolution failed: broken trust chain" delv.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 NS output from delv +ns ($n)"
   ret=0
diff --git a/bin/tests/system/digdelv/tests_delv.py b/bin/tests/system/digdelv/tests_delv.py
new file mode 100644 (file)
index 0000000..d369784
--- /dev/null
@@ -0,0 +1,274 @@
+# 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 delv tool.
+"""
+
+from re import compile as Re
+
+import os
+import re
+
+import pytest
+
+from digdelv.common import ARTIFACTS, check_ttl_range, needs_pyyaml, parse_yaml
+from isctest.util import param
+
+import isctest
+import isctest.mark
+
+pytestmark = [
+    pytest.mark.skipif(
+        not os.access(os.environ.get("DELV", ""), os.X_OK),
+        reason="delv executable not available",
+    ),
+    pytest.mark.extra_artifacts(ARTIFACTS),
+]
+
+
+@pytest.fixture(name="delv")
+def delv_fixture(named_port):
+    # use delv insecure mode by default, as we're mostly not testing dnssec
+    return isctest.run.EnvCmd("DELV", f"+noroot -p {named_port}")
+
+
+@pytest.mark.parametrize("option", ["+short", "+sh"])
+def test_short(delv, ns3, option):
+    """Check that delv +short (and its +sh abbreviation) returns a
+    single-line answer."""
+    result = delv(f"@{ns3.ip} {option} a a.example")
+    assert len(result.out.splitlines()) == 1
+
+
+def test_cookie(delv, ns3):
+    """Check that delv +cookie sends a DNS COOKIE option."""
+    result = delv(f"@{ns3.ip} +cookie a a.example -d 10")
+    assert "; COOKIE:" in result.err
+    assert "; answer not validated" in result.out
+
+
+def test_nocookie(delv, ns3):
+    """Check that delv +nocookie does not send a DNS COOKIE option."""
+    result = delv(f"@{ns3.ip} +nocookie +strace a a.example -d 10")
+    assert "; COOKIE:" not in result.err
+    assert "; answer not validated" in result.out
+
+
+@pytest.mark.parametrize("option", ["+split=4", "+sp=4"])
+def test_split_width(delv, ns3, option):
+    """Check that delv +split (and its +sp abbreviation) splits hex data
+    into fields of the requested width."""
+    result = delv(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(delv, ns3):
+    """Check that delv +unknownformat prints RFC 3597 format."""
+    result = delv(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_4_and_6_mutually_exclusive(delv, ns3):
+    """Check that delv rejects -4 combined with -6."""
+    result = delv(f"@{ns3.ip} -4 -6 A a.example", raise_on_exception=False)
+    assert result.rc != 0
+    assert "only one of -4 and -6 allowed" in result.err
+
+
+def test_malformed_query_name(delv, ns3):
+    """Check that delv exits cleanly on a malformed query name instead of
+    aborting in the dns_client_detach(NULL) cleanup path."""
+    longlabel = "a" * 64
+    result = delv(f"@{ns3.ip} -t a {longlabel}.example.com", raise_on_exception=False)
+    assert result.rc >= 0
+    assert "label too long" in result.err
+
+
+@isctest.mark.with_ipv6
+@pytest.mark.parametrize(
+    "server_args,message",
+    [
+        param(
+            "@fd92:7065:b8e:ffff::3 @{ns3} -6",
+            "Use of IPv4 disabled by -6",
+            id="ipv4-server-with-6",
+        ),
+        param(
+            "@{ns3} @fd92:7065:b8e:ffff::3 -4",
+            "Use of IPv6 disabled by -4",
+            id="ipv6-server-with-4",
+        ),
+    ],
+)
+def test_address_family_mismatch(delv, ns3, server_args, message):
+    """Check that the last @server option overrides earlier ones and that
+    the forced address family makes such a lookup fail."""
+    result = delv(
+        server_args.format(ns3=ns3.ip) + " -t txt foo.example",
+        raise_on_exception=False,
+    )
+    assert result.rc != 0
+    # it should have no results but error output
+    assert "testing" not in result.out
+    assert message in result.err
+
+
+def test_reverse_lookup(delv, ns3):
+    """Check that delv -x works."""
+    result = delv(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, r"\-ANY", 10800)
+
+
+def test_tcp(delv, ns3):
+    """Check that delv over TCP works."""
+    result = delv(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,ttl_rrtype",
+    [
+        param(
+            "+multi +norrcomments DNSKEY example",
+            False,
+            "DNSKEY",
+            id="multi-norrcomments-dnskey",
+        ),
+        param(
+            "+multi +norrcomments SOA example",
+            False,
+            "SOA",
+            id="multi-norrcomments-soa",
+        ),
+        param("+rrcomments DNSKEY example", True, "DNSKEY", id="rrcomments"),
+        param("+short +rrcomments DNSKEY example", True, None, id="short-rrcomments"),
+    ],
+)
+def test_rrcomments(delv, ns3, zsk, args, expect_rrcomment, ttl_rrtype):
+    """Check that +[no]rrcomments controls the DNSKEY comment
+    (the default is rrcomments, even with +multi)."""
+    result = delv(f"+tcp @{ns3.ip} {args}")
+    assert (zsk.rrcomment in result.out) == expect_rrcomment
+    if ttl_rrtype:
+        assert check_ttl_range(result.out, ttl_rrtype, 300)
+
+
+def test_short_rrcomments_line(delv, ns3, zsk):
+    """Check the exact delv +short +rrcomments output line."""
+    result = delv(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example")
+    assert f"{zsk.keydata}  {zsk.rrcomment}" in result.out
+
+
+def test_short_nosplit(delv, ns3, zsk):
+    """Check that delv +short +nosplit does not split the key data."""
+    result = delv(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example")
+    assert zsk.keydata.replace(" ", "") in result.out
+    assert len(result.out.splitlines()) == 1
+    assert len(result.out.split()) == 14
+
+
+def test_short_nosplit_norrcomments(delv, ns3, zsk):
+    """Check that delv +short +nosplit +norrcomments prints the bare
+    unsplit rdata."""
+    result = delv(f"+tcp @{ns3.ip} +short +nosplit +norrcomments DNSKEY example")
+    nosplit = zsk.keydata.replace(" ", "")
+    assert Re(re.escape(nosplit) + "$") in result.out
+    assert len(result.out.splitlines()) == 1
+    assert len(result.out.split()) == 4
+
+
+@pytest.mark.parametrize(
+    "qclass",
+    [
+        param("IN", id="in"),
+        param("CH", id="ch-ignored"),
+    ],
+)
+def test_class_option(delv, ns3, qclass):
+    """Check that delv -c IN works and that -c CH is ignored and treated
+    like IN."""
+    result = delv(f"@{ns3.ip} -c {qclass} -t a a.example")
+    assert "a.example." in result.out
+    assert check_ttl_range(result.out, "A", 300)
+
+
+def test_q_m(delv, ns3):
+    """Check that -q -m treats -m as a query name, not as the memory
+    debugging flag."""
+    result = delv(f"@{ns3.ip} -q -m")
+    assert Re(r"^; -m\..*\d*.*IN.*ANY.*;") in result.out
+    for stream in (result.out, result.err):
+        assert Re(r"^add ") not in stream
+        assert Re(r"^del ") not in stream
+    assert check_ttl_range(result.out, r"\-ANY", 300)
+
+
+def test_any_query(delv, ns3):
+    """Check that delv -t ANY works."""
+    result = delv(f"@{ns3.ip} -t ANY example")
+    assert Re(r"^example\.") in result.out
+    assert check_ttl_range(result.out, "NS", 300)
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+@pytest.mark.parametrize(
+    "anchor",
+    [
+        param("anchor.dnskey", id="key-style"),
+        param("anchor.ds", id="ds-style"),
+    ],
+)
+def test_trust_anchors(delv, ns3, anchor):
+    """Check that delv loads key-style and DS-style trust anchors and
+    validates with them."""
+    result = delv(f"-a ns3/{anchor} +root=example @{ns3.ip} -t DNSKEY example")
+    assert "fully validated" in result.out
+
+
+def test_refused_chasing_ds(delv, ns2):
+    """Check that delv handles REFUSED when chasing DS records."""
+    result = delv(f"@{ns2.ip} +root xxx.example.tld A")
+    assert ";; resolution failed: broken trust chain" in result.err
+
+
+@needs_pyyaml
+def test_yaml_any(delv, ns3):
+    """Check the structure of delv +yaml output."""
+    result = delv(f"+yaml @{ns3.ip} any ns2.example")
+    data = parse_yaml(result.out)
+    assert data["status"] == "success"
+    assert data["query_name"] == "ns2.example"
+    answer = data["records"][0]["answer_not_validated"][0]
+    assert len(str(answer).split()) == 5
+
+
+@needs_pyyaml
+@pytest.mark.parametrize(
+    "qtype,qname,status",
+    [
+        param("type500", "ns2.example", "ncache nxrrset", id="nodata"),
+        param("a", "this-does-not-exist.ns2.example", "ncache nxdomain", id="nxdomain"),
+    ],
+)
+def test_yaml_negative(delv, ns3, qtype, qname, status):
+    """Check the structure of delv +yaml output for negative responses."""
+    result = delv(f"+yaml @{ns3.ip} {qtype} {qname}")
+    data = parse_yaml(result.out)
+    assert data["status"] == status
+    assert data["query_name"] == qname
+    answer = data["records"][0]["negative_response_answer_not_validated"][0]
+    assert len(str(answer).split()) == 5