diff --git a/Sources/SWBTestSupport/BuildOperationTester.swift b/Sources/SWBTestSupport/BuildOperationTester.swift index 14d5d249..cae93662 100644 --- a/Sources/SWBTestSupport/BuildOperationTester.swift +++ b/Sources/SWBTestSupport/BuildOperationTester.swift @@ -19,7 +19,7 @@ package import SWBBuildSystem @_spi(Testing) package import SWBCore package import SWBTaskConstruction package import SWBTaskExecution -package import SWBUtil +@_spi(Testing) package import SWBUtil private import class SWBLLBuild.BuildDB private import class SWBLLBuild.BuildKey @@ -713,6 +713,18 @@ package final class BuildOperationTester { } + package func checkNoTaskWithBacktraces(_ conditions: TaskCondition..., sourceLocation: SourceLocation = #_sourceLocation) { + for matchedTask in findMatchingTasks(conditions) { + Issue.record("found unexpected task matching conditions '\(conditions)', found: \(matchedTask)", sourceLocation: sourceLocation) + + if let frameID = getBacktraceID(matchedTask, sourceLocation: sourceLocation) { + enumerateBacktraces(frameID) { _, category, description in + Issue.record("...", sourceLocation: sourceLocation) + } + } + } + } + /// Check whether the results contains a dependency cycle error. If so, then consume the error and create a `CycleChecking` object and pass it to the block. Otherwise fail. package func checkDependencyCycle(_ pattern: StringPattern, kind: DiagnosticKind = .error, failIfNotFound: Bool = true, sourceLocation: SourceLocation = #_sourceLocation, body: (CycleChecker) async throws -> Void) async throws { guard let message = getDiagnosticMessage(pattern, kind: kind, checkDiagnostic: { _ in true }) else { @@ -1011,8 +1023,21 @@ package final class BuildOperationTester { startedTasks.remove(task) } - package func checkBacktrace(_ identifier: BuildOperationBacktraceFrameEmitted.Identifier, _ patterns: [StringPattern], sourceLocation: SourceLocation = #_sourceLocation) { - var frameDescriptions: [String] = [] + private func getBacktraceID(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) -> BuildOperationBacktraceFrameEmitted.Identifier? { + guard let frameID: BuildOperationBacktraceFrameEmitted.Identifier = events.compactMap ({ (event) -> BuildOperationBacktraceFrameEmitted.Identifier? in + guard case .emittedBuildBacktraceFrame(identifier: let identifier, previousFrameIdentifier: _, category: _, description: _) = event, case .task(let signature) = identifier, BuildOperationTaskSignature.taskIdentifier(ByteString(encodingAsUTF8: task.identifier.rawValue)) == signature else { + return nil + } + return identifier + // Iff the task is a dynamic task, there may be more than one corresponding frame if it was requested multiple times, in which case we choose the first. Non-dynamic tasks always have a 1-1 relationship with frames. + }).sorted().first else { + Issue.record("Did not find a single build backtrace frame for task: \(task.identifier)", sourceLocation: sourceLocation) + return nil + } + return frameID + } + + private func enumerateBacktraces(_ identifier: BuildOperationBacktraceFrameEmitted.Identifier, _ handleFrameInfo: (_ identifier: BuildOperationBacktraceFrameEmitted.Identifier?, _ category: BuildOperationBacktraceFrameEmitted.Category, _ description: String) -> ()) { var currentFrameID: BuildOperationBacktraceFrameEmitted.Identifier? = identifier while let id = currentFrameID { if let frameInfo: (BuildOperationBacktraceFrameEmitted.Identifier?, BuildOperationBacktraceFrameEmitted.Category, String) = events.compactMap({ (event) -> (BuildOperationBacktraceFrameEmitted.Identifier?, BuildOperationBacktraceFrameEmitted.Category, String)? in @@ -1022,28 +1047,29 @@ package final class BuildOperationTester { return (previousFrameIdentifier, category, description) // Iff the task is a dynamic task, there may be more than one corresponding frame if it was requested multiple times, in which case we choose the first. Non-dynamic tasks always have a 1-1 relationship with frames. }).sorted(by: { $0.0 }).first { - frameDescriptions.append("") + handleFrameInfo(frameInfo.0, frameInfo.1, frameInfo.2) currentFrameID = frameInfo.0 } else { currentFrameID = nil } } + } + + package func checkBacktrace(_ identifier: BuildOperationBacktraceFrameEmitted.Identifier, _ patterns: [StringPattern], sourceLocation: SourceLocation = #_sourceLocation) { + var frameDescriptions: [String] = [] + enumerateBacktraces(identifier) { (_, category, description) in + frameDescriptions.append("") + } XCTAssertMatch(frameDescriptions, patterns, sourceLocation: sourceLocation) } package func checkBacktrace(_ task: Task, _ patterns: [StringPattern], sourceLocation: SourceLocation = #_sourceLocation) { - guard let frameID: BuildOperationBacktraceFrameEmitted.Identifier = events.compactMap ({ (event) -> BuildOperationBacktraceFrameEmitted.Identifier? in - guard case .emittedBuildBacktraceFrame(identifier: let identifier, previousFrameIdentifier: _, category: _, description: _) = event, case .task(let signature) = identifier, BuildOperationTaskSignature.taskIdentifier(ByteString(encodingAsUTF8: task.identifier.rawValue)) == signature else { - return nil - } - return identifier - // Iff the task is a dynamic task, there may be more than one corresponding frame if it was requested multiple times, in which case we choose the first. Non-dynamic tasks always have a 1-1 relationship with frames. - }).sorted().first else { - Issue.record("Did not find a single build backtrace frame for task: \(task.identifier)", sourceLocation: sourceLocation) - return + if let frameID = getBacktraceID(task, sourceLocation: sourceLocation) { + checkBacktrace(frameID, patterns, sourceLocation: sourceLocation) + } else { + // already recorded an issue } - checkBacktrace(frameID, patterns, sourceLocation: sourceLocation) } private class TaskDependencyResolver { @@ -1520,7 +1546,7 @@ package final class BuildOperationTester { func body(results: BuildResults) throws -> Void { results.consumeTasksMatchingRuleTypes(excludedTasks) - results.checkNoTask(sourceLocation: sourceLocation) + results.checkNoTaskWithBacktraces(sourceLocation: sourceLocation) results.checkNote(.equal("Building targets in dependency order"), failIfNotFound: false) results.checkNote(.prefix("Target dependency graph"), failIfNotFound: false) @@ -1546,7 +1572,9 @@ package final class BuildOperationTester { } } - try await checkBuild(name, parameters: parameters, runDestination: runDestination, buildRequest: inputBuildRequest, buildCommand: buildCommand, schemeCommand: schemeCommand, persistent: persistent, serial: serial, buildOutputMap: buildOutputMap, signableTargets: signableTargets, signableTargetInputs: signableTargetInputs, clientDelegate: clientDelegate, sourceLocation: sourceLocation, body: body) + try await UserDefaults.withEnvironment(["EnableBuildBacktraceRecording": "true"]) { + try await checkBuild(name, parameters: parameters, runDestination: runDestination, buildRequest: inputBuildRequest, buildCommand: buildCommand, schemeCommand: schemeCommand, persistent: persistent, serial: serial, buildOutputMap: buildOutputMap, signableTargets: signableTargets, signableTargetInputs: signableTargetInputs, clientDelegate: clientDelegate, sourceLocation: sourceLocation, body: body) + } } package static func buildRequestForIndexOperation(