Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Run tests

on:
workflow_call:
workflow_dispatch:
push:
branches: [master, release]
Expand All @@ -13,20 +14,65 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ios, mac, tvos]
include:
- platform: ios
scheme: Backtrace-iOS-lib
- platform: mac
scheme: Backtrace-macOS-lib
- platform: tvos
scheme: Backtrace-tvOS-lib

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies
run: sh scripts/install.sh
- name: Run test
- name: Run tests
run: fastlane ${{ matrix.platform }} tests

- name: Extract coverage summary
if: always()
run: |
XCRESULT_PATH="fastlane/test_output/${{ matrix.scheme }}/${{ matrix.scheme }}.xcresult"
if [ -e "$XCRESULT_PATH" ]; then
scripts/coverage.sh "$XCRESULT_PATH" \
--json-output "coverage-${{ matrix.platform }}.json" \
--markdown-output "coverage-${{ matrix.platform }}.md"
else
echo "Warning: xcresult not found at $XCRESULT_PATH"
find fastlane/test_output -name "*.xcresult" -type d 2>/dev/null || true
fi

- name: Add coverage to job summary
if: always()
run: |
if [ -f "coverage-${{ matrix.platform }}.md" ]; then
echo "### Code Coverage: ${{ matrix.platform }}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
cat "coverage-${{ matrix.platform }}.md" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload xcresult bundle
if: always()
uses: actions/upload-artifact@v4
with:
name: xcresult-${{ matrix.platform }}
path: fastlane/test_output/${{ matrix.scheme }}/${{ matrix.scheme }}.xcresult
retention-days: 30

- name: Upload coverage JSON
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-json-${{ matrix.platform }}
path: coverage-${{ matrix.platform }}.json
if-no-files-found: ignore
retention-days: 90

pod-lint:
runs-on: macos-14

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies
run: sh scripts/install.sh
- name: Run pod lib lint
Expand Down
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
function_body_length:
warning: 100
error: 200
file_length:
warning: 500
error: 1200
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C7E1E996357727965E56CE13F8FBC756"
BlueprintIdentifier = "F266B81121C77AC800D14417"
BuildableName = "Backtrace.framework"
BlueprintName = "Backtrace-macOS"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
ReferencedContainer = "container:Backtrace.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4DA5D25C396B92131F7C247CFFF86D3"
BlueprintIdentifier = "28F95BAF22525DCC003936E0"
BuildableName = "Backtrace.framework"
BlueprintName = "Backtrace-tvOS"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
ReferencedContainer = "container:Backtrace.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
Expand Down
16 changes: 10 additions & 6 deletions Tests/BacktraceDatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,28 @@ final class BacktraceDatabaseTests: QuickSpec {

throwingIt("supports concurrent read/write operations") {
try repository.clear()


// Pre-generate reports serially (PLCrashReporter is not reliably thread-safe)
let reports = try (1...5).map { _ in
try crashReporter.generateLiveReport(attributes: [:])
}

let writeGroup = DispatchGroup()
let readGroup = DispatchGroup()

// concurrent writes
for _ in 1...5 {
for report in reports {
writeGroup.enter()
DispatchQueue.global().async {
defer { writeGroup.leave() }
do {
let report = try crashReporter.generateLiveReport(attributes: [:])
try repository.save(report)
} catch {
fail("Failed to save concurrently: \(error)")
}
}
}

// concurrent reads
for _ in 1...5 {
readGroup.enter()
Expand All @@ -84,7 +88,7 @@ final class BacktraceDatabaseTests: QuickSpec {
}
}
}

writeGroup.wait()
readGroup.wait()
expect(try? repository.countResources()).to(equal(5))
Expand Down
2 changes: 2 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ lane :common_tests do |options|
scheme: options[:scheme],
clean: true,
code_coverage: true,
result_bundle: true,
output_directory: "fastlane/test_output/#{options[:scheme]}",
open_report: true,
disable_slide_to_type: options[:disable_slide_to_type],
disable_concurrent_testing: true
Expand Down
96 changes: 96 additions & 0 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset

# Extract code coverage summary from an xcresult bundle.
#
# Usage: scripts/coverage.sh <path-to-xcresult> [--json-output <path>] [--markdown-output <path>]
# Example: scripts/coverage.sh fastlane/test_output/Backtrace-iOS-lib/Backtrace-iOS-lib.xcresult

XCRESULT_PATH="${1:?Usage: coverage.sh <path-to-xcresult> [--json-output <path>] [--markdown-output <path>]}"
JSON_OUTPUT=""
MARKDOWN_OUTPUT=""

shift
while [[ $# -gt 0 ]]; do
case "$1" in
--json-output)
JSON_OUTPUT="$2"
shift 2
;;
--markdown-output)
MARKDOWN_OUTPUT="$2"
shift 2
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done

if [ ! -e "$XCRESULT_PATH" ]; then
echo "Error: xcresult bundle not found at $XCRESULT_PATH" >&2
exit 1
fi

# Extract coverage JSON from xcresult
COVERAGE_JSON=$(xcrun xccov view --report --json "$XCRESULT_PATH")

# Pass coverage data via environment variable so heredoc stdin is free for the script
export COVERAGE_JSON
python3 - "$JSON_OUTPUT" "$MARKDOWN_OUTPUT" <<'PYEOF'
import json, os, sys

coverage_json = json.loads(os.environ["COVERAGE_JSON"])
json_output_path = sys.argv[1] if sys.argv[1] else None
markdown_output_path = sys.argv[2] if sys.argv[2] else None

overall_pct = coverage_json.get("lineCoverage", 0) * 100

# Find Backtrace framework targets (exclude test targets, Pods internals, etc.)
backtrace_targets = []
for t in coverage_json.get("targets", []):
name = t.get("name", "")
if "Backtrace" in name and "Test" not in name and "Pods_" not in name:
pct = t.get("lineCoverage", 0) * 100
covered = t.get("coveredLines", 0)
executable = t.get("executableLines", 0)
backtrace_targets.append({
"name": name,
"lineCoverage": round(pct, 2),
"coveredLines": covered,
"executableLines": executable
})

# Print human-readable summary
print(f"Overall line coverage: {overall_pct:.2f}%")
for bt in backtrace_targets:
print(f" {bt['name']}: {bt['lineCoverage']:.2f}% ({bt['coveredLines']}/{bt['executableLines']} lines)")

# Write machine-readable JSON
summary = {
"overallLineCoverage": round(overall_pct, 2),
"targets": backtrace_targets
}

if json_output_path:
with open(json_output_path, "w") as f:
json.dump(summary, f, indent=2)
print(f"Coverage JSON written to {json_output_path}")

# Write Markdown table
if markdown_output_path:
lines = [
"| Target | Line Coverage | Covered / Executable |",
"|--------|:------------:|:--------------------:|",
]
for bt in backtrace_targets:
lines.append(f"| {bt['name']} | {bt['lineCoverage']:.2f}% | {bt['coveredLines']} / {bt['executableLines']} |")
lines.append("")
lines.append(f"**Overall: {overall_pct:.2f}%**")
with open(markdown_output_path, "w") as f:
f.write("\n".join(lines) + "\n")
print(f"Markdown summary written to {markdown_output_path}")
PYEOF