Skip to content

Commit 64adc92

Browse files
authored
Add swift-testing support to swift build --build-tests (#7377)
This PR generalizes the enable/disable XCTest/swift-testing options from `swift test` and `swift package init` and adds them to `swift build --build-tests` so that it is possible to build all test content without actually running the tests. If neither flag is specified, the default test content is built (always XCTest, and swift-testing if it's a dependency, same as `swift test`.) Resolves #7375.
1 parent f335083 commit 64adc92

File tree

4 files changed

+151
-76
lines changed

4 files changed

+151
-76
lines changed

Sources/Commands/PackageCommands/Init.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,9 @@ extension SwiftPackageCommand {
4242
"""))
4343
var initMode: InitPackage.PackageType = .library
4444

45-
/// Whether to enable support for XCTest.
46-
@Flag(name: .customLong("xctest"),
47-
inversion: .prefixedEnableDisable,
48-
help: "Enable support for XCTest")
49-
var enableXCTestSupport: Bool = true
50-
51-
/// Whether to enable support for swift-testing.
52-
@Flag(name: .customLong("experimental-swift-testing"),
53-
inversion: .prefixedEnableDisable,
54-
help: "Enable experimental support for swift-testing")
55-
var enableSwiftTestingLibrarySupport: Bool = false
45+
/// Which testing libraries to use (and any related options.)
46+
@OptionGroup()
47+
var testLibraryOptions: TestLibraryOptions
5648

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

57+
// NOTE: Do not use testLibraryOptions.enabledTestingLibraries(swiftCommandState:) here
58+
// because the package doesn't exist yet, so there are no dependencies for it to query.
6559
var testingLibraries: Set<BuildParameters.Testing.Library> = []
66-
if enableXCTestSupport {
60+
if testLibraryOptions.enableXCTestSupport {
6761
testingLibraries.insert(.xctest)
6862
}
69-
if enableSwiftTestingLibrarySupport {
63+
if testLibraryOptions.explicitlyEnableSwiftTestingLibrarySupport == true {
7064
testingLibraries.insert(.swiftTesting)
7165
}
7266
let packageName = self.packageName ?? cwd.basename

Sources/Commands/SwiftBuildCommand.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ struct BuildCommandOptions: ParsableArguments {
9696
/// If should link the Swift stdlib statically.
9797
@Flag(name: .customLong("static-swift-stdlib"), inversion: .prefixedNo, help: "Link Swift stdlib statically")
9898
public var shouldLinkStaticSwiftStdlib: Bool = false
99+
100+
/// Which testing libraries to use (and any related options.)
101+
@OptionGroup()
102+
var testLibraryOptions: TestLibraryOptions
103+
104+
func validate() throws {
105+
// If --build-tests was not specified, it does not make sense to enable
106+
// or disable either testing library.
107+
if !buildTests {
108+
if testLibraryOptions.explicitlyEnableXCTestSupport != nil
109+
|| testLibraryOptions.explicitlyEnableSwiftTestingLibrarySupport != nil {
110+
throw StringError("pass --build-tests to build test targets")
111+
}
112+
}
113+
}
99114
}
100115

101116
/// swift-build command namespace
@@ -137,9 +152,26 @@ public struct SwiftBuildCommand: AsyncSwiftCommand {
137152
guard let subset = options.buildSubset(observabilityScope: swiftCommandState.observabilityScope) else {
138153
throw ExitCode.failure
139154
}
155+
if case .allIncludingTests = subset {
156+
var buildParameters = try swiftCommandState.productsBuildParameters
157+
for library in try options.testLibraryOptions.enabledTestingLibraries(swiftCommandState: swiftCommandState) {
158+
buildParameters.testingParameters = .init(
159+
configuration: buildParameters.configuration,
160+
targetTriple: buildParameters.triple,
161+
library: library
162+
)
163+
try build(swiftCommandState, subset: subset, buildParameters: buildParameters)
164+
}
165+
} else {
166+
try build(swiftCommandState, subset: subset)
167+
}
168+
}
169+
170+
private func build(_ swiftCommandState: SwiftCommandState, subset: BuildSubset, buildParameters: BuildParameters? = nil) throws {
140171
let buildSystem = try swiftCommandState.createBuildSystem(
141172
explicitProduct: options.product,
142173
shouldLinkStaticSwiftStdlib: options.shouldLinkStaticSwiftStdlib,
174+
productsBuildParameters: buildParameters,
143175
// command result output goes on stdout
144176
// ie "swift build" should output to stdout
145177
outputStream: TSCBasic.stdoutStream

Sources/Commands/SwiftTestCommand.swift

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -75,63 +75,6 @@ struct SharedOptions: ParsableArguments {
7575
/// to choose from (usually in multiroot packages).
7676
@Option(help: .hidden)
7777
var testProduct: String?
78-
79-
/// Whether to enable support for XCTest.
80-
@Flag(name: .customLong("xctest"),
81-
inversion: .prefixedEnableDisable,
82-
help: "Enable support for XCTest")
83-
var enableXCTestSupport: Bool = true
84-
85-
/// Storage for whether to enable support for swift-testing.
86-
@Flag(name: .customLong("experimental-swift-testing"),
87-
inversion: .prefixedEnableDisable,
88-
help: "Enable experimental support for swift-testing")
89-
var _enableSwiftTestingLibrarySupport: Bool?
90-
91-
/// Whether to enable support for swift-testing.
92-
func enableSwiftTestingLibrarySupport(swiftCommandState: SwiftCommandState) throws -> Bool {
93-
// Honor the user's explicit command-line selection, if any.
94-
if let callerSuppliedValue = _enableSwiftTestingLibrarySupport {
95-
return callerSuppliedValue
96-
}
97-
98-
// If the active package has a dependency on swift-testing, automatically enable support for it so that extra steps are not needed.
99-
let workspace = try swiftCommandState.getActiveWorkspace()
100-
let root = try swiftCommandState.getWorkspaceRoot()
101-
let rootManifests = try temp_await {
102-
workspace.loadRootManifests(
103-
packages: root.packages,
104-
observabilityScope: swiftCommandState.observabilityScope,
105-
completion: $0
106-
)
107-
}
108-
109-
// Is swift-testing among the dependencies of the package being built?
110-
// If so, enable support.
111-
let isEnabledByDependency = rootManifests.values.lazy
112-
.flatMap(\.dependencies)
113-
.map(\.identity)
114-
.map(String.init(describing:))
115-
.contains("swift-testing")
116-
if isEnabledByDependency {
117-
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support due to its presence as a package dependency.")
118-
return true
119-
}
120-
121-
// Is swift-testing the package being built itself (unlikely)? If so,
122-
// enable support.
123-
let isEnabledByName = root.packages.lazy
124-
.map(PackageIdentity.init(path:))
125-
.map(String.init(describing:))
126-
.contains("swift-testing")
127-
if isEnabledByName {
128-
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support because it is a root package.")
129-
return true
130-
}
131-
132-
// Default to disabled since swift-testing is experimental (opt-in.)
133-
return false
134-
}
13578
}
13679

13780
struct TestCommandOptions: ParsableArguments {
@@ -141,6 +84,10 @@ struct TestCommandOptions: ParsableArguments {
14184
@OptionGroup()
14285
var sharedOptions: SharedOptions
14386

87+
/// Which testing libraries to use (and any related options.)
88+
@OptionGroup()
89+
var testLibraryOptions: TestLibraryOptions
90+
14491
/// If tests should run in parallel mode.
14592
@Flag(name: .customLong("parallel"),
14693
inversion: .prefixedNo,
@@ -425,10 +372,10 @@ public struct SwiftTestCommand: SwiftCommand {
425372
let command = try List.parse()
426373
try command.run(swiftCommandState)
427374
} else {
428-
if try options.sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
375+
if try options.testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
429376
try swiftTestingRun(swiftCommandState)
430377
}
431-
if options.sharedOptions.enableXCTestSupport {
378+
if options.testLibraryOptions.enableXCTestSupport {
432379
try xctestRun(swiftCommandState)
433380
}
434381
}
@@ -624,7 +571,7 @@ public struct SwiftTestCommand: SwiftCommand {
624571
throw StringError("'--num-workers' must be greater than zero")
625572
}
626573

627-
if !options.sharedOptions.enableXCTestSupport {
574+
if !options.testLibraryOptions.enableXCTestSupport {
628575
throw StringError("'--num-workers' is only supported when testing with XCTest")
629576
}
630577
}
@@ -680,6 +627,10 @@ extension SwiftTestCommand {
680627
@OptionGroup()
681628
var sharedOptions: SharedOptions
682629

630+
/// Which testing libraries to use (and any related options.)
631+
@OptionGroup()
632+
var testLibraryOptions: TestLibraryOptions
633+
683634
// for deprecated passthrough from SwiftTestTool (parse will fail otherwise)
684635
@Flag(name: [.customLong("list-tests"), .customShort("l")], help: .hidden)
685636
var _deprecated_passthrough: Bool = false
@@ -752,10 +703,10 @@ extension SwiftTestCommand {
752703
// MARK: - Common implementation
753704

754705
func run(_ swiftCommandState: SwiftCommandState) throws {
755-
if try sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
706+
if try testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
756707
try swiftTestingRun(swiftCommandState)
757708
}
758-
if sharedOptions.enableXCTestSupport {
709+
if testLibraryOptions.enableXCTestSupport {
759710
try xctestRun(swiftCommandState)
760711
}
761712
}
@@ -1327,7 +1278,7 @@ extension SwiftCommandState {
13271278
experimentalTestOutput: options.enableExperimentalTestOutput,
13281279
library: library
13291280
)
1330-
if try options.sharedOptions.enableSwiftTestingLibrarySupport(swiftCommandState: self) {
1281+
if try options.testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: self) {
13311282
result.flags.swiftCompilerFlags += ["-DSWIFT_PM_SUPPORTS_SWIFT_TESTING"]
13321283
}
13331284
return result

Sources/CoreCommands/Options.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,26 @@ import ArgumentParser
1515
import var Basics.localFileSystem
1616
import struct Basics.AbsolutePath
1717
import struct Basics.Triple
18+
import func Basics.temp_await
1819

1920
import struct Foundation.URL
2021

2122
import enum PackageModel.BuildConfiguration
2223
import struct PackageModel.BuildFlags
2324
import struct PackageModel.EnabledSanitizers
2425
import struct PackageModel.PackageIdentity
26+
import class PackageModel.Manifest
2527
import enum PackageModel.Sanitizer
2628

29+
import struct SPMBuildCore.BuildParameters
2730
@_spi(SwiftPMInternal)
2831
import struct SPMBuildCore.BuildSystemProvider
2932

3033
import struct TSCBasic.StringError
3134

3235
import struct TSCUtility.Version
3336

37+
import class Workspace.Workspace
3438
import struct Workspace.WorkspaceConfiguration
3539

3640
@_spi(SwiftPMInternal)
@@ -540,6 +544,100 @@ public struct LinkerOptions: ParsableArguments {
540544
public var shouldDisableLocalRpath: Bool = false
541545
}
542546

547+
/// Which testing libraries to use (and any related options.)
548+
@_spi(SwiftPMInternal)
549+
public struct TestLibraryOptions: ParsableArguments {
550+
public init() {}
551+
552+
/// Whether to enable support for XCTest (as explicitly specified by the user.)
553+
///
554+
/// Callers will generally want to use ``enableXCTestSupport`` since it will
555+
/// have the correct default value if the user didn't specify one.
556+
@Flag(name: .customLong("xctest"),
557+
inversion: .prefixedEnableDisable,
558+
help: "Enable support for XCTest")
559+
public var explicitlyEnableXCTestSupport: Bool?
560+
561+
/// Whether to enable support for XCTest.
562+
public var enableXCTestSupport: Bool {
563+
// Default to enabled.
564+
explicitlyEnableXCTestSupport ?? true
565+
}
566+
567+
/// Whether to enable support for swift-testing (as explicitly specified by the user.)
568+
///
569+
/// Callers (other than `swift package init`) will generally want to use
570+
/// ``enableSwiftTestingLibrarySupport(swiftCommandState:)`` since it will
571+
/// take into account whether the package has a dependency on swift-testing.
572+
@Flag(name: .customLong("experimental-swift-testing"),
573+
inversion: .prefixedEnableDisable,
574+
help: "Enable experimental support for swift-testing")
575+
public var explicitlyEnableSwiftTestingLibrarySupport: Bool?
576+
577+
/// Whether to enable support for swift-testing.
578+
public func enableSwiftTestingLibrarySupport(
579+
swiftCommandState: SwiftCommandState
580+
) throws -> Bool {
581+
// Honor the user's explicit command-line selection, if any.
582+
if let callerSuppliedValue = explicitlyEnableSwiftTestingLibrarySupport {
583+
return callerSuppliedValue
584+
}
585+
586+
// If the active package has a dependency on swift-testing, automatically enable support for it so that extra steps are not needed.
587+
let workspace = try swiftCommandState.getActiveWorkspace()
588+
let root = try swiftCommandState.getWorkspaceRoot()
589+
let rootManifests = try temp_await {
590+
workspace.loadRootManifests(
591+
packages: root.packages,
592+
observabilityScope: swiftCommandState.observabilityScope,
593+
completion: $0
594+
)
595+
}
596+
597+
// Is swift-testing among the dependencies of the package being built?
598+
// If so, enable support.
599+
let isEnabledByDependency = rootManifests.values.lazy
600+
.flatMap(\.dependencies)
601+
.map(\.identity)
602+
.map(String.init(describing:))
603+
.contains("swift-testing")
604+
if isEnabledByDependency {
605+
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support due to its presence as a package dependency.")
606+
return true
607+
}
608+
609+
// Is swift-testing the package being built itself (unlikely)? If so,
610+
// enable support.
611+
let isEnabledByName = root.packages.lazy
612+
.map(PackageIdentity.init(path:))
613+
.map(String.init(describing:))
614+
.contains("swift-testing")
615+
if isEnabledByName {
616+
swiftCommandState.observabilityScope.emit(debug: "Enabling swift-testing support because it is a root package.")
617+
return true
618+
}
619+
620+
// Default to disabled since swift-testing is experimental (opt-in.)
621+
return false
622+
}
623+
624+
/// Get the set of enabled testing libraries.
625+
public func enabledTestingLibraries(
626+
swiftCommandState: SwiftCommandState
627+
) throws -> Set<BuildParameters.Testing.Library> {
628+
var result = Set<BuildParameters.Testing.Library>()
629+
630+
if enableXCTestSupport {
631+
result.insert(.xctest)
632+
}
633+
if try enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) {
634+
result.insert(.swiftTesting)
635+
}
636+
637+
return result
638+
}
639+
}
640+
543641
// MARK: - Extensions
544642

545643
extension BuildConfiguration {

0 commit comments

Comments
 (0)