learning_ai_common_plat/packages/swift-diagnostics/Tests/ByteLystDiagnosticsTests/DiagnosticsClientTests.swift
saravanakumardb1 abcf817cb3 feat(swift-diagnostics): implement Phase 2.2 Swift Client SDK
New package ByteLystDiagnostics with:

- Core types: DiagnosticsSession, TraceSpan, LogEntry, Breadcrumb

- DiagnosticsClient: actor-based singleton with polling

- BreadcrumbTrail: ring buffer (max 100) for timeline

- NetworkInterceptor: URLProtocol-based HTTP capture

- DeviceState: battery, memory, storage, network, thermal

- 20+ XCTest unit tests

Features:

- configure()/start()/stop() lifecycle

- trace() async span wrapper

- log() with breadcrumb integration

- breadcrumb() manual timeline markers

- ETag-based config polling

- 30-second batch flush

Platforms: iOS 15+, macOS 13+, watchOS 8+, tvOS 15+
2026-03-03 09:28:11 -08:00

297 lines
9.0 KiB
Swift

import XCTest
@testable import ByteLystDiagnostics
final class DiagnosticsClientTests: XCTestCase {
override func setUp() {
super.setUp()
// Reset to initial state
Task {
// Client is actor, need to await
}
}
// MARK: - Singleton Tests
func testSharedInstance() {
let client1 = DiagnosticsClient.shared
let client2 = DiagnosticsClient.shared
XCTAssertTrue(client1 === client2, "Shared instance should be singleton")
}
// MARK: - Configuration Tests
func testConfiguration() async {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
let state = await DiagnosticsClient.shared.getState()
XCTAssertEqual(state, .idle)
}
// MARK: - Session State Tests
func testInitialSessionState() async {
let isActive = await DiagnosticsClient.shared.isSessionActive()
XCTAssertFalse(isActive)
let session = await DiagnosticsClient.shared.getCurrentSession()
XCTAssertNil(session)
}
// MARK: - Breadcrumb Tests
func testBreadcrumbAdding() async {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
await DiagnosticsClient.shared.breadcrumb(
category: "navigation",
message: "Page loaded",
data: ["path": AnyCodable("/home")]
)
let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs()
XCTAssertEqual(breadcrumbs.count, 1)
XCTAssertEqual(breadcrumbs.first?.category, "navigation")
XCTAssertEqual(breadcrumbs.first?.message, "Page loaded")
}
func testMultipleBreadcrumbs() async {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
for i in 1...5 {
await DiagnosticsClient.shared.breadcrumb(
category: "test",
message: "Message \(i)"
)
}
let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs()
XCTAssertEqual(breadcrumbs.count, 5)
}
// MARK: - Tracing Tests
func testSuccessfulTrace() async throws {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
let result = try await DiagnosticsClient.shared.trace(name: "test-operation") {
return 42
}
XCTAssertEqual(result, 42)
}
func testFailingTrace() async {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
do {
_ = try await DiagnosticsClient.shared.trace(name: "failing-operation") {
throw TestError.test
}
XCTFail("Should have thrown")
} catch {
// Expected
XCTAssertTrue(error is TestError)
}
}
// MARK: - Logging Tests
func testLogAdding() async {
let config = DiagnosticsConfiguration(
productId: "test-app",
anonymousInstallId: "install-123",
platform: "ios",
channel: "ios_app",
osFamily: "ios",
appVersion: "1.0.0",
buildNumber: "100",
releaseChannel: "beta",
serverUrl: "https://api.test.com"
)
await DiagnosticsClient.shared.configure(config)
await DiagnosticsClient.shared.log(
level: .info,
message: "Test message",
module: "TestModule"
)
// Log is buffered, no immediate assertion possible
// But breadcrumb should be created
let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs()
XCTAssertTrue(breadcrumbs.contains { $0.category == "log" })
}
// MARK: - Types Tests
func testDiagnosticsSessionEncoding() throws {
let session = DiagnosticsSession(
id: "ds_test123",
productId: "test-app",
status: .active,
collectionLevel: .debug,
captureLogs: true,
captureNetwork: true,
captureScreenshots: false,
screenshotOnError: true,
maxDurationMinutes: 60,
createdAt: "2026-03-03T12:00:00Z",
expiresAt: "2026-03-03T13:00:00Z"
)
let encoder = JSONEncoder()
let data = try encoder.encode(session)
let decoder = JSONDecoder()
let decoded = try decoder.decode(DiagnosticsSession.self, from: data)
XCTAssertEqual(decoded.id, session.id)
XCTAssertEqual(decoded.status, session.status)
XCTAssertEqual(decoded.collectionLevel, session.collectionLevel)
}
func testLogEntryCreation() {
let entry = DiagnosticsLogEntry(
level: .error,
message: "Something went wrong",
timestamp: "2026-03-03T12:00:00Z",
module: "TestModule",
file: "Test.swift",
line: 42,
function: "testFunction",
context: ["key": AnyCodable("value")],
correlationId: "corr-123"
)
XCTAssertEqual(entry.level, .error)
XCTAssertEqual(entry.message, "Something went wrong")
XCTAssertEqual(entry.module, "TestModule")
XCTAssertEqual(entry.line, 42)
XCTAssertEqual(entry.correlationId, "corr-123")
}
func testTraceSpanCreation() {
let span = DiagnosticsTraceSpan(
spanId: "span-123",
parentId: "parent-456",
name: "test-span",
kind: .internal,
startTime: "2026-03-03T12:00:00Z",
endTime: "2026-03-03T12:00:01Z",
durationMs: 1000,
attributes: ["key": AnyCodable("value")],
status: .ok,
statusMessage: nil
)
XCTAssertEqual(span.spanId, "span-123")
XCTAssertEqual(span.parentId, "parent-456")
XCTAssertEqual(span.name, "test-span")
XCTAssertEqual(span.status, .ok)
}
func testBreadcrumbCreation() {
let breadcrumb = DiagnosticsBreadcrumb(
timestamp: "2026-03-03T12:00:00Z",
category: "navigation",
message: "User tapped button",
data: ["buttonId": AnyCodable("submit")]
)
XCTAssertEqual(breadcrumb.category, "navigation")
XCTAssertEqual(breadcrumb.message, "User tapped button")
XCTAssertNotNil(breadcrumb.data)
}
func testAnyCodableEncoding() throws {
let value = AnyCodable("test-string")
let encoder = JSONEncoder()
let data = try encoder.encode(value)
// Should not throw
XCTAssertFalse(data.isEmpty)
}
func testAnyCodableIntEncoding() throws {
let value = AnyCodable(42)
let encoder = JSONEncoder()
let data = try encoder.encode(value)
XCTAssertFalse(data.isEmpty)
}
func testAnyCodableBoolEncoding() throws {
let value = AnyCodable(true)
let encoder = JSONEncoder()
let data = try encoder.encode(value)
XCTAssertFalse(data.isEmpty)
}
}
enum TestError: Error {
case test
}