diff --git a/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift b/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift index 5a42adec7c0..0b2059f561c 100644 --- a/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift +++ b/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift @@ -18,14 +18,15 @@ extension ProgressAnimation { @_spi(SwiftPMInternal) public static func ninja( stream: WritableByteStream, - verbose: Bool + verbose: Bool, + normalizeStep: Bool = true ) -> any ProgressAnimationProtocol { Self.dynamic( stream: stream, verbose: verbose, - ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0) }, + ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0, normalizeStep: normalizeStep) }, dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: nil) }, - defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream) } + defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream, normalizeStep: normalizeStep) } ) } } @@ -34,17 +35,24 @@ extension ProgressAnimation { final class RedrawingNinjaProgressAnimation: ProgressAnimationProtocol { private let terminal: TerminalController private var hasDisplayedProgress = false + private let normalizeStep: Bool - init(terminal: TerminalController) { + init(terminal: TerminalController, normalizeStep: Bool) { self.terminal = terminal + self.normalizeStep = normalizeStep } func update(step: Int, total: Int, text: String) { assert(step <= total) terminal.clearLine() - - let progressText = "[\(step)/\(total)] \(text)" + var progressText = "" + if step < 0 && normalizeStep { + let normalizedStep = max(0, step) + progressText = "[\(normalizedStep)/\(total)] \(text)" + } else { + progressText = "\(text)" + } let width = terminal.width if progressText.utf8.count > width { let suffix = "…" @@ -78,9 +86,11 @@ final class MultiLineNinjaProgressAnimation: ProgressAnimationProtocol { private let stream: WritableByteStream private var lastDisplayedText: String? = nil + private let normalizeStep: Bool - init(stream: WritableByteStream) { + init(stream: WritableByteStream, normalizeStep: Bool) { self.stream = stream + self.normalizeStep = normalizeStep } func update(step: Int, total: Int, text: String) { @@ -88,7 +98,12 @@ final class MultiLineNinjaProgressAnimation: ProgressAnimationProtocol { guard text != lastDisplayedText else { return } - stream.send("[\(step)/\(total)] ").send(text) + if step < 0 && normalizeStep { + let normalizedStep = max(0, step) + stream.send("[\(normalizedStep)/\(total)] ") + } + + stream.send(text) stream.send("\n") stream.flush() lastDisplayedText = text diff --git a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift index aa49fffcc94..4b865245600 100644 --- a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift +++ b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift @@ -119,7 +119,7 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider { // for raw output reporting func print(_ output: String, verbose: Bool) { self.queue.async(group: self.sync) { - guard !verbose || self.logLevel.isVerbose else { + guard verbose || self.logLevel.isVerbose else { return } self.write(output) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 004281991e7..3f153b086ef 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -64,7 +64,8 @@ public final class SwiftBuildSystemMessageHandler { self.logLevel = logLevel self.progressAnimation = ProgressAnimation.ninja( stream: outputStream, - verbose: self.logLevel.isVerbose + verbose: self.logLevel.isVerbose, + normalizeStep: false ) self.enableBacktraces = enableBacktraces self.buildDelegate = buildDelegate @@ -107,8 +108,9 @@ public final class SwiftBuildSystemMessageHandler { // Decode the buffer to a string let decodedOutput = String(decoding: buffer, as: UTF8.self) - // Emit message. - observabilityScope.print(decodedOutput, verbose: self.logLevel.isVerbose) + // Emit message. Setting verbose to true so as to ensure + // that this message is emitted. + observabilityScope.print(decodedOutput, verbose: true) // Record that we've emitted the output for a given task. self.tasksEmitted.insert(info) @@ -121,12 +123,8 @@ public final class SwiftBuildSystemMessageHandler { ) throws { // Begin by emitting the text received by the task started event. if let started = self.buildState.startedInfo(for: startedInfo) { - // Determine where to emit depending on the verbosity level. - if self.logLevel.isVerbose { - self.outputStream.send(started.description + "\n") - } else { - observabilityScope.emit(info: started) - } + // Emit depending on verbosity level. + self.observabilityScope.print(started, verbose: self.logLevel.isVerbose) } guard info.result == .success else { @@ -216,14 +214,18 @@ public final class SwiftBuildSystemMessageHandler { } } case .didUpdateProgress(let progressInfo): - var step = Int(progressInfo.percentComplete) - if step < 0 { step = 0 } + let step = Int(progressInfo.percentComplete) let message = if let targetName = progressInfo.targetName { "\(targetName) \(progressInfo.message)" } else { "\(progressInfo.message)" } - progressAnimation.update(step: step, total: 100, text: message) + + // Skip if message doesn't contain anything useful to display. + if message.contains(where: \.isLetter) { + progressAnimation.update(step: step, total: 100, text: message) + } + callback = { [weak self] buildSystem in self?.buildDelegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) } @@ -240,7 +242,7 @@ public final class SwiftBuildSystemMessageHandler { // Append to buffer-per-task storage buildState.appendToBuffer(info) case .taskStarted(let info): - try buildState.started(task: info) + try buildState.started(task: info, self.logLevel) let targetInfo = try buildState.target(for: info) callback = { [weak self] buildSystem in @@ -250,8 +252,8 @@ public final class SwiftBuildSystemMessageHandler { case .taskComplete(let info): let startedInfo = try buildState.completed(task: info) - // Handler for failed tasks, if applicable. - try handleTaskOutput(info, startedInfo, self.enableBacktraces) + // Handler for task output, handling failures if applicable. + try self.handleTaskOutput(info, startedInfo, self.enableBacktraces) let targetInfo = try buildState.target(for: startedInfo) callback = { [weak self] buildSystem in @@ -270,7 +272,23 @@ public final class SwiftBuildSystemMessageHandler { } case .targetComplete(let info): _ = try buildState.completed(target: info) - case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .taskUpToDate: + case .planningOperationStarted(_): + // Emitting under higher-level verbosity so as not to overwhelm output. + // This is the same behaviour as the native system. + if self.logLevel.isVerbose { + self.outputStream.send("Planning build" + "\n") + } + case .planningOperationCompleted(_): + // Emitting under higher-level verbosity so as not to overwhelm output. + if self.logLevel.isVerbose { + self.outputStream.send("Planning complete" + "\n") + } + case .targetUpToDate(let info): + // Received when a target is entirely up to date and did not need to be built. + if self.logLevel.isVerbose { + self.outputStream.send("Target \(info.guid) up to date." + "\n") + } + case .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .taskUpToDate: break case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: break // deprecated @@ -310,7 +328,7 @@ extension SwiftBuildSystemMessageHandler { /// Registers the start of a build task, validating that the task hasn't already been started. /// - Parameter task: The task start information containing task ID and signature /// - Throws: Fatal error if the task is already active - mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { + mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo, _ logLevel: Basics.Diagnostic.Severity) throws { if activeTasks[task.taskID] != nil { throw Diagnostics.fatalError } @@ -318,12 +336,12 @@ extension SwiftBuildSystemMessageHandler { taskIDToSignature[task.taskID] = task.taskSignature // Track relevant task info to emit to user. - let output = if let cmdLineDisplayStr = task.commandLineDisplayString { + let output = if let cmdLineDisplayStr = task.commandLineDisplayString, logLevel.isVerbose { "\(task.executionDescription)\n\(cmdLineDisplayStr)" } else { task.executionDescription } - taskDataBuffer[task] = output + taskDataBuffer.setTaskStartedInfo(task, output) } /// Marks a task as completed and removes it from active tracking. @@ -486,17 +504,16 @@ extension SwiftBuildSystemMessageHandler.BuildState { } } - subscript(task: SwiftBuildMessage.TaskStartedInfo) -> String? { - get { - guard let result = taskStartedNotifications[task.taskID] else { - return nil - } - - return result - } - set { - self.taskStartedNotifications[task.taskID] = newValue + func taskStartedInfo(_ task: SwiftBuildMessage.TaskStartedInfo) -> String? { + guard let result = taskStartedNotifications[task.taskID] else { + return nil } + + return result + } + + mutating func setTaskStartedInfo(_ task: SwiftBuildMessage.TaskStartedInfo, _ text: String) { + self.taskStartedNotifications[task.taskID] = text } } @@ -536,7 +553,7 @@ extension SwiftBuildSystemMessageHandler.BuildState { } func startedInfo(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> String? { - return self.taskDataBuffer[task] + return self.taskDataBuffer.taskStartedInfo(task) } } @@ -770,7 +787,6 @@ extension SwiftBuildMessage.LocationContext { } } - fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { var userDescription: String? { switch self { diff --git a/Tests/BuildTests/BuildSystemDelegateTests.swift b/Tests/BuildTests/BuildSystemDelegateTests.swift index 26ad17f4975..7b53d65b82d 100644 --- a/Tests/BuildTests/BuildSystemDelegateTests.swift +++ b/Tests/BuildTests/BuildSystemDelegateTests.swift @@ -35,7 +35,6 @@ struct BuildSystemDelegateTests { let (stdout, stderr) = try await executeSwiftBuild( fixturePath, configuration: data.config, - // extraArgs: ["--verbose"], buildSystem: data.buildSystem, ) switch data.buildSystem { @@ -45,13 +44,12 @@ struct BuildSystemDelegateTests { "log didn't contain expected linker diagnostics. stderr: '\(stderr)')", ) case .swiftbuild: - let searchPathRegex = try Regex("warning:(.*)Search path 'foobar' not found") #expect( stderr.contains("ld: warning: search path 'foobar' not found"), "log didn't contain expected linker diagnostics. stderr: '\(stderr)", ) #expect( - !stdout.contains(searchPathRegex), + !stdout.contains("ld: warning: search path 'foobar' not found"), "log didn't contain expected linker diagnostics. stderr: '\(stderr)')", ) case .xcode: diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index d8286dfce2c..de39387ac80 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -4544,9 +4544,12 @@ struct PackageCommandTests { // We expect a warning about `library.bar` but not about `library.foo`. let libraryFooPath = RelativePath("Sources/MyLibrary/library.foo").pathString #expect(!stderr.components(separatedBy: "\n").contains { $0.contains("warning: ") && $0.contains(libraryFooPath) }) - if data.buildSystem == .native { + switch data.buildSystem { + case .native: #expect(stderr.contains("found 1 file(s) which are unhandled")) #expect(stderr.contains(RelativePath("Sources/MyLibrary/library.bar").pathString)) + case .swiftbuild, .xcode: + return } } } diff --git a/Tests/CommandsTests/RunCommandTests.swift b/Tests/CommandsTests/RunCommandTests.swift index b93d4daaf0f..530efe18fb6 100644 --- a/Tests/CommandsTests/RunCommandTests.swift +++ b/Tests/CommandsTests/RunCommandTests.swift @@ -123,16 +123,8 @@ struct RunCommandTests { #expect(stdout.contains("sentinel")) // swift-build-tool output should go to stderr. - withKnownIssue { - #expect(stderr.contains("Compiling")) - } when: { - buildSystem == .swiftbuild - } - withKnownIssue { - #expect(stderr.contains("Linking")) - } when: { - buildSystem == .swiftbuild - } + #expect(stderr.contains("Compiling")) + #expect(stderr.contains("Linking")) } } @@ -160,16 +152,8 @@ struct RunCommandTests { """)) // swift-build-tool output should go to stderr. - withKnownIssue { - #expect(stderr.contains("Compiling")) - } when: { - buildSystem == .swiftbuild - } - withKnownIssue { - #expect(stderr.contains("Linking")) - } when: { - buildSystem == .swiftbuild - } + #expect(stderr.contains("Compiling")) + #expect(stderr.contains("Linking")) } } diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift index 48c43b288d5..984642f8af4 100644 --- a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift @@ -24,25 +24,78 @@ import _InternalTestSupport @Suite struct SwiftBuildSystemMessageHandlerTests { - private func createMessageHandler( - _ logLevel: Basics.Diagnostic.Severity = .warning - ) -> (handler: SwiftBuildSystemMessageHandler, outputStream: BufferedOutputByteStream, observability: TestingObservability) { - let outputStream = BufferedOutputByteStream() - let observability = ObservabilitySystem.makeForTesting(outputStream: outputStream) - - let handler = SwiftBuildSystemMessageHandler( - observabilityScope: observability.topScope, - outputStream: outputStream, - logLevel: logLevel + struct MockMessageHandlerProvider { + private let warningMessageHandler: SwiftBuildSystemMessageHandler + private let errorMessageHandler: SwiftBuildSystemMessageHandler + private let debugMessageHandler: SwiftBuildSystemMessageHandler + + public init( + outputStream: BufferedOutputByteStream, + observabilityScope: ObservabilityScope, + ) { + self.warningMessageHandler = .init( + observabilityScope: observabilityScope, + outputStream: outputStream, + logLevel: .warning + ) + self.errorMessageHandler = .init( + observabilityScope: observabilityScope, + outputStream: outputStream, + logLevel: .error + ) + self.debugMessageHandler = .init( + observabilityScope: observabilityScope, + outputStream: outputStream, + logLevel: .debug + ) + } + + public var warning: SwiftBuildSystemMessageHandler { + return warningMessageHandler + } + + public var error: SwiftBuildSystemMessageHandler { + return errorMessageHandler + } + + public var debug: SwiftBuildSystemMessageHandler { + return debugMessageHandler + } + } + + let outputStream: BufferedOutputByteStream + let observability: TestingObservability + let messageHandler: MockMessageHandlerProvider + + init() { + self.outputStream = BufferedOutputByteStream() + self.observability = ObservabilitySystem.makeForTesting( + outputStream: outputStream ) + self.messageHandler = .init( + outputStream: self.outputStream, + observabilityScope: self.observability.topScope + ) + } + + @Test + func testExceptionThrownWhenTaskCompleteEventReceivedWithoutTaskStart() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .taskCompleteInfo(result: .success) + ] - return (handler, outputStream, observability) + #expect(throws: (any Error).self) { + for event in events { + _ = try messageHandler.emitEvent(event) + } + } } @Test func testNoDiagnosticsReported() throws { - let (messageHandler, outputStream, observability) = createMessageHandler() - + let messageHandler = self.messageHandler.warning let events: [SwiftBuildMessage] = [ .taskStartedInfo(), .taskCompleteInfo(), @@ -54,20 +107,20 @@ struct SwiftBuildSystemMessageHandlerTests { } // Check output stream - let output = outputStream.bytes.description + let output = self.outputStream.bytes.description #expect(!output.contains("error")) // Check observability diagnostics - expectNoDiagnostics(observability.diagnostics) + expectNoDiagnostics(self.observability.diagnostics) } @Test func testSimpleDiagnosticReported() throws { - let (messageHandler, _, observability) = createMessageHandler() + let messageHandler = self.messageHandler.warning let events: [SwiftBuildMessage] = [ .taskStartedInfo(taskSignature: "simple-diagnostic"), - .diagnosticInfo(locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true), + .diagnostic(locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true), .taskCompleteInfo(taskSignature: "simple-diagnostic", result: .failed) // Handler only emits when a task is completed. ] @@ -75,41 +128,76 @@ struct SwiftBuildSystemMessageHandlerTests { _ = try messageHandler.emitEvent(event) } - #expect(observability.hasErrorDiagnostics) + #expect(self.observability.hasErrorDiagnostics) try expectDiagnostics(observability.diagnostics) { result in result.check(diagnostic: "Simple diagnostic", severity: .error) } } + @Test + func testTwoDifferentDiagnosticsReported() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskSignature: "diagnostics"), + .diagnostic( + locationContext2: .init( + taskSignature: "diagnostics" + ), + message: "First diagnostic", + appendToOutputStream: true + ), + .diagnostic( + locationContext2: .init( + taskSignature: "diagnostics" + ), + message: "Second diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskSignature: "diagnostics", result: .failed) // Handler only emits when a task is completed. + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(self.observability.hasErrorDiagnostics) + + try expectDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: "First diagnostic", severity: .error) + result.check(diagnostic: "Second diagnostic", severity: .error) + } + } + @Test func testManyDiagnosticsReported() throws { - let (messageHandler, _, observability) = createMessageHandler() + let messageHandler = self.messageHandler.warning let events: [SwiftBuildMessage] = [ .taskStartedInfo(taskID: 1, taskSignature: "simple-diagnostic"), - .diagnosticInfo( + .diagnostic( locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true ), .taskStartedInfo(taskID: 2, taskSignature: "another-diagnostic"), .taskStartedInfo(taskID: 3, taskSignature: "warning-diagnostic"), - .diagnosticInfo( + .diagnostic( kind: .warning, locationContext2: .init(taskSignature: "warning-diagnostic"), message: "Warning diagnostic", appendToOutputStream: true ), .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic", result: .failed), - .diagnosticInfo( + .diagnostic( kind: .warning, locationContext2: .init(taskSignature: "warning-diagnostic"), message: "Another warning diagnostic", appendToOutputStream: true ), .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic", result: .success), - .diagnosticInfo( + .diagnostic( kind: .note, locationContext2: .init(taskSignature: "another-diagnostic"), message: "Another diagnostic", @@ -122,11 +210,11 @@ struct SwiftBuildSystemMessageHandlerTests { _ = try messageHandler.emitEvent(event) } - #expect(observability.hasErrorDiagnostics) + #expect(self.observability.hasErrorDiagnostics) try expectDiagnostics(observability.diagnostics) { result in result.check(diagnostic: "Simple diagnostic", severity: .error) - result.check(diagnostic: "Another diagnostic", severity: .debug) + result.check(diagnostic: "Another diagnostic", severity: .info) result.check(diagnostic: "Another warning diagnostic", severity: .warning) result.check(diagnostic: "Warning diagnostic", severity: .warning) } @@ -134,7 +222,7 @@ struct SwiftBuildSystemMessageHandlerTests { @Test func testCompilerOutputDiagnosticsWithoutDuplicatedLogging() throws { - let (messageHandler, outputStream, observability) = createMessageHandler() + let messageHandler = self.messageHandler.warning let simpleDiagnosticString: String = "[error]: Simple diagnostic\n" let simpleOutputInfo: SwiftBuildMessage = .outputInfo( @@ -166,14 +254,14 @@ struct SwiftBuildSystemMessageHandlerTests { let events: [SwiftBuildMessage] = [ .taskStartedInfo(taskID: 1, taskSignature: "simple-diagnostic"), - .diagnosticInfo( + .diagnostic( locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true ), .taskStartedInfo(taskID: 2, taskSignature: "another-diagnostic"), .taskStartedInfo(taskID: 3, taskSignature: "warning-diagnostic"), - .diagnosticInfo( + .diagnostic( kind: .warning, locationContext2: .init(taskSignature: "warning-diagnostic"), message: "Warning diagnostic", @@ -182,7 +270,7 @@ struct SwiftBuildSystemMessageHandlerTests { anotherWarningOutputInfo, simpleOutputInfo, .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic"), - .diagnosticInfo( + .diagnostic( kind: .warning, locationContext2: .init(taskSignature: "warning-diagnostic"), message: "Another warning diagnostic", @@ -190,7 +278,7 @@ struct SwiftBuildSystemMessageHandlerTests { ), warningOutputInfo, .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic"), - .diagnosticInfo( + .diagnostic( kind: .note, locationContext2: .init(taskSignature: "another-diagnostic"), message: "Another diagnostic", @@ -204,30 +292,199 @@ struct SwiftBuildSystemMessageHandlerTests { _ = try messageHandler.emitEvent(event) } - let outputText = outputStream.bytes.description + let outputText = self.outputStream.bytes.description #expect(outputText.contains("error")) } @Test func testDiagnosticOutputWhenOnlyWarnings() throws { - let (messageHandler, outputStream, observability) = createMessageHandler() + let messageHandler = self.messageHandler.warning let events: [SwiftBuildMessage] = [ .taskStartedInfo(taskID: 1, taskSignature: "simple-warning-diagnostic"), - .diagnosticInfo( + .diagnostic( kind: .warning, locationContext2: .init(taskSignature: "simple-warning-diagnostic"), message: "Simple warning diagnostic", appendToOutputStream: true ), - .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic", result: .success) + .taskCompleteInfo(taskID: 1, taskSignature: "simple-warning-diagnostic", result: .success) ] for event in events { _ = try messageHandler.emitEvent(event) } - #expect(observability.hasWarningDiagnostics) + #expect(self.observability.hasWarningDiagnostics) + } + + @Test + func testDiagnosticOutputWhenOnlyNotes() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskID: 1, taskSignature: "simple-note-diagnostic"), + .diagnostic( + kind: .note, + locationContext2: .init(taskSignature: "simple-note-diagnostic"), + message: "Simple note diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 1, taskSignature: "simple-note-diagnostic", result: .success) + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 1) + try expectDiagnostics(self.observability.diagnostics) { result in + result.check(diagnostic: "Simple note diagnostic", severity: .info) + } + } + + @Test + func testDiagnosticOutputWhenOnlyDebugs() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskID: 1, taskSignature: "simple-debug-diagnostic"), + .diagnostic( + kind: .remark, + locationContext2: .init(taskSignature: "simple-debug-diagnostic"), + message: "Simple debug diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 1, taskSignature: "simple-debug-diagnostic", result: .success) + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 1) + try expectDiagnostics(self.observability.diagnostics) { result in + result.check(diagnostic: "Simple debug diagnostic", severity: .debug) + } + } + + @Test + func testPlanningOperationStartAndCompleteMessagesVerboseOnly() throws { + let verboseMessageHandler = self.messageHandler.debug + + let events: [SwiftBuildMessage] = [ + .planningOperationStartedInfo(), + .planningOperationCompletedInfo() + ] + + for event in events { + _ = try verboseMessageHandler.emitEvent(event) + } + + let verboseOutput = self.outputStream.bytes.description + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 0) + + #expect(verboseOutput.contains("Planning build")) + #expect(verboseOutput.contains("Planning complete")) + } + + @Test + func testPlanningOperationStartAndCompleteNoMessageWarningLogLevel() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .planningOperationStartedInfo(), + .planningOperationCompletedInfo() + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + let output = self.outputStream.bytes.description + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 0) + + #expect(!output.contains("Planning build")) + #expect(!output.contains("Planning complete")) + } + + @Test + func testPlanningOperationStartAndCompleteNoMessageErrorLogLevel() throws { + let messageHandler = self.messageHandler.error + + let events: [SwiftBuildMessage] = [ + .planningOperationStartedInfo(), + .planningOperationCompletedInfo() + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + let output = self.outputStream.bytes.description + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 0) + + #expect(!output.contains("Planning build")) + #expect(!output.contains("Planning complete")) + } + + @Test + func testTargetUpToDateMessage() throws { + let messageHandler = self.messageHandler.debug + + let events: [SwiftBuildMessage] = [ + .targetUpToDateInfo() + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 0) + + let output = self.outputStream.bytes.description + #expect(output.contains("Target mock-target-guid up to date.")) + } + + @Test + func testBuildProgressMessages() throws { + let messageHandler = self.messageHandler.warning + + let events: [SwiftBuildMessage] = [ + .progress(message: "Weird percent", percentComplete: -1), + .progress(message: "12 / 32", percentComplete: 0), + .progress(message: "Something useful", percentComplete: 12), + .progress(message: "Complete", percentComplete: 100) + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(!self.observability.hasWarningDiagnostics) + #expect(!self.observability.hasErrorDiagnostics) + #expect(self.observability.diagnostics.count == 0) + + let output = self.outputStream.bytes.description + #expect(output.contains("Weird percent")) + #expect(!output.contains("12 / 32")) + #expect(output.contains("Something useful")) + #expect(output.contains("Complete")) } } @@ -284,7 +541,7 @@ extension SwiftBuildMessage { } /// SwiftBuildMessage.DiagnosticInfo - package static func diagnosticInfo( + package static func diagnostic( kind: DiagnosticInfo.Kind = .error, location: DiagnosticInfo.Location = .unknown, locationContext: LocationContext = .task(taskID: 1, targetID: 1), @@ -352,4 +609,48 @@ extension SwiftBuildMessage { ) ) } + + /// SwiftBuildMessage.PlanningOperationStartedInfo + package static func planningOperationStartedInfo( + planningOperationID: String = "mock-planning-operation-id" + ) -> SwiftBuildMessage { + .planningOperationStarted( + .init(planningOperationID: planningOperationID) + ) + } + + /// SwiftBuildMessage.PlanningOperationCompletedInfo + package static func planningOperationCompletedInfo( + planningOperationID: String = "mock-planning-operation-id" + ) -> SwiftBuildMessage { + .planningOperationCompleted( + .init(planningOperationID: planningOperationID) + ) + } + + /// SwiftBuildMessage.TargetUpToDateInfo + package static func targetUpToDateInfo( + guid: String = "mock-target-guid" + ) -> SwiftBuildMessage { + .targetUpToDate( + .init(guid: guid) + ) + } + + /// SwiftBuildMessage.DidUpdateProgressInfo + package static func progress( + message: String, + percentComplete: Double, + showInLog: Bool = false, + targetName: String? = nil + ) -> SwiftBuildMessage { + .didUpdateProgress( + .init( + message: message, + percentComplete: percentComplete, + showInLog: showInLog, + targetName: targetName + ) + ) + } }