Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dnsdiag for openSUSE:Factory checked in at 2026-01-18 22:20:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dnsdiag (Old) and /work/SRC/openSUSE:Factory/.dnsdiag.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dnsdiag" Sun Jan 18 22:20:26 2026 rev:12 rq:1327918 version:2.9.2 Changes: -------- --- /work/SRC/openSUSE:Factory/dnsdiag/dnsdiag.changes 2025-11-18 15:37:32.349633683 +0100 +++ /work/SRC/openSUSE:Factory/.dnsdiag.new.1928/dnsdiag.changes 2026-01-18 22:22:11.747816616 +0100 @@ -1,0 +2,32 @@ +Sun Jan 18 09:07:10 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 2.9.2 + Bug Fixes + * Network Resilience: dnsping now survives transient network + errors instead of exiting fatally. When WiFi is toggled, + network cable is unplugged, or the system sleeps/resumes, + dnsping prints the error message and continues with the next + ping attempt, behaving like traditional ping. The fix also + improves error handling and makes it platform-portable. + * Answer Display: The -a flag in dnsping now displays the first + answer from the response regardless of record type, with the + type prepended for clarity + (e.g., [RDATA: CNAME res130.qams5.on.quad9.net.] or + [RDATA: A 142.250.179.174]). Previously it filtered by the + requested type only, which meant CNAME responses were silently + ignored. + * JSONL Output: The -j flag in dnseval now outputs valid JSONL + format with one JSON object per line, instead of + concatenating JSON objects back-to-back. The old format was + not parse-able by standard JSON parsers. Each line can now be + independently parsed with tools like jq. + * NSID Option Consistency: Fixed NSID EDNS option in dnsping to + use bytes instead of string for consistency with the rest of + the codebase and correct wire format. + * Exit Code Handling: All tools now return exit code 0 when + invoked with the --help flag, following standard conventions. + * Race Condition Fix: Fixed a time-of-check to time-of-use race + condition in dnsping that could cause negative sleep duration + in the interruptible sleep loop. + +------------------------------------------------------------------- Old: ---- dnsdiag-2.9.1.tar.gz New: ---- dnsdiag-2.9.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dnsdiag.spec ++++++ --- /var/tmp/diff_new_pack.PbzxXy/_old 2026-01-18 22:22:12.267837940 +0100 +++ /var/tmp/diff_new_pack.PbzxXy/_new 2026-01-18 22:22:12.267837940 +0100 @@ -1,8 +1,8 @@ # # spec file for package dnsdiag # -# Copyright (c) 2025 SUSE LLC and contributors -# Copyright (c) 2017-2025, Martin Hauke <[email protected]> +# Copyright (c) 2026 SUSE LLC and contributors +# Copyright (c) 2017-2026, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -20,7 +20,7 @@ %define pythons python3 %bcond_with test Name: dnsdiag -Version: 2.9.1 +Version: 2.9.2 Release: 0 Summary: DNS request auditing toolset License: BSD-3-Clause ++++++ dnsdiag-2.9.1.tar.gz -> dnsdiag-2.9.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/.github/workflows/build-release.yml new/dnsdiag-2.9.2/.github/workflows/build-release.yml --- old/dnsdiag-2.9.1/.github/workflows/build-release.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dnsdiag-2.9.2/.github/workflows/build-release.yml 2026-01-13 11:12:08.000000000 +0100 @@ -0,0 +1,134 @@ +name: Build Release Artifacts + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to build (leave empty to auto-detect from code)' + required: false + type: string + +permissions: + contents: read + +jobs: + build-artifacts: + name: Build ${{ matrix.platform }} binary + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Linux x86_64 + - os: ubuntu-latest + platform: linux-x86_64 + shell: bash + + # Linux ARM64 + - os: ubuntu-24.04-arm + platform: linux-arm64 + shell: bash + + # macOS Intel x86_64 + - os: macos-15-intel + platform: darwin-x86_64 + shell: bash + + # macOS Apple Silicon ARM64 + - os: macos-latest + platform: darwin-arm64 + shell: bash + + # Windows x86_64 + - os: windows-latest + platform: win-amd64 + shell: bash + + # Windows ARM64 + - os: windows-11-arm + platform: win-arm64 + shell: bash + + defaults: + run: + shell: ${{ matrix.shell }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: 'pip' + cache-dependency-path: 'requirements.txt' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip || exit 1 + pip install virtualenv || exit 1 + + - name: Install UPX (Ubuntu only) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y upx + + - name: Extract version from code + id: get_version + run: | + if [ -n "${{ inputs.version }}" ]; then + VERSION="${{ inputs.version }}" + else + VERSION=$(python3 -c "import sys; sys.path.insert(0, '.'); from dnsdiag.shared import __version__; print(__version__)") + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "Building version: $VERSION" + + - name: Build package + run: | + bash build-pkgs.sh --venv + + - name: List built packages + run: | + echo "Built packages:" + ls -lh pkg/ + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dnsdiag-${{ matrix.platform }}-bin + path: | + pkg/*.tar.gz + pkg/*.zip + retention-days: 30 + if-no-files-found: error + + summary: + name: Build Summary + needs: build-artifacts + runs-on: ubuntu-latest + steps: + - name: Generate summary + run: | + echo "## Release Build Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Binary packages have been built for the following platforms:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Linux x86_64" >> $GITHUB_STEP_SUMMARY + echo "- Linux ARM64" >> $GITHUB_STEP_SUMMARY + echo "- macOS Intel x86_64" >> $GITHUB_STEP_SUMMARY + echo "- macOS Apple Silicon ARM64" >> $GITHUB_STEP_SUMMARY + echo "- Windows x86_64" >> $GITHUB_STEP_SUMMARY + echo "- Windows ARM64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "1. Download artifacts from the workflow run (6 packages total)" >> $GITHUB_STEP_SUMMARY + echo "2. GPG sign each package (.tar.gz/.zip files)" >> $GITHUB_STEP_SUMMARY + echo "3. Build FreeBSD package manually (not available on GitHub runners)" >> $GITHUB_STEP_SUMMARY + echo "4. Create GitHub release and upload all signed packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Manual Build Required:" >> $GITHUB_STEP_SUMMARY + echo "FreeBSD packages must still be built manually (GitHub does not provide FreeBSD runners)" >> $GITHUB_STEP_SUMMARY diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/.github/workflows/packages.yml new/dnsdiag-2.9.2/.github/workflows/packages.yml --- old/dnsdiag-2.9.1/.github/workflows/packages.yml 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/.github/workflows/packages.yml 2026-01-13 11:12:08.000000000 +0100 @@ -26,17 +26,11 @@ - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 mypy pyinstaller + pip install pyinstaller if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=60 --max-line-length=127 --statistics - - name: Type check with mypy - run: | - mypy dnsping.py dnstraceroute.py dnseval.py dnsdiag/ + + # Note: Linting and type checking are performed in tests.yml workflow + # to avoid redundant checks. This workflow focuses on building packages. - name: Build package run: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/.github/workflows/tests.yml new/dnsdiag-2.9.2/.github/workflows/tests.yml --- old/dnsdiag-2.9.1/.github/workflows/tests.yml 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/.github/workflows/tests.yml 2026-01-13 11:12:08.000000000 +0100 @@ -5,9 +5,42 @@ pull_request: branches: [ master, main ] +# Cancel in-progress runs when a new commit is pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: + # Fast linting and type checking (runs once) + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: 'pip' + cache-dependency-path: 'requirements.txt' + + - name: Install linting dependencies + run: | + python -m pip install --upgrade pip || exit 1 + pip install mypy flake8 Flake8-pyproject || exit 1 + pip install -r requirements.txt || exit 1 + + - name: Run mypy type checks + run: | + mypy dnsdiag/ dnsping.py dnseval.py dnstraceroute.py + + - name: Run flake8 linting + run: | + flake8 dnsdiag/ dnsping.py dnseval.py dnstraceroute.py + # Fast unit tests (no network required) unit-tests: + needs: [lint] runs-on: ubuntu-latest strategy: fail-fast: false @@ -26,32 +59,32 @@ - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-timeout mypy flake8 Flake8-pyproject - - - name: Run mypy type checks - run: | - mypy dnsdiag/ dnsping.py dnseval.py dnstraceroute.py - - - name: Run flake8 linting - run: | - flake8 dnsdiag/ dnsping.py dnseval.py dnstraceroute.py + python -m pip install --upgrade pip || exit 1 + pip install -r requirements.txt || exit 1 + pip install pytest build || exit 1 - name: Run unit tests (no network) run: | - python -m pytest tests/test_shared.py -v --tb=short + python -m pytest tests/test_shared.py tests/test_packaging.py -v --tb=short env: PYTHONPATH: . - # Integration tests (network required) - integration-tests: + # Integration tests (network required) - Full matrix + integration-tests-full: + needs: [lint, unit-tests] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + # Full OS coverage for critical Python versions + os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest, windows-latest, windows-11-arm] + python-version: ["3.10", "3.13", "3.14"] + exclude: + # Python 3.10 and 3.13 not available on Windows ARM64 (only 3.14+) + - os: windows-11-arm + python-version: "3.10" + - os: windows-11-arm + python-version: "3.13" steps: - uses: actions/checkout@v5 @@ -66,20 +99,62 @@ - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-timeout mypy flake8 Flake8-pyproject + pip install -r requirements.txt || exit 1 + pip install pytest || exit 1 - - name: Run mypy type checks + - name: Verify critical dependencies run: | - mypy dnsdiag/ dnsping.py dnseval.py dnstraceroute.py + python -c "import dns; print(f'dnspython version: {dns.version.version}')" || exit 1 + python -c "import httpx; print(f'httpx version: {httpx.__version__}')" || exit 1 - - name: Run flake8 linting + - name: Run integration tests run: | - flake8 dnsdiag/ dnsping.py dnseval.py dnstraceroute.py + python -m pytest tests/ -v --tb=short -m "network and not privileged and not ipv6" + env: + PYTHONPATH: . + + - name: CLI smoke tests (Unix) + if: runner.os != 'Windows' + run: | + python3 dnsping.py -c 1 -s 8.8.8.8 google.com + echo "8.8.8.8" | python3 dnseval.py -c 1 --skip-warmup -f - google.com + + - name: CLI smoke tests (Windows) + if: runner.os == 'Windows' + run: | + python dnsping.py -c 1 -s 8.8.8.8 google.com + echo 8.8.8.8 | python dnseval.py -c 1 --skip-warmup -f - google.com + + # Integration tests (network required) - Reduced matrix + integration-tests-reduced: + needs: [lint, unit-tests] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Reduced OS coverage for stable Python versions + os: [ubuntu-latest, macos-latest] + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'requirements.txt' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip || exit 1 + pip install -r requirements.txt || exit 1 + pip install pytest || exit 1 - name: Run integration tests run: | - python -m pytest tests/ -v --tb=short --timeout=120 -m "network and not privileged and not ipv6" + python -m pytest tests/ -v --tb=short -m "network and not privileged and not ipv6" env: PYTHONPATH: . diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/LICENSE new/dnsdiag-2.9.2/LICENSE --- old/dnsdiag-2.9.1/LICENSE 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/LICENSE 2026-01-13 11:12:08.000000000 +0100 @@ -1,4 +1,4 @@ -Copyright (c) 2025, Babak Farrokhi +Copyright (c) 2026, Babak Farrokhi All rights reserved. Redistribution and use in source and binary forms, with or without diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/README.md new/dnsdiag-2.9.2/README.md --- old/dnsdiag-2.9.1/README.md 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/README.md 2026-01-13 11:12:08.000000000 +0100 @@ -280,6 +280,18 @@ 8.8.8.8 21.22 16.22 24.93 2.39 %0 299 QR -- -- RD RA AD -- DO NOERROR ``` +You can also save results in JSONL format for further processing. Each line in the output file is a valid JSON object containing the full measurement results for one DNS server. + +```shell +./dnseval.py -c 5 -j results.jsonl -f public-servers.txt example.com +``` + +The output can be parsed line by line with standard JSON tools like `jq`. + +```shell +cat results.jsonl | jq -r 'select(.data.r_lost_percent == 0) | .data.resolver' +``` + ### Author Babak Farrokhi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/build-pkgs.sh new/dnsdiag-2.9.2/build-pkgs.sh --- old/dnsdiag-2.9.1/build-pkgs.sh 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/build-pkgs.sh 2026-01-13 11:12:08.000000000 +0100 @@ -90,6 +90,26 @@ --hidden-import=httpx done +msg "Verifying built binaries..." +for tool in dnsping dnstraceroute dnseval; do + BINARY="${PKG_PATH}/${tool}" + [ "${PLATFORM}" = "windows" ] && BINARY="${BINARY}.exe" + + if [ ! -f "${BINARY}" ]; then + die "Binary not found: ${BINARY}" + fi + + if [ ! -x "${BINARY}" ]; then + die "Binary is not executable: ${BINARY}" + fi + + msg "Testing ${tool}..." + if ! "${BINARY}" --help > /dev/null 2>&1; then + die "Binary failed to execute: ${BINARY}" + fi +done +msg "All binaries verified successfully" + msg "Adding extra files..." for i in public-servers.txt public-v4.txt rootservers.txt; do cp ${i} "${PKG_PATH}/" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnsdiag/dns.py new/dnsdiag-2.9.2/dnsdiag/dns.py --- old/dnsdiag-2.9.1/dnsdiag/dns.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnsdiag/dns.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import datetime +import errno import socket from statistics import stdev from typing import Optional, List, Any @@ -36,7 +37,7 @@ import dns.rcode import dns.rdataclass -from dnsdiag.shared import random_string, die, err, unsupported_feature +from dnsdiag.shared import random_string, err, unsupported_feature # Transport protocols PROTO_UDP: int = 0 @@ -169,11 +170,9 @@ except dns.exception.Timeout: break except OSError as e: - # Check for fatal network errors - if e.errno == 65: # EHOSTUNREACH - die("ERROR: No route to host") - elif e.errno == 51: # ENETUNREACH - die("ERROR: Network unreachable") + # Transient network errors should be re-raised for caller to handle + if e.errno in (errno.EHOSTUNREACH, errno.ENETUNREACH): + raise elif socket_ttl: # other acceptable errors while doing traceroute break err(f"ERROR: {e.strerror}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnsdiag/shared.py new/dnsdiag-2.9.2/dnsdiag/shared.py --- old/dnsdiag-2.9.1/dnsdiag/shared.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnsdiag/shared.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,7 +28,7 @@ import string import sys -__version__ = '2.9.1' +__version__ = '2.9.2' def random_string(min_length: int = 5, max_length: int = 10) -> str: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnsdiag/whois.py new/dnsdiag-2.9.2/dnsdiag/whois.py --- old/dnsdiag-2.9.1/dnsdiag/whois.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnsdiag/whois.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnseval.py new/dnsdiag-2.9.2/dnseval.py --- old/dnsdiag-2.9.1/dnseval.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnseval.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -84,7 +84,7 @@ -Q, --quic Use QUIC as the transport protocol (DoQ) -H, --doh Use HTTPS as the transport protocol (DoH) -3, --http3 Use HTTP/3 as the transport protocol (DoH3) - -j, --json Save the results to a specified file in JSON format + -j, --json Save the results to a specified file in JSONL format (one JSON object per line) -p, --port Specify the DNS server port number (default: protocol-specific) -S, --srcip Set the query source IP address -e, --edns Enable EDNS0 in requests @@ -174,11 +174,12 @@ } if json_filename == '-': - output_lines.append(json.dumps(outer_data, indent=2)) + output_lines.append(json.dumps(outer_data)) else: with print_lock: with open(json_filename, 'a+') as outfile: - json.dump(outer_data, outfile, indent=2) + json.dump(outer_data, outfile) + outfile.write('\n') else: result = "%s %-7.2f %-7.2f %-7.2f %-10.2f %s%%%-3d%s %-7s %-26s %-12s" % ( @@ -231,6 +232,10 @@ err(str(getopt_err)) usage(1) + for o, a in opts: + if o in ("-h", "--help"): + usage() + if args and len(args) == 1: qname = args[0] if not valid_hostname(qname, allow_underscore=True): @@ -239,9 +244,7 @@ usage(1) for o, a in opts: - if o in ("-h", "--help"): - usage() - elif o in ("-c", "--count"): + if o in ("-c", "--count"): try: count = int(a) if count < 1: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnsping.py new/dnsdiag-2.9.2/dnsping.py --- old/dnsdiag-2.9.1/dnsping.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnsping.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -26,6 +26,7 @@ import datetime +import errno import getopt import ipaddress import os @@ -198,6 +199,10 @@ err(str(getopt_err)) usage(1) + for o, a in opts: + if o in ("-h", "--help"): + usage() + if args and len(args) == 1: qname = args[0] if not valid_hostname(qname, allow_underscore=True): @@ -206,10 +211,7 @@ usage(1) for o, a in opts: - if o in ("-h", "--help"): - usage() - - elif o in ("-c", "--count"): + if o in ("-c", "--count"): if a.isdigit(): count = abs(int(a)) else: @@ -396,7 +398,7 @@ if use_edns: edns_options: list[Any] = [] if want_nsid: - edns_options.append(dns.edns.GenericOption(dns.edns.NSID, '')) + edns_options.append(dns.edns.GenericOption(dns.edns.NSID, b'')) if client_subnet: try: ecs_option = dns.edns.ECSOption.from_text(client_subnet) @@ -504,10 +506,12 @@ else: sys.exit(1) except OSError as e: - if e.errno == 65: # EHOSTUNREACH - die("ERROR: No route to host") - elif e.errno == 51: # ENETUNREACH - die("ERROR: Network unreachable") + if e.errno == errno.EHOSTUNREACH: + if not quiet: + print("No route to host", flush=True) + elif e.errno == errno.ENETUNREACH: + if not quiet: + print("Network unreachable", flush=True) elif not quiet: die(f"ERROR: {e}") else: @@ -590,11 +594,10 @@ if edns_parts: extras += " [%s]" % ", ".join(edns_parts) - if show_answer: # The answer should be displayed at the rightmost - for ans in answers.answer: - if ans.rdtype == dns.rdatatype.from_text(rdatatype): # is this the answer to our question? - extras += " [RDATA: %s]" % ans[0] - break + if show_answer and answers.answer: + ans = answers.answer[0] + rtype = dns.rdatatype.to_text(ans.rdtype) + extras += " [RDATA: %s %s]" % (rtype, ans[0]) print("%-3d bytes from %s: seq=%-3d time=%-7.3f ms %s" % ( len(answers.to_wire()), server_display, i, elapsed, extras), flush=True) @@ -664,7 +667,9 @@ while time.time() - sleep_start < time_to_next: if shutdown: break - time.sleep(min(0.1, time_to_next - (time.time() - sleep_start))) + sleep_duration = time_to_next - (time.time() - sleep_start) + if sleep_duration > 0: + time.sleep(min(0.1, sleep_duration)) r_sent = i r_received = len(response_time) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/dnstraceroute.py new/dnsdiag-2.9.2/dnstraceroute.py --- old/dnsdiag-2.9.1/dnstraceroute.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/dnstraceroute.py 2026-01-13 11:12:08.000000000 +0100 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2016-2025, Babak Farrokhi +# Copyright (c) 2016-2026, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -208,6 +208,10 @@ err(str(getopt_err)) usage(1) + for o, a in opts: + if o in ("-h", "--help"): + usage() + if args and len(args) == 1: qname = args[0] if not valid_hostname(qname, allow_underscore=True): @@ -216,9 +220,7 @@ usage(1) for o, a in opts: - if o in ("-h", "--help"): - usage() - elif o in ("-c", "--count"): + if o in ("-c", "--count"): try: count = int(a) if count < 1: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/pyproject.toml new/dnsdiag-2.9.2/pyproject.toml --- old/dnsdiag-2.9.1/pyproject.toml 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/pyproject.toml 2026-01-13 11:12:08.000000000 +0100 @@ -4,7 +4,7 @@ [project] name = "dnsdiag" -version = "2.9.1" +version = "2.9.2" description = "DNS Measurement, Troubleshooting and Security Auditing Toolset (ping, traceroute)" readme = "README.md" requires-python = ">=3.10" @@ -29,7 +29,7 @@ ] dependencies = [ "aioquic>=1.2.0", - "cryptography>=42.0.5,<46", + "cryptography>=42.0.5,<47", "cymruwhois>=1.6", "dnspython>=2.8.0", "h2>=4.1.0", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/requirements.txt new/dnsdiag-2.9.2/requirements.txt --- old/dnsdiag-2.9.1/requirements.txt 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/requirements.txt 2026-01-13 11:12:08.000000000 +0100 @@ -1,5 +1,5 @@ aioquic>=1.2.0 -cryptography>=42.0.5,<46 +cryptography>=42.0.5,<47 cymruwhois>=1.6 dnspython>=2.8.0 h2>=4.1.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/tests/test_dnseval_pytest.py new/dnsdiag-2.9.2/tests/test_dnseval_pytest.py --- old/dnsdiag-2.9.1/tests/test_dnseval_pytest.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/tests/test_dnseval_pytest.py 2026-01-13 11:12:08.000000000 +0100 @@ -44,7 +44,7 @@ def run(self, args: List[str], stdin: Optional[bytes] = None) -> DNSEvalResult: """Run dnseval with given arguments""" - cmd = ['python3', self.dnseval_path] + args + cmd = [sys.executable, self.dnseval_path] + args try: stdin_text = stdin.decode('utf-8') if stdin else None result = subprocess.run( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/tests/test_dnsping_pytest.py new/dnsdiag-2.9.2/tests/test_dnsping_pytest.py --- old/dnsdiag-2.9.1/tests/test_dnsping_pytest.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/tests/test_dnsping_pytest.py 2026-01-13 11:12:08.000000000 +0100 @@ -18,10 +18,14 @@ import sys import pytest import time +import platform from typing import Tuple, Optional, List from dataclasses import dataclass # Test configuration +# Check if running on ARM64 (GitHub Actions ARM runners may have network restrictions) +IS_ARM64 = platform.machine().lower() in ('aarch64', 'arm64') + RESOLVERS = { 'cloudflare_ip': '1.1.1.1', 'cloudflare_hostname': 'one.one.one.one', @@ -51,7 +55,7 @@ def run(self, args: List[str]) -> DNSResult: """Execute dnsping with given arguments""" - cmd = ['python3', self.dnsping_path] + args + cmd = [sys.executable, self.dnsping_path] + args try: result = subprocess.run( @@ -223,7 +227,8 @@ @pytest.mark.parametrize("ip,hostname,name", [ ('1.1.1.1', 'one.one.one.one', 'cloudflare'), ('8.8.8.8', 'dns.google', 'google'), - ('9.9.9.9', 'dns.quad9.net', 'quad9'), + pytest.param('9.9.9.9', 'dns.quad9.net', 'quad9', + marks=pytest.mark.xfail(IS_ARM64, reason="ARM64 runners may have network restrictions")), ]) def test_hostname_vs_ip_basic_protocols(self, dnsping_runner, protocol, flag, ip, hostname, name): """Test hostname vs IP consistency for basic protocols""" @@ -237,8 +242,10 @@ assert hostname_result.success, f"{protocol.upper()} with hostname {hostname} failed: {hostname_result.error}" @pytest.mark.parametrize("ip,hostname,name", [ - ('8.8.8.8', 'dns.google', 'google'), - pytest.param('9.9.9.9', 'dns.quad9.net', 'quad9', marks=pytest.mark.xfail(reason="GitHub Actions may block Quad9 port 443")), + pytest.param('8.8.8.8', 'dns.google', 'google', + marks=pytest.mark.xfail(IS_ARM64, reason="ARM64 runners may have network restrictions")), + pytest.param('9.9.9.9', 'dns.quad9.net', 'quad9', + marks=pytest.mark.xfail(reason="GitHub Actions may block Quad9 port 443")), ]) def test_hostname_vs_ip_doh(self, dnsping_runner, ip, hostname, name): """Test DoH works with both IP and hostname (critical after hostname fix)""" @@ -272,7 +279,10 @@ """Test handling of invalid IP address""" result = dnsping_runner.run(['-c', '1', '-s', '192.0.2.1', 'google.com']) # RFC 5737 documentation range assert not result.success, "Query to invalid IP should fail" - assert "timed out" in result.error.lower() or result.output.count("0 responses received") > 0 + # Some runners may return "Network unreachable" instead of timeout + assert ("timed out" in result.error.lower() or + result.output.count("0 responses received") > 0 or + "network unreachable" in result.output.lower()) def test_invalid_hostname(self, dnsping_runner): """Test handling of invalid hostname""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/tests/test_dnstraceroute_pytest.py new/dnsdiag-2.9.2/tests/test_dnstraceroute_pytest.py --- old/dnsdiag-2.9.1/tests/test_dnstraceroute_pytest.py 2025-10-31 08:36:48.000000000 +0100 +++ new/dnsdiag-2.9.2/tests/test_dnstraceroute_pytest.py 2026-01-13 11:12:08.000000000 +0100 @@ -14,6 +14,7 @@ """ import subprocess +import sys import pytest import time from typing import Tuple, Optional @@ -54,7 +55,7 @@ def run(self, args: list) -> TracerouteResult: """Run dnstraceroute with given arguments""" - cmd = ['python3', self.dnstraceroute_path] + args + cmd = [sys.executable, self.dnstraceroute_path] + args try: result = subprocess.run( cmd, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dnsdiag-2.9.1/tests/test_packaging.py new/dnsdiag-2.9.2/tests/test_packaging.py --- old/dnsdiag-2.9.1/tests/test_packaging.py 1970-01-01 01:00:00.000000000 +0100 +++ new/dnsdiag-2.9.2/tests/test_packaging.py 2026-01-13 11:12:08.000000000 +0100 @@ -0,0 +1,130 @@ +""" +Test package distribution and module availability. + +This test suite ensures that the package is correctly configured for distribution, +particularly that root-level Python modules are included in the package. +""" + +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + + +class TestPackageDistribution: + """Tests for package distribution configuration.""" + + def test_root_modules_importable(self): + """Test that root-level modules can be imported directly. + + This verifies the fix for the issue where py-modules was missing + from pyproject.toml, causing ModuleNotFoundError when installed + via pip or uvx. + """ + root_modules = ['dnsping', 'dnstraceroute', 'dnseval'] + + for module_name in root_modules: + try: + __import__(module_name) + except ModuleNotFoundError as e: + pytest.fail(f"Failed to import {module_name}: {e}") + + def test_module_main_functions_exist(self): + """Test that each root module has a main() function.""" + import dnsping + import dnstraceroute + import dnseval + + modules = [ + ('dnsping', dnsping), + ('dnstraceroute', dnstraceroute), + ('dnseval', dnseval) + ] + + for name, module in modules: + assert hasattr(module, 'main'), f"{name} module missing main() function" + assert callable(module.main), f"{name}.main is not callable" + + def test_pyproject_includes_py_modules(self): + """Test that pyproject.toml includes py-modules configuration. + + This is a regression test for the packaging bug where root-level + modules were not included in the distribution. + """ + pyproject_path = Path(__file__).parent.parent / 'pyproject.toml' + + assert pyproject_path.exists(), "pyproject.toml not found" + + content = pyproject_path.read_text() + + # Check that py-modules is defined + assert 'py-modules' in content, "py-modules not found in pyproject.toml" + + # Check that all three root modules are listed + assert '"dnsping"' in content or "'dnsping'" in content + assert '"dnstraceroute"' in content or "'dnstraceroute'" in content + assert '"dnseval"' in content or "'dnseval'" in content + + def test_entry_points_configured(self): + """Test that console script entry points are properly configured.""" + pyproject_path = Path(__file__).parent.parent / 'pyproject.toml' + content = pyproject_path.read_text() + + # Check for [project.scripts] section + assert '[project.scripts]' in content, "[project.scripts] section missing" + + # Check for entry points + expected_scripts = [ + 'dnsping = "dnsping:main"', + 'dnstraceroute = "dnstraceroute:main"', + 'dnseval = "dnseval:main"' + ] + + for script in expected_scripts: + assert script in content, f"Entry point not found: {script}" + + @pytest.mark.skipif(sys.platform == "win32", reason="sdist build test not reliable on Windows") + def test_sdist_includes_root_modules(self): + """Test that building sdist includes root-level Python modules. + + This test actually builds a source distribution and verifies that + the root modules are included in the tarball. + """ + try: + import build # noqa: F401 + except ImportError: + pytest.skip("build package not available") + + project_root = Path(__file__).parent.parent + + with tempfile.TemporaryDirectory() as tmpdir: + # Build sdist + result = subprocess.run( + [sys.executable, "-m", "build", "--sdist", "--outdir", tmpdir], + cwd=project_root, + capture_output=True, + text=True + ) + + if result.returncode != 0: + pytest.skip(f"Failed to build sdist: {result.stderr}") + + # Find the created tarball + dist_dir = Path(tmpdir) + tarballs = list(dist_dir.glob("*.tar.gz")) + + assert len(tarballs) == 1, f"Expected 1 tarball, found {len(tarballs)}" + + # Extract and check contents + import tarfile + with tarfile.open(tarballs[0], 'r:gz') as tar: + names = tar.getnames() + + # Check that root modules are included + root_modules = ['dnsping.py', 'dnstraceroute.py', 'dnseval.py'] + for module in root_modules: + # Files are typically in a directory like dnsdiag-2.9.1/dnsping.py + matching = [n for n in names if n.endswith(module)] + assert matching, f"{module} not found in sdist"
