//===----------------------------------------------------------------------===// // Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import Foundation import Testing @testable import ContainerBuildReporting @Suite struct BaseProgressConsumerTests { // MARK: - Test Configuration struct TestConfiguration: Sendable { let name: String let bufferSize: Int init(name: String = "test", bufferSize: Int = 100) { self.name = name self.bufferSize = bufferSize } } // Test concrete implementation of BaseProgressConsumer @MainActor final class TestProgressConsumer: BaseProgressConsumer, @unchecked Sendable { private var formattedEvents: [String] = [] override func formatAndOutput(_ event: BuildEvent) async throws { formattedEvents.append("Formatted: \(event)") } func getFormattedEvents() -> [String] { formattedEvents } func clearFormattedEvents() { formattedEvents.removeAll() } } // MARK: - Initialization Tests @Test("Initialization with configuration") @MainActor func initializationWithConfiguration() { let config = TestConfiguration(name: "test-consumer", bufferSize: 200) let consumer = TestProgressConsumer(configuration: config) #expect(consumer.configuration.name == "test-consumer") #expect(consumer.configuration.bufferSize == 200) #expect(consumer.getEvents().isEmpty) #expect(consumer.getFormattedEvents().isEmpty) } @Test("Initialization with default configuration") @MainActor func initializationWithDefaultConfiguration() { let consumer = TestProgressConsumer(configuration: TestConfiguration()) #expect(consumer.configuration.name == "test") #expect(consumer.configuration.bufferSize == 100) #expect(consumer.getEvents().isEmpty) } // MARK: - Event Handling Tests @Test("Handles build started event") @MainActor func handlesBuildStartedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.buildStarted(totalOperations: 10, stages: 1, timestamp: Date()) try await consumer.handle(event) let events = consumer.getEvents() #expect(events.count == 1) let statistics = consumer.getStatistics() #expect(statistics.startTime != nil) #expect(statistics.totalOperations == 10) #expect(statistics.endTime == nil) #expect(statistics.success == nil) } @Test("Handles build completed event") @MainActor func handlesBuildCompletedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.buildCompleted(success: true, timestamp: Date()) try await consumer.handle(event) let events = consumer.getEvents() #expect(events.count == 1) let statistics = consumer.getStatistics() #expect(statistics.endTime != nil) #expect(statistics.success == true) } @Test("Handles stage started event") @MainActor func handlesStageStartedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.stageStarted(stageName: "stage1", timestamp: Date()) try await consumer.handle(event) let statistics = consumer.getStatistics() #expect(statistics.totalStages == 1) #expect(statistics.stageStatistics["stage1"] != nil) #expect(statistics.stageStatistics["stage1"]?.startTime != nil) #expect(statistics.stageStatistics["stage1"]?.endTime == nil) } @Test("Handles stage completed event") @MainActor func handlesStageCompletedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) // Start stage first let startEvent = BuildEvent.stageStarted(stageName: "stage1", timestamp: Date()) try await consumer.handle(startEvent) // Complete stage let completeEvent = BuildEvent.stageCompleted(stageName: "stage1", timestamp: Date()) try await consumer.handle(completeEvent) let statistics = consumer.getStatistics() #expect(statistics.totalStages == 1) #expect(statistics.stageStatistics["stage1"]?.startTime != nil) #expect(statistics.stageStatistics["stage1"]?.endTime != nil) } @Test("Handles operation started event") @MainActor func handlesOperationStartedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let context = ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 1") let event = BuildEvent.operationStarted(context: context) try await consumer.handle(event) let statistics = consumer.getStatistics() #expect(statistics.stageStatistics["stage1"]?.operationCount == 1) #expect(statistics.stageStatistics["stage1"]?.cacheHits == 0) #expect(statistics.stageStatistics["stage1"]?.failures == 0) } @Test("Handles operation finished event") @MainActor func handlesOperationFinishedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.operationFinished(context: ReportContext(nodeId: UUID(), description: "Operation 1"), duration: 1.0) try await consumer.handle(event) let statistics = consumer.getStatistics() #expect(statistics.executedOperations == 1) #expect(statistics.failedOperations == 0) #expect(statistics.cacheHits == 0) } @Test("Handles operation failed event") @MainActor func handlesOperationFailedEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let context = ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 1") let error = BuildEventError(type: .executionFailed, description: "Test failure") let event = BuildEvent.operationFailed(context: context, error: error) try await consumer.handle(event) let statistics = consumer.getStatistics() #expect(statistics.failedOperations == 1) #expect(statistics.executedOperations == 0) #expect(statistics.stageStatistics["stage1"]?.failures == 1) } @Test("Handles operation cache hit event") @MainActor func handlesOperationCacheHitEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let context = ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 1") let event = BuildEvent.operationCacheHit(context: context) try await consumer.handle(event) let statistics = consumer.getStatistics() #expect(statistics.cacheHits == 1) #expect(statistics.stageStatistics["stage1"]?.cacheHits == 1) } @Test("Handles operation progress event") @MainActor func handlesOperationProgressEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.operationProgress(context: ReportContext(nodeId: UUID(), description: "Operation 1"), fraction: 0.5) try await consumer.handle(event) let events = consumer.getEvents() #expect(events.count == 1) // Progress events don't affect statistics let statistics = consumer.getStatistics() #expect(statistics.executedOperations == 0) #expect(statistics.failedOperations == 0) #expect(statistics.cacheHits == 0) } @Test("Handles operation log event") @MainActor func handlesOperationLogEvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.operationLog(context: ReportContext(nodeId: UUID(), description: "Operation 1"), message: "Log message") try await consumer.handle(event) let events = consumer.getEvents() #expect(events.count == 1) // Log events don't affect statistics let statistics = consumer.getStatistics() #expect(statistics.executedOperations == 0) #expect(statistics.failedOperations == 0) #expect(statistics.cacheHits == 0) } @Test("Handles IR event") @MainActor func handlesIREvent() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) let event = BuildEvent.irEvent(context: ReportContext(nodeId: UUID(), description: "IR event"), type: .graphStarted) try await consumer.handle(event) let events = consumer.getEvents() #expect(events.count == 1) // IR events don't affect statistics let statistics = consumer.getStatistics() #expect(statistics.executedOperations == 0) #expect(statistics.failedOperations == 0) #expect(statistics.cacheHits == 0) } // MARK: - Statistics Accumulation Tests @Test("Statistics accumulation with complex sequence") @MainActor func statisticsAccumulationWithComplexSequence() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) // Build sequence: start -> 2 stages -> multiple operations -> complete let events = [ BuildEvent.buildStarted(totalOperations: 6, stages: 2, timestamp: Date()), BuildEvent.stageStarted(stageName: "stage1", timestamp: Date()), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 1")), BuildEvent.operationFinished(context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 1"), duration: 1.0), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 2")), BuildEvent.operationCacheHit(context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 2")), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 3")), BuildEvent.operationFailed( context: ReportContext(nodeId: UUID(), stageId: "stage1", description: "Operation 3"), error: BuildEventError(type: .executionFailed, description: "Failed")), BuildEvent.stageCompleted(stageName: "stage1", timestamp: Date()), BuildEvent.stageStarted(stageName: "stage2", timestamp: Date()), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 4")), BuildEvent.operationFinished(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 4"), duration: 1.0), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 5")), BuildEvent.operationCacheHit(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 5")), BuildEvent.operationStarted(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 6")), BuildEvent.operationFinished(context: ReportContext(nodeId: UUID(), stageId: "stage2", description: "Operation 6"), duration: 1.0), BuildEvent.stageCompleted(stageName: "stage2", timestamp: Date()), BuildEvent.buildCompleted(success: false, timestamp: Date()), ] for event in events { try await consumer.handle(event) } let statistics = consumer.getStatistics() // Verify build-level statistics #expect(statistics.totalOperations == 6) #expect(statistics.executedOperations == 3) // op1, op4, op6 #expect(statistics.cacheHits == 2) // op2, op5 #expect(statistics.failedOperations == 1) // op3 #expect(statistics.success == false) #expect(statistics.totalStages == 2) #expect(statistics.startTime != nil) #expect(statistics.endTime != nil) #expect(statistics.events.count == 18) // Verify stage1 statistics let stage1Stats = statistics.stageStatistics["stage1"] #expect(stage1Stats != nil) #expect(stage1Stats?.operationCount == 3) #expect(stage1Stats?.cacheHits == 1) #expect(stage1Stats?.failures == 1) #expect(stage1Stats?.startTime != nil) #expect(stage1Stats?.endTime != nil) // Verify stage2 statistics let stage2Stats = statistics.stageStatistics["stage2"] #expect(stage2Stats != nil) #expect(stage2Stats?.operationCount == 3) #expect(stage2Stats?.cacheHits == 1) #expect(stage2Stats?.failures == 0) #expect(stage2Stats?.startTime != nil) #expect(stage2Stats?.endTime != nil) } @Test("Statistics with multiple stages") @MainActor func statisticsWithMultipleStages() async throws { let consumer = TestProgressConsumer(configuration: TestConfiguration()) // Create multiple stages with different characteristics let stages = [ ("stage1", 5, 2, 1), // 5 ops, 2 cache hits, 1 failure ("stage2", 3, 1, 0), // 3 ops, 1 cache hit, 0 failures ("stage3", 2, 0, 2), // 2 ops, 0 cache hits, 2 failures ("stage4", 1, 1, 0), // 1 op, 1 cache hit, 0 failures ] for (stageName, opCount, cacheHits, failures) in stages { let stageStartEvent = BuildEvent.stageStarted(stageName: stageName, timestamp: Date()) try await consumer.handle(stageStartEvent) // Add operations for i in 0..