]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Migrate digdelv dig failover checks to pytest
authorMartin Basti <mbasti@isc.org>
Wed, 22 Jul 2026 21:24:12 +0000 (21:24 +0000)
committerMartin Basti <mbasti@isc.org>
Wed, 29 Jul 2026 11:54:10 +0000 (13:54 +0200)
Move the dig connection-handling checks from tests.sh into
tests_dig.py: AXFR retries upon TCP EOF driven by the ans5 response
sequences, trying the next server after unreachable/read/connection
errors and timeouts, timeout followed by SERVFAIL, the retry comment
with +short +comments, the startup banner and +nocmd on the error
path, +yaml validity when no server is reached, the IPv4-mapped IPv6
server refusal, -4/-6 handling and the source-address crash check.
The ans5 sequence cases and the UDP/TCP pairs are parametrized, and
each case now arms its own response sequence so the tests are
order-independent.

Assisted-by: Claude:claude-fable-5
bin/tests/system/digdelv/tests.sh
bin/tests/system/digdelv/tests_dig.py

index 64220697f0035602d4aba4dc91f3e3cabd100bad..a00494dcafad93e8062b8b1640baf21e16130c35 100644 (file)
@@ -74,140 +74,6 @@ HAS_PYYAML=0
 $PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1
 
 if [ -x "$DIG" ]; then
-  n=$((n + 1))
-  echo_i "checking dig preserves origin on TCP retries ($n)"
-  ret=0
-  dig_with_opts -d +tcp @10.53.0.4 +retry=1 +time=1 +domain=bar foo >dig.out.test$n 2>&1 && ret=1
-  test "$(grep -c "trying origin bar" dig.out.test$n)" -eq 2 || ret=1
-  grep "using root origin" <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 -6 -4 ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.2 -4 -6 A a.example >dig.out.test$n 2>&1 && ret=1
-  grep "only one of -4 and -6 allowed" <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 @IPv6addr -4 A a.example ($n)"
-  if testsock6 fd92:7065:b8e:ffff::2 2>/dev/null; then
-    ret=0
-    dig_with_opts +tcp @fd92:7065:b8e:ffff::2 -4 A a.example >dig.out.test$n 2>&1 && ret=1
-    grep "address family not supported" <dig.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 dig +tcp @IPv4addr -6 A a.example ($n)"
-  if testsock6 fd92:7065:b8e:ffff::2 2>/dev/null; then
-    ret=0
-    dig_with_opts +tcp @10.53.0.2 -6 A a.example >dig.out.test$n 2>&1 || ret=1
-    grep "SERVER: ::ffff:10.53.0.2#$PORT" <dig.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 dig +notcp @IPv4addr -6 A a.example ($n)"
-  if testsock6 fd92:7065:b8e:ffff::2 2>/dev/null; then
-    ret=0
-    dig_with_opts +notcp @10.53.0.2 -6 A a.example >dig.out.test$n 2>&1 || ret=1
-    grep "SERVER: ::ffff:10.53.0.2#$PORT" <dig.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))
-
-  n=$((n + 1))
-  echo_i "checking exit code for a retry upon TCP EOF (immediate -> immediate) ($n)"
-  ret=0
-  set_response_sequence no-response $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || 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 (partial AXFR -> partial AXFR) ($n)"
-  ret=0
-  set_response_sequence partial-axfr $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || 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 -> partial AXFR) ($n)"
-  ret=0
-  set_response_sequence no-response.partial-axfr $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || 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 (partial AXFR -> immediate) ($n)"
-  ret=0
-  set_response_sequence partial-axfr.no-response $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || 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 -> complete AXFR) ($n)"
-  ret=0
-  set_response_sequence no-response.complete-axfr $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 || ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || 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 (partial AXFR -> complete AXFR) ($n)"
-  ret=0
-  set_response_sequence partial-axfr.complete-axfr $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 || ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking +tries=1 won't retry twice upon TCP EOF ($n)"
-  ret=0
-  set_response_sequence no-response $n
-  dig_with_opts @10.53.0.5 example AXFR +tries=1 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  n=$((n + 1))
-  echo_i "checking +retry=0 won't retry twice upon TCP EOF ($n)"
-  ret=0
-  dig_with_opts @10.53.0.5 example AXFR +retry=0 >dig.out.test$n 2>&1 && ret=1
-  # Sanity check: ensure ans5 behaves as expected.
-  [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || 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)"
@@ -234,182 +100,7 @@ if [ -x "$DIG" ]; then
     [ "$value" = "d.example. 300 IN AAAA fd92:7065:b8e:ffff::0" ] || ret=1
     if [ $ret -ne 0 ]; then echo_i "failed"; fi
     status=$((status + ret))
-
-    # When all servers fail (here: a UDP query that times out with no
-    # response), dig must not emit the ";"-prefixed startup banner ahead of
-    # the "- type: DIG_ERROR" block, as that would make the +yaml output
-    # invalid YAML.  The query name is deliberately placed before +yaml on the
-    # command line: that is what makes dig build the banner (while +cmd is
-    # still in effect) before switching to YAML output, which is the ordering
-    # that regressed.
-    n=$((n + 1))
-    echo_i "check that dig +yaml produces valid YAML when no servers could be reached ($n)"
-    ret=0
-    dig_with_opts silent.example @10.53.0.7 +notcp +timeout=1 +tries=1 +yaml >dig.out.test$n 2>&1 && ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 type >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "DIG_ERROR" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
   fi
-
-  # +nocmd placed after the query name must suppress the startup banner
-  # ("<<>> DiG ..." lines), including on the error path.  This regressed
-  # because the banner was built as soon as the query name was seen, before
-  # +nocmd had been parsed; it is now built after the whole command line has
-  # been processed.  The default (+cmd) case is checked first so the absence
-  # check below is meaningful.
-  n=$((n + 1))
-  echo_i "check that dig prints the startup banner by default ($n)"
-  ret=0
-  dig_with_opts silent.example @10.53.0.7 +notcp +timeout=1 +tries=1 >dig.out.test$n 2>&1 && ret=1
-  grep -F "<<>> DiG" dig.out.test$n >/dev/null || ret=1
-  grep -F "no servers could be reached" 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 +nocmd after the query name suppresses the startup banner ($n)"
-  ret=0
-  dig_with_opts silent.example @10.53.0.7 +notcp +timeout=1 +tries=1 +nocmd >dig.out.test$n 2>&1 && ret=1
-  grep -F "<<>> DiG" dig.out.test$n >/dev/null && ret=1
-  grep -F "no servers could be reached" 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)"
-  ret=0
-  dig_with_opts +timeout=1 +nofail @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: SERVFAIL" 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 handles TCP timeout followed by a SERVFAIL correctly ($n)"
-  ret=0
-  dig_with_opts +timeout=1 +nofail +tcp @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  # With +short, dig must not emit the ";; " progress/error comments (here:
-  # the "Got SERVFAIL reply from ..." note printed while retrying).  +short
-  # normally turns comments off, but "+short +comments" re-enables them while
-  # short form is still in effect; the comment output then belongs to the
-  # verbose form and would corrupt the short output.  The "+comments" case
-  # (without +short) is checked first so the absence check below is meaningful.
-  n=$((n + 1))
-  echo_i "check that dig +comments emits the retry comment ($n)"
-  ret=0
-  dig_with_opts +timeout=1 +nofail +comments @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
-  grep -F ";; Got SERVFAIL reply from" 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 +comments suppresses the retry comment ($n)"
-  ret=0
-  dig_with_opts +timeout=1 +nofail +short +comments @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
-  grep -F ";; Got SERVFAIL reply from" 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 tries the next server after a UDP socket network unreachable error ($n)"
-  ret=0
-  dig_with_opts @192.0.2.128 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  test $(grep -F -e "connection refused" -e "timed out" -e "network unreachable" -e "host unreachable" dig.out.test$n | wc -l) -eq 3 || ret=1
-  grep -F "status: NOERROR" 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 tries the next server after a TCP socket network unreachable error ($n)"
-  ret=0
-  dig_with_opts +tcp @192.0.2.128 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  test $(grep -F -e "connection refused" -e "timed out" -e "network unreachable" -e "host unreachable" dig.out.test$n | wc -l) -eq 3 || ret=1
-  grep -F "status: NOERROR" 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 tries the next server after a UDP socket read error ($n)"
-  ret=0
-  dig_with_opts @10.53.0.99 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: NOERROR" 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 tries the next server after a TCP socket read error ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.7 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  # Note that we combine TCP socket "connection error" and "timeout" cases in
-  # one, because it is not trivial to simulate the timeout case in a system test
-  # in Linux without a firewall, but the code which handles error cases during
-  # the connection establishment time does not differentiate between timeout and
-  # other types of errors (unlike during reading), so this one check should be
-  # sufficient for both cases.
-  n=$((n + 1))
-  echo_i "check that dig tries the next server after a TCP socket connection error/timeout ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.99 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  test $(grep -F -e "connection refused" -e "timed out" -e "network unreachable" -e "host unreachable" dig.out.test$n | wc -l) -eq 3 || ret=1
-  grep -F "status: NOERROR" 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 tries the next server after UDP socket read timeouts ($n)"
-  ret=0
-  dig_with_opts +timeout=1 @10.53.0.7 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: NOERROR" 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 tries the next server after TCP socket read timeouts ($n)"
-  ret=0
-  dig_with_opts +timeout=1 +tcp @10.53.0.7 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  # See [GL #3248] for more information
-  n=$((n + 1))
-  echo_i "check that dig correctly refuses to use a server with a IPv4 mapped IPv6 address after failing with a regular IP address ($n)"
-  ret=0
-  dig_with_opts @10.53.0.7 @::ffff:10.53.0.7 silent.example >dig.out.test$n 2>&1 || ret=1
-  grep -F ";; Skipping mapped address" dig.out.test$n >/dev/null || ret=1
-  grep -F ";; No acceptable nameservers" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-
-  # See [GL #3244] for more information
-  n=$((n + 1))
-  echo_i "check that dig handles printing query information with +qr and +y when multiple queries are involved (including a failed query) ($n)"
-  ret=0
-  dig_with_opts +timeout=1 +qr +y @127.0.0.1 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep -F "IN A 10.0.0.1" dig.out.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)"
-  ret=0
-  dig_with_opts @localhost example -b 10.53.0.1 >dig.out.test$n 2>&1 || ret=1
-  # We only care about an assertion failure, otherwise reset 'ret' to 0, because
-  # @localhost is't really expected to have an answer for our query.
-  grep -F "core dumped" dig.out.test$n >/dev/null || ret=0
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
 else
   echo_i "$DIG is needed, so skipping these dig tests"
 fi
index 0ff03409c4401a3f180bebcf199865fa16440193..6c1ec1b6798c93dee53bc0012fb1c501b896984c 100644 (file)
@@ -25,6 +25,7 @@ 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(
@@ -771,3 +772,245 @@ def test_subnet_raw_unknown_family(dig, ns2):
     result = dig(f"+qr +tcp @{ns2.ip} +ednsopt=8:00030000 A a.example")
     assert "status: FORMERR" in result.out
     assert len(result.out.grep("CLIENT-SUBNET: 00 03 00 00")) == 1
+
+
+def test_origin_preserved_on_tcp_retries(dig, ans4):
+    """Check that dig preserves the search origin when retrying over
+    TCP."""
+    result = dig(
+        f"-d +tcp @{ans4.ip} +retry=1 +time=1 +domain=bar foo",
+        raise_on_exception=False,
+    )
+    assert result.rc != 0
+    assert len(result.err.grep("trying origin bar")) == 2
+    assert "using root origin" not in result.err
+
+
+def test_4_and_6_mutually_exclusive(dig, ns2):
+    """Check that dig rejects -4 combined with -6."""
+    result = dig(f"+tcp @{ns2.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
+
+
+@isctest.mark.with_ipv6
+def test_ipv6_server_with_ipv4_only(dig):
+    """Check that dig -4 rejects an IPv6 server address."""
+    result = dig("+tcp @fd92:7065:b8e:ffff::2 -4 A a.example", raise_on_exception=False)
+    assert result.rc != 0
+    assert "address family not supported" in result.err
+
+
+@isctest.mark.with_ipv6
+@pytest.mark.parametrize("option", ["+tcp", "+notcp"])
+def test_ipv4_server_with_ipv6_only(dig, ns2, option):
+    """Check that dig -6 does not use a mapped form of an IPv4 server
+    address."""
+    result = dig(f"{option} @{ns2.ip} -6 A a.example")
+    assert f"SERVER: ::ffff:{ns2.ip}#" not in result.out
+
+
+@pytest.fixture(name="set_response_sequence")
+def set_response_sequence_fixture(dig, ans5):
+    """Arm the sequence of AXFR responses served by ans5."""
+
+    def _set(sequence):
+        dig(f"@{ans5.ip} {sequence}.response-sequence._control TXT")
+
+    return _set
+
+
+@pytest.mark.parametrize(
+    "sequence,tries,expect_failure,eof_errors",
+    [
+        param("no-response", 2, True, 2, id="immediate-immediate"),
+        param("partial-axfr", 2, True, 2, id="partial-partial"),
+        param("no-response.partial-axfr", 2, True, 2, id="immediate-partial"),
+        param("partial-axfr.no-response", 2, True, 2, id="partial-immediate"),
+        param("no-response.complete-axfr", 2, False, 1, id="immediate-complete"),
+        param("partial-axfr.complete-axfr", 2, False, 1, id="partial-complete"),
+        param("no-response", 1, True, 1, id="tries-1-no-second-retry"),
+    ],
+)
+def test_axfr_retry_upon_tcp_eof(
+    dig, ans5, set_response_sequence, sequence, tries, expect_failure, eof_errors
+):
+    """Check the exit code and the number of retries for an AXFR retried
+    upon TCP EOF."""
+    set_response_sequence(sequence)
+    result = dig(f"@{ans5.ip} example AXFR +tries={tries}", raise_on_exception=False)
+    assert (result.rc != 0) == expect_failure
+    # Sanity check: ensure ans5 behaves as expected.
+    eof_pattern = Re("communications error.*end of file")
+    assert len(result.out.grep(eof_pattern)) == eof_errors
+
+
+def test_axfr_no_retry_with_retry_0(dig, ans5, set_response_sequence):
+    """Check that +retry=0 does not retry upon TCP EOF."""
+    set_response_sequence("no-response")
+    result = dig(f"@{ans5.ip} example AXFR +retry=0", raise_on_exception=False)
+    assert result.rc != 0
+    # Sanity check: ensure ans5 behaves as expected.
+    eof_pattern = Re("communications error.*end of file")
+    assert len(result.out.grep(eof_pattern)) == 1
+
+
+@pytest.mark.parametrize(
+    "option",
+    [
+        param("", id="udp"),
+        param("+tcp", id="tcp"),
+    ],
+)
+def test_timeout_then_servfail(dig, ans7, option):
+    """Check that dig handles a timeout followed by a SERVFAIL
+    correctly.  See GL #3020 for more information."""
+    result = dig(f"+timeout=1 +nofail {option} @{ans7.ip} silent-then-servfail.example")
+    assert "status: SERVFAIL" in result.out
+
+
+def test_comments_retry_comment(dig, ans7):
+    """Check that dig +comments emits the retry comment."""
+    result = dig(
+        f"+timeout=1 +nofail +comments @{ans7.ip} silent-then-servfail.example"
+    )
+    assert ";; Got SERVFAIL reply from" in result.out
+
+
+def test_short_comments_suppresses_retry_comment(dig, ans7):
+    """Check that dig +short +comments does not leak the ";; " comments
+    into the short-form output.  +short normally turns comments off, but
+    "+short +comments" re-enables them while short form is still in
+    effect; the comment output then belongs to the verbose form and
+    would corrupt the short output."""
+    result = dig(
+        f"+timeout=1 +nofail +short +comments @{ans7.ip} silent-then-servfail.example"
+    )
+    assert ";; Got SERVFAIL reply from" not in result.out
+
+
+ERROR_PATTERN = Re("connection refused|timed out|network unreachable|host unreachable")
+
+
+@pytest.mark.parametrize(
+    "option",
+    [
+        param("", id="udp"),
+        param("+tcp", id="tcp"),
+    ],
+)
+def test_next_server_after_network_unreachable(dig, ns3, option):
+    """Check that dig tries the next server after a socket network
+    unreachable error."""
+    result = dig(f"{option} @192.0.2.128 @{ns3.ip} a.example")
+    assert len(result.out.grep(ERROR_PATTERN)) == 3
+    assert "status: NOERROR" in result.out
+
+
+def test_next_server_after_udp_read_error(dig, ns3):
+    """Check that dig tries the next server after a UDP socket read
+    error."""
+    result = dig(f"@10.53.0.99 @{ns3.ip} a.example")
+    assert "status: NOERROR" in result.out
+
+
+def test_next_server_after_tcp_read_error(dig, ans7, ns3):
+    """Check that dig tries the next server after a TCP socket read
+    error."""
+    result = dig(f"+tcp @{ans7.ip} @{ns3.ip} close.example")
+    assert "status: NOERROR" in result.out
+
+
+def test_next_server_after_tcp_connection_error(dig, ns3):
+    """Check that dig tries the next server after a TCP socket connection
+    error/timeout.  The connection error and timeout cases are combined,
+    because it is not trivial to simulate the timeout case in a system
+    test in Linux without a firewall, but the code which handles error
+    cases during connection establishment does not differentiate between
+    timeout and other types of errors (unlike during reading), so this
+    one check should be sufficient for both cases."""
+    result = dig(f"+tcp @10.53.0.99 @{ns3.ip} a.example")
+    assert len(result.out.grep(ERROR_PATTERN)) == 3
+    assert "status: NOERROR" in result.out
+
+
+@pytest.mark.parametrize(
+    "option",
+    [
+        param("", id="udp"),
+        param("+tcp", id="tcp"),
+    ],
+)
+def test_next_server_after_read_timeout(dig, ans7, ns3, option):
+    """Check that dig tries the next server after socket read timeouts."""
+    result = dig(f"+timeout=1 {option} @{ans7.ip} @{ns3.ip} silent.example")
+    assert "status: NOERROR" in result.out
+
+
+def test_mapped_ipv6_server_refused(dig, ans7):
+    """Check that dig refuses to use a server with an IPv4-mapped IPv6
+    address after failing with the regular IP address.  See GL #3248
+    for more information."""
+    result = dig(f"@{ans7.ip} @::ffff:{ans7.ip} silent.example")
+    assert ";; Skipping mapped address" in result.out
+    assert ";; No acceptable nameservers" in result.out
+
+
+def test_qr_and_y_with_failed_query(dig, ns3):
+    """Check that dig handles printing query information with +qr and +y
+    when multiple queries are involved, including a failed one.  See
+    GL #3244 for more information."""
+    result = dig(f"+timeout=1 +qr +y @127.0.0.1 @{ns3.ip} a.example")
+    assert "IN A 10.0.0.1" in result.out
+
+
+def test_startup_banner_default(dig, ans7):
+    """Check that dig prints the startup banner by default, including on
+    the error path.  This makes the absence check with +nocmd
+    meaningful."""
+    result = dig(
+        f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1",
+        raise_on_exception=False,
+    )
+    assert result.rc != 0
+    assert "<<>> DiG" in result.out
+    assert "no servers could be reached" in result.out
+
+
+def test_nocmd_after_query_name(dig, ans7):
+    """Check that +nocmd placed after the query name suppresses the
+    startup banner, including on the error path.  This regressed because
+    the banner was built as soon as the query name was seen, before
+    +nocmd had been parsed."""
+    result = dig(
+        f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +nocmd",
+        raise_on_exception=False,
+    )
+    assert result.rc != 0
+    assert "<<>> DiG" not in result.out
+    assert "no servers could be reached" in result.out
+
+
+@needs_pyyaml
+def test_yaml_valid_when_no_server_reached(dig, ans7):
+    """Check that dig +yaml produces valid YAML when no servers could be
+    reached; the ";"-prefixed startup banner must not precede the
+    DIG_ERROR block.  The query name is deliberately placed before +yaml
+    on the command line: that is what makes dig build the banner (while
+    +cmd is still in effect) before switching to YAML output, which is
+    the ordering that regressed."""
+    result = dig(
+        f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +yaml",
+        raise_on_exception=False,
+    )
+    assert result.rc != 0
+    assert parse_yaml(result.out)[0]["type"] == "DIG_ERROR"
+
+
+def test_source_address_both_families_no_crash(dig, ns1):
+    """Check that dig with an IPv4 source address and a server with both
+    IPv4 and IPv6 addresses does not crash.  @localhost is not really
+    expected to have an answer for the query; only a crash (termination
+    by a signal) is an error.  See GL #5609 for more information."""
+    result = dig(f"@localhost example -b {ns1.ip}", raise_on_exception=False)
+    assert result.rc >= 0