// // UnitPerformanceTests.swift // KordantTests // // Unit-level performance tests using manual timing and XCTMetric // for measuring view model operations, API serialization, cache // operations, and cryptographic primitives with mocked data. // import Testing @testable import Kordant import Foundation // MARK: - JSON Deserialization Performance struct DeserializationPerformanceTests { /// Measures the time to decode a full Alert array from JSON. /// Baseline: < 50ms for 1000 alerts on iPhone 12. @Test("Decode 1000 alerts under 50ms") func decodeAlerts() throws { let alertsJSON = generateAlertsJSON(count: 1000) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let start = Date() let data = try #require(alertsJSON.data(using: .utf8)) let alerts = try decoder.decode([Alert].self, from: data) let elapsed = -start.timeIntervalSinceNow #expect(alerts.count == 1000, "Should decode 1000 alerts") #expect(elapsed < 0.05, "Decoding 1000 alerts took \(elapsed)s (expected < 50ms)") } /// Measures the time to decode a full Exposure array from JSON. /// Baseline: < 50ms for 1000 exposures on iPhone 12. @Test("Decode 1000 exposures under 50ms") func decodeExposures() throws { let exposuresJSON = generateExposuresJSON(count: 1000) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let start = Date() let data = try #require(exposuresJSON.data(using: .utf8)) let exposures = try decoder.decode([Exposure].self, from: data) let elapsed = -start.timeIntervalSinceNow #expect(exposures.count == 1000, "Should decode 1000 exposures") #expect(elapsed < 0.05, "Decoding 1000 exposures took \(elapsed)s (expected < 50ms)") } /// Measures User JSON decoding performance. @Test("Decode user object under 5ms") func decodeUser() throws { let userJSON = """ { "id": "user-1", "name": "Test User", "email": "test@kordant.com", "subscriptionTier": "premium", "createdAt": "2026-01-15T00:00:00Z", "updatedAt": "2026-06-01T00:00:00Z" } """ let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let start = Date() let data = try #require(userJSON.data(using: .utf8)) let user = try decoder.decode(User.self, from: data) let elapsed = -start.timeIntervalSinceNow #expect(user.id == "user-1") #expect(elapsed < 0.005, "User deserialization took \(elapsed)s (expected < 5ms)") } // MARK: - Helpers private func generateAlertsJSON(count: Int) -> String { let alerts = (0.. String { let exposures = (0.. [Alert] { (0.. [Exposure] { (0.. [WatchlistItem] { (0.. ($1.createdAt ?? .distantPast) } let elapsed = -start.timeIntervalSinceNow #expect(sorted.count == 500) #expect(elapsed < 0.005, "Sorting 500 alerts took \(elapsed)s (expected < 5ms)") } } // MARK: - XCTMetric-Based Performance Tests (XCTestCase) /// XCTestCase-based performance tests using XCTMetric for baseline recording. /// These tests use `measure(metrics:)` which supports automatic baseline comparison /// and 10% regression detection in Xcode. /// /// Note: XCTMetric requires XCTestCase, not the Testing framework. /// These tests are run via the KordantTests target alongside Testing-based tests. final class XCTMetricPerformanceTests: XCTestCase { private static let encoder: JSONEncoder = { let enc = JSONEncoder() enc.dateEncodingStrategy = .iso8601 return enc }() private static let decoder: JSONDecoder = { let dec = JSONDecoder() dec.dateDecodingStrategy = .iso8601 return dec }() // MARK: - JSON Encoding Performance /// Measures JSON encoder performance for encoding Alert objects. /// XCTMetric captures clock, CPU, and memory. func testJSONEncodingPerformance() throws { let alerts = (0..<500).map { i in Alert( id: "alert-\(i)", userId: "user-1", type: .exposure, severity: .medium, title: "Test Alert \(i)", message: "This is a test alert message with some detail text that might appear in the actual app.", read: i % 2 == 0, createdAt: Date() ) } measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let encoder = Self.encoder for alert in alerts { _ = try? encoder.encode(alert) } } } // MARK: - JSON Decoding Performance /// Measures JSON decoder performance for decoding Alert array. func testJSONDecodingPerformance() throws { let alerts = (0..<500).map { i in Alert( id: "alert-\(i)", userId: "user-1", type: .exposure, severity: .medium, title: "Test Alert \(i)", message: "Test message", read: i % 2 == 0, createdAt: Date() ) } let data = try Self.encoder.encode(alerts) measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { _ = try? Self.decoder.decode([Alert].self, from: data) } } // MARK: - ViewModel Data Processing Performance /// Measures threat score calculation performance with XCTMetric. func testThreatScoreCalculationPerformance() throws { let alerts = (0..<200).map { i in Alert( id: "alert-\(i)", userId: "user-1", type: i % 3 == 0 ? .breach : .exposure, severity: i % 5 == 0 ? .critical : .low, title: "Alert", message: "Message", read: i % 2 == 0, createdAt: Date().addingTimeInterval(-Double(i) * 3600) ) } let exposures = (0..<100).map { i in Exposure( id: "exp-\(i)", userId: "user-1", source: .darkWeb, dataType: "Email", exposedData: "test@example.com", severity: i % 3 == 0 ? "high" : "medium", status: i % 2 == 0 ? .new : .reviewed, discoveredAt: Date() ) } measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let unreadCritical = alerts.filter { !$0.read && $0.isCritical }.count let newExposures = exposures.filter { $0.status == .new }.count let score = min(Double(unreadCritical * 25 + newExposures * 10), 100) _ = score / 100.0 } } // MARK: - Image Cache Metadata Persistence Performance /// Measures metadata JSON persistence encoding/decoding performance. func testImageCacheMetadataPersistencePerformance() throws { let metadata = (0..<500).map { i in ( key: "https://images.kordant.com/photo-\(i).jpg", value: ImageCacheMetadata( url: "https://images.kordant.com/photo-\(i).jpg", contentType: "image/jpeg", fileSize: 1024 * (50 + i % 100), cachedAt: Date(), expirationDate: Date().addingTimeInterval(7 * 24 * 60 * 60) ) ) } let dict = Dictionary(uniqueKeysWithValues: metadata) measure(metrics: [XCTClockMetric()]) { if let encoded = try? Self.encoder.encode(dict) { _ = try? Self.decoder.decode([String: ImageCacheMetadata].self, from: encoded) } } } // MARK: - Alert Sorting Performance /// Measures the sorting performance of alerts (used by DashboardViewModel). func testAlertSortingPerformance() throws { let alerts = (0..<500).map { i in Alert( id: "alert-\(i)", userId: "user-1", type: .exposure, severity: .medium, title: "Alert", message: "Message", read: i % 2 == 0, createdAt: Date().addingTimeInterval(Double(i) * 3600 - 500 * 3600) ) } measure(metrics: [XCTClockMetric()]) { let sorted = alerts.sorted { ($0.createdAt ?? .distantPast) > ($1.createdAt ?? .distantPast) } _ = sorted } } }