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"

Reply via email to