From a984e5340f48975b80b8cf646f9b211e840d9c65 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Thu, 19 Mar 2026 10:53:56 -0700 Subject: [PATCH 1/2] Refactor compat test scripts and add Docker support for local testing Extract shared logic (build_rust, check_total, print_summary, generate_result_json) into util/common.sh. Make test directories configurable via GNU_DIR/BFS_DIR env vars so scripts work in both CI and local/Docker environments. Add Dockerfile.compat and util/docker-compat.sh for running the full compat suites locally. Simplify the CI workflow: remove empty "Extract testing info" step, hoist CARGO_INCREMENTAL=0 to workflow-level env, drop ||: that masked script errors, pass explicit GNU_DIR/BFS_DIR, and remove redundant artifact download-and-compare steps. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/compat.yml | 104 +++-------------------------------- Dockerfile.compat | 36 ++++++++++++ util/build-bfs.sh | 52 ++++++++---------- util/build-gnu.sh | 50 +++++++---------- util/common.sh | 46 ++++++++++++++++ util/docker-compat.sh | 49 +++++++++++++++++ 6 files changed, 185 insertions(+), 152 deletions(-) create mode 100644 Dockerfile.compat create mode 100755 util/common.sh create mode 100755 util/docker-compat.sh diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index 7f5278f5..0f027078 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -2,6 +2,9 @@ on: [push, pull_request] name: External-testsuites +env: + CARGO_INCREMENTAL: 0 + jobs: gnu-tests: permissions: @@ -40,12 +43,9 @@ jobs: shell: bash run: | cd findutils - export CARGO_INCREMENTAL=0 - bash util/build-gnu.sh ||: - - name: Extract testing info - shell: bash - run: | - + bash util/build-gnu.sh + env: + GNU_DIR: ${{ github.workspace }}/findutils.gnu - name: Upload gnu-test-report uses: actions/upload-artifact@v7 with: @@ -59,54 +59,10 @@ jobs: with: name: gnu-result path: gnu-result.json - - name: Download artifacts (gnu-result and gnu-test-report) - uses: actions/github-script@v8 - with: - script: | - let fs = require('fs'); - fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true }); - - async function downloadArtifact(artifactName) { - // List all artifacts from the workflow run - let artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{ github.run_id }}, - }); - - // Find the specified artifact - let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName); - if (!matchArtifact) { - throw new Error(`Artifact "${artifactName}" not found.`); - } - - // Download the artifact - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - - // Save the artifact to a file - fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data)); - } - - // Download the required artifacts - await downloadArtifact("gnu-result"); - await downloadArtifact("gnu-test-report"); - - name: Compare failing tests against master shell: bash run: | ./findutils/util/diff-gnu.sh ./dl ./findutils.gnu - - name: Compare against main results - shell: bash - run: | - unzip dl/gnu-result.zip -d dl/ - unzip dl/gnu-test-report.zip -d dl/ - mv dl/gnu-result.json latest-gnu-result.json - python findutils/util/compare_gnu_result.py bfs-tests: name: Run BFS tests @@ -133,8 +89,9 @@ jobs: shell: bash run: | cd findutils - export CARGO_INCREMENTAL=0 - bash util/build-bfs.sh ||: + bash util/build-bfs.sh + env: + BFS_DIR: ${{ github.workspace }}/bfs - name: Upload bfs-test-report uses: actions/upload-artifact@v7 with: @@ -145,53 +102,10 @@ jobs: with: name: bfs-result path: bfs-result.json - - name: Download artifacts (gnu-result and bfs-test-report) - uses: actions/github-script@v8 - with: - script: | - let fs = require('fs'); - fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true }); - - async function downloadArtifact(artifactName) { - // List all artifacts from the workflow run - let artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{ github.run_id }}, - }); - - // Find the specified artifact - let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName); - if (!matchArtifact) { - throw new Error(`Artifact "${artifactName}" not found.`); - } - - // Download the artifact - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - - // Save the artifact to a file - fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data)); - } - - // Download the required artifacts - await downloadArtifact("bfs-result"); - await downloadArtifact("bfs-test-report"); - name: Compare failing tests against main shell: bash run: | ./findutils/util/diff-bfs.sh dl/tests.log bfs/tests.log - - name: Compare against main results - shell: bash - run: | - unzip dl/bfs-result.zip -d dl/ - unzip dl/bfs-test-report.zip -d dl/ - mv dl/bfs-result.json latest-bfs-result.json - python findutils/util/compare_bfs_result.py upload-annotations: name: Upload annotations diff --git a/Dockerfile.compat b/Dockerfile.compat new file mode 100644 index 00000000..ee9a1712 --- /dev/null +++ b/Dockerfile.compat @@ -0,0 +1,36 @@ +FROM ubuntu:24.04 + +RUN apt-get update \ + && sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + git curl wget ca-certificates build-essential jq \ + autoconf automake autopoint texinfo dejagnu libcap2-bin \ + && apt-get build-dep -y findutils \ + && rm -rf /var/lib/apt/lists/* + +# Install Rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Clone and pre-build GNU findutils (matches CI ref) +RUN git clone https://github.com/gnu-mirror-unofficial/findutils.git /findutils.gnu \ + && cd /findutils.gnu \ + && git checkout 5768a03ddfb5e18b1682e339d6cdd24ff721c510 \ + && git submodule sync --recursive \ + && git config submodule.gnulib.url https://github.com/coreutils/gnulib.git \ + && git submodule update --init --recursive --depth 1 \ + && ./bootstrap \ + && ./configure --quiet \ + && make -j "$(nproc)" + +# Clone and pre-build BFS test utilities (matches CI ref) +RUN git clone --branch 4.0 https://github.com/tavianator/bfs.git /bfs \ + && cd /bfs \ + && ./configure NOLIBS=y \ + && make -j "$(nproc)" bin/tests/mksock bin/tests/xtouch + +# Build Rust artifacts to /target so we don't clobber the host's target/ +ENV CARGO_TARGET_DIR=/target + +WORKDIR /findutils diff --git a/util/build-bfs.sh b/util/build-bfs.sh index c394112e..3357c3f8 100755 --- a/util/build-bfs.sh +++ b/util/build-bfs.sh @@ -2,17 +2,27 @@ set -eo pipefail -if ! test -d ../bfs; then - echo "Could not find ../bfs" - echo "git clone https://github.com/tavianator/bfs.git" - exit 1 +# shellcheck source=util/common.sh +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +if test -z "$BFS_DIR"; then + if test -d "$FINDUTILS_DIR/../bfs"; then + BFS_DIR="$FINDUTILS_DIR/../bfs" + elif test -d "$FINDUTILS_DIR/../../tavianator/bfs"; then + BFS_DIR="$FINDUTILS_DIR/../../tavianator/bfs" + else + echo "Could not find bfs checkout" + echo "Set BFS_DIR or clone:" + echo " git clone https://github.com/tavianator/bfs.git $FINDUTILS_DIR/../bfs" + exit 1 + fi fi -# build the rust implementation -cargo build --release -FIND=$(readlink -f target/release/find) +# Build the Rust implementation +build_rust +FIND="$FIND_BIN" -cd ../bfs +cd "$BFS_DIR" ./configure NOLIBS=y make -j "$(nproc)" bin/tests/{mksock,xtouch} @@ -28,26 +38,12 @@ PASS=$(sed -En 's|^\[PASS] *([0-9]+) / .*|\1|p' "$LOG_FILE") SKIP=$(sed -En 's|^\[SKIP] *([0-9]+) / .*|\1|p' "$LOG_FILE") FAIL=$(sed -En 's|^\[FAIL] *([0-9]+) / .*|\1|p' "$LOG_FILE") -# Default any missing numbers to zero (e.g. no tests skipped) -: ${PASS:=0} -: ${SKIP:=0} -: ${FAIL:=0} +: "${PASS:=0}" +: "${SKIP:=0}" +: "${FAIL:=0}" TOTAL=$((PASS + SKIP + FAIL)) -if (( TOTAL <= 1 )); then - echo "Error in the execution, failing early" - exit 1 -fi -output="BFS tests summary = TOTAL: $TOTAL / PASS: $PASS / SKIP: $SKIP / FAIL: $FAIL" -echo "${output}" -if (( FAIL > 0 )); then echo "::warning ::${output}"; fi - -jq -n \ - --arg date "$(date --rfc-email)" \ - --arg sha "$GITHUB_SHA" \ - --arg total "$TOTAL" \ - --arg pass "$PASS" \ - --arg skip "$SKIP" \ - --arg fail "$FAIL" \ - '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, }}' > ../bfs-result.json +check_total "$TOTAL" +print_summary "BFS tests" "$TOTAL" "$PASS" "$SKIP" "$FAIL" +generate_result_json "${RESULT_FILE:-$BFS_DIR/../bfs-result.json}" "$TOTAL" "$PASS" "$SKIP" "$FAIL" diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 7de96759..39b5ce02 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -2,26 +2,32 @@ set -e -if test ! -d ../findutils.gnu; then - echo "Could not find ../findutils.gnu" - echo "git clone https://git.savannah.gnu.org/git/findutils.git findutils.gnu" +# shellcheck source=util/common.sh +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +GNU_DIR="${GNU_DIR:-$FINDUTILS_DIR/../findutils.gnu}" + +if ! test -d "$GNU_DIR"; then + echo "Could not find $GNU_DIR" + echo "Set GNU_DIR or clone:" + echo " git clone https://git.savannah.gnu.org/git/findutils.git $GNU_DIR" exit 1 fi -# build the rust implementation -cargo build --release -cp target/release/find ../findutils.gnu/find.rust -cp target/release/xargs ../findutils.gnu/xargs.rust +# Build the Rust implementation +build_rust +cp "$FIND_BIN" "$GNU_DIR/find.rust" +cp "$XARGS_BIN" "$GNU_DIR/xargs.rust" -# Clone and build upstream repo -cd ../findutils.gnu -if test ! -f configure; then +# Build upstream GNU findutils if needed +cd "$GNU_DIR" +if ! test -f configure; then ./bootstrap ./configure --quiet make -j "$(nproc)" fi -# overwrite the GNU version with the rust impl +# Overwrite the GNU versions with the Rust impl cp find.rust find/find cp xargs.rust xargs/xargs @@ -35,6 +41,7 @@ make check-TESTS $RUN_TEST || : make -C find/testsuite check || : make -C xargs/testsuite check || : +# Collect results PASS=0 SKIP=0 FAIL=0 @@ -65,21 +72,6 @@ if test -f "$LOG_FILE"; then ((ERROR += $(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : fi -if ((TOTAL <= 1)); then - echo "Error in the execution, failing early" - exit 1 -fi - -output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" -echo "${output}" -if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi -jq -n \ - --arg date "$(date --rfc-email)" \ - --arg sha "$GITHUB_SHA" \ - --arg total "$TOTAL" \ - --arg pass "$PASS" \ - --arg skip "$SKIP" \ - --arg fail "$FAIL" \ - --arg xpass "$XPASS" \ - --arg error "$ERROR" \ - '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > ../gnu-result.json +check_total "$TOTAL" +print_summary "GNU tests" "$TOTAL" "$PASS" "$SKIP" "$FAIL" "$ERROR" +generate_result_json "${RESULT_FILE:-$GNU_DIR/../gnu-result.json}" "$TOTAL" "$PASS" "$SKIP" "$FAIL" "$XPASS" "$ERROR" diff --git a/util/common.sh b/util/common.sh new file mode 100755 index 00000000..4c4fff3a --- /dev/null +++ b/util/common.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Common utilities for findutils compatibility test scripts. + +COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FINDUTILS_DIR="$(cd "$COMMON_DIR/.." && pwd)" +CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$FINDUTILS_DIR/target}" + +FIND_BIN="$CARGO_TARGET_DIR/release/find" +XARGS_BIN="$CARGO_TARGET_DIR/release/xargs" + +build_rust() { + echo "Building Rust findutils..." + (cd "$FINDUTILS_DIR" && cargo build --release) +} + +check_total() { + local total="$1" + if (( total <= 1 )); then + echo "Error in the execution, failing early" + exit 1 + fi +} + +print_summary() { + local label="$1" total="$2" pass="$3" skip="$4" fail="$5" error="${6:-0}" + local output="$label summary = TOTAL: $total / PASS: $pass / SKIP: $skip / FAIL: $fail / ERROR: $error" + echo "$output" + if (( fail > 0 || error > 0 )); then + echo "::warning ::$output" + fi +} + +generate_result_json() { + local output_file="$1" total="$2" pass="$3" skip="$4" fail="$5" + local xpass="${6:-0}" error="${7:-0}" + jq -n \ + --arg date "$(date --rfc-email 2>/dev/null || date '+%a, %d %b %Y %T %z')" \ + --arg sha "${GITHUB_SHA:-$(git -C "$FINDUTILS_DIR" rev-parse HEAD 2>/dev/null || echo unknown)}" \ + --arg total "$total" \ + --arg pass "$pass" \ + --arg skip "$skip" \ + --arg fail "$fail" \ + --arg xpass "$xpass" \ + --arg error "$error" \ + '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > "$output_file" +} diff --git a/util/docker-compat.sh b/util/docker-compat.sh new file mode 100755 index 00000000..c23df929 --- /dev/null +++ b/util/docker-compat.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Run compatibility tests inside Docker. +# +# Usage: +# util/docker-compat.sh gnu # Run GNU findutils tests +# util/docker-compat.sh bfs # Run BFS tests +# util/docker-compat.sh gnu sv-bug # Run a single GNU test +# util/docker-compat.sh bfs --verbose=tests --gnu # Custom BFS flags + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FINDUTILS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +IMAGE_NAME="findutils-compat" + +# Build the Docker image if it doesn't exist (or if --build is passed) +if [[ "$1" == "--build" ]]; then + shift + docker build -t "$IMAGE_NAME" -f "$FINDUTILS_DIR/Dockerfile.compat" "$FINDUTILS_DIR" +elif ! docker image inspect "$IMAGE_NAME" &>/dev/null; then + echo "Image '$IMAGE_NAME' not found, building (this takes a while the first time)..." + docker build -t "$IMAGE_NAME" -f "$FINDUTILS_DIR/Dockerfile.compat" "$FINDUTILS_DIR" +fi + +suite="${1:?Usage: $0 [--build] [args...]}" +shift + +case "$suite" in + gnu) + docker run --rm \ + -v "$FINDUTILS_DIR:/findutils" \ + -e GNU_DIR=/findutils.gnu \ + "$IMAGE_NAME" \ + bash util/build-gnu.sh "$@" + ;; + bfs) + docker run --rm \ + -v "$FINDUTILS_DIR:/findutils" \ + -e BFS_DIR=/bfs \ + "$IMAGE_NAME" \ + bash util/build-bfs.sh "$@" + ;; + *) + echo "Unknown suite: $suite" + echo "Usage: $0 [--build] [args...]" + exit 1 + ;; +esac From 7761a3d8bfbd6b93a7b331ed3907f4053a560663 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Thu, 19 Mar 2026 11:19:02 -0700 Subject: [PATCH 2/2] Fix flaky find_printf test on Windows The %A+ format uses chrono's %.f which trims trailing zeros from fractional seconds. On Windows (100ns resolution) this produces fewer digits than on Linux (nanosecond resolution), causing the regex to fail intermittently. Use \d+0 instead of \d{9}0. Co-Authored-By: Claude Opus 4.6 --- tests/find_cmd_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/find_cmd_tests.rs b/tests/find_cmd_tests.rs index 59638b90..4350771e 100644 --- a/tests/find_cmd_tests.rs +++ b/tests/find_cmd_tests.rs @@ -568,7 +568,7 @@ fn find_printf() { println!("Actual output: '{}'", output_str.trim()); - let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\+\d{2}:\d{2}:\d{2}\.\d{9}0$") + let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\+\d{2}:\d{2}:\d{2}\.\d+0$") .expect("Failed to compile regex"); assert!(