Skip to content

Commit

Permalink
On Darwin, allow XCTest to be missing if we're only building swift-te…
Browse files Browse the repository at this point in the history
…sting tests. (#7426)

This PR removes the constraint on Darwin that XCTest.framework must be
present in order to build tests using swift-testing. On Darwin, XCTest
is included as a framework inside Xcode, but if a developer installs the
Xcode Command Line Tools instead of the full IDE, XCTest is not
included. They then get a diagnostic of the form:

> error: XCTest not available: terminated(1): /usr/bin/xcrun --sdk
macosx --show-sdk-platform-path output:
> xcrun: error: unable to lookup item 'PlatformPath' from command line
tools installation
> xcrun: error: unable to lookup item 'PlatformPath' in SDK
'/Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk'

Which is a poor experience if they aren't even using XCTest.

This change, as a (positive) side effect, suppresses the same diagnostic
when running commands that are not usually dependent on the presence of
XCTest such as `swift build`.

Note that swift-corelibs-xctest is not supported on Darwin, so
installing the Xcode Command Line Tools and adding an explicit
dependency on swift-corelibs-xctest will not produce a functional test
target bundle. Supporting swift-corelibs-xctest on Darwin is a potential
future direction.

Automated testing for this change is difficult because it relies on a
build environment that is not supported in CI (namely the presence of
the CL tools but not Xcode nor XCTest.framework.) I have manually tested
the change against swift-testing's own test target.

A separate PR will be necessary in swift-testing to remove some
remaining XCTest dependencies. Those changes are not covered by this PR.

Resolves rdar://125372431.
  • Loading branch information
grynspan committed Mar 26, 2024
1 parent 2bc24f0 commit 601d040
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 21 deletions.
22 changes: 15 additions & 7 deletions Sources/Commands/SwiftTestCommand.swift
Expand Up @@ -40,7 +40,8 @@ private enum TestError: Swift.Error {
case testProductNotFound(productName: String)
case productIsNotTest(productName: String)
case multipleTestProducts([String])
case xctestNotAvailable
case xctestNotAvailable(reason: String)
case xcodeNotInstalled
}

extension TestError: CustomStringConvertible {
Expand All @@ -57,8 +58,10 @@ extension TestError: CustomStringConvertible {
return "invalid list test JSON structure, produced by \(context)\(underlying)"
case .multipleTestProducts(let products):
return "found multiple test products: \(products.joined(separator: ", ")); use --test-product to select one"
case .xctestNotAvailable:
return "XCTest not available"
case let .xctestNotAvailable(reason):
return "XCTest not available: \(reason)"
case .xcodeNotInstalled:
return "XCTest not available; download and install Xcode to use XCTest on this platform"
}
}
}
Expand Down Expand Up @@ -203,9 +206,14 @@ package struct SwiftTestCommand: AsyncSwiftCommand {
private func xctestRun(_ swiftCommandState: SwiftCommandState) async throws {
// validate XCTest available on darwin based systems
let toolchain = try swiftCommandState.getTargetToolchain()
let isHostTestingAvailable = try swiftCommandState.getHostToolchain().swiftSDK.supportsTesting
if (toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil) || !isHostTestingAvailable {
throw TestError.xctestNotAvailable
if case let .unsupported(reason) = try swiftCommandState.getHostToolchain().swiftSDK.xctestSupport {
if let reason {
throw TestError.xctestNotAvailable(reason: reason)
} else {
throw TestError.xcodeNotInstalled
}
} else if toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil {
throw TestError.xcodeNotInstalled
}

let buildParameters = try swiftCommandState.buildParametersForTest(options: self.options, library: .xctest)
Expand Down Expand Up @@ -814,7 +822,7 @@ final class TestRunner {
#if os(macOS)
if library == .xctest {
guard let xctestPath = self.toolchain.xctestPath else {
throw TestError.xctestNotAvailable
throw TestError.xcodeNotInstalled
}
args = [xctestPath.pathString]
args += additionalArguments
Expand Down
11 changes: 6 additions & 5 deletions Sources/Commands/Utilities/TestingSupport.swift
Expand Up @@ -178,11 +178,12 @@ enum TestingSupport {
#endif
return env
#else
// Add the sdk platform path if we have it. If this is not present, we might always end up failing.
let sdkPlatformFrameworksPath = try SwiftSDK.sdkPlatformFrameworkPaths()
// appending since we prefer the user setting (if set) to the one we inject
env.appendPath("DYLD_FRAMEWORK_PATH", value: sdkPlatformFrameworksPath.fwk.pathString)
env.appendPath("DYLD_LIBRARY_PATH", value: sdkPlatformFrameworksPath.lib.pathString)
// Add the sdk platform path if we have it.
if let sdkPlatformFrameworksPath = try? SwiftSDK.sdkPlatformFrameworkPaths() {
// appending since we prefer the user setting (if set) to the one we inject
env.appendPath("DYLD_FRAMEWORK_PATH", value: sdkPlatformFrameworksPath.fwk.pathString)
env.appendPath("DYLD_LIBRARY_PATH", value: sdkPlatformFrameworksPath.lib.pathString)
}

// Fast path when no sanitizers are enabled.
if sanitizers.isEmpty {
Expand Down
64 changes: 55 additions & 9 deletions Sources/PackageModel/SwiftSDKs/SwiftSDK.swift
Expand Up @@ -147,7 +147,29 @@ public struct SwiftSDK: Equatable {
public var architectures: [String]? = nil

/// Whether or not the receiver supports testing.
public let supportsTesting: Bool
@available(*, deprecated, message: "Use `xctestSupport` instead")
public var supportsTesting: Bool {
if case .supported = xctestSupport {
return true
}
return false
}

/// Whether or not the receiver supports testing using XCTest.
package enum XCTestSupport: Sendable, Equatable {
/// XCTest is supported.
case supported

/// XCTest is not supported.
///
/// - Parameters:
/// - reason: A string explaining why XCTest is not supported. If
/// `nil`, no additional information is available.
case unsupported(reason: String?)
}

/// Whether or not the receiver supports using XCTest.
package let xctestSupport: XCTestSupport

/// Root directory path of the SDK used to compile for the target triple.
@available(*, deprecated, message: "use `pathsConfiguration.sdkRootPath` instead")
Expand Down Expand Up @@ -418,18 +440,43 @@ public struct SwiftSDK: Equatable {
}

/// Creates a Swift SDK with the specified properties.
@available(*, deprecated, message: "use `init(hostTriple:targetTriple:toolset:pathsConfiguration:xctestSupport:)` instead")
public init(
hostTriple: Triple? = nil,
targetTriple: Triple? = nil,
toolset: Toolset,
pathsConfiguration: PathsConfiguration,
supportsTesting: Bool = true
supportsTesting: Bool
) {
let xctestSupport: XCTestSupport
if supportsTesting {
xctestSupport = .supported
} else {
xctestSupport = .unsupported(reason: nil)
}

self.init(
hostTriple: hostTriple,
targetTriple: targetTriple,
toolset: toolset,
pathsConfiguration: pathsConfiguration,
xctestSupport: xctestSupport
)
}

/// Creates a Swift SDK with the specified properties.
package init(
hostTriple: Triple? = nil,
targetTriple: Triple? = nil,
toolset: Toolset,
pathsConfiguration: PathsConfiguration,
xctestSupport: XCTestSupport = .supported
) {
self.hostTriple = hostTriple
self.targetTriple = targetTriple
self.toolset = toolset
self.pathsConfiguration = pathsConfiguration
self.supportsTesting = supportsTesting
self.xctestSupport = xctestSupport
}

/// Returns the bin directory for the host.
Expand Down Expand Up @@ -496,7 +543,7 @@ public struct SwiftSDK: Equatable {
#endif

// Compute common arguments for clang and swift.
let supportsTesting: Bool
let xctestSupport: XCTestSupport
var extraCCFlags: [String] = []
var extraSwiftCFlags: [String] = []
#if os(macOS)
Expand All @@ -506,13 +553,12 @@ public struct SwiftSDK: Equatable {
extraSwiftCFlags += ["-F", sdkPaths.fwk.pathString]
extraSwiftCFlags += ["-I", sdkPaths.lib.pathString]
extraSwiftCFlags += ["-L", sdkPaths.lib.pathString]
supportsTesting = true
xctestSupport = .supported
} catch {
supportsTesting = false
observabilityScope?.emit(warning: "could not determine XCTest paths: \(error)")
xctestSupport = .unsupported(reason: String(describing: error))
}
#else
supportsTesting = true
xctestSupport = .supported
#endif

#if !os(Windows)
Expand All @@ -528,7 +574,7 @@ public struct SwiftSDK: Equatable {
rootPaths: [binDir]
),
pathsConfiguration: .init(sdkRootPath: sdkPath),
supportsTesting: supportsTesting
xctestSupport: xctestSupport
)
}

Expand Down

0 comments on commit 601d040

Please sign in to comment.