Skip to content

Commit b29b9e1

Browse files
committed
WIP: Introduce SwiftSetting API for controlling whether testability is enabled for a target
This introduces a new `enableTesting` API in SwiftSetting which allows targets to explicitly control whether testability is enabled, as some projects do not want to use the @testable feature. This can also improve debug build performance as significantly fewer symbols will be exported from the binary in the case where @testable is disabled. The default is equivalent to `enableTesting(true, .when(configuration: .debug))`, so there is no behavior change from today without explicitly adopting this new API. The --enable-testable-imports/--disable-testable-imports command line flags now acts as overrides -- if specified, they will override any build settings configured at the target level, and if unspecified, the target-level settings will be respected.
1 parent df04096 commit b29b9e1

File tree

11 files changed

+76
-24
lines changed

11 files changed

+76
-24
lines changed

Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -954,15 +954,33 @@ public final class SwiftModuleBuildDescription {
954954

955955
/// Testing arguments according to the build configuration.
956956
private var testingArguments: [String] {
957+
let enableTesting: Bool
958+
957959
if self.isTestTarget {
958960
// test targets must be built with -enable-testing
959961
// since its required for test discovery (the non objective-c reflection kind)
960-
return ["-enable-testing"]
961-
} else if self.buildParameters.testingParameters.enableTestability {
962-
return ["-enable-testing"]
962+
enableTesting = true
963+
} else if let enableTestability = self.buildParameters.testingParameters.enableTestability {
964+
// Let the command line flag override
965+
enableTesting = enableTestability
963966
} else {
964-
return []
967+
// Use the target settings
968+
let enableTestabilitySetting = self.buildParameters.createScope(for: self.target).evaluate(.ENABLE_TESTABILITY)
969+
if !enableTestabilitySetting.isEmpty {
970+
enableTesting = enableTestabilitySetting.contains(where: { $0 == "YES" })
971+
} else {
972+
// By default, decide on testability based on debug/release config
973+
// the goals of this being based on the build configuration is
974+
// that `swift build` followed by a `swift test` will need to do minimal rebuilding
975+
// given that the default configuration for `swift build` is debug
976+
// and that `swift test` requires building with testable enabled if @testable is being used.
977+
// when building and testing in release mode, one can use the '--disable-testable-imports' flag
978+
// to disable testability in `swift test`, but that requires that the tests do not use the @testable imports feature
979+
enableTesting = self.buildParameters.configuration == .debug
980+
}
965981
}
982+
983+
return enableTesting ? ["-enable-testing"] : []
966984
}
967985

968986
/// Module cache arguments.
@@ -1001,4 +1019,4 @@ extension SwiftModuleBuildDescription {
10011019
) -> [ModuleBuildDescription.Dependency] {
10021020
ModuleBuildDescription.swift(self).recursiveDependencies(using: plan)
10031021
}
1004-
}
1022+
}

Sources/Commands/SwiftTestCommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ struct TestCommandOptions: ParsableArguments {
181181
var xUnitOutput: AbsolutePath?
182182

183183
/// Generate LinuxMain entries and exit.
184-
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Enabled by default.")
185-
var enableTestableImports: Bool = true
184+
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Based on target settings by default.")
185+
var enableTestableImports: Bool?
186186

187187
/// Whether to enable code coverage.
188188
@Flag(name: .customLong("code-coverage"),

Sources/Commands/Utilities/TestingSupport.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ extension SwiftCommandState {
270270
parameters.testingParameters.enableCodeCoverage = enableCodeCoverage
271271
// for test commands, we normally enable building with testability
272272
// but we let users override this with a flag
273-
parameters.testingParameters.enableTestability = enableTestability ?? true
273+
parameters.testingParameters.enableTestability = enableTestability
274274
parameters.shouldSkipBuilding = shouldSkipBuilding
275275
parameters.testingParameters.experimentalTestOutput = experimentalTestOutput
276276
return parameters

Sources/PackageDescription/BuildSettings.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,22 @@ public struct SwiftSetting: Sendable {
441441
return SwiftSetting(
442442
name: "swiftLanguageMode", value: [.init(describing: mode)], condition: condition)
443443
}
444+
445+
/// Whether `@testable` is enabled by passing the `-enable-testing` to the Swift compiler.
446+
///
447+
/// - Since: First available in PackageDescription 6.1.
448+
///
449+
/// - Parameters:
450+
/// - enable: Whether to enable `@testable`.
451+
/// - condition: A condition that restricts the application of the build setting.
452+
@available(_PackageDescription, introduced: 6.1)
453+
public static func enableTesting(
454+
_ enable: Bool,
455+
_ condition: BuildSettingCondition? = nil
456+
) -> SwiftSetting {
457+
return SwiftSetting(
458+
name: "enableTesting", value: [.init(describing: enable)], condition: condition)
459+
}
444460
}
445461

446462
/// A linker build setting.

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,20 @@ extension TargetBuildSettingDescription.Kind {
554554
}
555555

556556
return .swiftLanguageMode(version)
557+
case "enableTesting":
558+
guard let rawVersion = values.first else {
559+
throw InternalError("invalid (empty) build settings value")
560+
}
561+
562+
if values.count > 1 {
563+
throw InternalError("invalid build settings value")
564+
}
565+
566+
guard let value = Bool(rawVersion) else {
567+
throw InternalError("invalid boolean value: \(rawVersion)")
568+
}
569+
570+
return .enableTesting(value)
557571
default:
558572
throw InternalError("invalid build setting \(name)")
559573
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,10 @@ public final class PackageBuilder {
11921192
}
11931193

11941194
values = [version.rawValue]
1195+
1196+
case .enableTesting(let enable):
1197+
decl = .ENABLE_TESTABILITY
1198+
values = enable ? ["YES"] : ["NO"]
11951199
}
11961200

11971201
// Create an assignment for this setting.

Sources/PackageModel/BuildSettings.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
public enum BuildSettings {
1515
/// Build settings declarations.
1616
public struct Declaration: Hashable {
17+
public static let ENABLE_TESTABILITY: Declaration = .init("ENABLE_TESTABILITY")
18+
1719
// Swift.
1820
public static let SWIFT_ACTIVE_COMPILATION_CONDITIONS: Declaration =
1921
.init("SWIFT_ACTIVE_COMPILATION_CONDITIONS")

Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ public enum TargetBuildSettingDescription {
4141

4242
case swiftLanguageMode(SwiftLanguageVersion)
4343

44+
case enableTesting(Bool)
45+
4446
public var isUnsafeFlags: Bool {
4547
switch self {
4648
case .unsafeFlags(let flags):
4749
// If `.unsafeFlags` is used, but doesn't specify any flags, we treat it the same way as not specifying it.
4850
return !flags.isEmpty
4951
case .headerSearchPath, .define, .linkedLibrary, .linkedFramework, .interoperabilityMode,
50-
.enableUpcomingFeature, .enableExperimentalFeature, .swiftLanguageMode:
52+
.enableUpcomingFeature, .enableExperimentalFeature, .swiftLanguageMode, .enableTesting:
5153
return false
5254
}
5355
}

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,12 @@ fileprivate extension SourceCodeFragment {
531531
params.append(SourceCodeFragment(from: condition))
532532
}
533533
self.init(enum: setting.kind.name, subnodes: params)
534+
case .enableTesting(let enable):
535+
params.append(SourceCodeFragment(boolean: enable))
536+
if let condition = setting.condition {
537+
params.append(SourceCodeFragment(from: condition))
538+
}
539+
self.init(enum: setting.kind.name, subnodes: params)
534540
}
535541
}
536542
}
@@ -685,6 +691,8 @@ extension TargetBuildSettingDescription.Kind {
685691
return "enableExperimentalFeature"
686692
case .swiftLanguageMode:
687693
return "swiftLanguageMode"
694+
case .enableTesting:
695+
return "enableTesting"
688696
}
689697
}
690698
}

Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ extension BuildParameters {
7878
public var enableCodeCoverage: Bool
7979

8080
/// Whether building for testability is enabled.
81-
public var enableTestability: Bool
81+
public var enableTestability: Bool?
8282

8383
/// Whether or not to enable the experimental test output mode.
8484
public var experimentalTestOutput: Bool
@@ -97,14 +97,7 @@ extension BuildParameters {
9797
) {
9898
self.enableCodeCoverage = enableCodeCoverage
9999
self.experimentalTestOutput = experimentalTestOutput
100-
// decide on testability based on debug/release config
101-
// the goals of this being based on the build configuration is
102-
// that `swift build` followed by a `swift test` will need to do minimal rebuilding
103-
// given that the default configuration for `swift build` is debug
104-
// and that `swift test` normally requires building with testable enabled.
105-
// when building and testing in release mode, one can use the '--disable-testable-imports' flag
106-
// to disable testability in `swift test`, but that requires that the tests do not use the testable imports feature
107-
self.enableTestability = enableTestability ?? (.debug == configuration)
100+
self.enableTestability = enableTestability
108101
self.testProductStyle = targetTriple.isDarwin() ? .loadableBundle : .entryPointExecutable(
109102
explicitlyEnabledDiscovery: forceTestDiscovery,
110103
explicitlySpecifiedPath: testEntryPointPath

Sources/XCBuildSupport/PIFBuilder.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ struct PIFBuilderParameters {
3030
let isPackageAccessModifierSupported: Bool
3131

3232
/// Whether or not build for testability is enabled.
33-
let enableTestability: Bool
33+
let enableTestability: Bool?
3434

3535
/// Whether to create dylibs for dynamic library products.
3636
let shouldCreateDylibForDynamicProducts: Bool
@@ -343,7 +343,6 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
343343
debugSettings[.GCC_OPTIMIZATION_LEVEL] = "0"
344344
debugSettings[.ONLY_ACTIVE_ARCH] = "YES"
345345
debugSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Onone"
346-
debugSettings[.ENABLE_TESTABILITY] = "YES"
347346
debugSettings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS, default: []].append("DEBUG")
348347
debugSettings[.GCC_PREPROCESSOR_DEFINITIONS, default: ["$(inherited)"]].append("DEBUG=1")
349348
addBuildConfiguration(name: "Debug", settings: debugSettings)
@@ -354,10 +353,6 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
354353
releaseSettings[.GCC_OPTIMIZATION_LEVEL] = "s"
355354
releaseSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Owholemodule"
356355

357-
if parameters.enableTestability {
358-
releaseSettings[.ENABLE_TESTABILITY] = "YES"
359-
}
360-
361356
addBuildConfiguration(name: "Release", settings: releaseSettings)
362357

363358
for product in package.products.sorted(by: { $0.name < $1.name }) {

0 commit comments

Comments
 (0)