Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions Sources/SWBTestSupport/BuildOperationTester.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1414,12 +1414,16 @@ package final class BuildOperationTester {
}

/// Construct the tasks for the given build parameters, and test the result.
@discardableResult package func checkBuild<T>(_ name: String? = nil, parameters: BuildParameters? = nil, runDestination: SWBProtocol.RunDestinationInfo?, buildRequest inputBuildRequest: BuildRequest? = nil, buildCommand: BuildCommand? = nil, schemeCommand: SchemeCommand? = .launch, persistent: Bool = false, serial: Bool = false, buildOutputMap: [String:String]? = nil, signableTargets: Set<String> = [], signableTargetInputs: [String: ProvisioningTaskInputs] = [:], clientDelegate: (any ClientDelegate)? = nil, sourceLocation: SourceLocation = #_sourceLocation, body: (BuildResults) async throws -> T) async throws -> T {
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, performBuild: { try await $0.buildWithTimeout() })
///
/// This variant of `checkBuild()` differs from the main versiion in these ways:
/// - Does not have a `operationBuildRequest` parameter.
/// - Has a default value of the `performBuild` parameter which performs a nortmal build.
@discardableResult package func checkBuild<T>(_ name: String? = nil, parameters: BuildParameters? = nil, runDestination: SWBProtocol.RunDestinationInfo?, buildRequest inputBuildRequest: BuildRequest? = nil, buildCommand: BuildCommand? = nil, schemeCommand: SchemeCommand? = .launch, persistent: Bool = false, serial: Bool = false, buildOutputMap: [String:String]? = nil, signableTargets: Set<String> = [], signableTargetInputs: [String: ProvisioningTaskInputs] = [:], attachBuildArifacts: Bool = true, clientDelegate: (any ClientDelegate)? = nil, sourceLocation: SourceLocation = #_sourceLocation, body: (BuildResults) async throws -> T) async throws -> T {
try await checkBuild(name, parameters: parameters, runDestination: runDestination, buildRequest: inputBuildRequest, buildCommand: buildCommand, schemeCommand: schemeCommand, persistent: persistent, serial: serial, buildOutputMap: buildOutputMap, signableTargets: signableTargets, signableTargetInputs: signableTargetInputs, attachBuildArifacts: attachBuildArifacts, clientDelegate: clientDelegate, sourceLocation: sourceLocation, body: body, performBuild: { try await $0.buildWithTimeout() })
}

/// Construct the tasks for the given build parameters, and test the result.
@discardableResult package func checkBuild<T>(_ name: String? = nil, parameters: BuildParameters? = nil, runDestination: RunDestinationInfo?, buildRequest inputBuildRequest: BuildRequest? = nil, operationBuildRequest: BuildRequest? = nil, buildCommand: BuildCommand? = nil, schemeCommand: SchemeCommand? = .launch, persistent: Bool = false, serial: Bool = false, buildOutputMap: [String:String]? = nil, signableTargets: Set<String> = [], signableTargetInputs: [String: ProvisioningTaskInputs] = [:], clientDelegate: (any ClientDelegate)? = nil, sourceLocation: SourceLocation = #_sourceLocation, body: (BuildResults) async throws -> T, performBuild: @escaping (any BuildSystemOperation) async throws -> Void) async throws -> T {
@discardableResult package func checkBuild<T>(_ name: String? = nil, parameters: BuildParameters? = nil, runDestination: SWBProtocol.RunDestinationInfo?, buildRequest inputBuildRequest: BuildRequest? = nil, operationBuildRequest: BuildRequest? = nil, buildCommand: BuildCommand? = nil, schemeCommand: SchemeCommand? = .launch, persistent: Bool = false, serial: Bool = false, buildOutputMap: [String:String]? = nil, signableTargets: Set<String> = [], signableTargetInputs: [String: ProvisioningTaskInputs] = [:], attachBuildArifacts: Bool = true, clientDelegate: (any ClientDelegate)? = nil, sourceLocation: SourceLocation = #_sourceLocation, body: (BuildResults) async throws -> T, performBuild: @escaping (any BuildSystemOperation) async throws -> Void) async throws -> T {
try await checkBuildDescription(parameters, runDestination: runDestination, buildRequest: inputBuildRequest, buildCommand: buildCommand, schemeCommand: schemeCommand, persistent: persistent, serial: serial, signableTargets: signableTargets, signableTargetInputs: signableTargetInputs, clientDelegate: clientDelegate) { results throws in
// Check that there are no duplicate task identifiers - it is a fatal error if there are, unless `continueBuildingAfterErrors` is set.
var tasksByTaskIdentifier: [TaskIdentifier: Task] = [:]
Expand Down Expand Up @@ -1476,19 +1480,14 @@ package final class BuildOperationTester {
// Check the results.
let results = try BuildResults(core: core, workspace: workspace, buildDescriptionResults: results, tasksByTaskIdentifier: delegate.tasksByTaskIdentifier.merging(delegate.dynamicTasksByTaskIdentifier, uniquingKeysWith: { a, b in a }), fs: fs, events: events, dynamicTaskDependencies: dynamicDependencies, buildDatabasePath: persistent ? results.buildDescription.buildDatabasePath : nil)

/*@MainActor func addAttachments() {
// TODO: This `runActivity` call should be wider in scope, but this would significantly complicate the code flow due to threading requirements without having async/await.
XCTContext.runActivity(named: "Execute Build Operation" + (name.map({ " \"\($0)\"" }) ?? "")) { activity in
// TODO: <rdar://59432231> Longer term, we should find a way to share code with CoreQualificationTester, which has a number of APIs for emitting build operation debug info.
activity.attach(name: "Build Transcript", string: results.buildTranscript)
if localFS.exists(results.buildDescription.packagePath) {
activity.attach(name: "Build Description", from: results.buildDescription.packagePath)
}
// TODO: <rdar://59432231> Longer term, we should find a way to share code with CoreQualificationTester, which has a number of APIs for emitting build operation debug info.
if attachBuildArifacts {
Attachment.record(results.buildTranscript, named: "Build Transcript" + (name.map({ " for Build Operation \"\($0)\"" }) ?? ""))
if localFS.exists(results.buildDescription.packagePath) {
Attachment.record(results.buildDescription.packagePath.str, named: "Build Description" + (name.map({ " for Build Operation \"\($0)\"" }) ?? ""))
}
}

await addAttachments()*/

defer {
let validationResults = results.validate(sourceLocation: sourceLocation)

Expand Down
6 changes: 4 additions & 2 deletions Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,16 @@ fileprivate struct BuildTaskBehaviorTests: CoreBasedTests {
let startBuilds = WaitCondition()
let buildsDone = WaitCondition()

// The threads this test spawns to run the build can outlive the lifetime of the test itself, so it passed attachBuildArifacts = false since if that happens then the test might crash because the reporter needed to add the attachments will no longer exist.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa, that seems bad, nothing should outlive the test itself. I'm actually quite surprised this is here.

Can you make an array of Task<Void, Never> just before the for loop, and then append the Task handles to that array as you go? Then after the for loop, iterate the array and await the value of each of the tasks. That should guarantee we never have dangling work past the end of the test.

Copy link
Contributor Author

@mhrawdon mhrawdon Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already spent a couple of hours trying to fix up this test, and I'm not going to mess with it further as part of this PR. I suggest opening a new Issue for someone who is deeply familiar with it to investigate. I was able to reproduce it most of the time by running the whole BuildSystemTests suite inside of Xcode (removing the change to cause it to not add attachments, of course).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. Good that it's easy to reproduce.


// build1 cancels immediately
_Concurrency.Task<Void, Never> {
do {
let (tester, _, _) = try await self.createBuildOperationTesterForCancellation(temporaryDirectory: temporaryDirectory, contents: "")

tester.userPreferences = prefs

try await tester.checkBuild(runDestination: .host, body: { results in
try await tester.checkBuild(runDestination: .host, attachBuildArifacts: false, body: { results in
results.checkCapstoneEvents(last: .buildCancelled)
}) { operation in
build1Ready.signal()
Expand Down Expand Up @@ -264,7 +266,7 @@ fileprivate struct BuildTaskBehaviorTests: CoreBasedTests {

tester.userPreferences = prefs

try await tester.checkBuild(runDestination: .host, body: { results in
try await tester.checkBuild(runDestination: .host, attachBuildArifacts: false, body: { results in
#expect(results.events.first! == .buildStarted)

let waitTask = try #require(results.getTask(.matchRule(["wait"])))
Expand Down
Loading