From 205bb38e17e77315f26e8f772a4ecb9fbf7c10e5 Mon Sep 17 00:00:00 2001 From: NSStudent Date: Sat, 18 Apr 2026 13:19:26 +0200 Subject: [PATCH 1/2] test: add unit tests for transport, scanner, and protocol constants Signed-off-by: NSStudent --- .github/workflows/test-spm-codecov.yml | 3 +- .../CoreBluetoothEasyLinkTransportTests.swift | 80 +++++++++++++++++++ .../EasyLinkScannerTests.swift | 20 +++++ .../ProtocolConstantsTests.swift | 43 ++++++++++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 Tests/EasyLinkSwiftSDKTests/CoreBluetoothEasyLinkTransportTests.swift create mode 100644 Tests/EasyLinkSwiftSDKTests/EasyLinkScannerTests.swift create mode 100644 Tests/EasyLinkSwiftSDKTests/ProtocolConstantsTests.swift diff --git a/.github/workflows/test-spm-codecov.yml b/.github/workflows/test-spm-codecov.yml index f233616..74f807c 100644 --- a/.github/workflows/test-spm-codecov.yml +++ b/.github/workflows/test-spm-codecov.yml @@ -28,7 +28,8 @@ jobs: run: xcrun llvm-cov export -format="lcov" .build/debug/${{ inputs.test_bundle }}.xctest/Contents/MacOS/${{ inputs.test_bundle }} -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage_report.lcov + slug: NSStudent/EasyLinkSwiftSDK diff --git a/Tests/EasyLinkSwiftSDKTests/CoreBluetoothEasyLinkTransportTests.swift b/Tests/EasyLinkSwiftSDKTests/CoreBluetoothEasyLinkTransportTests.swift new file mode 100644 index 0000000..1e720a0 --- /dev/null +++ b/Tests/EasyLinkSwiftSDKTests/CoreBluetoothEasyLinkTransportTests.swift @@ -0,0 +1,80 @@ +import EasyLinkSwiftSDK +import XCTest + +final class CoreBluetoothEasyLinkTransportTests: XCTestCase { + func testWriteThrowsDisconnectedWhenNotConnected() async throws { + let transport = CoreBluetoothEasyLinkTransport(profile: .classic) + + do { + try await transport.write([0x01, 0x02]) + XCTFail("Expected disconnected error") + } catch EasyLinkError.disconnected { + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testDisconnectYieldsDisconnectedNotification() async { + let transport = CoreBluetoothEasyLinkTransport(profile: .move) + var iterator = transport.notifications.makeAsyncIterator() + + await transport.disconnect() + + let notification = await iterator.next() + XCTAssertEqual(notification, .disconnected) + } + + func testDisconnectWhenNeverConnectedCompletes() async { + let transport = CoreBluetoothEasyLinkTransport(profile: .classic) + await transport.disconnect() + } + + func testWriteAfterDisconnectThrowsDisconnected() async throws { + let transport = CoreBluetoothEasyLinkTransport(profile: .move) + await transport.disconnect() + + do { + try await transport.write([0x21, 0x01, 0x00]) + XCTFail("Expected disconnected error") + } catch EasyLinkError.disconnected { + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testInitWithDeviceID() { + let id = UUID() + let transport = CoreBluetoothEasyLinkTransport(profile: .move, deviceID: id) + XCTAssertNotNil(transport) + } + + func testInitWithoutDeviceID() { + let transport = CoreBluetoothEasyLinkTransport(profile: .classic) + XCTAssertNotNil(transport) + } + + func testConnectCancellationDoesNotHang() async { + let transport = CoreBluetoothEasyLinkTransport(profile: .classic) + + let task = Task { + try await transport.connect() + } + + try? await Task.sleep(nanoseconds: 50_000_000) + task.cancel() + + do { + try await task.value + } catch is CancellationError { + } catch is EasyLinkError { + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testMultipleDisconnectsDoNotCrash() async { + let transport = CoreBluetoothEasyLinkTransport(profile: .classic) + await transport.disconnect() + await transport.disconnect() + } +} diff --git a/Tests/EasyLinkSwiftSDKTests/EasyLinkScannerTests.swift b/Tests/EasyLinkSwiftSDKTests/EasyLinkScannerTests.swift new file mode 100644 index 0000000..860f7b1 --- /dev/null +++ b/Tests/EasyLinkSwiftSDKTests/EasyLinkScannerTests.swift @@ -0,0 +1,20 @@ +import EasyLinkSwiftSDK +import XCTest + +final class EasyLinkScannerTests: XCTestCase { + func testScanImmediateCancellationDoesNotCrash() async { + let task = Task { + for await _ in EasyLinkScanner.scan(profile: .classic) {} + } + task.cancel() + await task.value + } + + func testScanCancellationForMoveProfileDoesNotCrash() async { + let task = Task { + for await _ in EasyLinkScanner.scan(profile: .move) {} + } + task.cancel() + await task.value + } +} diff --git a/Tests/EasyLinkSwiftSDKTests/ProtocolConstantsTests.swift b/Tests/EasyLinkSwiftSDKTests/ProtocolConstantsTests.swift new file mode 100644 index 0000000..2b9fe87 --- /dev/null +++ b/Tests/EasyLinkSwiftSDKTests/ProtocolConstantsTests.swift @@ -0,0 +1,43 @@ +@testable import EasyLinkSwiftSDK +import XCTest + +final class ProtocolConstantsTests: XCTestCase { + func testClassicBatteryCommand() { + XCTAssertEqual(BoardProfile.classic.batteryCommand, [0x29, 0x01, 0x00]) + } + + func testMoveBatteryCommand() { + XCTAssertEqual(BoardProfile.move.batteryCommand, [0x41, 0x01, 0x0C]) + } + + func testEnableRealtimeModeCommand() { + XCTAssertEqual(ProtocolConstants.enableRealtimeMode, [0x21, 0x01, 0x00]) + } + + func testClassicMatchesChessnutBoardNames() { + XCTAssertTrue(BoardProfile.classic.matchesPeripheralName("Chessnut Air")) + XCTAssertTrue(BoardProfile.classic.matchesPeripheralName("Chessnut Air+")) + XCTAssertTrue(BoardProfile.classic.matchesPeripheralName("Chessnut Pro")) + XCTAssertTrue(BoardProfile.classic.matchesPeripheralName("Chessnut Go")) + } + + func testClassicDoesNotMatchChessnutMove() { + XCTAssertFalse(BoardProfile.classic.matchesPeripheralName("Chessnut Move")) + } + + func testClassicDoesNotMatchUnrelatedNames() { + XCTAssertFalse(BoardProfile.classic.matchesPeripheralName("Unknown Device")) + XCTAssertFalse(BoardProfile.classic.matchesPeripheralName("")) + } + + func testMoveMatchesExactName() { + XCTAssertTrue(BoardProfile.move.matchesPeripheralName("Chessnut Move")) + } + + func testMoveDoesNotMatchOtherBoardNames() { + XCTAssertFalse(BoardProfile.move.matchesPeripheralName("Chessnut Air")) + XCTAssertFalse(BoardProfile.move.matchesPeripheralName("Chessnut Pro")) + XCTAssertFalse(BoardProfile.move.matchesPeripheralName("Chessnut")) + XCTAssertFalse(BoardProfile.move.matchesPeripheralName("")) + } +} From 1d05ef056a0c86b99122319d7951dddf03fa1544 Mon Sep 17 00:00:00 2001 From: NSStudent Date: Sat, 18 Apr 2026 13:29:17 +0200 Subject: [PATCH 2/2] feat: update readme shields --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 592dba6..2d4bf0e 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ [![Swift Package Manager](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://www.swift.org/package-manager/) [![Platforms](https://img.shields.io/badge/platforms-iOS%2016%20%7C%20macOS%2013-lightgrey.svg)](Package.swift) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Swift Package Index Swift Versions](https://img.shields.io/endpoint?url=https://swiftpackageindex.com/api/packages/NSStudent/EasyLinkSwiftSDK/badge?type=swift-versions)](https://swiftpackageindex.com/NSStudent/EasyLinkSwiftSDK) -[![Swift Package Index Platforms](https://img.shields.io/endpoint?url=https://swiftpackageindex.com/api/packages/NSStudent/EasyLinkSwiftSDK/badge?type=platforms)](https://swiftpackageindex.com/NSStudent/EasyLinkSwiftSDK) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNSStudent%2FEasyLinkSwiftSDK%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/NSStudent/EasyLinkSwiftSDK) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNSStudent%2FEasyLinkSwiftSDK%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/NSStudent/EasyLinkSwiftSDK) `EasyLinkSwiftSDK` is a native Swift package for discovering and communicating with [Chessnut](https://www.chessnutech.com) electronic chessboards over Bluetooth Low Energy.