From: Martin Basti Date: Wed, 22 Jul 2026 21:24:12 +0000 (+0000) Subject: Migrate digdelv dig failover checks to pytest X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d3e05994a5b2d9b810b795d24b2e5fe3a166b1f6;p=thirdparty%2Fbind9.git Migrate digdelv dig failover checks to pytest 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 --- diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index 64220697f00..a00494dcafa 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -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" /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" /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" /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" /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" /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 > 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 diff --git a/bin/tests/system/digdelv/tests_dig.py b/bin/tests/system/digdelv/tests_dig.py index 0ff03409c44..6c1ec1b6798 100644 --- a/bin/tests/system/digdelv/tests_dig.py +++ b/bin/tests/system/digdelv/tests_dig.py @@ -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