Skip to content

Add swift-testing support to swift build --build-tests #7377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 29, 2024
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
20 changes: 7 additions & 13 deletions Sources/Commands/PackageCommands/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,9 @@ extension SwiftPackageCommand {
"""))
var initMode: InitPackage.PackageType = .library

/// Whether to enable support for XCTest.
@Flag(name: .customLong("xctest"),
inversion: .prefixedEnableDisable,
help: "Enable support for XCTest")
var enableXCTestSupport: Bool = true

/// Whether to enable support for swift-testing.
@Flag(name: .customLong("experimental-swift-testing"),
inversion: .prefixedEnableDisable,
help: "Enable experimental support for swift-testing")
var enableSwiftTestingLibrarySupport: Bool = false
/// Which testing libraries to use (and any related options.)
@OptionGroup()
var testLibraryOptions: TestLibraryOptions

@Option(name: .customLong("name"), help: "Provide custom package name")
var packageName: String?
Expand All @@ -62,11 +54,13 @@ extension SwiftPackageCommand {
throw InternalError("Could not find the current working directory")
}

// NOTE: Do not use testLibraryOptions.enabledTestingLibraries(swiftCommandState:) here
// because the package doesn't exist yet, so there are no dependencies for it to query.
var testingLibraries: Set<BuildParameters.Testing.Library> = []
if enableXCTestSupport {
if testLibraryOptions.enableXCTestSupport {
testingLibraries.insert(.xctest)
}
if enableSwiftTestingLibrarySupport {
if testLibraryOptions.explicitlyEnableSwiftTestingLibrarySupport == true {
testingLibraries.insert(.swiftTesting)
}
let packageName = self.packageName ?? cwd.basename
Expand Down
32 changes: 32 additions & 0 deletions Sources/Commands/SwiftBuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ struct BuildCommandOptions: ParsableArguments {
/// If should link the Swift stdlib statically.
@Flag(name: .customLong("static-swift-stdlib"), inversion: .prefixedNo, help: "Link Swift stdlib statically")
public var shouldLinkStaticSwiftStdlib: Bool = false

/// Which testing libraries to use (and any related options.)
@OptionGroup()
var testLibraryOptions: TestLibraryOptions

func validate() throws {
// If --build-tests was not specified, it does not make sense to enable
// or disable either testing library.
if !buildTests {
if testLibraryOptions.explicitlyEnableXCTestSupport != nil
|| testLibraryOptions.explicitlyEnableSwiftTestingLibrarySupport != nil {
throw StringError("pass --build-tests to build test targets")
}
}
}
}

/// swift-build command namespace
Expand Down Expand Up @@ -137,9 +152,26 @@ public struct SwiftBuildCommand: AsyncSwiftCommand {
guard let subset = options.buildSubset(observabilityScope: swiftCommandState.observabilityScope) else {
throw ExitCode.failure
}
if case .allIncludingTests = subset {
var buildParameters = try swiftCommandState.productsBuildParameters
for library in try options.testLibraryOptions.enabledTestingLibraries(swiftCommandState: swiftCommandState) {
buildParameters.testingParameters = .init(
configuration: buildParameters.configuration,
targetTriple: buildParameters.triple,
library: library
)
try build(swiftCommandState, subset: subset, buildParameters: buildParameters)
}
} else {
try build(swiftCommandState, subset: subset)
}
}

private func build(_ swiftCommandState: SwiftCommandState, subset: BuildSubset, buildParameters: BuildParameters? = nil) throws {
let buildSystem = try swiftCommandState.createBuildSystem(
explicitProduct: options.product,
shouldLinkStaticSwiftStdlib: options.shouldLinkStaticSwiftStdlib,
productsBuildParameters: buildParameters,
// command result output goes on stdout
// ie "swift build" should output to stdout
outputStream: TSCBasic.stdoutStream
Expand Down
77 changes: 14 additions & 63 deletions Sources/Commands/SwiftTestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,63 +75,6 @@ struct SharedOptions: ParsableArguments {
/// to choose from (usually in multiroot packages).
@Option(help: .hidden)
var testProduct: String?

/// Whether to enable support for XCTest.
@Flag(name: .customLong("xctest"),
inversion: .prefixedEnableDisable,
help: "Enable support for XCTest")
var enableXCTestSupport: Bool = true

/// Storage for whether to enable support for swift-testing.
@Flag(name: .customLong("experimental-swift-testing"),
inversion: .prefixedEnableDisable,
help: "Enable experimental support for swift-testing")
var _enableSwiftTestingLibrarySupport: Bool?

/// Whether to enable support for swift-testing.
func enableSwiftTestingLibrarySupport(swiftCommandState: SwiftCommandState) throws -> Bool {
// Honor the user's explicit command-line selection, if any.
if let callerSuppliedValue = _enableSwiftTestingLibrarySupport {
return callerSuppliedValue
}

// If the active package has a dependency on swift-testing, automatically enable support for it so that extra steps are not needed.
let workspace = try swiftCommandState.getActiveWorkspace()
let root = try swiftCommandState.getWorkspaceRoot()
let rootManifests = try temp_await {
workspace.loadRootManifests(
packages: root.packages,
observabilityScope: swiftCommandState.observabilityScope,
completion: $0
)
}

// Is swift-testing among the dependencies of the package being built?
// If so, enable support.
let isEnabledByDependency = rootManifests.values.lazy
.flatMap(\.dependencies)
.map(\.identity)
.map(String.init(describing:))
.contains("swift-testing")
if isEnabledByDependency {
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support due to its presence as a package dependency.")
return true
}

// Is swift-testing the package being built itself (unlikely)? If so,
// enable support.
let isEnabledByName = root.packages.lazy
.map(PackageIdentity.init(path:))
.map(String.init(describing:))
.contains("swift-testing")
if isEnabledByName {
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support because it is a root package.")
return true
}

// Default to disabled since swift-testing is experimental (opt-in.)
return false
}
}

struct TestCommandOptions: ParsableArguments {
Expand All @@ -141,6 +84,10 @@ struct TestCommandOptions: ParsableArguments {
@OptionGroup()
var sharedOptions: SharedOptions

/// Which testing libraries to use (and any related options.)
@OptionGroup()
var testLibraryOptions: TestLibraryOptions

/// If tests should run in parallel mode.
@Flag(name: .customLong("parallel"),
inversion: .prefixedNo,
Expand Down Expand Up @@ -425,10 +372,10 @@ public struct SwiftTestCommand: SwiftCommand {
let command = try List.parse()
try command.run(swiftCommandState)
} else {
if try options.sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
if try options.testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
try swiftTestingRun(swiftCommandState)
}
if options.sharedOptions.enableXCTestSupport {
if options.testLibraryOptions.enableXCTestSupport {
try xctestRun(swiftCommandState)
}
}
Expand Down Expand Up @@ -624,7 +571,7 @@ public struct SwiftTestCommand: SwiftCommand {
throw StringError("'--num-workers' must be greater than zero")
}

if !options.sharedOptions.enableXCTestSupport {
if !options.testLibraryOptions.enableXCTestSupport {
throw StringError("'--num-workers' is only supported when testing with XCTest")
}
}
Expand Down Expand Up @@ -680,6 +627,10 @@ extension SwiftTestCommand {
@OptionGroup()
var sharedOptions: SharedOptions

/// Which testing libraries to use (and any related options.)
@OptionGroup()
var testLibraryOptions: TestLibraryOptions

// for deprecated passthrough from SwiftTestTool (parse will fail otherwise)
@Flag(name: [.customLong("list-tests"), .customShort("l")], help: .hidden)
var _deprecated_passthrough: Bool = false
Expand Down Expand Up @@ -752,10 +703,10 @@ extension SwiftTestCommand {
// MARK: - Common implementation

func run(_ swiftCommandState: SwiftCommandState) throws {
if try sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
if try testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
try swiftTestingRun(swiftCommandState)
}
if sharedOptions.enableXCTestSupport {
if testLibraryOptions.enableXCTestSupport {
try xctestRun(swiftCommandState)
}
}
Expand Down Expand Up @@ -1327,7 +1278,7 @@ extension SwiftCommandState {
experimentalTestOutput: options.enableExperimentalTestOutput,
library: library
)
if try options.sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: self) {
if try options.testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: self) {
result.flags.swiftCompilerFlags += ["-DSWIFT_PM_SUPPORTS_SWIFT_TESTING"]
}
return result
Expand Down
98 changes: 98 additions & 0 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@ import ArgumentParser
import var Basics.localFileSystem
import struct Basics.AbsolutePath
import struct Basics.Triple
import func Basics.temp_await

import struct Foundation.URL

import enum PackageModel.BuildConfiguration
import struct PackageModel.BuildFlags
import struct PackageModel.EnabledSanitizers
import struct PackageModel.PackageIdentity
import class PackageModel.Manifest
import enum PackageModel.Sanitizer

import struct SPMBuildCore.BuildParameters
@_spi(SwiftPMInternal)
import struct SPMBuildCore.BuildSystemProvider

import struct TSCBasic.StringError

import struct TSCUtility.Version

import class Workspace.Workspace
import struct Workspace.WorkspaceConfiguration

@_spi(SwiftPMInternal)
Expand Down Expand Up @@ -540,6 +544,100 @@ public struct LinkerOptions: ParsableArguments {
public var shouldDisableLocalRpath: Bool = false
}

/// Which testing libraries to use (and any related options.)
@_spi(SwiftPMInternal)
public struct TestLibraryOptions: ParsableArguments {
public init() {}

/// Whether to enable support for XCTest (as explicitly specified by the user.)
///
/// Callers will generally want to use ``enableXCTestSupport`` since it will
/// have the correct default value if the user didn't specify one.
@Flag(name: .customLong("xctest"),
inversion: .prefixedEnableDisable,
help: "Enable support for XCTest")
public var explicitlyEnableXCTestSupport: Bool?

/// Whether to enable support for XCTest.
public var enableXCTestSupport: Bool {
// Default to enabled.
explicitlyEnableXCTestSupport ?? true
}

/// Whether to enable support for swift-testing (as explicitly specified by the user.)
///
/// Callers (other than `swift package init`) will generally want to use
/// ``enableSwiftTestingLibrarySupport(swiftCommandState:)`` since it will
/// take into account whether the package has a dependency on swift-testing.
@Flag(name: .customLong("experimental-swift-testing"),
inversion: .prefixedEnableDisable,
help: "Enable experimental support for swift-testing")
public var explicitlyEnableSwiftTestingLibrarySupport: Bool?

/// Whether to enable support for swift-testing.
public func enableSwiftTestingLibrarySupport(
swiftCommandState: SwiftCommandState
) throws -> Bool {
// Honor the user's explicit command-line selection, if any.
if let callerSuppliedValue = explicitlyEnableSwiftTestingLibrarySupport {
return callerSuppliedValue
}

// If the active package has a dependency on swift-testing, automatically enable support for it so that extra steps are not needed.
let workspace = try swiftCommandState.getActiveWorkspace()
let root = try swiftCommandState.getWorkspaceRoot()
let rootManifests = try temp_await {
workspace.loadRootManifests(
packages: root.packages,
observabilityScope: swiftCommandState.observabilityScope,
completion: $0
)
}

// Is swift-testing among the dependencies of the package being built?
// If so, enable support.
let isEnabledByDependency = rootManifests.values.lazy
.flatMap(\.dependencies)
.map(\.identity)
.map(String.init(describing:))
.contains("swift-testing")
if isEnabledByDependency {
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support due to its presence as a package dependency.")
return true
}

// Is swift-testing the package being built itself (unlikely)? If so,
// enable support.
let isEnabledByName = root.packages.lazy
.map(PackageIdentity.init(path:))
.map(String.init(describing:))
.contains("swift-testing")
if isEnabledByName {
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support because it is a root package.")
return true
}

// Default to disabled since swift-testing is experimental (opt-in.)
return false
}

/// Get the set of enabled testing libraries.
public func enabledTestingLibraries(
swiftCommandState: SwiftCommandState
) throws -> Set<BuildParameters.Testing.Library> {
var result = Set<BuildParameters.Testing.Library>()

if enableXCTestSupport {
result.insert(.xctest)
}
if try enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
result.insert(.swiftTesting)
}

return result
}
}

// MARK: - Extensions

extension BuildConfiguration {
Expand Down