Skip to content

Commit 100783c

Browse files
committed
Add a facility for tests to opt in to loading the downloadable Metal toolchain
This will greatly reduce the number of tests which fail when there is a problem with the downloadable Metal toolchain. This is off by default, since very few tests need this. Two tests currently opt in.
1 parent a98bf52 commit 100783c

File tree

4 files changed

+46
-30
lines changed

4 files changed

+46
-30
lines changed

Sources/SWBTestSupport/CoreBasedTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ package protocol CoreBasedTests {
2323
}
2424

2525
extension CoreBasedTests {
26-
package static func makeCore(skipLoadingPluginsNamed: Set<String> = [], registerExtraPlugins: @PluginExtensionSystemActor (PluginManager) -> Void = { _ in }, simulatedInferiorProductsPath: Path? = nil, environment: [String: String] = [:], _ delegate: TestingCoreDelegate? = nil, sourceLocation: SourceLocation = #_sourceLocation) async throws -> Core {
26+
/// This will create a customized `Core` object using the specified parameters, providing a test with detailed control over the contents of the `Core` it uses.
27+
package static func makeCore(skipLoadingPluginsNamed: Set<String> = [], registerExtraPlugins: @PluginExtensionSystemActor (PluginManager) -> Void = { _ in }, simulatedInferiorProductsPath: Path? = nil, environment: [String: String] = [:], _ delegate: TestingCoreDelegate? = nil, configurationDelegate: TestingCoreConfigurationDelegate? = nil, sourceLocation: SourceLocation = #_sourceLocation) async throws -> Core {
2728
let core: Result<Core, any Error>
2829
do {
29-
let theCore = try await Core.createInitializedTestingCore(skipLoadingPluginsNamed: skipLoadingPluginsNamed, registerExtraPlugins: registerExtraPlugins, simulatedInferiorProductsPath: simulatedInferiorProductsPath, environment: environment, delegate: delegate)
30+
let theCore = try await Core.createInitializedTestingCore(skipLoadingPluginsNamed: skipLoadingPluginsNamed, registerExtraPlugins: registerExtraPlugins, simulatedInferiorProductsPath: simulatedInferiorProductsPath, environment: environment, delegate: delegate, configurationDelegate: configurationDelegate)
3031
core = .success(theCore)
3132
} catch {
3233
core = .failure(error)
@@ -48,6 +49,7 @@ extension CoreBasedTests {
4849
return try Self._getCore(core: core, sourceLocation: sourceLocation)
4950
}
5051

52+
/// This private method caches the `Core` object created in `GetCore(sourceLocation:)`, and returns the same object if queried again.
5153
private static func _getCore(core: Result<Core, any Error>, sourceLocation: SourceLocation = #_sourceLocation) throws -> Core {
5254
do {
5355
return try core.get()

Sources/SWBTestSupport/CoreTestSupport.swift

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ extension Core {
4747
/// Get an initialized Core suitable for testing.
4848
///
4949
/// This function requires there to be no errors during loading the core.
50-
package static func createInitializedTestingCore(skipLoadingPluginsNamed: Set<String>, registerExtraPlugins: @PluginExtensionSystemActor (PluginManager) -> Void, simulatedInferiorProductsPath: Path? = nil, environment: [String:String] = [:], delegate: TestingCoreDelegate? = nil) async throws -> Core {
50+
package static func createInitializedTestingCore(skipLoadingPluginsNamed: Set<String>, registerExtraPlugins: @PluginExtensionSystemActor (PluginManager) -> Void, simulatedInferiorProductsPath: Path? = nil, environment: [String:String] = [:], delegate: TestingCoreDelegate? = nil, configurationDelegate: TestingCoreConfigurationDelegate? = nil) async throws -> Core {
5151
// When this code is being loaded directly via unit tests, find the running Xcode path.
5252
//
5353
// This is a "well known" launch parameter set in Xcode's schemes.
@@ -74,33 +74,35 @@ extension Core {
7474
//
7575
// If the given environment already contains `EXTERNAL_TOOLCHAINS_DIR` and `TOOLCHAINS`, we're assuming that we do not have to obtain any toolchain information.
7676
var environment = environment
77-
if (try? ProcessInfo.processInfo.hostOperatingSystem()) == .macOS, !(environment.contains("EXTERNAL_TOOLCHAINS_DIR") && environment.contains("TOOLCHAINS")) {
78-
let activeDeveloperPath: Path
79-
if let developerPath {
80-
activeDeveloperPath = developerPath.path
81-
} else {
82-
activeDeveloperPath = try await Xcode.getActiveDeveloperDirectoryPath()
83-
}
84-
let defaultToolchainPath = activeDeveloperPath.join("Toolchains/XcodeDefault.xctoolchain")
85-
86-
if !localFS.exists(defaultToolchainPath.join("usr/metal/current")) {
87-
struct MetalToolchainInfo: Decodable {
88-
let buildVersion: String
89-
let status: String
90-
let toolchainIdentifier: String
91-
let toolchainSearchPath: String
77+
if configurationDelegate?.loadMetalToolchain == true {
78+
if (try? ProcessInfo.processInfo.hostOperatingSystem()) == .macOS, !(environment.contains("EXTERNAL_TOOLCHAINS_DIR") && environment.contains("TOOLCHAINS")) {
79+
let activeDeveloperPath: Path
80+
if let developerPath {
81+
activeDeveloperPath = developerPath.path
82+
} else {
83+
activeDeveloperPath = try await Xcode.getActiveDeveloperDirectoryPath()
9284
}
93-
94-
let result = try await Process.getOutput(url: URL(fileURLWithPath: activeDeveloperPath.join("usr/bin/xcodebuild").str), arguments: ["-showComponent", "metalToolchain", "-json"], environment: ["DEVELOPER_DIR": activeDeveloperPath.str])
95-
if result.exitStatus != .exit(0) {
96-
throw StubError.error("xcodebuild failed: \(String(data: result.stdout, encoding: .utf8) ?? "")\n\(String(data: result.stderr, encoding: .utf8) ?? "")")
85+
let defaultToolchainPath = activeDeveloperPath.join("Toolchains/XcodeDefault.xctoolchain")
86+
87+
if !localFS.exists(defaultToolchainPath.join("usr/metal/current")) {
88+
struct MetalToolchainInfo: Decodable {
89+
let buildVersion: String
90+
let status: String
91+
let toolchainIdentifier: String
92+
let toolchainSearchPath: String
93+
}
94+
95+
let result = try await Process.getOutput(url: URL(fileURLWithPath: activeDeveloperPath.join("usr/bin/xcodebuild").str), arguments: ["-showComponent", "metalToolchain", "-json"], environment: ["DEVELOPER_DIR": activeDeveloperPath.str])
96+
if result.exitStatus != .exit(0) {
97+
throw StubError.error("xcodebuild failed: \(String(data: result.stdout, encoding: .utf8) ?? "")\n\(String(data: result.stderr, encoding: .utf8) ?? "")")
98+
}
99+
100+
let metalToolchainInfo = try JSONDecoder().decode(MetalToolchainInfo.self, from: result.stdout)
101+
environment.addContents(of: [
102+
"TOOLCHAINS": "\(metalToolchainInfo.toolchainIdentifier) $(inherited)",
103+
"EXTERNAL_TOOLCHAINS_DIR": metalToolchainInfo.toolchainSearchPath,
104+
])
97105
}
98-
99-
let metalToolchainInfo = try JSONDecoder().decode(MetalToolchainInfo.self, from: result.stdout)
100-
environment.addContents(of: [
101-
"TOOLCHAINS": "\(metalToolchainInfo.toolchainIdentifier) $(inherited)",
102-
"EXTERNAL_TOOLCHAINS_DIR": metalToolchainInfo.toolchainSearchPath,
103-
])
104106
}
105107
}
106108

@@ -246,3 +248,15 @@ package final class TestingCoreDelegate: CoreDelegate, Sendable {
246248
return _diagnosticsEngine.diagnostics.pathMessageTuples(.warning)
247249
}
248250
}
251+
252+
/// Individual classes may pass an instance of this protocol to `CoreBasedTests.makeCore()` to configure which special elements of the testing core they need. `Core.createInitializedTestingCore()` (above) will configure the core based on what's passed here.
253+
///
254+
/// This allows tests which don't care about those elements to not fail because of errors trying to load them.
255+
package struct TestingCoreConfigurationDelegate: Sendable {
256+
/// Only tests which are exercising Metal should need to load the Metal toolchain, so only those tests will fail if loading the toolchain fails.
257+
package let loadMetalToolchain: Bool
258+
259+
package init(loadMetalToolchain: Bool = false) {
260+
self.loadMetalToolchain = loadMetalToolchain
261+
}
262+
}

Tests/SWBBuildSystemTests/BuildCommandTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests {
325325

326326
@Test(.requireSDKs(.macOS), .requireXcode16())
327327
func singleFileCompileMetal() async throws {
328-
let core = try await getCore()
328+
let core = try await Self.makeCore(configurationDelegate: TestingCoreConfigurationDelegate(loadMetalToolchain: true))
329329
try await withTemporaryDirectory { tmpDirPath async throws -> Void in
330330
let testWorkspace = try await TestWorkspace(
331331
"Test",

Tests/SWBBuildSystemTests/BuildOperationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5794,7 +5794,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
57945794

57955795
@Test(.requireSDKs(.macOS))
57965796
func incrementalMetalLinkWithCodeSign() async throws {
5797-
let core = try await getCore()
5797+
let core = try await Self.makeCore(configurationDelegate: TestingCoreConfigurationDelegate(loadMetalToolchain: true))
57985798
try await withTemporaryDirectory { tmpDirPath async throws -> Void in
57995799
let testWorkspace = try await TestWorkspace(
58005800
"Test",

0 commit comments

Comments
 (0)