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+
297 lines
9.0 KiB
Swift
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
|
|
}
|