From 12757597032785276fb5cd355723752221f007f6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 22:48:29 -0400 Subject: [PATCH 1/6] Add coverage script --- Scripts/coverage.sh | 197 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100755 Scripts/coverage.sh diff --git a/Scripts/coverage.sh b/Scripts/coverage.sh new file mode 100755 index 0000000..9de23e3 --- /dev/null +++ b/Scripts/coverage.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# +# coverage.sh — run a SwiftPM test suite with code coverage, export LCOV and +# Cobertura reports, and enforce a minimum line-coverage threshold. +# +# Coverage is measured against the package's OWN sources only (everything under +# the repo's `Sources/` directory). Dependencies (checked out under `.build`), +# generated sources, and the test target are excluded, so the number reflects +# the coverage of this package's code rather than being diluted by third-party +# code that the tests happen to exercise. +# +# The Cobertura XML (coverage.xml) is the format GitHub Code Quality consumes via +# `actions/upload-code-coverage`; the LCOV report (coverage.lcov) is kept for +# other tools (Codecov, Coveralls, editors) that prefer it. +# +# Usage: +# Scripts/coverage.sh [threshold] +# +# Environment variables: +# COVERAGE_THRESHOLD Minimum line coverage percentage (default: 80). +# Overridden by the optional [threshold] argument. +# COVERAGE_SOURCE_PREFIX Absolute path prefix selecting the sources that count +# toward coverage (default: "$PWD/Sources/"). Narrow it +# to a single target, e.g. "$PWD/Sources/MyLibrary/", to +# gate on one target instead of every source file. +# COVERAGE_OUTPUT LCOV output file (default: .build/coverage/coverage.lcov). +# COBERTURA_OUTPUT Cobertura XML output file (default: .build/coverage/coverage.xml). +# SKIP_TEST If set to 1, reuse existing coverage data instead of +# re-running `swift test` (useful while iterating). + +set -euo pipefail + +THRESHOLD="${1:-${COVERAGE_THRESHOLD:-80}}" +SOURCE_PREFIX="${COVERAGE_SOURCE_PREFIX:-$PWD/Sources/}" +OUTPUT="${COVERAGE_OUTPUT:-.build/coverage/coverage.lcov}" +COBERTURA_OUTPUT="${COBERTURA_OUTPUT:-.build/coverage/coverage.xml}" + +# Resolve the correct llvm-cov (xcrun on Apple platforms, plain llvm-cov elsewhere). +if command -v xcrun >/dev/null 2>&1; then + LLVM_COV="xcrun llvm-cov" +else + LLVM_COV="llvm-cov" +fi + +# 1. Run the tests with coverage instrumentation. +if [ "${SKIP_TEST:-0}" != "1" ]; then + echo "==> Running tests with code coverage" + swift test --enable-code-coverage +fi + +# 2. Locate the coverage artifacts SwiftPM produced. +CODECOV_JSON="$(swift test --enable-code-coverage --show-codecov-path)" +COV_DIR="$(dirname "$CODECOV_JSON")" +PROFDATA="$COV_DIR/default.profdata" + +if [ ! -f "$CODECOV_JSON" ]; then + echo "error: coverage report not found at $CODECOV_JSON" >&2 + exit 1 +fi + +# 3. Locate the instrumented test binary (differs by platform: on macOS it lives +# inside the .xctest bundle, on Linux the .xctest file is the binary itself). +BIN_PATH="$(swift build --show-bin-path)" +TEST_BINARY="" +for candidate in \ + "$BIN_PATH"/*PackageTests.xctest/Contents/MacOS/*PackageTests \ + "$BIN_PATH"/*PackageTests.xctest; do + if [ -f "$candidate" ]; then + TEST_BINARY="$candidate" + break + fi +done + +# 4. Export an LCOV report (for Codecov / Coveralls / editors). +mkdir -p "$(dirname "$OUTPUT")" +if [ -n "$TEST_BINARY" ] && [ -f "$PROFDATA" ]; then + echo "==> Exporting LCOV report to $OUTPUT" + $LLVM_COV export \ + -format=lcov \ + -instr-profile "$PROFDATA" \ + "$TEST_BINARY" \ + -ignore-filename-regex='.build/(checkouts|.*\.build)/|Tests/|\.derived/|DerivedSources/' \ + > "$OUTPUT" +else + echo "warning: could not locate test binary or profdata; skipping LCOV export" >&2 +fi + +# 5. Generate a Cobertura XML report (for GitHub Code Quality / upload-code-coverage). +echo "==> Writing Cobertura report to $COBERTURA_OUTPUT" +mkdir -p "$(dirname "$COBERTURA_OUTPUT")" +python3 - "$CODECOV_JSON" "$SOURCE_PREFIX" "$PWD" "$COBERTURA_OUTPUT" <<'PY' +import json, os, sys, time +from xml.sax.saxutils import escape, quoteattr + +report_path, source_prefix, repo_root, output = sys.argv[1:5] + +with open(report_path) as f: + report = json.load(f) + +files = [] +total_covered = total_lines = 0 +for file in report["data"][0]["files"]: + name = file["filename"] + if not name.startswith(source_prefix): + continue + # Per-line hit count: the greatest region count starting on each line (a line + # is covered if any region on it executed, so branch sub-regions don't hide it). + line_hits = {} + for seg in file["segments"]: + line, count, has_count = seg[0], seg[2], seg[3] + if has_count: + line_hits[line] = max(line_hits.get(line, 0), count) + covered = sum(1 for h in line_hits.values() if h > 0) + total_covered += covered + total_lines += len(line_hits) + files.append((os.path.relpath(name, repo_root), line_hits, covered, len(line_hits))) + +def rate(c, t): + return c / t if t else 0.0 + +overall = rate(total_covered, total_lines) +timestamp = int(time.time()) + +out = [ + '', + '', + '' + % (overall, total_covered, total_lines, timestamp), + " ", + " %s" % escape(repo_root), + " ", + " ", + ' ' + % (escape(os.path.basename(repo_root)), overall), + " ", +] +for rel, line_hits, covered, total in sorted(files): + out.append( + ' ' + % (quoteattr(os.path.basename(rel)), quoteattr(rel), rate(covered, total)) + ) + out.append(" ") + out.append(" ") + for line in sorted(line_hits): + out.append(' ' % (line, line_hits[line])) + out.append(" ") + out.append(" ") +out += [" ", " ", " ", ""] + +with open(output, "w") as f: + f.write("\n".join(out) + "\n") + +print(" %d/%d lines (%.2f%%)" % (total_covered, total_lines, 100 * overall)) +PY + +# 6. Compute line coverage for the package sources and enforce the threshold. +echo "==> Computing coverage for ${SOURCE_PREFIX}" +python3 - "$CODECOV_JSON" "$SOURCE_PREFIX" "$THRESHOLD" "$PWD" <<'PY' +import json, os, sys + +report_path, source_prefix, threshold, repo_root = sys.argv[1], sys.argv[2], float(sys.argv[3]), sys.argv[4] + +with open(report_path) as f: + report = json.load(f) + +covered = total = 0 +rows = [] +for file in report["data"][0]["files"]: + name = file["filename"] + if not name.startswith(source_prefix): + continue + lines = file["summary"]["lines"] + covered += lines["covered"] + total += lines["count"] + rows.append((lines["percent"], os.path.relpath(name, repo_root))) + +if total == 0: + print("error: no source files matched prefix %r" % source_prefix, file=sys.stderr) + print(" (set COVERAGE_SOURCE_PREFIX to your package's Sources path)", file=sys.stderr) + sys.exit(1) + +percent = 100.0 * covered / total + +for pct, name in sorted(rows): + print(" %6.2f%% %s" % (pct, name)) + +print("-" * 48) +print("Total line coverage: %.2f%% (%d/%d lines)" % (percent, covered, total)) +print("Required threshold: %.2f%%" % threshold) + +if percent < threshold: + print("FAILED: coverage %.2f%% is below the %.2f%% threshold" % (percent, threshold), file=sys.stderr) + sys.exit(1) + +print("PASSED: coverage meets the threshold") +PY From 3f6a204af6bc33bd8dc8619e4a7db56d4e1a43df Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 22:48:37 -0400 Subject: [PATCH 2/6] Update GitHub CI --- .github/workflows/swift.yml | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 369d024..b5ca3bd 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -26,6 +26,45 @@ jobs: - name: Test (Debug) run: swift test --configuration debug + coverage: + name: Code Coverage + strategy: + matrix: + swift: [5.7.3] + os: [macos-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + code-quality: write # required by actions/upload-code-coverage + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.3.0 + with: + version: ${{ matrix.swift }} + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Export coverage and enforce threshold + run: Scripts/coverage.sh 70 + - name: Upload coverage to GitHub Code Quality + uses: actions/upload-code-coverage@v1 + with: + file: .build/coverage/coverage.xml + language: Swift + label: code-coverage/llvm-cov + # Code Quality was in public preview until 2026-07-20; keep this while + # the repo might not have the feature enabled, so a failed upload + # doesn't break CI. Remove once GA is enabled everywhere. + fail-on-error: false + - name: Upload coverage artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: code-coverage + path: .build/coverage/ + if-no-files-found: error + linux: name: Linux strategy: From 8b88ee606d470ab65bf6c77386ac10d79970330a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 22:57:33 -0400 Subject: [PATCH 3/6] Update CI to Swift 6.3.3 and fix job setup failures Replace the unmaintained slashmo/install-swift action, which pulled in the deprecated actions/cache@v2 and caused the macOS jobs to hard-fail at "Set up job". macOS now uses the runner's preinstalled Swift 6.3.3 toolchain and Linux runs in the official swift:6.3.3 container. Bump actions/checkout to v4. --- .github/workflows/swift.yml | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index b5ca3bd..675ccd2 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -5,18 +5,10 @@ on: [push] jobs: mac: name: macOS - strategy: - matrix: - swift: [5.7.3] - os: [macos-latest] - runs-on: ${{ matrix.os }} + runs-on: macos-latest steps: - - name: Install Swift - uses: slashmo/install-swift@v0.3.0 - with: - version: ${{ matrix.swift }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Swift Version run: swift --version - name: Build (Debug) @@ -28,21 +20,13 @@ jobs: coverage: name: Code Coverage - strategy: - matrix: - swift: [5.7.3] - os: [macos-latest] - runs-on: ${{ matrix.os }} + runs-on: macos-latest permissions: contents: read code-quality: write # required by actions/upload-code-coverage steps: - - name: Install Swift - uses: slashmo/install-swift@v0.3.0 - with: - version: ${{ matrix.swift }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Swift Version run: swift --version - name: Export coverage and enforce threshold @@ -69,16 +53,12 @@ jobs: name: Linux strategy: matrix: - swift: [5.1.5, 5.5, 5.6.3, 5.7.3] - os: [ubuntu-20.04] - runs-on: ${{ matrix.os }} + swift: [6.3.3] + runs-on: ubuntu-latest + container: swift:${{ matrix.swift }} steps: - - name: Install Swift - uses: slashmo/install-swift@v0.3.0 - with: - version: ${{ matrix.swift }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Swift Version run: swift --version - name: Build (Debug) From 2a2696f24faa34b84dfe363291b2db8ce477d70e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 23:04:52 -0400 Subject: [PATCH 4/6] Modernize Swift CI and drop armv7 jobs Follow the junkbot-swift CI layout: macOS on macos-26 and Linux across a swift:latest + nightly container matrix, each building and testing debug and release with actions/checkout@v4. Remove the Buildroot and Linux armv7 cross-compile workflows, which were failing. --- .github/workflows/buildroot.yml | 26 --------------- .github/workflows/swift-arm.yml | 24 -------------- .github/workflows/swift.yml | 57 +++++++++++++++++---------------- 3 files changed, 29 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/buildroot.yml delete mode 100644 .github/workflows/swift-arm.yml diff --git a/.github/workflows/buildroot.yml b/.github/workflows/buildroot.yml deleted file mode 100644 index c26e7cd..0000000 --- a/.github/workflows/buildroot.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Buildroot - -on: [push] - -jobs: - - buildroot-armv7-build: - name: Build Armv7 - runs-on: ubuntu-20.04 - container: colemancda/swift-buildroot:amd64-prebuilt-armv7 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Swift Version - run: swift --version - - name: Build - run: | - cd /usr/src/buildroot-external - export SWIFT_ARCH=armv7 - export SWIFT_PACKAGE_PATH=$GITHUB_WORKSPACE - ./build-swift-package.sh - - name: Archive Build artifacts - uses: actions/upload-artifact@v3 - with: - name: swiftpm-build-armv7 - path: .build/*/*.xctest diff --git a/.github/workflows/swift-arm.yml b/.github/workflows/swift-arm.yml deleted file mode 100644 index de3f377..0000000 --- a/.github/workflows/swift-arm.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Swift ARM -on: [push] -jobs: - - linux-armv7-crosscompile-build: - name: Crosscompile for Linux Armv7 - runs-on: ubuntu-latest - container: colemancda/swift-armv7:latest-prebuilt - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Swift Version - run: swift --version - - name: Build (Release) - run: | - cd $SRC_ROOT - export SWIFT_PACKAGE_SRCDIR=$GITHUB_WORKSPACE - export SWIFT_PACKAGE_BUILDDIR=$SWIFT_PACKAGE_SRCDIR/.build - $SRC_ROOT/build-swift-package.sh - - name: Archive Build artifacts - uses: actions/upload-artifact@v3 - with: - name: linux-armv7-crosscompile-test - path: .build/*/*.xctest diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 675ccd2..85a2a44 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -3,24 +3,44 @@ name: Swift on: [push] jobs: - mac: + macos: name: macOS - runs-on: macos-latest + runs-on: macos-26 + strategy: + matrix: + config: ["debug", "release"] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Swift Version + run: swift --version + - name: Build + run: swift build -c ${{ matrix.config }} + - name: Test + run: swift test -c ${{ matrix.config }} + + linux: + name: Linux (${{ matrix.container }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + container: ["swift:latest", "swiftlang/swift:nightly-noble"] + config: ["debug", "release"] + container: ${{ matrix.container }} steps: - name: Checkout uses: actions/checkout@v4 - name: Swift Version run: swift --version - - name: Build (Debug) - run: swift build -c debug - - name: Build (Release) - run: swift build -c release - - name: Test (Debug) - run: swift test --configuration debug + - name: Build + run: swift build -c ${{ matrix.config }} + - name: Test + run: swift test -c ${{ matrix.config }} coverage: name: Code Coverage - runs-on: macos-latest + runs-on: macos-26 permissions: contents: read code-quality: write # required by actions/upload-code-coverage @@ -48,22 +68,3 @@ jobs: name: code-coverage path: .build/coverage/ if-no-files-found: error - - linux: - name: Linux - strategy: - matrix: - swift: [6.3.3] - runs-on: ubuntu-latest - container: swift:${{ matrix.swift }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Swift Version - run: swift --version - - name: Build (Debug) - run: swift build -c debug - - name: Build (Release) - run: swift build -c release - - name: Test (Debug) - run: swift test --configuration debug From eaca02a669568dfac07aadf20b06f471ebf34986 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 23:17:24 -0400 Subject: [PATCH 5/6] Add Windows and Android CI Windows builds and runs the unit tests via compnerd/gha-setup-swift on windows-latest. Android cross-compiles and runs the unit tests on an x86_64 emulator via skiptools/swift-android-action, plus a separate armv7 cross-compile check using the swift.org Android SDK. --- .github/workflows/swift-android.yml | 47 +++++++++++++++++++++++++++++ .github/workflows/swift-windows.yml | 21 +++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/swift-android.yml create mode 100644 .github/workflows/swift-windows.yml diff --git a/.github/workflows/swift-android.yml b/.github/workflows/swift-android.yml new file mode 100644 index 0000000..5671b05 --- /dev/null +++ b/.github/workflows/swift-android.yml @@ -0,0 +1,47 @@ +name: Swift Android + +on: [push] + +jobs: + android: + name: Android (unit tests) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + # Cross-compiles the package for Android and runs the XCTest suite on an + # Android emulator (x86_64) provisioned by the action. + - name: Build and test on Android emulator + uses: skiptools/swift-android-action@v2 + with: + swift-version: 6.3.3 + + android-armv7: + name: Android (armv7 cross-compile) + runs-on: ubuntu-latest + container: swift:6.3.3 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies + run: apt-get update -y && apt-get install -y curl + - name: Install Swift Android SDK + # The Android SDK artifact bundle is only published for 6.3.3, and the + # host toolchain must match the bundle version exactly (Swift's module + # format isn't cross-patch-compatible) — hence the swift:6.3.3 container. + run: | + set -eux + url="https://download.swift.org/swift-6.3.3-release/android-sdk/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE_android.artifactbundle.tar.gz" + curl -fsSL "$url" -o android.artifactbundle.tar.gz + swift sdk install android.artifactbundle.tar.gz \ + --checksum d160cc3206dd1886dae3fef2337af5e25ec034692cd0ec225721c56cc69da7f5 + swift sdk list + # armv7 can't run on the x86_64 emulator, so this is a cross-compile check + # only. The armv7 SDK id is resolved from `swift sdk list` to avoid pinning + # the exact API-level suffix in the target triple. + - name: Build (armv7) + run: | + set -eux + SDK=$(swift sdk list | grep -iE 'armv7|armeabi' | head -1) + test -n "$SDK" + swift build --swift-sdk "$SDK" diff --git a/.github/workflows/swift-windows.yml b/.github/workflows/swift-windows.yml new file mode 100644 index 0000000..78d9922 --- /dev/null +++ b/.github/workflows/swift-windows.yml @@ -0,0 +1,21 @@ +name: Swift Windows + +on: [push] + +jobs: + windows: + name: Windows + runs-on: windows-latest + steps: + - uses: compnerd/gha-setup-swift@main + with: + branch: swift-6.3.3-release + tag: 6.3.3-RELEASE + - name: Checkout + uses: actions/checkout@v4 + - name: Swift Version + run: swift --version + - name: Build + run: swift build + - name: Test + run: swift test From 54224938f40a8254bce9a44e30b3aeaf569bdf3e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 17 Jul 2026 23:33:49 -0400 Subject: [PATCH 6/6] Fix armv7 Android cross-compile SDK selection The Swift Android SDK installs as a single multi-arch bundle, so the armv7 slice is selected by target triple, not a distinct `swift sdk list` id. Resolve the exact armv7 triple from the installed SDK metadata and pass it to --swift-sdk. --- .github/workflows/swift-android.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/swift-android.yml b/.github/workflows/swift-android.yml index 5671b05..2c4fbd8 100644 --- a/.github/workflows/swift-android.yml +++ b/.github/workflows/swift-android.yml @@ -37,11 +37,13 @@ jobs: --checksum d160cc3206dd1886dae3fef2337af5e25ec034692cd0ec225721c56cc69da7f5 swift sdk list # armv7 can't run on the x86_64 emulator, so this is a cross-compile check - # only. The armv7 SDK id is resolved from `swift sdk list` to avoid pinning - # the exact API-level suffix in the target triple. + # only. The bundle installs as a single multi-arch SDK; the architecture is + # selected by target triple. Read the exact armv7 triple (with its + # API-level suffix) from the installed SDK metadata rather than pinning it. - name: Build (armv7) run: | set -eux - SDK=$(swift sdk list | grep -iE 'armv7|armeabi' | head -1) - test -n "$SDK" - swift build --swift-sdk "$SDK" + TRIPLE=$(grep -rhoE 'armv7-unknown-linux-androideabi[0-9]*' ~/.swiftpm | sort -u | head -1) + test -n "$TRIPLE" + echo "Using target triple: $TRIPLE" + swift build --swift-sdk "$TRIPLE"