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/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 004281991e7..37eccdd0935 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 @@ -125,7 +126,7 @@ public final class SwiftBuildSystemMessageHandler { if self.logLevel.isVerbose { self.outputStream.send(started.description + "\n") } else { - observabilityScope.emit(info: started) + observabilityScope.print(started, verbose: self.logLevel.isVerbose) } } @@ -216,13 +217,13 @@ 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)" } + // TODO bp: some message suffixes seems to have its own stepping fraction. progressAnimation.update(step: step, total: 100, text: message) callback = { [weak self] buildSystem in self?.buildDelegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) @@ -770,7 +771,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..574944ba512 100644 --- a/Tests/BuildTests/BuildSystemDelegateTests.swift +++ b/Tests/BuildTests/BuildSystemDelegateTests.swift @@ -45,13 +45,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 6aa4315a857..6afb0026721 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -4537,9 +4537,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/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift index 48c43b288d5..da92886d5ba 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 - return (handler, outputStream, observability) + let events: [SwiftBuildMessage] = [ + .taskCompleteInfo(result: .success) + ] + + #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,84 @@ 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(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) + } + } - #expect(observability.hasWarningDiagnostics) + @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) + } } } @@ -284,7 +426,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),