From 83d49b3c7f0b39b99f4069af3128b3db8cf5a85d Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 21 Jan 2026 08:24:46 -0600 Subject: [PATCH] ci: add run-tests composite action (#8159) * ci: add run-tests composite action * fixup! ci: add run-tests composite action send sanitizer log messages to stderr otherwise, they will break transmission-show tests * fixup! ci: add run-tests composite action fix windows, alpine breakage * fixup! ci: add run-tests composite action fix: sanitizer logging to stderr * fixup! ci: add run-tests composite action disable asan leak detection on macOS: the feature is unsupported there * fixup! refactor: extract platform detection into its own composite action (#8158) ensure bash is installed on alpine linux --- .github/actions/prepare-deps-win32/action.yml | 6 +- .github/actions/run-tests/action.yml | 157 ++++++++++++++++++ .github/actions/set-test-env/action.yml | 98 +++++++++++ .github/workflows/actions.yml | 70 +++++--- 4 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 .github/actions/run-tests/action.yml create mode 100644 .github/actions/set-test-env/action.yml diff --git a/.github/actions/prepare-deps-win32/action.yml b/.github/actions/prepare-deps-win32/action.yml index 88d8d00c0..92fefe4fe 100644 --- a/.github/actions/prepare-deps-win32/action.yml +++ b/.github/actions/prepare-deps-win32/action.yml @@ -29,8 +29,9 @@ runs: id: cache-key shell: pwsh run: | + $RepoRoot = if (Test-Path (Join-Path . 'src/release/windows/main.ps1')) { Join-Path . 'src' } else { (Get-Item .).FullName } try { - $DepsHash = & (Join-Path . src release windows main.ps1) -Mode DepsHash -BuildArch ${{ inputs.arch }} -BuildPart ${{ inputs.type }} + $DepsHash = & (Join-Path $RepoRoot release windows main.ps1) -Mode DepsHash -BuildArch ${{ inputs.arch }} -BuildPart ${{ inputs.type }} "hash=${DepsHash}" | Out-File $Env:GITHUB_OUTPUT -Append } catch { Write-Error ("{1}{0}{2}{0}{3}" -f [Environment]::NewLine, $_.ToString(), $_.InvocationInfo.PositionMessage, $_.ScriptStackTrace) -ErrorAction Continue @@ -46,8 +47,9 @@ runs: if: steps.restore-cache.outputs.cache-hit != 'true' shell: pwsh run: | + $RepoRoot = if (Test-Path (Join-Path . 'src/release/windows/main.ps1')) { Join-Path . 'src' } else { (Get-Item .).FullName } try { - & (Join-Path . src release windows main.ps1) -Mode Build -BuildArch ${{ inputs.arch }} -BuildPart ${{ inputs.type }} + & (Join-Path $RepoRoot release windows main.ps1) -Mode Build -BuildArch ${{ inputs.arch }} -BuildPart ${{ inputs.type }} } catch { Write-Error ("{1}{0}{2}{0}{3}" -f [Environment]::NewLine, $_.ToString(), $_.InvocationInfo.PositionMessage, $_.ScriptStackTrace) -ErrorAction Continue exit 1 diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 000000000..041763259 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,157 @@ +name: Run tests +description: Run ctest with crash-friendly settings and optional fallbacks +inputs: + setup-env: + description: "Call set-test-env before running tests" + required: false + default: "true" + build-dir: + description: "CTest build directory" + required: false + default: "obj" + build-config: + description: "CTest build configuration" + required: false + default: "" + parallel: + description: "Number of parallel jobs (auto if empty)" + required: false + default: "" + timeout: + description: "CTest timeout in seconds" + required: false + default: "" + extra-ctest-args: + description: "Additional ctest arguments" + required: false + default: "" + enable-sanitizers: + description: "Whether sanitizer options are enabled (affects fallback choices)" + required: false + default: "false" + enable-asan: + description: "Explicitly enable ASan options" + required: false + default: "" + enable-ubsan: + description: "Explicitly enable UBSan options" + required: false + default: "" + enable-lsan: + description: "Explicitly enable LSan options" + required: false + default: "" + extra-asan-options: + description: "Additional ASan options to append" + required: false + default: "" + extra-ubsan-options: + description: "Additional UBSan options to append" + required: false + default: "" + extra-lsan-options: + description: "Additional LSan options to append" + required: false + default: "" + use-catchsegv: + description: "Use catchsegv on Linux if available" + required: false + default: "true" + enable-macos-crash-reports: + description: "Attempt to collect macOS crash reports on failure" + required: false + default: "true" +runs: + using: composite + steps: + - name: Set test environment + if: ${{ inputs.setup-env == 'true' }} + uses: ./.github/actions/set-test-env + with: + enable-sanitizers: ${{ inputs.enable-sanitizers }} + enable-asan: ${{ inputs.enable-asan }} + enable-ubsan: ${{ inputs.enable-ubsan }} + enable-lsan: ${{ inputs.enable-lsan }} + extra-asan-options: ${{ inputs.extra-asan-options }} + extra-ubsan-options: ${{ inputs.extra-ubsan-options }} + extra-lsan-options: ${{ inputs.extra-lsan-options }} + - name: Detect platform + id: platform + uses: ./.github/actions/detect-platform + - name: Run ctest + shell: bash + env: + OS_FAMILY: ${{ steps.platform.outputs.os-family }} + run: | + set -euo pipefail + + build_dir="${{ inputs.build-dir }}" + build_config="${{ inputs.build-config }}" + parallel="${{ inputs.parallel }}" + timeout="${{ inputs.timeout }}" + extra_args="${{ inputs.extra-ctest-args }}" + enable_sanitizers="${{ inputs.enable-sanitizers }}" + use_catchsegv="${{ inputs.use-catchsegv }}" + enable_macos_crash_reports="${{ inputs.enable-macos-crash-reports }}" + + if [[ -z "$parallel" ]]; then + if command -v nproc >/dev/null 2>&1; then + parallel=$(nproc) + elif command -v sysctl >/dev/null 2>&1; then + parallel=$(sysctl -n hw.logicalcpu) + elif [[ -n "${NUMBER_OF_PROCESSORS:-}" ]]; then + parallel="$NUMBER_OF_PROCESSORS" + else + parallel=1 + fi + fi + + args=("--test-dir" "$build_dir" "-j" "$parallel" "--output-on-failure") + if [[ -n "$build_config" ]]; then + args+=("--build-config" "$build_config") + fi + if [[ -n "$timeout" ]]; then + args+=("--timeout" "$timeout") + fi + if [[ -n "$extra_args" ]]; then + args+=($extra_args) + fi + + crash_marker="" + if [[ "${OS_FAMILY:-}" == "macos" && "$enable_macos_crash_reports" == "true" ]]; then + crash_marker="$RUNNER_TEMP/ctest-crash-marker" + touch "$crash_marker" + fi + + run_ctest() { + if [[ "${OS_FAMILY:-}" == "linux" && "$enable_sanitizers" != "true" && "$use_catchsegv" == "true" ]] && command -v catchsegv >/dev/null 2>&1; then + catchsegv ctest "${args[@]}" + else + ctest "${args[@]}" + fi + } + + set +e + run_ctest + status=$? + set -e + + if [[ $status -ne 0 && "${OS_FAMILY:-}" == "macos" && "$enable_macos_crash_reports" == "true" ]]; then + echo "::group::macOS crash reports" + reports_found=0 + for report_dir in "$HOME/Library/Logs/DiagnosticReports" "/Library/Logs/DiagnosticReports"; do + if [[ -d "$report_dir" ]]; then + while IFS= read -r report; do + reports_found=1 + echo "--- $report ---" + tail -n 200 "$report" || true + done < <(find "$report_dir" -maxdepth 1 -type f \( -name "*.crash" -o -name "*.ips" \) -newer "$crash_marker" -print 2>/dev/null || true) + fi + done + if [[ $reports_found -eq 0 ]]; then + echo "No new crash reports found after test run." + fi + echo "::endgroup::" + fi + + exit $status diff --git a/.github/actions/set-test-env/action.yml b/.github/actions/set-test-env/action.yml new file mode 100644 index 000000000..16a197c07 --- /dev/null +++ b/.github/actions/set-test-env/action.yml @@ -0,0 +1,98 @@ +name: Set test environment +description: Configure environment variables for richer crash diagnostics in CI +inputs: + enable-sanitizers: + description: "Enable sanitizer runtime options" + required: false + default: "false" + enable-asan: + description: "Explicitly enable ASan options" + required: false + default: "" + enable-ubsan: + description: "Explicitly enable UBSan options" + required: false + default: "" + enable-lsan: + description: "Explicitly enable LSan options" + required: false + default: "" + extra-asan-options: + description: "Additional ASan options to append" + required: false + default: "" + extra-ubsan-options: + description: "Additional UBSan options to append" + required: false + default: "" + extra-lsan-options: + description: "Additional LSan options to append" + required: false + default: "" +runs: + using: composite + steps: + - name: Detect platform + id: platform + uses: ./.github/actions/detect-platform + - name: Configure test environment + shell: bash + env: + OS_FAMILY: ${{ steps.platform.outputs.os-family }} + run: | + set -euo pipefail + + echo "CTEST_OUTPUT_ON_FAILURE=1" >> "$GITHUB_ENV" + echo "QT_QPA_PLATFORM=offscreen" >> "$GITHUB_ENV" + + enable_sanitizers="${{ inputs.enable-sanitizers }}" + enable_asan="${{ inputs.enable-asan }}" + enable_ubsan="${{ inputs.enable-ubsan }}" + enable_lsan="${{ inputs.enable-lsan }}" + + if [[ -z "$enable_asan" && -z "$enable_ubsan" && -z "$enable_lsan" ]]; then + enable_asan="$enable_sanitizers" + enable_ubsan="$enable_sanitizers" + enable_lsan="$enable_sanitizers" + fi + + if command -v llvm-symbolizer >/dev/null 2>&1; then + echo "ASAN_SYMBOLIZER_PATH=$(command -v llvm-symbolizer)" >> "$GITHUB_ENV" + echo "LLVM_SYMBOLIZER_PATH=$(command -v llvm-symbolizer)" >> "$GITHUB_ENV" + elif command -v llvm-symbolizer-20 >/dev/null 2>&1; then + echo "ASAN_SYMBOLIZER_PATH=$(command -v llvm-symbolizer-20)" >> "$GITHUB_ENV" + echo "LLVM_SYMBOLIZER_PATH=$(command -v llvm-symbolizer-20)" >> "$GITHUB_ENV" + elif command -v llvm-symbolizer-19 >/dev/null 2>&1; then + echo "ASAN_SYMBOLIZER_PATH=$(command -v llvm-symbolizer-19)" >> "$GITHUB_ENV" + echo "LLVM_SYMBOLIZER_PATH=$(command -v llvm-symbolizer-19)" >> "$GITHUB_ENV" + fi + + if [[ "$enable_asan" == "true" ]]; then + os_family="${OS_FAMILY:-unknown}" + detect_leaks_opt="detect_leaks=1" + if [[ "$os_family" == "macos" ]]; then + # don't run on unsupported platforms + detect_leaks_opt="detect_leaks=0" + fi + asan_opts="abort_on_error=1:allocator_may_return_null=1:check_initialization_order=1:${detect_leaks_opt}:halt_on_error=1:print_stacktrace=1:strict_string_checks=1:symbolize=1:verbosity=0" + if [[ -n "${{ inputs.extra-asan-options }}" ]]; then + asan_opts+="${asan_opts:+:}${{ inputs.extra-asan-options }}" + fi + echo "ASAN_OPTIONS=$asan_opts" >> "$GITHUB_ENV" + fi + + if [[ "$enable_ubsan" == "true" ]]; then + ubsan_opts="halt_on_error=1:print_stacktrace=1:report_error_type=1:symbolize=1" + if [[ -n "${{ inputs.extra-ubsan-options }}" ]]; then + ubsan_opts+="${ubsan_opts:+:}${{ inputs.extra-ubsan-options }}" + fi + echo "UBSAN_OPTIONS=$ubsan_opts" >> "$GITHUB_ENV" + fi + + if [[ "$enable_lsan" == "true" ]]; then + lsan_opts="verbosity=0:log_threads=0:print_suppressions=1" + if [[ -n "${{ inputs.extra-lsan-options }}" ]]; then + lsan_opts+="${lsan_opts:+:}${{ inputs.extra-lsan-options }}" + fi + echo "LSAN_OPTIONS=$lsan_opts" >> "$GITHUB_ENV" + fi diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 2d6aa4b35..43df717c3 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -166,7 +166,11 @@ jobs: - name: Make run: cmake --build obj --config Debug --target libtransmission-test transmission-show - name: Test with sanitizers - run: cmake -E chdir obj ctest -j $(nproc) --build-config Debug --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: Debug + enable-sanitizers: true sanitizer-tests-macos: runs-on: macos-26 @@ -208,7 +212,11 @@ jobs: - name: Make run: cmake --build obj --config Debug --target libtransmission-test transmission-show - name: Test with sanitizers - run: cmake -E chdir obj ctest -j $(nproc) --build-config Debug --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: Debug + enable-sanitizers: true clang-tidy-libtransmission: runs-on: ubuntu-24.04 @@ -385,8 +393,10 @@ jobs: if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} env: TMPDIR: /private/tmp - QT_QPA_PLATFORM: offscreen - run: cmake -E chdir obj ctest -j $(sysctl -n hw.logicalcpu) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6 @@ -412,6 +422,7 @@ jobs: apk update apk add \ ca-certificates \ + bash \ clang \ cmake \ crc32c-dev \ @@ -436,12 +447,11 @@ jobs: - name: Get Source uses: actions/checkout@v6 with: - path: src submodules: recursive - name: Configure run: | cmake \ - -S src \ + -S . \ -B obj \ -G Ninja \ -DCMAKE_C_COMPILER='clang' \ @@ -465,8 +475,10 @@ jobs: if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} env: TMPDIR: /private/tmp - QT_QPA_PLATFORM: offscreen - run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6 @@ -496,10 +508,9 @@ jobs: - name: Get Source uses: actions/checkout@v6 with: - path: src submodules: recursive - name: Prepare Build Deps - uses: ./src/.github/actions/prepare-deps-win32 + uses: ./.github/actions/prepare-deps-win32 with: arch: ${{ matrix.arch }} type: Deps @@ -507,7 +518,7 @@ jobs: run: | Import-VisualStudioVars -VisualStudioVersion 2022 -Architecture ${{ matrix.arch }} cmake ` - -S src ` + -S . ` -B obj ` -G Ninja ` -DCMAKE_BUILD_TYPE=RelWithDebInfo ` @@ -529,7 +540,11 @@ jobs: cmake --build obj --config RelWithDebInfo - name: Test if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} - run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure --timeout 600 + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo + timeout: 600 - name: Install run: cmake --install obj --config RelWithDebInfo - name: Package @@ -620,6 +635,12 @@ jobs: - name: Get Dependencies (Qt) if: ${{ needs.what-to-make.outputs.make-qt == 'true' }} run: brew install --formula qt + - name: Get Composite Actions + uses: actions/checkout@v6 + with: + sparse-checkout: | + .github/actions + sparse-checkout-cone-mode: false - name: Get Source uses: actions/download-artifact@v7 with: @@ -659,8 +680,10 @@ jobs: if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} env: TMPDIR: /private/tmp - QT_QPA_PLATFORM: offscreen - run: cmake -E chdir obj ctest -j $(sysctl -n hw.logicalcpu) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6 @@ -726,9 +749,10 @@ jobs: run: cmake --build obj --config RelWithDebInfo - name: Test if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} - env: - QT_QPA_PLATFORM: offscreen - run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6 @@ -800,9 +824,10 @@ jobs: run: cmake --build obj --config RelWithDebInfo - name: Test if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} - env: - QT_QPA_PLATFORM: offscreen - run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6 @@ -870,7 +895,10 @@ jobs: if: ${{ needs.what-to-make.outputs.make-tests == 'true' }} env: TMPDIR: /private/tmp - run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure + uses: ./.github/actions/run-tests + with: + build-dir: obj + build-config: RelWithDebInfo - name: Install run: cmake --install obj --config RelWithDebInfo --strip - uses: actions/upload-artifact@v6