Skip to content

Commit d5e933e

Browse files
committed
Get various code paths working across platforms including on Darwin (with a bit of hackery we'll want to replace ASAP)
1 parent 2d31852 commit d5e933e

File tree

7 files changed

+120
-53
lines changed

7 files changed

+120
-53
lines changed

Sources/Build/BuildPlan/BuildPlan+Test.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ extension BuildPlan {
3434
_ fileSystem: FileSystem,
3535
_ observabilityScope: ObservabilityScope
3636
) throws -> [(product: ResolvedProduct, discoveryTargetBuildDescription: SwiftModuleBuildDescription?, entryPointTargetBuildDescription: SwiftModuleBuildDescription)] {
37-
guard destinationBuildParameters.testingParameters.testProductStyle.requiresAdditionalDerivedTestTargets,
38-
case .entryPointExecutable(let explicitlyEnabledDiscovery, let explicitlySpecifiedPath) =
39-
destinationBuildParameters.testingParameters.testProductStyle
40-
else {
37+
guard destinationBuildParameters.testingParameters.testProductStyle.requiresAdditionalDerivedTestTargets else {
4138
throw InternalError("makeTestManifestTargets should not be used for build plan which does not require additional derived test targets")
4239
}
4340

41+
var explicitlyEnabledDiscovery = false
42+
var explicitlySpecifiedPath: AbsolutePath?
43+
if case let .entryPointExecutable(caseExplicitlyEnabledDiscovery, caseExplicitlySpecifiedPath) = destinationBuildParameters.testingParameters.testProductStyle {
44+
explicitlyEnabledDiscovery = caseExplicitlyEnabledDiscovery
45+
explicitlySpecifiedPath = caseExplicitlySpecifiedPath
46+
}
4447
let isEntryPointPathSpecifiedExplicitly = explicitlySpecifiedPath != nil
4548

4649
var isDiscoveryEnabledRedundantly = explicitlyEnabledDiscovery && !isEntryPointPathSpecifiedExplicitly

Sources/Build/LLBuildCommands.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ final class TestDiscoveryCommand: CustomLLBuildCommand, TestBuildCommand {
105105
private func execute(fileSystem: Basics.FileSystem, tool: TestDiscoveryTool) throws {
106106
let outputs = tool.outputs.compactMap { try? AbsolutePath(validating: $0.name) }
107107

108+
if case .loadableBundle = context.productsBuildParameters.testingParameters.testProductStyle {
109+
// When building an XCTest bundle, test discovery is handled by the
110+
// test harness process (i.e. this is the Darwin path.)
111+
for file in outputs {
112+
try fileSystem.writeIfChanged(path: file, string: "")
113+
}
114+
return
115+
}
116+
108117
let index = self.context.productsBuildParameters.indexStore
109118
let api = try self.context.indexStoreAPI.get()
110119
let store = try IndexStore.open(store: TSCAbsolutePath(index), api: api)
@@ -216,13 +225,21 @@ final class TestEntryPointCommand: CustomLLBuildCommand, TestBuildCommand {
216225
testObservabilitySetup = ""
217226
}
218227

228+
let swiftTestingImportCondition = "canImport(Testing)"
229+
let xctestImportCondition: String = switch buildParameters.testingParameters.testProductStyle {
230+
case .entryPointExecutable:
231+
"canImport(XCTest)"
232+
case .loadableBundle:
233+
"false"
234+
}
235+
219236
stream.send(
220237
#"""
221-
#if canImport(Testing)
238+
#if \#(swiftTestingImportCondition)
222239
import Testing
223240
#endif
224241
225-
#if canImport(XCTest)
242+
#if \#(xctestImportCondition)
226243
\#(generateTestObservationCode(buildParameters: buildParameters))
227244

228245
import XCTest
@@ -243,14 +260,15 @@ final class TestEntryPointCommand: CustomLLBuildCommand, TestBuildCommand {
243260
// Fallback if not specified: run XCTest (legacy behavior)
244261
return "XCTest"
245262
}
263+
246264
static func main() async {
247265
let which = Self.which()
248-
#if canImport(Testing)
266+
#if \#(swiftTestingImportCondition)
249267
if which == "swift-testing" {
250268
await Testing.__swiftPMEntryPoint() as Never
251269
}
252270
#endif
253-
#if canImport(XCTest)
271+
#if \#(xctestImportCondition)
254272
if which == "XCTest" {
255273
\#(testObservabilitySetup)
256274
#if os(WASI)

Sources/Commands/SwiftTestCommand.swift

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -223,18 +223,6 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
223223
var options: TestCommandOptions
224224

225225
private func run(_ swiftCommandState: SwiftCommandState, buildParameters: BuildParameters, testProducts: [BuiltTestProduct]) async throws {
226-
// validate XCTest available on darwin based systems
227-
let toolchain = try swiftCommandState.getTargetToolchain()
228-
if case let .unsupported(reason) = try swiftCommandState.getHostToolchain().swiftSDK.xctestSupport {
229-
if let reason {
230-
throw TestError.xctestNotAvailable(reason: reason)
231-
} else {
232-
throw TestError.xcodeNotInstalled
233-
}
234-
} else if toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil {
235-
throw TestError.xcodeNotInstalled
236-
}
237-
238226
// Remove test output from prior runs and validate priors.
239227
if self.options.enableExperimentalTestOutput && buildParameters.triple.supportsTestSummary {
240228
_ = try? localFileSystem.removeFileTree(buildParameters.testOutputPath)
@@ -244,6 +232,18 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
244232

245233
// Run XCTest.
246234
if options.testLibraryOptions.isEnabled(.xctest) {
235+
// validate XCTest available on darwin based systems
236+
let toolchain = try swiftCommandState.getTargetToolchain()
237+
if case let .unsupported(reason) = try swiftCommandState.getHostToolchain().swiftSDK.xctestSupport {
238+
if let reason {
239+
throw TestError.xctestNotAvailable(reason: reason)
240+
} else {
241+
throw TestError.xcodeNotInstalled
242+
}
243+
} else if toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil {
244+
throw TestError.xcodeNotInstalled
245+
}
246+
247247
if !self.options.shouldRunInParallel {
248248
let xctestArgs = try xctestArgs(for: testProducts, swiftCommandState: swiftCommandState)
249249
ranSuccessfully = try await runTestProducts(
@@ -398,14 +398,6 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
398398
swiftCommandState: SwiftCommandState,
399399
library: BuildParameters.Testing.Library
400400
) async throws -> Bool {
401-
#if os(macOS)
402-
if library == .swiftTesting {
403-
// On macOS, the xctest executable provided with Xcode acts as a harness
404-
// for Swift Testing, so we don't need to run Swift Testing tests separately.
405-
return true
406-
}
407-
#endif
408-
409401
// Pass through all arguments from the command line to Swift Testing.
410402
var additionalArguments = additionalArguments
411403
if library == .swiftTesting {
@@ -417,11 +409,19 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
417409
let testEnv = try TestingSupport.constructTestEnvironment(
418410
toolchain: toolchain,
419411
destinationBuildParameters: productsBuildParameters,
420-
sanitizers: globalOptions.build.sanitizers
412+
sanitizers: globalOptions.build.sanitizers,
413+
library: library
421414
)
422415

416+
let runnerPaths: [AbsolutePath] = switch library {
417+
case .xctest:
418+
testProducts.map(\.bundlePath)
419+
case .swiftTesting:
420+
testProducts.map(\.binaryPath)
421+
}
422+
423423
let runner = TestRunner(
424-
bundlePaths: testProducts.map(\.bundlePath),
424+
bundlePaths: runnerPaths,
425425
additionalArguments: additionalArguments,
426426
cancellator: swiftCommandState.cancellator,
427427
toolchain: toolchain,
@@ -657,7 +657,8 @@ extension SwiftTestCommand {
657657
let testEnv = try TestingSupport.constructTestEnvironment(
658658
toolchain: toolchain,
659659
destinationBuildParameters: productsBuildParameters,
660-
sanitizers: globalOptions.build.sanitizers
660+
sanitizers: globalOptions.build.sanitizers,
661+
library: .swiftTesting
661662
)
662663

663664
if testLibraryOptions.isEnabled(.xctest) {
@@ -810,22 +811,34 @@ final class TestRunner {
810811
/// Constructs arguments to execute XCTest.
811812
private func args(forTestAt testPath: AbsolutePath) throws -> [String] {
812813
var args: [String] = []
813-
#if os(macOS)
814-
guard let xctestPath = self.toolchain.xctestPath else {
815-
throw TestError.xcodeNotInstalled
814+
#if os(macOS)
815+
switch library {
816+
case .xctest:
817+
guard let xctestPath = self.toolchain.xctestPath else {
818+
throw TestError.xcodeNotInstalled
819+
}
820+
args += [xctestPath.pathString]
821+
case .swiftTesting:
822+
// FIXME: better way to get path to self
823+
let toolPath = String(unsafeUninitializedCapacity: 2048) { buffer in
824+
var count = UInt32(buffer.count)
825+
_NSGetExecutablePath(buffer.baseAddress!, &count)
826+
return Int(count)
827+
}
828+
args += [toolPath, "--test-bundle-path", testPath.pathString]
816829
}
817-
args = [xctestPath.pathString]
818830
args += additionalArguments
819831
args += [testPath.pathString]
820-
#else
832+
#else
821833
args += [testPath.pathString]
822834
args += additionalArguments
835+
#endif
836+
823837
if library == .swiftTesting {
824838
// HACK: tell the test bundle/executable that we want to run Swift Testing, not XCTest.
825-
// On macOS, this is controlled by the xctest runner process so we don't need to pass it.
839+
// XCTest doesn't understand this argument (yet), so don't pass it there.
826840
args += ["--testing-library=swift-testing"]
827841
}
828-
#endif
829842

830843
return args
831844
}
@@ -979,7 +992,8 @@ final class ParallelTestRunner {
979992
let testEnv = try TestingSupport.constructTestEnvironment(
980993
toolchain: self.toolchain,
981994
destinationBuildParameters: self.productsBuildParameters,
982-
sanitizers: self.buildOptions.sanitizers
995+
sanitizers: self.buildOptions.sanitizers,
996+
library: .xctest // swift-testing does not use ParallelTestRunner
983997
)
984998

985999
// Enqueue all the tests.

Sources/Commands/Utilities/PluginDelegate.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,6 @@ final class PluginDelegate: PluginInvocationDelegate {
218218
subset: PluginInvocationTestSubset,
219219
parameters: PluginInvocationTestParameters
220220
) throws -> PluginInvocationTestResult {
221-
// FIXME: support Swift Testing
222-
223221
// Build the tests. Ideally we should only build those that match the subset, but we don't have a way to know
224222
// which ones they are until we've built them and can examine the binaries.
225223
let toolchain = try swiftCommandState.getHostToolchain()
@@ -242,7 +240,8 @@ final class PluginDelegate: PluginInvocationDelegate {
242240
let testEnvironment = try TestingSupport.constructTestEnvironment(
243241
toolchain: toolchain,
244242
destinationBuildParameters: toolsBuildParameters,
245-
sanitizers: swiftCommandState.options.build.sanitizers
243+
sanitizers: swiftCommandState.options.build.sanitizers,
244+
library: .xctest // FIXME: support both libraries
246245
)
247246

248247
// Iterate over the tests and run those that match the filter.
@@ -285,7 +284,7 @@ final class PluginDelegate: PluginInvocationDelegate {
285284
toolchain: toolchain,
286285
testEnv: testEnvironment,
287286
observabilityScope: swiftCommandState.observabilityScope,
288-
library: .xctest)
287+
library: .xctest) // FIXME: support both libraries
289288

290289
// Run the test — for now we run the sequentially so we can capture accurate timing results.
291290
let startTime = DispatchTime.now()

Sources/Commands/Utilities/TestingSupport.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ enum TestingSupport {
120120
shouldSkipBuilding: shouldSkipBuilding,
121121
experimentalTestOutput: experimentalTestOutput
122122
).productsBuildParameters,
123-
sanitizers: sanitizers
123+
sanitizers: sanitizers,
124+
library: .xctest
124125
)
125126

126127
try AsyncProcess.checkNonZeroExit(arguments: args, environment: env)
@@ -134,7 +135,8 @@ enum TestingSupport {
134135
enableCodeCoverage: enableCodeCoverage,
135136
shouldSkipBuilding: shouldSkipBuilding
136137
).productsBuildParameters,
137-
sanitizers: sanitizers
138+
sanitizers: sanitizers,
139+
library: .xctest
138140
)
139141
args = [path.description, "--dump-tests-json"]
140142
let data = try AsyncProcess.checkNonZeroExit(arguments: args, environment: env)
@@ -147,7 +149,8 @@ enum TestingSupport {
147149
static func constructTestEnvironment(
148150
toolchain: UserToolchain,
149151
destinationBuildParameters buildParameters: BuildParameters,
150-
sanitizers: [Sanitizer]
152+
sanitizers: [Sanitizer],
153+
library: BuildParameters.Testing.Library
151154
) throws -> Environment {
152155
var env = Environment.current
153156

@@ -202,6 +205,10 @@ enum TestingSupport {
202205
}
203206

204207
env["DYLD_INSERT_LIBRARIES"] = runtimes.joined(separator: ":")
208+
209+
if library == .xctest {
210+
env["SWIFT_TESTING_ENABLED"] = "0"
211+
}
205212
return env
206213
#endif
207214
}

Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,9 @@ extension BuildParameters {
4040

4141
/// Whether this test product style requires additional, derived test targets, i.e. there must be additional test targets, beyond those
4242
/// listed explicitly in the package manifest, created in order to add additional behavior (such as entry point logic).
43+
/// FIXME: remove this property since it's always true now.
4344
public var requiresAdditionalDerivedTestTargets: Bool {
44-
switch self {
45-
case .loadableBundle:
46-
return false
47-
case .entryPointExecutable:
48-
return true
49-
}
45+
true
5046
}
5147

5248
/// The explicitly-specified entry point file path, if this style of test product supports it and a path was specified.

Sources/swift-test/Entrypoint.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,40 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Commands
14+
#if canImport(Darwin.C)
15+
private import Darwin.C
16+
#endif
1417

1518
@main
1619
struct Entrypoint {
17-
static func main() async {
20+
static func main() async throws {
21+
// HACK: use the swift-test executable as a host for the .xctest bundle
22+
// when running Swift Testing tests.
23+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
24+
let args = CommandLine.arguments
25+
if args.count >= 3, args[1] == "--test-bundle-path" {
26+
let bundlePath = args[2]
27+
guard let image = dlopen(bundlePath, RTLD_LAZY) else {
28+
let errorMessage: String = dlerror().flatMap {
29+
String(validatingCString: $0)
30+
} ?? "An unknown error occurred."
31+
fatalError("Failed to open test bundle at path \(bundlePath): \(errorMessage)")
32+
}
33+
defer {
34+
dlclose(image)
35+
}
36+
37+
// Find and call the main function from the image. This function may
38+
// link to the copy of Swift Testing included with Xcode, or may link to
39+
// a copy that's included as a package dependency.
40+
let main = dlsym(image, "main").map {
41+
unsafeBitCast($0, to: (@convention(c) (CInt, UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>) -> CInt).self)
42+
}
43+
if let main {
44+
exit(main(CommandLine.argc, CommandLine.unsafeArgv))
45+
}
46+
}
47+
#endif
1848
await SwiftTestCommand.main()
1949
}
2050
}

0 commit comments

Comments
 (0)