Skip to content
Merged
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Build
run: swift build

- name: Test with coverage
run: swift test --enable-code-coverage -Xcc -fprofile-instr-generate -Xcc -fcoverage-mapping

- name: Export coverage to lcov
run: |
BIN=$(swift build --show-bin-path)
xcrun llvm-cov export \
"$BIN/FaketoothPackageTests.xctest/Contents/MacOS/FaketoothPackageTests" \
-instr-profile "$(find .build -name default.profdata -type f)" \
-format lcov > coverage.lcov

- name: Upload coverage report artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.lcov

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.lcov
144 changes: 144 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Release

on:
push:
branches: [master]

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Resolve version and create release
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail

# --- Determine base version and commit range -----------------------

LATEST_TAG=$(git tag --list 'v*' --sort=-v:refname | head -n1)

if [ -z "$LATEST_TAG" ]; then
BASE_VERSION="0.0.0"
RANGE="HEAD"
else
BASE_VERSION="${LATEST_TAG#v}"
RANGE="${LATEST_TAG}..HEAD"
fi

COMMITS=$(git log --format="%H %s" "$RANGE")

if [ -z "$COMMITS" ]; then
echo "No new commits since ${LATEST_TAG}. Skipping."
exit 0
fi

# --- Analyse commits (Conventional Commits) ------------------------

BUMP="none"
FEAT_LOG=""
FIX_LOG=""
PERF_LOG=""
DOCS_LOG=""
OTHER_LOG=""

while IFS= read -r line; do
HASH="${line%% *}"
MSG="${line#* }"
SHORT_HASH="${HASH:0:7}"

# Detect breaking changes (! suffix or BREAKING CHANGE trailer)
if echo "$MSG" | grep -qE '^[a-z]+(\(.+\))?!:'; then
BUMP="major"
elif echo "$MSG" | grep -qi 'BREAKING CHANGE'; then
BUMP="major"
fi

# Extract type and description
TYPE=$(echo "$MSG" | sed -nE 's/^([a-z]+)(\(.+\))?(!)? *:.*/\1/p')
DESC=$(echo "$MSG" | sed -nE 's/^[a-z]+(\(.+\))?(!)? *: *(.*)/\3/p')

if [ -z "$TYPE" ]; then
TYPE="other"
DESC="$MSG"
fi

ENTRY="- ${DESC} (\`${SHORT_HASH}\`)"

case "$TYPE" in
feat)
[ "$BUMP" != "major" ] && BUMP="minor"
FEAT_LOG="${FEAT_LOG}${ENTRY}"$'\n'
;;
fix)
[ "$BUMP" = "none" ] && BUMP="patch"
FIX_LOG="${FIX_LOG}${ENTRY}"$'\n'
;;
perf)
[ "$BUMP" = "none" ] && BUMP="patch"
PERF_LOG="${PERF_LOG}${ENTRY}"$'\n'
;;
docs)
[ "$BUMP" = "none" ] && BUMP="patch"
DOCS_LOG="${DOCS_LOG}${ENTRY}"$'\n'
;;
*)
[ "$BUMP" = "none" ] && BUMP="patch"
OTHER_LOG="${OTHER_LOG}${ENTRY}"$'\n'
;;
esac
done <<< "$COMMITS"

if [ "$BUMP" = "none" ]; then
echo "No version-bumping commits (feat/fix/perf/breaking). Skipping."
exit 0
fi

# --- Calculate new version -----------------------------------------

IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"

case "$BUMP" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac

NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
echo "Bump: ${BUMP} ${BASE_VERSION} → ${NEW_TAG}"

# --- Build changelog ------------------------------------------------

BODY="## What's Changed"

[ -n "$FEAT_LOG" ] && BODY="${BODY}"$'\n\n'"### Features"$'\n'"${FEAT_LOG}"
[ -n "$FIX_LOG" ] && BODY="${BODY}"$'\n\n'"### Bug Fixes"$'\n'"${FIX_LOG}"
[ -n "$PERF_LOG" ] && BODY="${BODY}"$'\n\n'"### Performance"$'\n'"${PERF_LOG}"
[ -n "$DOCS_LOG" ] && BODY="${BODY}"$'\n\n'"### Documentation"$'\n'"${DOCS_LOG}"
[ -n "$OTHER_LOG" ] && BODY="${BODY}"$'\n\n'"### Other Changes"$'\n'"${OTHER_LOG}"

if [ -n "$LATEST_TAG" ]; then
BODY="${BODY}"$'\n'"**Full Changelog**: https://github.com/${REPO}/compare/${LATEST_TAG}...${NEW_TAG}"
fi

# --- Tag and release ------------------------------------------------

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git tag "$NEW_TAG"
git push origin "$NEW_TAG"

gh release create "$NEW_TAG" \
--title "$NEW_TAG" \
--notes "$BODY"

echo "Released ${NEW_TAG}"
19 changes: 0 additions & 19 deletions .github/workflows/swift.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/Packages
/*.xcodeproj
xcuserdata/

.claude/settings.local.json
49 changes: 49 additions & 0 deletions Tests/FaketoothTests/CBCentralManagerTest+Fallbacks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import XCTest
@testable import Faketooth

extension CBCentralManagerTest {

/// When simulatedPeripherals is nil, scanForPeripherals should fall through
/// to the original CoreBluetooth implementation without crashing.
func testScanForPeripheralsFallbackToBluetooth() {
// Ensure simulation is off
XCTAssertNil(CBCentralManager.simulatedPeripherals)

// Should not crash — falls through to real CoreBluetooth
centralManager.scanForPeripherals(withServices: nil, options: nil)

// isScanning should reflect real CoreBluetooth state (not the faketooth state)
// We just verify no crash occurred
}

func testStopScanFallbackToBluetooth() {
XCTAssertNil(CBCentralManager.simulatedPeripherals)

// Should not crash
centralManager.stopScan()
}

func testRetrievePeripheralsFallbackToBluetooth() {
XCTAssertNil(CBCentralManager.simulatedPeripherals)

// Should return real CoreBluetooth result (likely empty)
let result = centralManager.retrievePeripherals(withIdentifiers: [.testPeripheral])
XCTAssertTrue(result.isEmpty, "No real Bluetooth peripherals expected in test environment")
}

func testConnectPeripheralFallbackToBluetooth() {
let peripheral = faketoothSetupPeripheral()
XCTAssertNil(CBCentralManager.simulatedPeripherals)

// Should not crash — falls through to real CoreBluetooth connect
centralManager.connect(peripheral, options: nil)
}

func testCancelPeripheralConnectionFallbackToBluetooth() {
let peripheral = faketoothSetupPeripheral()
XCTAssertNil(CBCentralManager.simulatedPeripherals)

// Should not crash
centralManager.cancelPeripheralConnection(peripheral)
}
}
15 changes: 1 addition & 14 deletions Tests/FaketoothTests/CBCentralManagerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,7 @@ final class CBCentralManagerTest: XCTestCase {
XCTAssertFalse(CBCentralManager.isSimulated)
}

// MARK: Test fallbacks to CoreBluetooth implementation

func testFallbackToBluetoothScanForPeripherals() {

let expectattion = XCTestExpectation(description: "Fallback to Bluetooth scanForPeripherals() method")
expectattion.isInverted = true

centralManagerDelegate.onDidDiscoverPeripheral = { _, _, _ in
expectattion.fulfill()
}
centralManager.scanForPeripherals(withServices: nil, options: nil)

wait(for: [expectattion], timeout: Double(FaketoothSettings.delay.scanForPeripheralDelayInSeconds) + 0.1)
}
// MARK: Faketooth private methods

func testFaketoothMethods() {

Expand Down
Loading
Loading