Skip to content

Commit 43390e3

Browse files
authored
Merge branch 'master' into feature/embedded
2 parents d5087ed + c63dc31 commit 43390e3

4 files changed

Lines changed: 311 additions & 24 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Swift Android
2+
3+
on: [push]
4+
5+
jobs:
6+
android:
7+
name: Android (unit tests)
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Checkout
11+
uses: actions/checkout@v4
12+
# Cross-compiles the package for Android and runs the XCTest suite on an
13+
# Android emulator (x86_64) provisioned by the action.
14+
- name: Build and test on Android emulator
15+
uses: skiptools/swift-android-action@v2
16+
with:
17+
swift-version: 6.3.3
18+
19+
android-armv7:
20+
name: Android (armv7 cross-compile)
21+
runs-on: ubuntu-latest
22+
container: swift:6.3.3
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
- name: Install dependencies
27+
run: apt-get update -y && apt-get install -y curl
28+
- name: Install Swift Android SDK
29+
# The Android SDK artifact bundle is only published for 6.3.3, and the
30+
# host toolchain must match the bundle version exactly (Swift's module
31+
# format isn't cross-patch-compatible) — hence the swift:6.3.3 container.
32+
run: |
33+
set -eux
34+
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"
35+
curl -fsSL "$url" -o android.artifactbundle.tar.gz
36+
swift sdk install android.artifactbundle.tar.gz \
37+
--checksum d160cc3206dd1886dae3fef2337af5e25ec034692cd0ec225721c56cc69da7f5
38+
swift sdk list
39+
# armv7 can't run on the x86_64 emulator, so this is a cross-compile check
40+
# only. The bundle installs as a single multi-arch SDK; the architecture is
41+
# selected by target triple. Read the exact armv7 triple (with its
42+
# API-level suffix) from the installed SDK metadata rather than pinning it.
43+
- name: Build (armv7)
44+
run: |
45+
set -eux
46+
TRIPLE=$(grep -rhoE 'armv7-unknown-linux-androideabi[0-9]*' ~/.swiftpm | sort -u | head -1)
47+
test -n "$TRIPLE"
48+
echo "Using target triple: $TRIPLE"
49+
swift build --swift-sdk "$TRIPLE"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Swift Windows
2+
3+
on: [push]
4+
5+
jobs:
6+
windows:
7+
name: Windows
8+
runs-on: windows-latest
9+
steps:
10+
- uses: compnerd/gha-setup-swift@main
11+
with:
12+
branch: swift-6.3.3-release
13+
tag: 6.3.3-RELEASE
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
- name: Swift Version
17+
run: swift --version
18+
- name: Build
19+
run: swift build
20+
- name: Test
21+
run: swift test

.github/workflows/swift.yml

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,68 @@ name: Swift
33
on: [push]
44

55
jobs:
6-
mac:
6+
macos:
77
name: macOS
8-
runs-on: macos-latest
8+
runs-on: macos-26
9+
strategy:
10+
matrix:
11+
config: ["debug", "release"]
912
steps:
1013
- name: Checkout
1114
uses: actions/checkout@v4
1215
- name: Swift Version
1316
run: swift --version
14-
- name: Build (Debug)
15-
run: swift build -c debug
16-
- name: Build (Release)
17-
run: swift build -c release
18-
- name: Test (Debug)
19-
run: swift test --configuration debug
17+
- name: Build
18+
run: swift build -c ${{ matrix.config }}
19+
- name: Test
20+
run: swift test -c ${{ matrix.config }}
2021

2122
linux:
22-
name: Linux
23+
name: Linux (${{ matrix.container }})
24+
runs-on: ubuntu-latest
2325
strategy:
26+
fail-fast: false
2427
matrix:
25-
swift: ["6.0", "6.1"]
26-
runs-on: ubuntu-latest
27-
container: swift:${{ matrix.swift }}
28+
container: ["swift:latest", "swiftlang/swift:nightly-noble"]
29+
config: ["debug", "release"]
30+
container: ${{ matrix.container }}
2831
steps:
2932
- name: Checkout
3033
uses: actions/checkout@v4
3134
- name: Swift Version
3235
run: swift --version
33-
- name: Build (Debug)
34-
run: swift build -c debug
35-
- name: Build (Release)
36-
run: swift build -c release
37-
- name: Test (Debug)
38-
run: swift test --configuration debug
36+
- name: Build
37+
run: swift build -c ${{ matrix.config }}
38+
- name: Test
39+
run: swift test -c ${{ matrix.config }}
3940

40-
embedded:
41-
name: Embedded Swift
42-
runs-on: ubuntu-latest
43-
container: swift:6.1
41+
coverage:
42+
name: Code Coverage
43+
runs-on: macos-26
44+
permissions:
45+
contents: read
46+
code-quality: write # required by actions/upload-code-coverage
4447
steps:
4548
- name: Checkout
4649
uses: actions/checkout@v4
4750
- name: Swift Version
4851
run: swift --version
49-
- name: Build (ARMv7-M bare metal)
50-
run: SWIFTPM_ENABLE_MACROS=0 swift build --target TLVCoding --disable-index-store --triple armv7em-none-none-eabi -Xswiftc -enable-experimental-feature -Xswiftc Embedded -Xswiftc -wmo
52+
- name: Export coverage and enforce threshold
53+
run: Scripts/coverage.sh 70
54+
- name: Upload coverage to GitHub Code Quality
55+
uses: actions/upload-code-coverage@v1
56+
with:
57+
file: .build/coverage/coverage.xml
58+
language: Swift
59+
label: code-coverage/llvm-cov
60+
# Code Quality was in public preview until 2026-07-20; keep this while
61+
# the repo might not have the feature enabled, so a failed upload
62+
# doesn't break CI. Remove once GA is enabled everywhere.
63+
fail-on-error: false
64+
- name: Upload coverage artifacts
65+
if: always()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: code-coverage
69+
path: .build/coverage/
70+
if-no-files-found: error

Scripts/coverage.sh

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env bash
2+
#
3+
# coverage.sh — run a SwiftPM test suite with code coverage, export LCOV and
4+
# Cobertura reports, and enforce a minimum line-coverage threshold.
5+
#
6+
# Coverage is measured against the package's OWN sources only (everything under
7+
# the repo's `Sources/` directory). Dependencies (checked out under `.build`),
8+
# generated sources, and the test target are excluded, so the number reflects
9+
# the coverage of this package's code rather than being diluted by third-party
10+
# code that the tests happen to exercise.
11+
#
12+
# The Cobertura XML (coverage.xml) is the format GitHub Code Quality consumes via
13+
# `actions/upload-code-coverage`; the LCOV report (coverage.lcov) is kept for
14+
# other tools (Codecov, Coveralls, editors) that prefer it.
15+
#
16+
# Usage:
17+
# Scripts/coverage.sh [threshold]
18+
#
19+
# Environment variables:
20+
# COVERAGE_THRESHOLD Minimum line coverage percentage (default: 80).
21+
# Overridden by the optional [threshold] argument.
22+
# COVERAGE_SOURCE_PREFIX Absolute path prefix selecting the sources that count
23+
# toward coverage (default: "$PWD/Sources/"). Narrow it
24+
# to a single target, e.g. "$PWD/Sources/MyLibrary/", to
25+
# gate on one target instead of every source file.
26+
# COVERAGE_OUTPUT LCOV output file (default: .build/coverage/coverage.lcov).
27+
# COBERTURA_OUTPUT Cobertura XML output file (default: .build/coverage/coverage.xml).
28+
# SKIP_TEST If set to 1, reuse existing coverage data instead of
29+
# re-running `swift test` (useful while iterating).
30+
31+
set -euo pipefail
32+
33+
THRESHOLD="${1:-${COVERAGE_THRESHOLD:-80}}"
34+
SOURCE_PREFIX="${COVERAGE_SOURCE_PREFIX:-$PWD/Sources/}"
35+
OUTPUT="${COVERAGE_OUTPUT:-.build/coverage/coverage.lcov}"
36+
COBERTURA_OUTPUT="${COBERTURA_OUTPUT:-.build/coverage/coverage.xml}"
37+
38+
# Resolve the correct llvm-cov (xcrun on Apple platforms, plain llvm-cov elsewhere).
39+
if command -v xcrun >/dev/null 2>&1; then
40+
LLVM_COV="xcrun llvm-cov"
41+
else
42+
LLVM_COV="llvm-cov"
43+
fi
44+
45+
# 1. Run the tests with coverage instrumentation.
46+
if [ "${SKIP_TEST:-0}" != "1" ]; then
47+
echo "==> Running tests with code coverage"
48+
swift test --enable-code-coverage
49+
fi
50+
51+
# 2. Locate the coverage artifacts SwiftPM produced.
52+
CODECOV_JSON="$(swift test --enable-code-coverage --show-codecov-path)"
53+
COV_DIR="$(dirname "$CODECOV_JSON")"
54+
PROFDATA="$COV_DIR/default.profdata"
55+
56+
if [ ! -f "$CODECOV_JSON" ]; then
57+
echo "error: coverage report not found at $CODECOV_JSON" >&2
58+
exit 1
59+
fi
60+
61+
# 3. Locate the instrumented test binary (differs by platform: on macOS it lives
62+
# inside the .xctest bundle, on Linux the .xctest file is the binary itself).
63+
BIN_PATH="$(swift build --show-bin-path)"
64+
TEST_BINARY=""
65+
for candidate in \
66+
"$BIN_PATH"/*PackageTests.xctest/Contents/MacOS/*PackageTests \
67+
"$BIN_PATH"/*PackageTests.xctest; do
68+
if [ -f "$candidate" ]; then
69+
TEST_BINARY="$candidate"
70+
break
71+
fi
72+
done
73+
74+
# 4. Export an LCOV report (for Codecov / Coveralls / editors).
75+
mkdir -p "$(dirname "$OUTPUT")"
76+
if [ -n "$TEST_BINARY" ] && [ -f "$PROFDATA" ]; then
77+
echo "==> Exporting LCOV report to $OUTPUT"
78+
$LLVM_COV export \
79+
-format=lcov \
80+
-instr-profile "$PROFDATA" \
81+
"$TEST_BINARY" \
82+
-ignore-filename-regex='.build/(checkouts|.*\.build)/|Tests/|\.derived/|DerivedSources/' \
83+
> "$OUTPUT"
84+
else
85+
echo "warning: could not locate test binary or profdata; skipping LCOV export" >&2
86+
fi
87+
88+
# 5. Generate a Cobertura XML report (for GitHub Code Quality / upload-code-coverage).
89+
echo "==> Writing Cobertura report to $COBERTURA_OUTPUT"
90+
mkdir -p "$(dirname "$COBERTURA_OUTPUT")"
91+
python3 - "$CODECOV_JSON" "$SOURCE_PREFIX" "$PWD" "$COBERTURA_OUTPUT" <<'PY'
92+
import json, os, sys, time
93+
from xml.sax.saxutils import escape, quoteattr
94+
95+
report_path, source_prefix, repo_root, output = sys.argv[1:5]
96+
97+
with open(report_path) as f:
98+
report = json.load(f)
99+
100+
files = []
101+
total_covered = total_lines = 0
102+
for file in report["data"][0]["files"]:
103+
name = file["filename"]
104+
if not name.startswith(source_prefix):
105+
continue
106+
# Per-line hit count: the greatest region count starting on each line (a line
107+
# is covered if any region on it executed, so branch sub-regions don't hide it).
108+
line_hits = {}
109+
for seg in file["segments"]:
110+
line, count, has_count = seg[0], seg[2], seg[3]
111+
if has_count:
112+
line_hits[line] = max(line_hits.get(line, 0), count)
113+
covered = sum(1 for h in line_hits.values() if h > 0)
114+
total_covered += covered
115+
total_lines += len(line_hits)
116+
files.append((os.path.relpath(name, repo_root), line_hits, covered, len(line_hits)))
117+
118+
def rate(c, t):
119+
return c / t if t else 0.0
120+
121+
overall = rate(total_covered, total_lines)
122+
timestamp = int(time.time())
123+
124+
out = [
125+
'<?xml version="1.0" ?>',
126+
'<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">',
127+
'<coverage line-rate="%.4f" branch-rate="0" lines-covered="%d" lines-valid="%d" '
128+
'branches-covered="0" branches-valid="0" complexity="0" version="llvm-cov" timestamp="%d">'
129+
% (overall, total_covered, total_lines, timestamp),
130+
" <sources>",
131+
" <source>%s</source>" % escape(repo_root),
132+
" </sources>",
133+
" <packages>",
134+
' <package name="%s" line-rate="%.4f" branch-rate="0" complexity="0">'
135+
% (escape(os.path.basename(repo_root)), overall),
136+
" <classes>",
137+
]
138+
for rel, line_hits, covered, total in sorted(files):
139+
out.append(
140+
' <class name=%s filename=%s line-rate="%.4f" branch-rate="0" complexity="0">'
141+
% (quoteattr(os.path.basename(rel)), quoteattr(rel), rate(covered, total))
142+
)
143+
out.append(" <methods/>")
144+
out.append(" <lines>")
145+
for line in sorted(line_hits):
146+
out.append(' <line number="%d" hits="%d"/>' % (line, line_hits[line]))
147+
out.append(" </lines>")
148+
out.append(" </class>")
149+
out += [" </classes>", " </package>", " </packages>", "</coverage>"]
150+
151+
with open(output, "w") as f:
152+
f.write("\n".join(out) + "\n")
153+
154+
print(" %d/%d lines (%.2f%%)" % (total_covered, total_lines, 100 * overall))
155+
PY
156+
157+
# 6. Compute line coverage for the package sources and enforce the threshold.
158+
echo "==> Computing coverage for ${SOURCE_PREFIX}"
159+
python3 - "$CODECOV_JSON" "$SOURCE_PREFIX" "$THRESHOLD" "$PWD" <<'PY'
160+
import json, os, sys
161+
162+
report_path, source_prefix, threshold, repo_root = sys.argv[1], sys.argv[2], float(sys.argv[3]), sys.argv[4]
163+
164+
with open(report_path) as f:
165+
report = json.load(f)
166+
167+
covered = total = 0
168+
rows = []
169+
for file in report["data"][0]["files"]:
170+
name = file["filename"]
171+
if not name.startswith(source_prefix):
172+
continue
173+
lines = file["summary"]["lines"]
174+
covered += lines["covered"]
175+
total += lines["count"]
176+
rows.append((lines["percent"], os.path.relpath(name, repo_root)))
177+
178+
if total == 0:
179+
print("error: no source files matched prefix %r" % source_prefix, file=sys.stderr)
180+
print(" (set COVERAGE_SOURCE_PREFIX to your package's Sources path)", file=sys.stderr)
181+
sys.exit(1)
182+
183+
percent = 100.0 * covered / total
184+
185+
for pct, name in sorted(rows):
186+
print(" %6.2f%% %s" % (pct, name))
187+
188+
print("-" * 48)
189+
print("Total line coverage: %.2f%% (%d/%d lines)" % (percent, covered, total))
190+
print("Required threshold: %.2f%%" % threshold)
191+
192+
if percent < threshold:
193+
print("FAILED: coverage %.2f%% is below the %.2f%% threshold" % (percent, threshold), file=sys.stderr)
194+
sys.exit(1)
195+
196+
print("PASSED: coverage meets the threshold")
197+
PY

0 commit comments

Comments
 (0)