diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 8d30e4096..188481ed5 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -30,16 +30,20 @@ permissions: jobs: actionlint: runs-on: ubuntu-latest + timeout-minutes: 15 name: actionlint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: install actionlint - # Pin a version so this job is reproducible; bump deliberately. - # The download script verifies a SHA256 of the release tarball. run: | - bash <(curl --proto '=https' --tlsv1.2 -fsSL \ - https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) \ - 1.7.12 + archive=actionlint_1.7.12_linux_amd64.tar.gz + curl --proto '=https' --tlsv1.2 -fsSLO \ + "https://github.com/rhysd/actionlint/releases/download/v1.7.12/$archive" + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $archive" \ + | sha256sum --check --strict + tar -xzf "$archive" actionlint echo "$PWD" >>"$GITHUB_PATH" - name: actionlint --version run: actionlint -version diff --git a/.github/workflows/almalinux-8-build.yml b/.github/workflows/almalinux-8-build.yml index 8eb4598c3..c3d8c4986 100644 --- a/.github/workflows/almalinux-8-build.yml +++ b/.github/workflows/almalinux-8-build.yml @@ -24,9 +24,13 @@ on: schedule: - cron: '42 8 * * *' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 45 container: image: almalinux:8 name: Test rsync on AlmaLinux 8 @@ -34,9 +38,10 @@ jobs: - name: install git # actions/checkout needs git in the container before the checkout step. run: dnf -y install git - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep # PowerTools is needed for libzstd-devel etc; xxhash and lz4 dev # headers live in EPEL on RHEL 8. The default python3 on RHEL 8 @@ -67,14 +72,14 @@ jobs: # crtimes-not-supported skip matches the other Linux jobs; # daemon-chroot-acl and proxy-response-line-too-long skip because # the default (secure) transport opens no listening socket. - run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check + run: RSYNC_TEST_PROFILES=pipe,non-asan,root,self-peer,linux,no-zstd-threads RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check - name: check (TCP daemon transport) # Second run exercising the real loopback-TCP daemon path. - run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 + run: RSYNC_TEST_PROFILES=non-asan,root,self-peer,linux,no-zstd-threads ./testsuite/runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 - name: ssl file list run: ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: almalinux-8-bin diff --git a/.github/workflows/android-static-build.yml b/.github/workflows/android-static-build.yml index 14593a6dc..4e34a00ec 100644 --- a/.github/workflows/android-static-build.yml +++ b/.github/workflows/android-static-build.yml @@ -28,6 +28,9 @@ on: - cron: '42 8 * * 1' workflow_dispatch: +permissions: + contents: read + env: # Minimum supported API level. 24 (Android 7.0) runs on every modern # phone while keeping broad reach; bump if you need newer Bionic APIs. @@ -36,6 +39,7 @@ env: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 45 name: ${{ matrix.abi }} strategy: fail-fast: false @@ -48,9 +52,10 @@ jobs: triple: armv7a-linux-androideabi qemu: qemu-arm-static steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Install build prerequisites run: sudo apt-get update && sudo apt-get install -y autoconf automake gawk qemu-user-static @@ -118,7 +123,7 @@ jobs: echo "ARTIFACT_NAME=rsync-android-${{ matrix.abi }}" >>"$GITHUB_ENV" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: ${{ env.ARTIFACT_NAME }} diff --git a/.github/workflows/asan-build.yml b/.github/workflows/asan-build.yml index 246a31f9d..308e4fbdd 100644 --- a/.github/workflows/asan-build.yml +++ b/.github/workflows/asan-build.yml @@ -23,9 +23,13 @@ on: - cron: '42 9 * * 1' workflow_dispatch: +permissions: + contents: read + jobs: asan: runs-on: ubuntu-latest + timeout-minutes: 45 name: rsync ASan+UBSan (clang) env: # rsync intentionally leaks small allocations at process exit, so leak @@ -39,9 +43,10 @@ jobs: # intentional unaligned accessors are suppressed, with no_sanitize. UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -69,8 +74,8 @@ jobs: - name: check (stdio-pipe transport) # ASan+UBSan-instrumented coverage of the transfer, daemon, sender, # receiver and metadata paths over the default stdio-pipe transport. - run: ./runtests.py --rsync-bin="$PWD/rsync" -j8 + run: ./testsuite/runtests.py --rsync-bin="$PWD/rsync" -j8 - name: check (TCP daemon transport) # --use-tcp also exercises the loopback rsyncd listener and the client's # TCP connection path. - run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j8 + run: ./testsuite/runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j8 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6e0eccaff..e4d86e925 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,14 +19,19 @@ on: - cron: '42 9 * * 1' workflow_dispatch: +permissions: + contents: read + jobs: coverage: runs-on: ubuntu-latest + timeout-minutes: 45 name: gcov coverage steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -67,7 +72,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: upload HTML reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: coverage-html diff --git a/.github/workflows/cygwin-build.yml b/.github/workflows/cygwin-build.yml index b82e18b12..b6b83f249 100644 --- a/.github/workflows/cygwin-build.yml +++ b/.github/workflows/cygwin-build.yml @@ -18,14 +18,19 @@ on: schedule: - cron: '42 8 * * *' +permissions: + contents: read + jobs: test: runs-on: windows-2022 + timeout-minutes: 60 name: Test rsync on Cygwin steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: cygwin run: | $setup = Join-Path $Env:RUNNER_TEMP 'setup-x86_64.exe' @@ -87,15 +92,15 @@ jobs: # chown/devices tests still skip (need root/mknod), as do the # RESOLVE_BENEATH symlink-race tests. Cygwin runs non-root, so the # namecvt empty-response regression can run despite its common root skip. - run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt,-daemon-namecvt-empty-response make check' + run: bash -c 'RSYNC_TEST_PROFILES=pipe,non-asan,nonroot,self-peer,cygwin RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt,-daemon-namecvt-empty-response make check' - name: check (TCP daemon transport) # Second run with daemon tests over a real loopback rsyncd; the default # 'make check' above uses the secure stdio-pipe transport. - run: bash -c './runtests.py --rsync-bin=`pwd`/rsync.exe --use-tcp -j 8' + run: bash -c 'RSYNC_TEST_PROFILES=non-asan,nonroot,self-peer,cygwin ./testsuite/runtests.py --rsync-bin=`pwd`/rsync.exe --use-tcp -j 8' - name: ssl file list run: bash -c 'PATH="/usr/local/bin:$PATH" rsync-ssl --no-motd download.samba.org::rsyncftp/ || true' - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: cygwin-bin diff --git a/.github/workflows/fleettest.yml b/.github/workflows/fleettest.yml index 40f6f8fc6..724191647 100644 --- a/.github/workflows/fleettest.yml +++ b/.github/workflows/fleettest.yml @@ -16,29 +16,42 @@ on: paths: - 'testsuite/fleettest.py' - '.github/workflows/fleettest.yml' - - 'runtests.py' + - 'testsuite/runtests.py' + - 'testsuite/harness/**' + - 'testsuite/profiles/**' + - 'testsuite/tests/**' + - 'testsuite/exitcodes.py' - 'testsuite/skiplist/**' - - 'testsuite/skiplist-spec_test.py' + - 'testsuite/tests/skiplist-spec_test.py' pull_request: types: [opened, synchronize, reopened] paths: - 'testsuite/fleettest.py' - '.github/workflows/fleettest.yml' - - 'runtests.py' + - 'testsuite/runtests.py' + - 'testsuite/harness/**' + - 'testsuite/profiles/**' + - 'testsuite/tests/**' + - 'testsuite/exitcodes.py' - 'testsuite/skiplist/**' - - 'testsuite/skiplist-spec_test.py' + - 'testsuite/tests/skiplist-spec_test.py' workflow_dispatch: schedule: - cron: '17 7 * * 1' +permissions: + contents: read + jobs: fleettest: runs-on: ubuntu-latest + timeout-minutes: 45 name: fleettest against localhost steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update diff --git a/.github/workflows/freebsd-build.yml b/.github/workflows/freebsd-build.yml index fe87913f5..32de3e78b 100644 --- a/.github/workflows/freebsd-build.yml +++ b/.github/workflows/freebsd-build.yml @@ -18,19 +18,32 @@ on: schedule: - cron: '42 8 * * 1' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 45 name: Test rsync on FreeBSD steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false + - name: prepare VM host + timeout-minutes: 10 + run: | + packages=(qemu-utils qemu-system-x86 ovmf) + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" || { + sudo apt-get update -q + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" + } - name: Test in FreeBSD VM id: test - uses: vmactions/freebsd-vm@v1 + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9 with: - usesh: true + cache-after-prepare: true prepare: | pkg install -y bash autotools m4 devel/xxhash zstd liblz4 python3 archivers/liblz4 git run: | @@ -41,10 +54,10 @@ jobs: make ./rsync --version make check - ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 + ./testsuite/runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: freebsd-bin diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index b20638a8f..2d39c2fd0 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -18,14 +18,19 @@ on: schedule: - cron: '42 8 * * *' +permissions: + contents: read + jobs: test: runs-on: macos-latest + timeout-minutes: 45 name: Test rsync on macOS steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | brew install automake openssl xxhash zstd lz4 libidn2 @@ -48,15 +53,15 @@ jobs: # chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on macOS # (rsyncfns.py drives xattrs via the `xattr` command), verified on a # real macOS host, so they're no longer in the skip set. - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/macos.txt make check + run: sudo RSYNC_TEST_PROFILES=pipe,non-asan,root,self-peer,macos RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/macos.txt make check - name: check (TCP daemon transport) # Second run with daemon tests over a real loopback rsyncd; the default # 'make check' above uses the secure stdio-pipe transport. - run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 + run: sudo RSYNC_TEST_PROFILES=non-asan,root,self-peer,macos ./testsuite/runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 - name: ssl file list run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: macos-bin diff --git a/.github/workflows/netbsd-build.yml b/.github/workflows/netbsd-build.yml index 4d2463b78..4ac4675ec 100644 --- a/.github/workflows/netbsd-build.yml +++ b/.github/workflows/netbsd-build.yml @@ -18,19 +18,32 @@ on: schedule: - cron: '42 8 * * 1' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 45 name: Test rsync on NetBSD steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false + - name: prepare VM host + timeout-minutes: 10 + run: | + packages=(qemu-utils qemu-system-x86 ovmf) + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" || { + sudo apt-get update -q + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" + } - name: Test in NetBSD VM id: test - uses: vmactions/netbsd-vm@v1 + uses: vmactions/netbsd-vm@c8a0d7ddb84619a7a8cc4e652efa7950b3fd9f92 # v1.5.2 with: - usesh: true + cache-after-prepare: true prepare: | PATH=/usr/sbin:$PATH pkg_add autoconf automake python312 ln -sf /usr/pkg/bin/python3.12 /usr/pkg/bin/python3 @@ -40,10 +53,10 @@ jobs: make ./rsync --version make check - ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 + ./testsuite/runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: netbsd-bin diff --git a/.github/workflows/openbsd-build.yml b/.github/workflows/openbsd-build.yml index 6218b9a24..0b7b177eb 100644 --- a/.github/workflows/openbsd-build.yml +++ b/.github/workflows/openbsd-build.yml @@ -18,19 +18,32 @@ on: schedule: - cron: '42 8 * * 1' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 45 name: Test rsync on OpenBSD steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false + - name: prepare VM host + timeout-minutes: 10 + run: | + packages=(qemu-utils qemu-system-x86 ovmf) + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" || { + sudo apt-get update -q + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" + } - name: Test in OpenBSD VM id: test - uses: vmactions/openbsd-vm@v1 + uses: vmactions/openbsd-vm@5f7b2c933b8846a138361d6843c52af4bef5d945 # v1.4.8 with: - usesh: true + cache-after-prepare: true prepare: | pkg_add -I bash autoconf%2.71 automake%1.16 run: | @@ -56,10 +69,10 @@ jobs: # deadlock-free and unreproducible elsewhere, even pinned to 1 CPU at # -j8); -j2 keeps the VM from over-subscribing. The pipe `make check` # above stays at the default parallelism. - ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 2 + ./testsuite/runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 2 ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: openbsd-bin diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index 6aafaa12e..021cf5ef0 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -17,6 +17,9 @@ on: - '!.github/workflows/scan-build.yml' workflow_dispatch: +permissions: + contents: read + jobs: # GATING run: pinned clang-18 on a pinned runner so the checker set -- and # thus the expected zero -- is deterministic. The tree is kept clean for @@ -25,11 +28,13 @@ jobs: # and the runner (ubuntu-24.04, whose apt repos carry those packages). gate-clang18: runs-on: ubuntu-24.04 + timeout-minutes: 45 name: scan-build gate (clang-18, pinned) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -51,7 +56,7 @@ jobs: exit $status - name: upload report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: scan-build-report-clang18 path: scan-report @@ -66,12 +71,14 @@ jobs: # broken run from affecting the workflow's required status. informational-latest: runs-on: ubuntu-latest + timeout-minutes: 45 name: scan-build (latest clang, informational) continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -85,7 +92,7 @@ jobs: grep -E 'scan-build: .* bugs? found|scan-build: No bugs found' scan-build.out >>"$GITHUB_STEP_SUMMARY" || true - name: upload report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: scan-build-report-latest path: scan-report diff --git a/.github/workflows/solaris-build.yml b/.github/workflows/solaris-build.yml index 1b328e4a1..a76736a82 100644 --- a/.github/workflows/solaris-build.yml +++ b/.github/workflows/solaris-build.yml @@ -18,19 +18,32 @@ on: schedule: - cron: '42 8 * * 1' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 45 name: Test rsync on Solaris steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false + - name: prepare VM host + timeout-minutes: 10 + run: | + packages=(qemu-utils qemu-system-x86 ovmf) + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" || { + sudo apt-get update -q + sudo apt-get install -y -q -o Acquire::Retries=3 --no-install-recommends "${packages[@]}" + } - name: Test in Solaris VM id: test - uses: vmactions/solaris-vm@v1 + uses: vmactions/solaris-vm@87d3c436511cef802cd1637f86f73b1a97715c8c # v1.4.1 with: - usesh: true + cache-after-prepare: true prepare: | pkg install bash automake gnu-m4 pkg://solaris/runtime/python-35 autoconf gcc git run: | @@ -39,10 +52,10 @@ jobs: make ./rsync --version make check - ./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 + ./testsuite/runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8 ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 name: solaris-bin diff --git a/.github/workflows/ubuntu-22.04-build.yml b/.github/workflows/ubuntu-22.04-build.yml deleted file mode 100644 index ce05cf1a2..000000000 --- a/.github/workflows/ubuntu-22.04-build.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Test rsync on Ubuntu 22.04 - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -# Older-LTS coverage to help with backporting security fixes. ubuntu-22.04 -# is currently the oldest GitHub Actions runner image (20.04 was retired -# in April 2025). - -on: - push: - branches: [ master ] - paths-ignore: - - '.github/workflows/*.yml' - - '!.github/workflows/ubuntu-22.04-build.yml' - pull_request: - types: [opened, synchronize, reopened] - paths-ignore: - - '.github/workflows/*.yml' - - '!.github/workflows/ubuntu-22.04-build.yml' - schedule: - - cron: '42 8 * * *' - -jobs: - test: - runs-on: ubuntu-22.04 - name: Test rsync on Ubuntu 22.04 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: prep - run: | - sudo apt-get update - sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl - echo "/usr/local/bin" >>"$GITHUB_PATH" - - name: configure - run: ./configure --with-rrsync - - name: make - run: make - - name: install - run: sudo make install - - name: info - run: rsync --version - - name: check - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check - - name: check30 - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto30.txt make check30 - - name: check29 - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt make check29 - - name: check (TCP daemon transport) - # Second run with daemon tests over a real loopback rsyncd; the default - # 'make check' above uses the secure stdio-pipe transport. - run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 - - name: ssl file list - run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - - name: save artifact - uses: actions/upload-artifact@v4 - with: - retention-days: 45 - name: ubuntu-22.04-bin - path: | - rsync - rsync-ssl - rsync.1 - rsync-ssl.1 - rsyncd.conf.5 - rrsync.1 - rrsync diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml index 3adab91e6..eee4a5b82 100644 --- a/.github/workflows/ubuntu-build.yml +++ b/.github/workflows/ubuntu-build.yml @@ -11,21 +11,42 @@ on: - '.github/workflows/*.yml' - '!.github/workflows/ubuntu-build.yml' pull_request: - types: [opened, synchronize, reopened] + branches: [ master ] paths-ignore: - '.github/workflows/*.yml' - '!.github/workflows/ubuntu-build.yml' schedule: - - cron: '42 8 * * *' + - cron: '42 8 * * 1' + +permissions: + contents: read jobs: test: - runs-on: ubuntu-latest - name: Test rsync on Ubuntu + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + name: Test rsync on Ubuntu latest + artifact: ubuntu-bin + nonroot: true + install_smoke: true + profiles: non-asan,root,self-peer,linux + - runner: ubuntu-22.04 + name: Test rsync on Ubuntu 22.04 + artifact: ubuntu-22.04-bin + nonroot: false + install_smoke: false + profiles: non-asan,root,self-peer,linux,no-zstd-threads + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + name: ${{ matrix.name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -35,23 +56,53 @@ jobs: run: ./configure --with-rrsync - name: make run: make + - name: install/uninstall DESTDIR smoke test + if: matrix.install_smoke + run: | + set -e + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + make install-all DESTDIR="$tmp" + + for path in \ + /usr/local/bin/rsync \ + /usr/local/bin/rsync-ssl \ + /usr/local/bin/rrsync \ + /usr/local/share/man/man1/rsync.1 \ + /usr/local/share/man/man1/rsync-ssl.1 \ + /usr/local/share/man/man1/rrsync.1 \ + /usr/local/share/man/man5/rsyncd.conf.5 \ + /etc/stunnel/rsyncd.conf + do + test -e "$tmp$path" + done + + make uninstall-all DESTDIR="$tmp" + + leftover="$(find "$tmp" -type f -print)" + if [ -n "$leftover" ]; then + printf '%s\n' "$leftover" + exit 1 + fi - name: install run: sudo make install - name: info run: rsync --version - name: check - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check + run: sudo RSYNC_TEST_PROFILES=pipe,${{ matrix.profiles }} RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check - name: check30 - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto30.txt make check30 + run: sudo RSYNC_TEST_PROFILES=pipe,${{ matrix.profiles }},protocol-30 RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto30.txt make check30 - name: check29 - run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt make check29 + run: sudo RSYNC_TEST_PROFILES=pipe,${{ matrix.profiles }},protocol-29 RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt make check29 - name: check (TCP daemon transport) # Second run with daemon tests over a real loopback rsyncd. The default # 'make check' above uses the secure stdio-pipe transport (no listening # sockets); this run exercises the real TCP accept/auth path. Skip-set # is env-dependent here (chroot-acl), so leave RSYNC_EXPECT_SKIPPED unset. - run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 + run: sudo RSYNC_TEST_PROFILES=${{ matrix.profiles }} ./testsuite/runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8 - name: check (non-root, targeted) + if: matrix.nonroot # Every run above is root (sudo), so privilege-sensitive tests never hit # their non-root path. Run those here as the unprivileged 'runner' user # (NO sudo). Explicit test names make runtests.py full_run False, so @@ -65,15 +116,15 @@ jobs: # harness's nonroot_tests). run: | sudo rm -rf testtmp # prior root steps left it root-owned - ./runtests.py --rsync-bin="$PWD/rsync" \ + ./testsuite/runtests.py --rsync-bin="$PWD/rsync" \ daemon-namecvt-empty-response ownership-depth daemon - name: ssl file list run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true - name: save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 45 - name: ubuntu-bin + name: ${{ matrix.artifact }} path: | rsync rsync-ssl diff --git a/.github/workflows/ubuntu-version-mix.yml b/.github/workflows/ubuntu-version-mix.yml index d16dd09dd..b3fc01110 100644 --- a/.github/workflows/ubuntu-version-mix.yml +++ b/.github/workflows/ubuntu-version-mix.yml @@ -4,26 +4,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true -# Runs the CURRENT test suite with two different rsync binaries: the freshly -# built ./rsync as the client/driver, and a committed OLD static binary -# (old_versions/rsync_) as the daemon / remote-shell peer. This exercises -# real version mixing over the wire -- more convincing than --protocol forcing, -# which only makes the current binary speak an old protocol. -# -# Direction is fixed: the current binary always drives (only it understands the -# new test scripts); the old binary is only ever the server/daemon side. The -# reverse (old client driving new scripts) is not possible -- but one test, -# reverse-daemon-delta, swaps the roles internally (current build as the daemon, -# old binary as the client) to cover the backward-compat direction: a current -# daemon serving the installed base of old clients. -# -# The per-version manifest testsuite/expect/rsync_.expect lists exactly -# which tests run and each one's expected outcome (pass/skip/fail/xfail), so an -# old peer's known feature gaps are recorded rather than treated as breakage. -# -# All peers run in a SINGLE job (looped, not a matrix) so the PR shows one check -# line rather than one per version. Each peer/transport is a foldable ::group:: -# in the log, and a failure annotates which peer/transport broke. +# Run the current suite against every retained peer over pipe and TCP transports. +# Test metadata selects applicable coverage while peer profiles record deviations. on: push: @@ -39,14 +21,19 @@ on: schedule: - cron: '52 8 * * 1' +permissions: + contents: read + jobs: version-mix: runs-on: ubuntu-latest + timeout-minutes: 45 name: rsync version-mix steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -55,24 +42,27 @@ jobs: - name: configure run: ./configure --with-rrsync - name: make - # check-progs builds rsync AND the test helper programs (tls, trimslash, - # t_unsafe, ...) that runtests.py requires; plain `make` does not. + # Build rsync and the helpers used by direct runner invocations. run: make check-progs - name: info run: ./rsync --version | head -1 + - name: validate profiles + run: ./testsuite/runtests.py profile --rsync-bin="$PWD/rsync" - name: version mixing (all peers, pipe + TCP transports) run: | + mkdir -p test-results/version-mix rc=0 for peer in old_versions/rsync_*; do chmod +x "$peer" name=$(basename "$peer") - expect="testsuite/expect/$name.expect" + profile="peer-${name#rsync_}" for transport in pipe tcp; do tcp=() [ "$transport" = tcp ] && tcp=(--use-tcp) echo "::group::$name ($transport): $("$peer" --version | head -1)" - if ! ./runtests.py --rsync-bin="$PWD/rsync" --rsync-bin2="$PWD/$peer" \ - --expect-result "$expect" "${tcp[@]}" -j 8; then + if ! ./testsuite/runtests.py --rsync-bin="$PWD/rsync" --rsync-bin2="$PWD/$peer" \ + --profiles="linux,$profile" --receipt="test-results/version-mix/$name-$transport.json" \ + "${tcp[@]}" -j 8; then echo "::error::version-mix failed: $name ($transport)" rc=1 fi @@ -80,3 +70,9 @@ jobs: done done exit $rc + - name: upload receipts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: version-mix-receipts + path: test-results/version-mix diff --git a/.github/workflows/valgrind.yml b/.github/workflows/valgrind.yml index 33be5f102..df4ab20ee 100644 --- a/.github/workflows/valgrind.yml +++ b/.github/workflows/valgrind.yml @@ -19,10 +19,13 @@ on: - cron: '17 4 * * *' workflow_dispatch: +permissions: + contents: read + jobs: memcheck: runs-on: ubuntu-latest - timeout-minutes: 120 + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -30,9 +33,10 @@ jobs: transport: [ pipe, tcp ] name: memcheck (${{ matrix.privilege }}, ${{ matrix.transport }}) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: prep run: | sudo apt-get update @@ -61,7 +65,7 @@ jobs: [ "${{ matrix.privilege }}" = root ] && SUDO="sudo -E" TCP= [ "${{ matrix.transport }}" = tcp ] && TCP="--use-tcp" - $SUDO ./runtests.py --valgrind \ + $SUDO ./testsuite/runtests.py --valgrind \ --valgrind-opts="--leak-check=no --error-exitcode=0" \ $TCP -j8 --preserve-scratch || true @@ -92,7 +96,7 @@ jobs: - name: upload valgrind logs on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: valgrind-logs-${{ matrix.privilege }}-${{ matrix.transport }} path: testtmp/**/valgrind.*.log diff --git a/.gitignore b/.gitignore index f73d65f27..a1927ff3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.[oa] *~ +__pycache__/ dummy ID Makefile diff --git a/Makefile.in b/Makefile.in index 716171db1..5f782c961 100644 --- a/Makefile.in +++ b/Makefile.in @@ -62,8 +62,8 @@ CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ t_rename_secure$(EXEEXT) t_symlink_secure$(EXEEXT) t_secure_relpath$(EXEEXT) t_acl$(EXEEXT) t_hashtable_overflow$(EXEEXT) t_iwildmatch$(EXEEXT) t_clean_fname$(EXEEXT) t_safe_arg$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT) -CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \ - testsuite/xattrs-hlink_test.py testsuite/exclude-lsh_test.py +CHECK_SYMLINKS = testsuite/tests/chown-fake_test.py testsuite/tests/devices-fake_test.py \ + testsuite/tests/xattrs-hlink_test.py testsuite/tests/exclude-lsh_test.py # Objects for CHECK_PROGS to clean CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o @@ -436,22 +436,22 @@ COVERAGE_EXCLUDE = -e '(^|/)zlib/' -e '(^|/)popt/' \ -e '(^|/)lib/(md5|snprintf)\.c$$' # Build everything the test suite needs (rsync + helper programs + symlinks) -# WITHOUT running it. Used by CI jobs that invoke runtests.py directly with -# custom options (e.g. the version-mix workflow's --rsync-bin2/--expect-result). +# WITHOUT running it. Used by CI jobs that invoke testsuite/runtests.py +# directly with options such as --rsync-bin2 and --profiles. .PHONY: check-progs check-progs: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) .PHONY: check check: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) - "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) + "$(srcdir)/testsuite/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) .PHONY: check29 check29: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) - "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=29 + "$(srcdir)/testsuite/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=29 .PHONY: check30 check30: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) - "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=30 + "$(srcdir)/testsuite/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=30 # Whole-suite gcov coverage report (HTML, with branch + decision coverage). # Requires a build configured with --enable-coverage and the `gcovr` tool @@ -479,7 +479,7 @@ coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS) chmod a+rwx "$$d"; \ setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \ done - @rc=0; "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \ + @rc=0; "$(srcdir)/testsuite/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \ rm -rf $(COVERAGE_DIR) && mkdir -p $(COVERAGE_DIR); \ gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \ --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \ @@ -519,7 +519,7 @@ coverage-all: all $(CHECK_PROGS) $(CHECK_SYMLINKS) @rc=0; \ for cfg in '' '--protocol=30' '--protocol=29' '--use-tcp'; do \ echo "===== coverage-all: runtests.py $$cfg ====="; \ - "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $$cfg || rc=$$?; \ + "$(srcdir)/testsuite/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $$cfg || rc=$$?; \ done; \ rm -rf coverage-all && mkdir -p coverage-all; \ gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \ @@ -550,17 +550,17 @@ simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS) touch $@; \ fi -testsuite/chown-fake_test.py: - ln -s chown_test.py $(srcdir)/testsuite/chown-fake_test.py +testsuite/tests/chown-fake_test.py: + ln -s chown_test.py $(srcdir)/testsuite/tests/chown-fake_test.py -testsuite/devices-fake_test.py: - ln -s devices_test.py $(srcdir)/testsuite/devices-fake_test.py +testsuite/tests/devices-fake_test.py: + ln -s devices_test.py $(srcdir)/testsuite/tests/devices-fake_test.py -testsuite/xattrs-hlink_test.py: - ln -s xattrs_test.py $(srcdir)/testsuite/xattrs-hlink_test.py +testsuite/tests/xattrs-hlink_test.py: + ln -s xattrs_test.py $(srcdir)/testsuite/tests/xattrs-hlink_test.py -testsuite/exclude-lsh_test.py: - ln -s exclude_test.py $(srcdir)/testsuite/exclude-lsh_test.py +testsuite/tests/exclude-lsh_test.py: + ln -s exclude_test.py $(srcdir)/testsuite/tests/exclude-lsh_test.py # This does *not* depend on building or installing: you can use it to # check a version installed from a binary or some other source tree, @@ -568,7 +568,7 @@ testsuite/exclude-lsh_test.py: .PHONY: installcheck installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS) - "$(srcdir)/runtests.py" --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir="`pwd`" -j $(CHECK_J) + "$(srcdir)/testsuite/runtests.py" --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir="`pwd`" -j $(CHECK_J) # TODO: Add 'dist' target; need to know which files will be included diff --git a/old_versions/README.md b/old_versions/README.md index a5ce3a5b6..d7653c18a 100644 --- a/old_versions/README.md +++ b/old_versions/README.md @@ -2,86 +2,50 @@ Static rsync binaries built from historical release tags. Two uses: -1. **Cross-version behaviour checks** — confirming whether a behaviour a user - reported on an old release is version-specific or option-driven. -2. **The version-mixing test suite** — `runtests.py --rsync-bin2=...` runs the - current code against one of these as the daemon / remote-shell peer; CI - (`.github/workflows/ubuntu-version-mix.yml`) does this for every binary - here against the per-version manifests in `testsuite/expect/`. - -Binaries are **statically linked** so they run regardless of the host's -shared libraries, and named `rsync_`: - -| Binary | Version | Protocol | Notes | -|----------------|---------|----------|-----------------------------------------| -| `rsync_2.6.0` | 2.6.0 | 27 | 2004; needs autoconf regen (see below) | -| `rsync_3.0.0` | 3.0.0 | 30 | 2008 | -| `rsync_3.1.0` | 3.1.0 | 31 | 2013 | -| `rsync_3.1.3` | 3.1.3 | 31 | Ubuntu 18.04 / Debian buster era (2018) | -| `rsync_3.2.0` | 3.2.0 | 31 | 2020 (zstd/lz4/xxhash negotiation added)| -| `rsync_3.2.7` | 3.2.7 | 31 | 2022 | -| `rsync_3.3.0` | 3.3.0 | 31 | 2024 | -| `rsync_3.4.0` | 3.4.0 | 32 | 2025 | -| `rsync_3.4.1` | 3.4.1 | 32 | 2025 | - -These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point -releases. 2.6.0 is the practical floor: older tags need progressively more -porting to build on a current toolchain. - -All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below); -xxhash/zstd/lz4 are compiled in where the version supports them. +1. **Cross-version behaviour checks** - confirming whether a behaviour a user reported on an old release is version-specific or option-driven. +2. **The version-mixing test suite:** `testsuite/runtests.py --rsync-bin2=...` runs the current code against one of these as the daemon or remote-shell peer. CI (`.github/workflows/ubuntu-version-mix.yml`) exercises every binary against its peer profile in `testsuite/profiles/`. -## Adding a version +Binaries are **statically linked** so they run regardless of the host's shared libraries and named `rsync_`: + +Binary | Version | Protocol | Notes +--- | --- | --- | --- +rsync_2.6.0 | 2.6.0 | 27 | 2004; needs autoconf regeneration +rsync_3.0.0 | 3.0.0 | 30 | 2008 +rsync_3.1.0 | 3.1.0 | 31 | 2013 +rsync_3.1.3 | 3.1.3 | 31 | Ubuntu 18.04 and Debian Buster era, 2018 +rsync_3.2.0 | 3.2.0 | 31 | 2020; zstd, lz4 and xxhash negotiation added +rsync_3.2.7 | 3.2.7 | 31 | 2022 +rsync_3.3.0 | 3.3.0 | 31 | 2024 +rsync_3.4.0 | 3.4.0 | 32 | 2025 +rsync_3.4.1 | 3.4.1 | 32 | 2025 + +These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point releases. 2.6.0 is the practical floor: older tags need progressively more porting to build on a current toolchain. +All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below); xxhash/zstd/lz4 are compiled in where the version supports them. + +## Adding a version ```bash ./build_static.sh 3.2.7 # uses git tag v3.2.7 ./build_static.sh 3.0.9 v3.0.9 # explicit tag if naming differs ``` -The script checks out the tag into a throwaway `git worktree`, applies the -minimal patches needed to compile old sources on a modern toolchain, links -statically, verifies the result is static and reports the requested version, -then installs `rsync_` here and removes the worktree. +The script checks out the tag into a throwaway `git worktree`, applies the minimal patches needed to compile old sources on a modern toolchain, links statically and verifies the result. It then installs `rsync_` here and removes the worktree. -Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...` -(defaults to `../rsync.4`). +Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...` (defaults to `../rsync.4`). ## Why the patches? -Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on. -`build_static.sh` handles these, each guarded so it's a no-op when not needed: - -1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real - prototype — removed. -2. **`gettimeofday()`** — glibc only has the 2-arg form; configure misdetects - the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`. -3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments - (`qsort` comparator, `pool->bomb`, etc.) — built with `-std=gnu11`. -4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit - declarations) downgraded to warnings; bundled zlib/popt used to keep the - static link self-contained. - -5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking - `libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's - `uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only - provided optional MD4/MD5, which rsync implements natively, so checksum - behaviour is unaffected. - -6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`): - modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn - latent (historically benign) over-reads in OLD rsync into hard - `*** buffer overflow detected ***` aborts when the binary runs as a - server/daemon — which made e.g. 3.1.3 and 3.2.7 unusable as peers. Disabling - it makes the archival binaries behave as the released versions did. - -7. **Pre-3.0 tags (e.g. 2.6.0)** ship `configure.in`, not a generated - `configure`. The script runs `autoheader`/`autoconf` to generate it, after - neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for - `inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` — modern autoconf emits - broken shell for those never-taken branches (the funcs exist in glibc). It - also generates `proto.h` (no make rule in that era) and stubs the vendored - `lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`). - All guarded so they no-op on 3.x. - -Newer versions may need fewer or different tweaks; if a build fails, the -script prints the first compiler errors from its log. +Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on. `build_static.sh` handles these, each guarded so it's a no-op when not needed: + +1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real prototype - removed. +2. **`gettimeofday()`** - glibc only has the 2-arg form; configure misdetects the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`. +3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments such as the `qsort` comparator and `pool->bomb` - built with `-std=gnu11`. +4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit declarations) downgraded to warnings; bundled zlib/popt used to keep the static link self-contained. + +5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking `libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's `uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only provided optional MD4/MD5, which rsync implements natively, so checksum behaviour is unaffected. + +6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`): modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn latent (historically benign) over-reads in OLD rsync into hard `*** buffer overflow detected ***` aborts when the binary runs as a server or daemon - which made 3.1.3 and 3.2.7 unusable as peers. Disabling it makes the archival binaries behave as the released versions did. + +7. **Pre-3.0 tags such as 2.6.0** ship `configure.in`, not a generated `configure`. The script runs `autoheader`/`autoconf` to generate it, after neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for `inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` - modern autoconf emits broken shell for those never-taken branches (the funcs exist in glibc). It also generates `proto.h` (no make rule in that era) and stubs the vendored `lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`). All guarded so they no-op on 3.x. + +Newer versions may need fewer or different tweaks; if a build fails, the script prints the first compiler errors from its log. diff --git a/testsuite/00-hello_test.py b/testsuite/00-hello_test.py deleted file mode 100644 index cdc153798..000000000 --- a/testsuite/00-hello_test.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -# Python rewrite of testsuite/00-hello.test. -# -# Foundational smoke test: --version / --info=help / --debug=help all -# work, plus a round-trip transfer of a directory whose name contains -# shell-special characters via the lsh.sh remote-shell stand-in. - -import os - -from rsyncfns import ( - FROMDIR, RSYNC, RSYNC_PEER, SRCDIR, TODIR, - checkit, run_rsync, test_fail, rsync_path_arg, rsh_cmd, -) - - -# Set RSYNC_RSH so rsync picks up lsh.sh for the "lh:" hosts below. -os.environ['RSYNC_RSH'] = rsh_cmd() - -# Basic help dumps must not crash. -if run_rsync('--version', check=False).returncode != 0: - test_fail('--version output failed') -if run_rsync('--info=help', check=False).returncode != 0: - test_fail('--info=help output failed') -if run_rsync('--debug=help', check=False).returncode != 0: - test_fail('--debug=help output failed') - -weird_name = "A weird)name" - -FROMDIR.mkdir(parents=True, exist_ok=True) -weird_dir = FROMDIR / weird_name -weird_dir.mkdir() - - -def append_line(line: str) -> None: - print(line) - with open(weird_dir / 'file', 'a') as f: - f.write(line + '\n') - - -def copy_weird(args: list, src_host: str, dst_host: str) -> None: - checkit( - [*args, f'--rsync-path={rsync_path_arg()}', - f'{src_host}{weird_dir}/', - f'{dst_host}{TODIR / weird_name}'], - FROMDIR, TODIR, - ) - - -append_line('test1') -checkit(['-ai', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR) - -append_line('test2') -copy_weird(['-ai'], 'lh:', '') - -append_line('test3') -copy_weird(['-ai'], '', 'lh:') - -append_line('test4') -copy_weird(['-ais'], 'lh:', '') - -append_line('test5') -copy_weird(['-ais'], '', 'lh:') - -# test6: --old-args lets two whitespace-separated names go through as a -# single "one two" remote argument to be re-split by the remote shell. -print('test6') -(FROMDIR / 'one').touch() -(FROMDIR / 'two').touch() - -saved = os.getcwd() -os.chdir(FROMDIR) -try: - run_rsync('-ai', '--old-args', f'--rsync-path={rsync_path_arg()}', - 'lh:one two', f'{TODIR}/') -finally: - os.chdir(saved) - -if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file(): - test_fail("old-args copy of 'one two' failed") - -# test7: the RSYNC_OLD_ARGS=1 env var should be equivalent to --old-args. -print('test7') -(TODIR / 'one').unlink() -(TODIR / 'two').unlink() - -env = os.environ.copy() -env['RSYNC_OLD_ARGS'] = '1' -import subprocess -from rsyncfns import rsync_argv - -os.chdir(FROMDIR) -try: - subprocess.run( - rsync_argv('-ai', f'--rsync-path={rsync_path_arg()}', - 'lh:one two', f'{TODIR}/'), - env=env, check=True, - ) -finally: - os.chdir(saved) - -# check=True only proves a zero exit; confirm the env-var path actually copied -# both files (as the explicit --old-args case above does). -if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file(): - test_fail("RSYNC_OLD_ARGS=1 copy of 'one two' failed") diff --git a/testsuite/COVERAGE.md b/testsuite/COVERAGE.md index 470d87a09..5cb2c3520 100644 --- a/testsuite/COVERAGE.md +++ b/testsuite/COVERAGE.md @@ -1,204 +1,201 @@ -# rsync option / daemon-parameter test coverage matrix - -Living checklist for the test-coverage effort that precedes the path-handling -restructure of rsync's path resolution. The restructure rewrites parent-directory -resolution for essentially every option, so the goal here is a regression net -that exercises each option **at directory depth** (≥3 levels) and, where the -option spans trees, **across directory boundaries**, asserting the *specific -property* the option controls — not just `dest == src`. - -How to read the columns: - -* **test(s)** — the `testsuite/*_test.py` that exercise the option. Tests added - by this effort are marked `*new*`. -* **depth** — Y = asserted on entries ≥3 levels deep; `~` = exercised only at/near - the tree root; `n/a` = not a path-resolution option. -* **x-dir** — Y = exercised with the relevant aux tree (temp/backup/dest/partial) - **outside** the main tree; `—` = not a cross-directory option. -* **gap** — what is still missing. - -Status legend: ✓ property asserted · `~` shallow / by an existing ported test · -✗ no coverage. - ---- - -## Command-line options - -### Recursion / structure -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -a, --archive | (all) | Y | — | ✓ ubiquitous | -| -r, --recursive | hands, delete-deep*new* | Y | — | ✓ | -| -R, --relative | relative, relative-implied*new* | Y | — | ✓ implied-dir attrs at depth | -| --no-implied-dirs | relative-implied*new* | Y | — | ✓ (proto 30+; proto 29 rejects multi-component path) | -| --inc-recursive / --no-inc-recursive | hardlinks | Y | — | `~` exercised, not isolated | -| -d, --dirs | dirs*new* | Y | — | ✓ no-recurse top layer | -| --old-dirs / --old-d | — | — | — | ✗ | -| -m, --prune-empty-dirs | prune-empty-dirs*new* | Y | — | ✓ incl. filter-emptied chains | - -### Links -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -l, --links | links*new*, symlink-ignore | Y | — | ✓ | -| -L, --copy-links | links*new* | Y | — | ✓ deref file+dir | -| -k, --copy-dirlinks | links*new* | Y | — | ✓ follow dir-symlink | -| -K, --keep-dirlinks | symlink-dirlink-basis | Y | — | ✓ #715; skips on no-RESOLVE_BENEATH / --disable-openat2 | -| -H, --hard-links | hardlinks, hardlinks-deep*new* | Y | Y | ✓ cross-directory hardlink | -| --copy-unsafe-links | unsafe-links | `~` | — | `~` | -| --safe-links | safe-links | `~` | — | `~` | -| --munge-links | (daemon-munge*new* covers the daemon param) | — | — | `~` client option not isolated; local mode is a near no-op | - -### Metadata / permissions / ownership -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -p, --perms | metadata-depth*new* | Y | — | ✓ exact modes per entry | -| -E, --executability | executability | `~` | — | `~` | -| --chmod | metadata-depth*new*, chmod-option | Y | — | ✓ | -| -A, --acls | acls, acls-depth*new* | Y | — | ✓ | -| -X, --xattrs | xattrs, xattrs-depth*new* | Y | — | ✓ | -| -t, --times | metadata-depth*new* | Y | — | ✓ | -| -U, --atimes | atimes | `~` | — | `~` (same set path as -t, covered deep) | -| --open-noatime | open-noatime | `~` | — | `~` | -| -N, --crtimes | crtimes | `~` | — | `~` (skips without crtimes support) | -| -O, --omit-dir-times | omit-times*new* | Y | — | ✓ | -| -J, --omit-link-times | omit-times*new* | Y | — | ✓ | -| -o, --owner | chown, ownership-depth*new* | Y | — | ✓ uid map root-gated | -| -g, --group | chgrp, ownership-depth*new* | Y | — | ✓ group remap non-root | -| --super / --fake-super | chown, chown-fake | `~` | — | `~` | -| --numeric-ids | — | — | — | ✗ client; daemon `numeric ids` also ✗ | -| --usermap / --groupmap | ownership-depth*new* | Y | — | ✓ groupmap non-root; usermap root-gated | -| --chown | ownership-depth*new* | Y | — | ✓ group half | -| -D / --devices / --specials | devices, devices-fake | `~` | — | `~` root/device-gated | -| --copy-devices / --write-devices | — | — | — | ✗ device-gated | -| -S, --sparse | sparse*new* | Y | — | ✓ hole preserved at depth | - -### Delta / temp / backup / dest (highest restructure risk) -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -T, --temp-dir | temp-dir*new*, chmod-temp-dir | Y | Y | ✓ cross-dir rename | -| --partial | partial*new* | Y | — | ✓ partial kept in dest file | -| --partial-dir | partial*new*, symlink-dirlink-basis | Y | Y | ✓ relative (in-tree) + absolute (outside), incl. delta resume from an absolute outside-tree partial | -| --delay-updates | delay-updates, delay-updates-deep*new* | Y | — | ✓ per-dir staging | -| --inplace | inplace*new*, alt-dest | Y | — | ✓ inode preserved | -| --append / --append-verify | append*new* | Y | — | ✓ verify split is proto 30+ | -| -b, --backup / --backup-dir / --suffix | backup, backup-deep*new* | Y | Y | ✓ | -| --compare-dest / --copy-dest / --link-dest | alt-dest, alt-dest-deep*new* | Y | Y | ✓ link=hardlink, copy=copy, compare=skip | -| -y, --fuzzy | fuzzy | `~` | — | `~` | -| -u, --update | update*new* | Y | — | ✓ keeps newer dest, updates older | -| -W, --whole-file | (used widely; --no-whole-file ubiquitous) | n/a | — | `~` | -| --mkpath | mkpath | `~` | — | `~` | -| -x, --one-file-system | — | — | — | ✗ (needs a mount boundary) | -| --preallocate / --fsync | — | — | — | ✗ | -| -B, --block-size | — | — | — | ✗ | -| --max-alloc | max-alloc-zero | — | — | ✓ zero resolves to each peer's supported ceiling; values above the limit are rejected | - -### Filtering -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -f, --filter / -F | filter-depth*new*, merge | Y | — | ✓ deep per-dir merge | -| --exclude / --include | filter-depth*new*, exclude, exclude-lsh | Y | — | ✓ | -| --exclude-from / --include-from | files-from-depth*new* | Y | — | ✓ | -| -C, --cvs-exclude | cvs-exclude*new* | Y | — | ✓ incl. deep .cvsignore | -| --files-from | files-from-depth*new* | Y | — | ✓ | -| -0, --from0 | files-from-depth*new* | Y | — | ✓ | -| --max-size / --min-size | size-filter*new* | Y | — | ✓ | -| --existing / --ignore-existing | delete-deep*new* | Y | — | ✓ | -| --ignore-missing-args / --delete-missing-args | — | — | — | ✗ | - -### Deletion -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| --delete / --del | delete, delete-deep*new* | Y | — | ✓ deep subtree | -| --delete-before/during/delay/after | delete-deep*new* | Y | — | ✓ all four agree | -| --delete-excluded | delete | `~` | — | `~` | -| --max-delete | delete-deep*new* | Y | — | ✓ caps deletions | -| --remove-source-files | delete | `~` | — | `~` | -| --force | update*new* | Y | — | ✓ replaces a non-empty dir with a file | -| --ignore-errors | — | — | — | ✗ (client; daemon `ignore errors` also ✗) | - -### Comparison / checksum / compression -| option | test(s) | depth | x-dir | notes / gap | -|---|---|---|---|---| -| -c, --checksum | compare*new* | Y | — | ✓ catches stealth change | -| -I, --ignore-times | compare*new* | Y | — | ✓ | -| --size-only | compare*new* | Y | — | ✓ | -| -@, --modify-window | compare*new* | Y | — | ✓ | -| --checksum-choice / --checksum-seed | compress-options*new* | Y | — | ✓ every advertised algo | -| -z, --compress | daemon-gzip-{up,down}load, daemon-refuse-compress | `~` | — | `~` | -| --compress-choice / --compress-level / --skip-compress | compress-options*new* | Y | — | ✓ | - -### Output / reporting (path-irrelevant — checked for output shape) -| option | test(s) | notes / gap | -|---|---|---| -| -i, --itemize-changes | output-options*new*, itemize | ✓ | -| -n, --dry-run | output-options*new* | ✓ | -| --stats | output-options*new* | ✓ | -| --out-format | output-options*new* | ✓ | -| --list-only | output-options*new* | ✓ | -| -q, --quiet | output-options*new* | ✓ | -| --progress / -P | output-options*new* | ✓ (--progress) | -| -h, --human-readable / -8, --8-bit-output | output-options*new* | ✓ smoke | -| --version / --help | output-options*new* | ✓ | -| --info / --debug / --stderr / --no-motd / --outbuf | — | ✗ | -| -M, --remote-option / --log-file / --log-file-format | — | ✗ (daemon `log file` covered) | - -### Batch / connection / misc -| option | test(s) | notes / gap | -|---|---|---| -| --write-batch / --only-write-batch / --read-batch | batch-mode | `~` | -| -e, --rsh / --rsync-path | ssh-basic, many | `~` | -| --protocol | check29 / check30 (whole suite) | ✓ | -| --address / --port | daemon tests under --use-tcp | `~` | -| --password-file | daemon-auth*new* | ✓ | -| --early-input / daemon `early exec` | — | ✗ | -| --sockopts / --blocking-io / --timeout / --contimeout | — | ✗ | -| -4/-6, --ipv4/--ipv6 | — | ✗ | -| --stop-after / --stop-at | — | ✗ | -| --bwlimit | partial*new* (used, not asserted) | `~` | -| --copy-as | — | ✗ root-gated | -| --iconv | — | ✗ | -| -s/--secluded-args, --old-args, --trust-sender | (default arg-protection exercised) | `~` | - ---- - -## Daemon (rsyncd.conf) parameters - -| parameter | test(s) | notes / gap | -|---|---|---| -| path | daemon-access*new*, all daemon tests | ✓ incl. deep sub-path | -| read only | daemon-access*new*, daemon | ✓ | -| write only | daemon-access*new* | ✓ | -| list | daemon-access*new*, daemon | ✓ hidden-but-usable | -| use chroot | sender-flist-symlink-leak, daemon-chroot-acl | `~` (no=most tests; yes needs root) | -| munge symlinks | daemon-munge*new* | ✓ /rsyncd-munged/ add+strip | -| exclude / include | daemon-filter*new*, daemon | ✓ exclude | -| filter / exclude from / include from | — | ✗ (exclude covers the mechanism) | -| incoming chmod | daemon-filter*new*, chmod-option | ✓ | -| outgoing chmod | daemon-filter*new* | ✓ | -| auth users / secrets file | daemon-auth*new* | ✓ accept/reject/unauth | -| strict modes | daemon-auth*new* | ✓ rejects world-readable secrets | -| refuse options | daemon-refuse*new*, daemon-refuse-compress | ✓ named/wildcard/allow-list | -| pre-xfer exec / post-xfer exec | daemon-exec*new* | ✓ env + abort | -| early exec | — | ✗ (needs --early-input) | -| hosts allow / hosts deny | daemon (allow), daemon-chroot-acl (deny) | `~` (needs --use-tcp for real peer) | -| reverse lookup / forward lookup | daemon-chroot-acl | `~` reverse only | -| log file / transfer logging / log format | daemon | `~` set, not asserted | -| max verbosity | daemon | `~` | -| comment | daemon, daemon-access*new* | ✓ | -| numeric ids | — | ✗ (hard to observe non-root) | -| fake super | chown-fake (client side) | ✗ as daemon param | -| timeout / max connections / lock file | — | ✗ (need --use-tcp + concurrency) | -| temp dir / open noatime / ignore errors / ignore nonreadable | — | ✗ | -| charset / name converter / dont compress | — | ✗ | -| uid / gid / daemon uid / daemon gid / daemon chroot | build_rsyncd_conf (uid/gid when root), daemon-chroot-acl | `~` root-gated | -| motd file / pid file / port / address / socket options / listen backlog / proxy protocol / syslog facility / syslog tag | — | ✗ (server-startup/connection params) | - ---- - -## Known gaps worth a future pass -* Connection/timeout params (`--timeout`, `--contimeout`, daemon `timeout`, - `max connections`) need a real socket + concurrency (run under `--use-tcp`). -* Root-only behaviours (`-o`/`--usermap` uid remap, real devices, `use chroot - = yes`, daemon uid/gid) skip as non-root; run the suite as root to cover. -* `--ignore-errors`, `-x/--one-file-system`, `--numeric-ids` have no dedicated - test yet (lower restructure risk). +# Test coverage + +Covered means a test directly checks the behaviour. Partial means the option is exercised without covering its whole contract. Missing identifies work still to do. + +Path depth is deep, shallow, untested or N/A. Auxiliary tree is outside, inside only, untested or N/A depending on where the relevant backup, basis, partial or temporary tree is exercised. + +## Recursion and structure + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-a, --archive | many | deep | N/A | Covered throughout the suite +-r, --recursive | hands, delete-deep | deep | N/A | Covered +-R, --relative | relative, relative-implied | deep | N/A | Covered with implied-directory attributes +--no-implied-dirs | relative-implied | deep | N/A | Covered for protocol 30 and later; protocol 29 rejects the multi-component case +--inc-recursive, --no-inc-recursive, --no-i-r | hardlinks | deep | N/A | Partial: exercised but not isolated +-d, --dirs | dirs | deep | N/A | Covered without recursion +--old-dirs, --old-d | old-dirs | shallow | N/A | Covered in both mixed-version remote directions +-m, --prune-empty-dirs | prune-empty-dirs | deep | N/A | Covered with filter-emptied chains + +## Links + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-l, --links | links, symlink-ignore | deep | N/A | Covered +-L, --copy-links | links | deep | N/A | Covered for file and directory links +-k, --copy-dirlinks | links | deep | N/A | Covered +-K, --keep-dirlinks | symlink-dirlink-basis | deep | N/A | Covered for issue 715; unavailable without the secure path resolver +-H, --hard-links | hardlinks, hardlinks-deep | deep | outside | Covered across directories +--copy-unsafe-links | unsafe-links | shallow | N/A | Partial +--safe-links | safe-links | shallow | N/A | Partial +--insecure-links | operator-path tests, insecure-links-admin-optout | deep | N/A | Covered for the local opt-out and daemon refusal +--confine-root | files-from-leak, relative-source-ancestor, rrsync-merge-file-confine | deep | N/A | Covered for direct arguments, files-from and restricted shells +--munge-links | daemon-munge | N/A | N/A | Partial: daemon behaviour is covered but the client option is not isolated + +## Metadata, permissions and ownership + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-p, --perms | metadata-depth | deep | N/A | Covered with exact per-entry modes +-E, --executability | executability | shallow | N/A | Partial +--chmod | metadata-depth, chmod-option | deep | N/A | Covered +-A, --acls | acls, acls-depth | deep | N/A | Covered when ACL support is available +-X, --xattrs | xattrs, xattrs-depth | deep | N/A | Covered when extended attributes are available +-t, --times | metadata-depth | deep | N/A | Covered +-U, --atimes | atimes | shallow | N/A | Partial +--open-noatime | open-noatime | shallow | N/A | Partial +-N, --crtimes | crtimes | shallow | N/A | Partial and platform-dependent +-O, --omit-dir-times | omit-times | deep | N/A | Covered +-J, --omit-link-times | omit-times | deep | N/A | Covered +-o, --owner | chown, ownership-depth | deep | N/A | Covered with root-gated UID mapping +-g, --group | chgrp, ownership-depth | deep | N/A | Covered with non-root group remapping +--super, --fake-super | chown, chown-fake | shallow | N/A | Partial +--numeric-ids | ownership-depth | deep | N/A | Covered for client UID and GID mapping +--usermap, --groupmap | ownership-depth | deep | N/A | Covered; user mapping needs root +--chown | ownership-depth | deep | N/A | Partial: group handling is covered +-D, --devices, --specials | devices, devices-fake | shallow | N/A | Partial and privilege-dependent +--drop-D | rrsync-specials-denied | N/A | N/A | Covered for restricted receivers +--copy-devices, --write-devices | none | untested | N/A | Missing +-S, --sparse | sparse | deep | N/A | Covered with a hole at depth + +## Delta, temporary, backup and basis paths + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-T, --temp-dir | temp-dir, chmod-temp-dir | deep | outside | Covered across filesystems +--partial | partial | deep | inside only | Covered with a retained destination partial +--partial-dir | partial, symlink-dirlink-basis | deep | outside | Covered for relative and absolute partial directories with delta resume +--delay-updates | delay-updates, delay-updates-deep | deep | inside only | Covered for per-directory staging +--inplace | inplace, alt-dest | deep | inside only | Covered with inode preservation +--append, --append-verify | append | deep | inside only | Covered; the verification split needs protocol 30 or later +-b, --backup, --backup-dir, --suffix | backup, backup-deep | deep | outside | Covered +--compare-dest, --copy-dest, --link-dest | alt-dest, alt-dest-deep | deep | outside | Covered for skip, copy and hard-link behaviour +-y, --fuzzy | fuzzy | shallow | N/A | Partial +-u, --update | update | deep | N/A | Covered for newer and older destinations +-W, --whole-file, --no-whole-file | many | N/A | N/A | Partial: widely exercised but not isolated +--mkpath | mkpath | shallow | N/A | Partial +-x, --one-file-system | none | untested | untested | Missing; needs a mount boundary +--preallocate | preallocate | deep | N/A | Covered for allocation and delta updates +--fsync | none | untested | N/A | Missing +-B, --block-size | hashsearch-chain, compress-zlib-insert, preallocate | deep | N/A | Covered +--max-alloc | max-alloc-zero | N/A | N/A | Covered for zero and values above the peer limit + +## Filtering + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-f, --filter, -F | filter-depth, merge | deep | N/A | Covered for deep per-directory merges +--exclude, --include | filter-depth, exclude, exclude-lsh | deep | N/A | Covered +--exclude-from, --include-from | files-from-depth | deep | N/A | Covered +-C, --cvs-exclude | cvs-exclude | deep | N/A | Covered with nested .cvsignore +--files-from | files-from-depth | deep | N/A | Covered +-0, --from0 | files-from-depth | deep | N/A | Covered +--max-size, --min-size | size-filter | deep | N/A | Covered +--existing, --ignore-non-existing, --ignore-existing | delete-deep | deep | N/A | Covered +--ignore-missing-args | ignore-missing-args | deep | N/A | Covered for direct, remote-shell and files-from inputs +--delete-missing-args | delete-missing-args-files-from | shallow | N/A | Covered with files-from + +## Deletion + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +--delete, --del | delete, delete-deep | deep | N/A | Covered for deep subtrees +--delete-before, --delete-during, --delete-delay, --delete-after | delete-deep | deep | N/A | Covered +--delete-excluded | delete | shallow | N/A | Partial +--max-delete | delete-deep | deep | N/A | Covered +--remove-source-files | delete | shallow | N/A | Partial +--force | update | deep | N/A | Covered when replacing a non-empty directory with a file +--ignore-errors | iconv | N/A | N/A | Covered for deletion after sender input failure + +## Comparison, checksum and compression + +Option | Tests | Path depth | Auxiliary tree | Coverage +--- | --- | --- | --- | --- +-c, --checksum | compare | deep | N/A | Covered for same-metadata content changes +-I, --ignore-times | compare | deep | N/A | Covered +--size-only | compare | deep | N/A | Covered +-@, --modify-window | compare | deep | N/A | Covered +--checksum-choice, --checksum-seed | compress-options | deep | N/A | Covered for advertised algorithms +-z, --compress | daemon-gzip-download, daemon-gzip-upload, daemon-refuse-compress | shallow | N/A | Partial +--compress-choice, --compress-level, --skip-compress | compress-options | deep | N/A | Covered +--compress-threads | daemon-refuse-compress-threads-alias, daemon-zstd-thread-exhaustion | N/A | N/A | Partial: refusal and the daemon worker limit are covered + +## Output and reporting + +Option | Tests | Coverage +--- | --- | --- +-v, --verbose | many | Partial: used widely but verbosity levels are not isolated +-i, --itemize-changes | output-options, itemize | Covered +-n, --dry-run | output-options | Covered +--stats | output-options | Covered +--out-format | output-options | Covered +--list-only | output-options | Covered +-q, --quiet | output-options | Covered +--progress, -P | output-options | Covered for progress output +-h, --human-readable, -8, --8-bit-output | output-options | Covered with format assertions +--version, --help | output-options | Covered +--info, --debug, --stderr, --no-motd, --outbuf | output-options, daemon-module-options | Covered for selection, routing, MOTD suppression and line buffering +-M, --remote-option, --log-file, --log-file-format | remote-logging | Covered for client and remote-shell sender or receiver logs + +## Batch, connection and miscellaneous options + +Option | Tests | Coverage +--- | --- | --- +--write-batch, --only-write-batch, --read-batch | batch-mode | Partial +-e, --rsh, --rsync-path | ssh-basic and many others | Partial +--protocol | check29 and check30 | Covered across the selected suite +--daemon, --config, --detach, --no-detach | daemon-standalone-detach, daemon-stdin tests | Covered for detached, foreground and inherited-socket startup +--dparam | none | Missing +--address, --port | daemon-address-family, daemon-standalone-detach | Covered for IPv4, IPv6 and configured binding +--password-file | daemon-auth | Covered +--early-input | daemon-early-exec-nameconv, early-input-symlink | Covered for data delivery and confined input paths +--sockopts | daemon-module-options | Covered for client and daemon configuration +--blocking-io | none | Missing +--timeout, --contimeout | daemon-handshake-timeout, msg-io-timeout-zero, msg-io-timeout-overflow, contimeout-rsh | Covered for precedence, bounds and remote-shell daemon paths +-4, -6, --ipv4, --ipv6 | daemon-address-family | Covered for daemon binding and client connection +--stop-after, --stop-at | stop-time | Covered for future, past and duration parsing +--bwlimit | partial | Partial: used but not asserted directly +--copy-as | none | Missing and root-gated +--iconv | iconv | Covered for names, arguments, file lists, protocol 30 link targets and invalid input; the raw-byte fixture is unavailable on macOS +-s, --secluded-args, --old-args, --trust-sender | iconv, smoke | Partial: secluded and legacy argument handling are asserted; trust-sender is not isolated + +## Daemon parameters + +Parameter | Tests | Coverage +--- | --- | --- +path | daemon-access and daemon tests | Covered with nested paths +read only | daemon-access, daemon | Covered +write only | daemon-access | Covered +list | daemon-access, daemon | Covered for hidden but usable modules +use chroot | sender-flist-symlink-leak, daemon-chroot-acl | Partial: the enabled case needs root +insecure links | insecure-links-admin-optout, daemon-symlink-escape-matrix | Covered for the administrative opt-out +munge symlinks | daemon-munge | Covered for prefix addition and removal +exclude, include | daemon-filter, daemon | Covered for exclusion +filter, exclude from, include from | none | Missing as daemon parameters +incoming chmod | daemon-filter, chmod-option | Covered +outgoing chmod | daemon-filter | Covered +auth users, secrets file | daemon-auth | Covered for acceptance and rejection +auth digest | daemon-auth-digest-floor | Covered for minimum strength, old peers and invalid configuration +strict modes | daemon-auth | Covered for unsafe secrets-file modes +refuse options | daemon-refuse, daemon-refuse-compress | Covered for names, wildcards and allow lists +pre-xfer exec, post-xfer exec | daemon-exec | Covered for environment and abort behaviour +early exec | daemon-early-exec-nameconv | Covered for environment and early input +hosts allow, hosts deny | daemon, daemon-chroot-acl | Partial: a real TCP peer is required +reverse lookup, forward lookup | daemon-chroot-acl | Partial: reverse lookup only +log file, transfer logging, log format | daemon | Partial: configured but not asserted +max verbosity | daemon | Partial +comment | daemon, daemon-access | Covered +numeric ids | daemon-early-exec-nameconv, daemon-namecvt tests | Covered for numeric ids = no +fake super | chown-fake, daemon-namecvt-empty-response | Covered +timeout | daemon-handshake-timeout | Covered with zero and precedence +max connections, lock file | daemon-include-maxconn, daemon-connection-limits | Covered for refusal and slot reuse +temp dir, open noatime, ignore errors, ignore nonreadable | iconv | Partial: ignore errors is asserted +charset | iconv | Covered for module override of the remote charset +name converter | daemon-early-exec-nameconv, daemon-namecvt tests | Covered for success, empty and malformed responses +dont compress | daemon-module-options | Partial: configured but compression choice is not asserted +uid, gid, daemon uid, daemon gid, daemon chroot | build_rsyncd_conf, daemon-chroot-acl | Partial and root-gated +motd file | daemon-module-options | Covered for banner content +socket options | daemon-module-options | Partial: the live socket path runs but kernel effects are not inspected +pid file, port, address | daemon-standalone-detach, daemon-address-family | Covered for configuration and CLI paths +proxy protocol, proxy protocol hosts | daemon-proxy-protocol, proxy-protocol-trusted-peer | Covered for enabled, disabled and trusted-peer policy +listen backlog, syslog facility, syslog tag | none | Missing diff --git a/testsuite/README.md b/testsuite/README.md index 3536a3660..6aca52497 100644 --- a/testsuite/README.md +++ b/testsuite/README.md @@ -1,304 +1,113 @@ # rsync testsuite -This directory holds rsync's automated regression tests. Ideally every code -change or bug fix comes with a test that would have caught the problem. +Rsync's automated tests live here. Bug fixes should include a regression test when practical. -The tests are Python scripts named `testsuite/*_test.py`, driven by the -`runtests.py` harness at the top of the tree (the old shell-based `runtests.sh` -is gone). Shared helpers live in `testsuite/rsyncfns.py`. A handful of C helper -programs (`tls`, `getgroups`, `trimslash`, …) are built alongside `rsync` and -used by some tests. Coverage notes are in [COVERAGE.md](COVERAGE.md). +## Layout +- `testsuite/tests/` contains the test scripts. Test names end in `_test.py` +- `testsuite/runtests.py` discovers and runs tests +- `testsuite/rsyncfns.py` contains the legacy test helpers +- `testsuite/harness/` contains metadata, profile, result and receipt support +- `testsuite/profiles/` records platform capabilities and peer deviations +- `testsuite/skiplist/` contains the temporary expected-skip lists +- `testsuite/fleettest.py` runs the suite across the maintainer fleet +- `testsuite/abdiff.py` compares two rsync versions over the same transfers +- [COVERAGE.md](COVERAGE.md) records option and daemon-parameter coverage + +Some tests also use C helpers built with rsync. ## Writing tests -Favour readability — a test is also documentation of the behaviour it pins, so -prefer clarity over cleverness: - -* When a test writes an `rsyncd.conf`, write it as a triple-quoted f-string so - the actual config is readable top-to-bottom, with module parameters indented - with plain spaces. Don't build it from adjacent string literals full of `\n` - (and `\t`) escapes. The daemon's parser accepts space-indented parameters. -* Better still, use the structured helpers in `rsyncfns.py` when a stock config - will do: `write_daemon_conf(modules, globals)` (per-test modules/params) or - `build_rsyncd_conf()` (the four standard modules). They also handle the - root-only `uid`/`gid` lines for you (needed so a `use chroot = no` daemon run - as root can read a root-owned module). -* For config that varies (e.g. those root-only `uid`/`gid` lines), interpolate a - single optional block that expands when needed and is an empty string - otherwise, rather than splicing pieces together: - - ```python - root = get_testuid() == get_rootuid() - ids = f"uid = {get_rootuid()}\ngid = {get_rootgid()}" if root else "" - conf.write_text(f"""\ - pid file = {base}/rsyncd.pid - use chroot = no - {ids} - log file = {base}/rsyncd.log - - [m] - path = {mod} - read only = yes - """) - ``` - -## Running the tests - -### Via make - -Run from the build directory: - -- **`make check`** — build the helper programs and run the whole suite in - parallel (`CHECK_J`, default 8) against the just-built `./rsync`. You do **not** - need `make install` first; indeed you generally should not install before - testing. Use `make check CHECK_J=1` to run serially. -- **`make check29`** / **`make check30`** — the same, forcing protocol version 29 - or 30. -- **`make installcheck`** — run the suite against the *installed* binary (e.g. - `/usr/local/bin/rsync`). Per the GNU standards this does not search `$PATH`. - Handy for testing a distribution build. -- **`make check-progs`** — (re)build just the C helper programs the tests need, - without running anything. -- **`make coverage`** / **`coverage-tcp`** / **`coverage-all`** — generate an HTML - coverage report (needs `./configure --enable-coverage` and `gcovr`); - `coverage-all` merges runs across protocol versions and the tcp transport. - -### Via runtests.py directly - -`make check` just drives `runtests.py`; run it directly for finer control. It -defaults `--rsync-bin` to `./rsync`, so run it from the build directory (or pass -`--rsync-bin` / `--tooldir`): +A regression test should assert the behaviour being fixed. A final source and destination comparison can miss the actual bug. + +Function-based tests use one `@requires(...)` decorator. Existing module tests use one top-level `metadata(...)` call while they are migrated. Declare any capability passed to `test_skipped()` and use `require_tcp()` or `require_asan()` for those checks. + +Use `TestContext` or the existing `rsyncfns.py` paths for scratch data. Do not write into the source tree or use sleeps for synchronisation and timestamp changes. Tests run in parallel by default and must clean up their processes, sockets and temporary files. Use `write_daemon_conf()` or `build_rsyncd_conf()` for ordinary daemon configurations. +## Running tests + +Run the standard suite from a configured build directory: ```sh -./runtests.py # all tests -./runtests.py chmod-temp-dir # a single test by name -./runtests.py 'xattr*' # a glob of test names +make check ``` -Useful options: - -- `-j N`, `--parallel N` — run up to N tests at once -- `--use-tcp` — run daemon tests against a real `rsyncd` on `127.0.0.1` (the - default runs them over a stdio pipe). **Read the security warning below before - using this on a shared machine.** -- `--protocol VER` — force a protocol version -- `--preserve-scratch` — keep each test's scratch dir afterwards -- `--log-level N`, `--always-log` — more verbose output / show logs for passing tests too -- `--stop-on-fail` — stop after the first failure -- `--timeout SECS` — per-test timeout (default 300) -- `--timing` — after the run, list the tests by wall-clock, slowest first, with - the serial sum and the floor set by the single longest test -- `--race-timeout SECS` — budget a TOCTOU race test may spend trying to win its - race. These are the suite's slowest tests: a race test is a negative oracle, - so it passes by spending its *whole* budget (5–15s each by default). Lowering - this speeds the suite up and weakens the oracle in equal measure. -- `--valgrind`, `--valgrind-opts OPTS` — run rsync under valgrind -- `--rsync-bin PATH`, `--tooldir DIR`, `--srcdir DIR` — locate the binary / build / source dirs -- `--expect-skipped LIST` — see skip enforcement below - -### Security warning: `--use-tcp` - -> **⚠️ Do not use `--use-tcp` on a machine with untrusted local users.** -> -> `--use-tcp` starts a real `rsync` daemon listening on a loopback TCP port -> (`127.0.0.1` / `::1`) and **deliberately configures insecure test scenarios** -> (daemon modules without authentication, unsafe options enabled, etc.). Loopback -> addresses are reachable by *every* local user, so for as long as the tests run, -> any other user on the machine can connect to that daemon and exploit those -> deliberately-insecure modules — potentially reading or writing files with the -> privileges of the user running the tests (which is **root** if you run the suite -> as root). -> -> Only run `--use-tcp` where there are **no possible local users who might try to -> exploit it** — a single-user workstation or a dedicated, isolated CI machine. -> The default stdio-pipe transport carries no such risk: it talks to the daemon -> over a private pipe with nothing listening on the network, so prefer it on any -> shared or multi-user host. - -### Results and exit codes - -Each test prints one result line — `PASS`, `FAIL`, `ERROR`, `SKIP` (with a -reason), or `XFAIL` (an expected failure) — and the run ends with a -`passed / failed / skipped` summary. Per-test exit-code convention: - -| code | meaning | -|------|---------| -| 0 | pass | -| 1 | fail | -| 2 | error | -| 77 | skip | -| 78 | xfail | - -`runtests.py` exits non-zero if any test fails. Some tests need root or another -precondition and otherwise `SKIP` — read the individual test scripts for details. - -**Skip enforcement:** on a full run, set `RSYNC_EXPECT_SKIPPED=a,b,c` (or -`--expect-skipped a,b,c`) and the run fails if the set of skipped tests does not -match. This is how the CI workflows pin each platform's expected skip set. An -`@FILE` entry reads a skip list (one test per line) instead, and several may be -composed: the workflows use -`@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt`. Keeping the -lists one-name-per-line is what stops two branches that each add a skipping test -from conflicting -- see `testsuite/skiplist/README.md`. - -### Scratch dirs and debugging - -Each test runs in `testtmp//`. On failure the scratch directory is left in -place (also `--preserve-scratch`); including its logs in a bug report is helpful. - -### Preconditions - -You need `python3`, `/bin/sh`, and the normal build toolchain. The ACL/xattr -tests need the `acl` and `attr` tools (`getfacl`/`setfacl`, -`getfattr`/`setfattr`) and skip if they are absent. Some tests need root. - -These tests also run in CI via GitHub Actions (see `.github/workflows/`). - -## Fleet testing (fleettest.py) - -`testsuite/fleettest.py` builds the committed HEAD of an rsync checkout on a -fleet of remote machines over ssh and runs the suite under both transports -(stdio-pipe and `--use-tcp`) in parallel, reporting only the *unexpected* -results. It is a fast local pre-flight for the GitHub CI matrix: each target -mirrors a `.github/workflows/*.yml` job — its configure flags, and the -`RSYNC_EXPECT_SKIPPED` list parsed straight from the workflow. - -Because every run includes a `--use-tcp` pass, the fleet stands up the insecure -loopback test daemon on each target — so only point it at machines with **no -untrusted local users** (see the [security warning](#security-warning---use-tcp) -above). - -The fleet — which machines, and how to reach and build on each — is described in -a JSON file. Copy the bundled example (it is git-ignored) and edit it for your -hosts: +Other make targets: +```sh +make check CHECK_J=1 +make check29 +make check30 +make check-progs +make installcheck +make coverage-all +``` +The runner accepts names and shell patterns: ```sh -cp testsuite/fleettest.json.example testsuite/fleettest.json # then edit -# (or symlink it, or point elsewhere with --fleet PATH) +./testsuite/runtests.py +./testsuite/runtests.py chmod-temp-dir +./testsuite/runtests.py 'xattr*' ``` -The config is looked up in order: `~/.fleettest.json` first, then -`testsuite/fleettest.json`, unless overridden with `--fleet PATH`. +The main controls are `-j`, `--rsync-bin`, `--rsync-bin2`, `--protocol`, `--profiles`, `--use-tcp`, `--race-timeout`, `--receipt`, `--describe-tests` and `--valgrind`. Run `./testsuite/runtests.py --help` for the full list. + +## TCP daemon mode + +`--use-tcp` starts unauthenticated test daemons on loopback addresses and some fixtures enable unsafe daemon options. Other local users can reach those listeners, so use the default pipe transport on shared hosts. + +## Results and profiles + +Result | Meaning +--- | --- +PASS | The assertion passed +FAIL | The assertion failed +ERROR | The test environment or harness failed +SKIP | The test did not run +UNSUPPORTED | A declared capability was unavailable +XFAIL | A known defect reproduced +XPASS | A known defect no longer reproduced +PROFILE_ERROR | The result disagreed with the active profile -Each entry names an ssh host (`null` to run locally), the workflow it mirrors, -and its configure flags, plus optional per-target settings (`make`, `privilege`, -`env_prefix`, …). See the comments in `fleettest.json.example`. +Exit codes are 0 for pass, 1 for fail, 2 for error, 77 for skip and 78 for expected failure. -A target with `"nonroot": true` does an extra pass, after the main (root) run, -that reruns the privilege-sensitive tests as the unprivileged ssh user. Which -tests those are is **not** listed in the fleet config — a test opts in by -setting a module-level `fleet_nonroot = True`, so the set is maintained in the -test files and new privilege-sensitive tests join automatically with no -fleet-config change. +Profiles compose by name. For example `--profiles=linux,peer-3.4.1` combines the Linux capabilities with the known deviations for that peer. Tests tagged `version-mix` are selected from metadata. -A target with `"protocols": [30, 29]` runs one extra stdio-pipe pass per listed -version, each forcing that older wire version with `runtests --protocol=N` — the -fleet analogue of a workflow's `check30`/`check29` steps. Each pass takes the -`RSYNC_EXPECT_SKIPPED` spec from the workflow's own `check30`/`check29` step, so -a lane with extra protocol-gated skips (`check29` adds -`@testsuite/skiplist/proto29.txt`) is enforced correctly. They show up as -`protoNN` columns in the report (and `--timing` breakdown); targets that don't -set `protocols` show `-` there. +An unsupported result is accepted only when the test declares the capability and the active profile permits its absence. A generic skip is a profile error. Receipts retain the raw outcome and the profile verdict. -Run it from inside a checkout (it builds the current directory's HEAD; use -`--repo PATH` for another tree): +CI also compares the exact skipped-test list while profiles are being rolled out. See [skiplist/README.md](skiplist/README.md). +## Scratch data and requirements + +Tests use `testtmp//`; failed scratch directories remain for inspection. The suite needs Python 3, `/bin/sh` and the normal build toolchain. ACL and extended-attribute tests also need the platform ACL and attr tools. + +## Fleet testing + +`testsuite/fleettest.py` builds the committed revision on configured hosts and runs the same pipe, TCP, protocol and non-root lanes used by CI. Configuration is read from `~/.fleettest.json` then `testsuite/fleettest.json` or from the path passed to `--fleet`. + +Start with the checked-in example: +```sh +cp testsuite/fleettest.json.example testsuite/fleettest.json +``` + +Common commands: ```sh -python3 testsuite/fleettest.py # whole fleet, both transports -python3 testsuite/fleettest.py --list # list configured targets -python3 testsuite/fleettest.py --targets NAME[,NAME] -python3 testsuite/fleettest.py --fleet other.json --transport pipe -python3 testsuite/fleettest.py --timing # per-target wall-clock breakdown -python3 testsuite/fleettest.py --keep-on-fail # keep logs + tree where it broke -python3 testsuite/fleettest.py --full-tcp # whole suite in the tcp pass too +python3 testsuite/fleettest.py +python3 testsuite/fleettest.py --list +python3 testsuite/fleettest.py --targets freebsd,netbsd +python3 testsuite/fleettest.py --keep-on-fail ``` -`--timing` adds a per-target breakdown after the report — total wall-clock plus -the push / build / pipe / tcp / protoNN / nonroot phases, sorted slowest-first. Targets -run in parallel, so the whole run is gated by the slowest one; the phase columns -show whether that target's hold-up is the push, the build, or a test pass. It -also passes `--timing` down to each target's `runtests.py`, so the captured -output attributes a slow pass to individual tests. - -The `tcp` pass runs **only the tests that can reach the daemon transport**, since -it follows a full pipe pass over the very same build. `--use-tcp` is observable -through exactly one path — `RSYNC_TEST_USE_TCP` is read once in `rsyncfns` -(`USE_TCP`) and acted on once, in `start_test_daemon()` — so a test that never -gets there produces an identical result twice. That drops 186 of the 340 tests -and roughly a third of the pass's work; the count skipped is always printed. -Pass `--full-tcp` to sweep the whole suite there anyway. The narrowing applies -only when both transports run: under `--transport tcp` that pass is the only -one, so it runs the whole suite regardless. - -`--keep-on-fail [DIR]` makes a failure inspectable without repeating the run. -For every target that came back with anything unexpected it writes the full -build and per-transport output to `DIR///` (default -`./fleettest-logs`) and keeps that target's remote run dir, with the scratch -trees its failing tests left behind. Targets that came back clean are swept as -usual. This matters most for the race tests, which may not fail the same way -twice — and because a re-run costs a full configure + build on every machine. - -Each run gets its own randomly-named build dir on every target -(`-`), so two or three runs can share the same fleet without -interfering. The dir is removed when the run ends — on success or failure, and -best-effort on Ctrl-C/kill; pass `--keep` to retain it for inspection. A hard -kill (`SIGKILL`), or a signal arriving mid-push, can leave a stray -`-` behind; sweep leftovers with -`python3 testsuite/fleettest.py --cleanup` (scope it with `--targets`, and only -run it when no other fleet runs are active, since it removes *all* matching run -dirs on the selected targets). - -Each target must be provisioned with the build toolchain its workflow installs -(autoconf, automake, a C compiler, perl, a python3 markdown module such as -cmarkgfm or commonmark unless the flags pass `--disable-md2man`, and the dev -libraries its configure flags enable). A missing piece shows up as `BUILD-FAIL`. - -## Differential regression hunting (abdiff.py) - -`testsuite/abdiff.py` is a developer tool — **not** a `*_test.py`, so `runtests.py` -ignores it. It hunts *regressions* by running the **same benign transfer** with -two rsync binaries (`A` = the build under test, `B` = a baseline) and comparing -the OUTCOME. The oracle is: for a benign input, a correctness/behaviour change -between the builds must be **invisible**, so A and B must produce an identical -result. Any divergence is a regression candidate to investigate and, if real, -minimize into a `*_test.py`. - -It compares exit code, stderr (error markers + normalised text), `--stats` -"Literal data", the destination tree (content + full metadata: mode/uid/gid/ -mtime/size/symlink target/xattrs/ACLs/hardlink grouping), the `--itemize` list, -and — with `--cost` — peak process-group RSS (a resource-regression oracle that -functional comparison misses). A **stability gate** runs each binary several -times and escalates on a candidate diff; nondeterministic scenarios are -quarantined `FLAKY`, never reported as regressions. - -Run it from the build directory (so `./rsync` and `old_versions/` resolve): +Target-specific skip allowances require a matching unsupported capability. Non-root and protocol passes are declared in the target entry. Do not point TCP fleet runs at shared hosts. + +## Differential testing +`testsuite/abdiff.py` runs the same transfer with two rsync binaries and compares the result, diagnostics, file data, metadata and optional peak memory. + +Examples: ```sh -testsuite/abdiff.py # default: ./rsync vs old_versions/rsync_3.4.1 -testsuite/abdiff.py --sweep all -j5 # broad single pass, 5-way parallel -testsuite/abdiff.py --loop --timelimit 3600 --cost # hunt for an hour, resource oracle on -testsuite/abdiff.py --list --sweep all # list scenarios without running +testsuite/abdiff.py +testsuite/abdiff.py --sweep all -j5 +testsuite/abdiff.py --loop --timelimit 3600 --cost ``` -Each finding is classed `DIFF` (regression candidate), `ALLOW` (an intentional, -documented behaviour change listed in the tool's allowlist), `BETTER` (A succeeds -where B fails), `FLAKY`, or `TIMEOUT`. Findings are printed and appended to a -per-run `abdiff-log_