Skip to content

Commit

Permalink
Detect if XCTest is installed on Darwin (#7805)
Browse files Browse the repository at this point in the history
This PR checks if XCTest is available before invoking XCTest-based tests
on Darwin. There are three possible outcomes:

1. If XCTest is available, we will run XCTest-based tests (as we have
historically.)
2. If XCTest is not available and the user explicitly passed
`--enable-xctest`, we will attempt to run XCTest-based tests, but in
general this code path will continue to fail as `swift test` has
historically done when XCTest is not available.
3. If XCTest is not available and the user did not pass
`--enable-xctest`, we skip running any XCTest logic.

On Linux/Windows/etc., XCTest is always present via
swift-corelibs-xctest and so this change has no practical effect there.
On Darwin, XCTest may be missing if the user has installed the Xcode
Command Line Tools, but not the full Xcode IDE. XCTest is not included
with the Xcode Command Line Tools package.

The purpose of this change is to allow running `swift test` when XCTest
is unavailable but Swift Testing _is_ available.
  • Loading branch information
grynspan authored and bnbarham committed Oct 12, 2024
1 parent 88e94ab commit a20491a
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 29 deletions.
8 changes: 4 additions & 4 deletions Sources/Commands/PackageCommands/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ extension SwiftPackageCommand {
// For macros this is reversed, since we don't support testing
// macros with Swift Testing yet.
var supportedTestingLibraries = Set<BuildParameters.Testing.Library>()
if testLibraryOptions.isExplicitlyEnabled(.xctest) ||
(initMode == .macro && testLibraryOptions.isEnabled(.xctest)) {
if testLibraryOptions.isExplicitlyEnabled(.xctest, swiftCommandState: swiftCommandState) ||
(initMode == .macro && testLibraryOptions.isEnabled(.xctest, swiftCommandState: swiftCommandState)) {
supportedTestingLibraries.insert(.xctest)
}
if testLibraryOptions.isExplicitlyEnabled(.swiftTesting) ||
(initMode != .macro && testLibraryOptions.isEnabled(.swiftTesting)) {
if testLibraryOptions.isExplicitlyEnabled(.swiftTesting, swiftCommandState: swiftCommandState) ||
(initMode != .macro && testLibraryOptions.isEnabled(.swiftTesting, swiftCommandState: swiftCommandState)) {
supportedTestingLibraries.insert(.swiftTesting)
}

Expand Down
28 changes: 15 additions & 13 deletions Sources/Commands/SwiftTestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,10 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
var results = [TestRunner.Result]()

// Run XCTest.
if options.testLibraryOptions.isEnabled(.xctest) {
// validate XCTest available on darwin based systems
if options.testLibraryOptions.isEnabled(.xctest, swiftCommandState: swiftCommandState) {
// Validate XCTest is available on Darwin-based systems. If it's not available and we're hitting this code
// path, that means the developer must have explicitly passed --enable-xctest (or the toolchain is
// corrupt, I suppose.)
let toolchain = try swiftCommandState.getTargetToolchain()
if case let .unsupported(reason) = try swiftCommandState.getHostToolchain().swiftSDK.xctestSupport {
if let reason {
Expand All @@ -276,7 +278,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
swiftCommandState: swiftCommandState,
library: .xctest
)
if result == .success, let testCount, testCount == 0 {
if result == .success, testCount == 0 {
results.append(.noMatchingTests)
} else {
results.append(result)
Expand Down Expand Up @@ -322,9 +324,9 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
}

// Run Swift Testing (parallel or not, it has a single entry point.)
if options.testLibraryOptions.isEnabled(.swiftTesting) {
if options.testLibraryOptions.isEnabled(.swiftTesting, swiftCommandState: swiftCommandState) {
lazy var testEntryPointPath = testProducts.lazy.compactMap(\.testEntryPointPath).first
if options.testLibraryOptions.isExplicitlyEnabled(.swiftTesting) || testEntryPointPath == nil {
if options.testLibraryOptions.isExplicitlyEnabled(.swiftTesting, swiftCommandState: swiftCommandState) || testEntryPointPath == nil {
results.append(
try await runTestProducts(
testProducts,
Expand Down Expand Up @@ -410,7 +412,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
public func run(_ swiftCommandState: SwiftCommandState) async throws {
do {
// Validate commands arguments
try self.validateArguments(observabilityScope: swiftCommandState.observabilityScope)
try self.validateArguments(swiftCommandState: swiftCommandState)
} catch {
swiftCommandState.observabilityScope.emit(error)
throw ExitCode.failure
Expand Down Expand Up @@ -464,7 +466,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
}
additionalArguments += commandLineArguments

if var xunitPath = options.xUnitOutput, options.testLibraryOptions.isEnabled(.xctest) {
if var xunitPath = options.xUnitOutput, options.testLibraryOptions.isEnabled(.xctest, swiftCommandState: swiftCommandState) {
// We are running Swift Testing, XCTest is also running in this session, and an xUnit path
// was specified. Make sure we don't stomp on XCTest's XML output by having Swift Testing
// write to a different path.
Expand Down Expand Up @@ -631,7 +633,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
/// Private function that validates the commands arguments
///
/// - Throws: if a command argument is invalid
private func validateArguments(observabilityScope: ObservabilityScope) throws {
private func validateArguments(swiftCommandState: SwiftCommandState) throws {
// Validation for --num-workers.
if let workers = options.numberOfWorkers {

Expand All @@ -646,13 +648,13 @@ public struct SwiftTestCommand: AsyncSwiftCommand {
throw StringError("'--num-workers' must be greater than zero")
}

guard options.testLibraryOptions.isEnabled(.xctest) else {
guard options.testLibraryOptions.isEnabled(.xctest, swiftCommandState: swiftCommandState) else {
throw StringError("'--num-workers' is only supported when testing with XCTest")
}
}

if options._deprecated_shouldListTests {
observabilityScope.emit(warning: "'--list-tests' option is deprecated; use 'swift test list' instead")
swiftCommandState.observabilityScope.emit(warning: "'--list-tests' option is deprecated; use 'swift test list' instead")
}
}

Expand Down Expand Up @@ -733,7 +735,7 @@ extension SwiftTestCommand {
library: .swiftTesting
)

if testLibraryOptions.isEnabled(.xctest) {
if testLibraryOptions.isEnabled(.xctest, swiftCommandState: swiftCommandState) {
let testSuites = try TestingSupport.getTestSuites(
in: testProducts,
swiftCommandState: swiftCommandState,
Expand All @@ -749,9 +751,9 @@ extension SwiftTestCommand {
}
}

if testLibraryOptions.isEnabled(.swiftTesting) {
if testLibraryOptions.isEnabled(.swiftTesting, swiftCommandState: swiftCommandState) {
lazy var testEntryPointPath = testProducts.lazy.compactMap(\.testEntryPointPath).first
if testLibraryOptions.isExplicitlyEnabled(.swiftTesting) || testEntryPointPath == nil {
if testLibraryOptions.isExplicitlyEnabled(.swiftTesting, swiftCommandState: swiftCommandState) || testEntryPointPath == nil {
let additionalArguments = ["--list-tests"] + CommandLine.arguments.dropFirst()
let runner = TestRunner(
bundlePaths: testProducts.map(\.binaryPath),
Expand Down
27 changes: 15 additions & 12 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import struct PackageModel.EnabledSanitizers
import struct PackageModel.PackageIdentity
import class PackageModel.Manifest
import enum PackageModel.Sanitizer
@_spi(SwiftPMInternal) import struct PackageModel.SwiftSDK

import struct SPMBuildCore.BuildParameters

Expand Down Expand Up @@ -596,28 +597,30 @@ public struct TestLibraryOptions: ParsableArguments {
help: .private)
public var explicitlyEnableExperimentalSwiftTestingLibrarySupport: Bool?

private func isEnabled(_ library: BuildParameters.Testing.Library, `default`: Bool) -> Bool {
private func isEnabled(_ library: BuildParameters.Testing.Library, `default`: Bool, swiftCommandState: SwiftCommandState) -> Bool {
switch library {
case .xctest:
explicitlyEnableXCTestSupport ?? `default`
if let explicitlyEnableXCTestSupport {
return explicitlyEnableXCTestSupport
}
if let toolchain = try? swiftCommandState.getHostToolchain(),
toolchain.swiftSDK.xctestSupport == .supported {
return `default`
}
return false
case .swiftTesting:
explicitlyEnableSwiftTestingLibrarySupport ?? explicitlyEnableExperimentalSwiftTestingLibrarySupport ?? `default`
return explicitlyEnableSwiftTestingLibrarySupport ?? explicitlyEnableExperimentalSwiftTestingLibrarySupport ?? `default`
}
}

/// Test whether or not a given library is enabled.
public func isEnabled(_ library: BuildParameters.Testing.Library) -> Bool {
isEnabled(library, default: true)
public func isEnabled(_ library: BuildParameters.Testing.Library, swiftCommandState: SwiftCommandState) -> Bool {
isEnabled(library, default: true, swiftCommandState: swiftCommandState)
}

/// Test whether or not a given library was explicitly enabled by the developer.
public func isExplicitlyEnabled(_ library: BuildParameters.Testing.Library) -> Bool {
isEnabled(library, default: false)
}

/// The list of enabled testing libraries.
public var enabledTestingLibraries: [BuildParameters.Testing.Library] {
[.xctest, .swiftTesting].filter(isEnabled)
public func isExplicitlyEnabled(_ library: BuildParameters.Testing.Library, swiftCommandState: SwiftCommandState) -> Bool {
isEnabled(library, default: false, swiftCommandState: swiftCommandState)
}
}

Expand Down

0 comments on commit a20491a

Please sign in to comment.