Skip to content

Commit 81526cf

Browse files
authored
Allow specifying testing libraries for swift package init (#7186)
This PR adds `-enable-experimental-swift-testing` (and `-disable-xctest` and their inverses) to `swift package init`. These options behave, broadly, the same as they do for `swift test`. They determine which testing library a new package will use and adjust the generated template to match. It is important to note that any combination of the two libraries is supported: a developer may wish to use only one or the other, or both, or may wish to opt out of a test target entirely. All four combinations are supported, however for simplicity's sake if both libraries are enabled, we only generate example code for swift-testing. Note that right now, correct macro target support is impeded by swiftlang/swift-syntax#2400. I don't think that issue blocks a change here (since it's in an experimental feature anyway!) but it does mean that `swift package init --type macro --enable-experimental-swift-testing` produces some dead tests. Once that issue is resolved, we can revise the template to produce meaningful tests instead. Resolves rdar://99279056.
1 parent 4bfc7aa commit 81526cf

File tree

6 files changed

+295
-43
lines changed

6 files changed

+295
-43
lines changed

Sources/Commands/PackageTools/Init.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ArgumentParser
1414
import Basics
1515
import CoreCommands
1616
import Workspace
17+
import SPMBuildCore
1718

1819
extension SwiftPackageTool {
1920
struct Init: SwiftCommand {
@@ -38,6 +39,18 @@ extension SwiftPackageTool {
3839
"""))
3940
var initMode: InitPackage.PackageType = .library
4041

42+
/// Whether to enable support for XCTest.
43+
@Flag(name: .customLong("xctest"),
44+
inversion: .prefixedEnableDisable,
45+
help: "Enable support for XCTest")
46+
var enableXCTestSupport: Bool = true
47+
48+
/// Whether to enable support for swift-testing.
49+
@Flag(name: .customLong("experimental-swift-testing"),
50+
inversion: .prefixedEnableDisable,
51+
help: "Enable experimental support for swift-testing")
52+
var enableSwiftTestingLibrarySupport: Bool = false
53+
4154
@Option(name: .customLong("name"), help: "Provide custom package name")
4255
var packageName: String?
4356

@@ -46,10 +59,18 @@ extension SwiftPackageTool {
4659
throw InternalError("Could not find the current working directory")
4760
}
4861

62+
var testingLibraries: Set<BuildParameters.Testing.Library> = []
63+
if enableXCTestSupport {
64+
testingLibraries.insert(.xctest)
65+
}
66+
if enableSwiftTestingLibrarySupport {
67+
testingLibraries.insert(.swiftTesting)
68+
}
4969
let packageName = self.packageName ?? cwd.basename
5070
let initPackage = try InitPackage(
5171
name: packageName,
5272
packageType: initMode,
73+
supportedTestingLibraries: testingLibraries,
5374
destinationPath: cwd,
5475
installedSwiftPMConfiguration: swiftTool.getHostToolchain().installedSwiftPMConfiguration,
5576
fileSystem: swiftTool.fileSystem

Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ extension BuildParameters {
9898
public var testProductStyle: TestProductStyle
9999

100100
/// The testing libraries supported by the package manager.
101-
public enum Library: String, Codable {
101+
public enum Library: String, Codable, CustomStringConvertible {
102102
/// The XCTest library.
103103
///
104104
/// This case represents both the open-source swift-corelibs-xctest
@@ -107,6 +107,10 @@ extension BuildParameters {
107107

108108
/// The swift-testing library.
109109
case swiftTesting = "swift-testing"
110+
111+
public var description: String {
112+
rawValue
113+
}
110114
}
111115

112116
/// Which testing library to use for this build.

Sources/SPMTestSupport/misc.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,13 @@ extension InitPackage {
445445
public convenience init(
446446
name: String,
447447
packageType: PackageType,
448+
supportedTestingLibraries: Set<BuildParameters.Testing.Library> = [.xctest],
448449
destinationPath: AbsolutePath,
449450
fileSystem: FileSystem
450451
) throws {
451452
try self.init(
452453
name: name,
453-
options: InitPackageOptions(packageType: packageType),
454+
options: InitPackageOptions(packageType: packageType, supportedTestingLibraries: supportedTestingLibraries),
454455
destinationPath: destinationPath,
455456
installedSwiftPMConfiguration: .default,
456457
fileSystem: fileSystem

Sources/Workspace/InitPackage.swift

Lines changed: 154 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import Basics
1414
import PackageModel
15+
import SPMBuildCore
1516

1617
import protocol TSCBasic.OutputByteStream
1718

@@ -25,16 +26,21 @@ public final class InitPackage {
2526
/// The type of package to create.
2627
public var packageType: PackageType
2728

29+
/// The set of supported testing libraries to include in the package.
30+
public var supportedTestingLibraries: Set<BuildParameters.Testing.Library>
31+
2832
/// The list of platforms in the manifest.
2933
///
3034
/// Note: This should only contain Apple platforms right now.
3135
public var platforms: [SupportedPlatform]
3236

3337
public init(
3438
packageType: PackageType,
39+
supportedTestingLibraries: Set<BuildParameters.Testing.Library> = [.xctest],
3540
platforms: [SupportedPlatform] = []
3641
) {
3742
self.packageType = packageType
43+
self.supportedTestingLibraries = supportedTestingLibraries
3844
self.platforms = platforms
3945
}
4046
}
@@ -87,13 +93,14 @@ public final class InitPackage {
8793
public convenience init(
8894
name: String,
8995
packageType: PackageType,
96+
supportedTestingLibraries: Set<BuildParameters.Testing.Library>,
9097
destinationPath: AbsolutePath,
9198
installedSwiftPMConfiguration: InstalledSwiftPMConfiguration,
9299
fileSystem: FileSystem
93100
) throws {
94101
try self.init(
95102
name: name,
96-
options: InitPackageOptions(packageType: packageType),
103+
options: InitPackageOptions(packageType: packageType, supportedTestingLibraries: supportedTestingLibraries),
97104
destinationPath: destinationPath,
98105
installedSwiftPMConfiguration: installedSwiftPMConfiguration,
99106
fileSystem: fileSystem
@@ -108,6 +115,11 @@ public final class InitPackage {
108115
installedSwiftPMConfiguration: InstalledSwiftPMConfiguration,
109116
fileSystem: FileSystem
110117
) throws {
118+
if options.packageType == .macro && options.supportedTestingLibraries.contains(.swiftTesting) {
119+
// FIXME: https://github.com/apple/swift-syntax/issues/2400
120+
throw InitError.unsupportedTestingLibraryForPackageType(.swiftTesting, .macro)
121+
}
122+
111123
self.options = options
112124
self.pkgname = name
113125
self.moduleName = name.spm_mangledToC99ExtendedIdentifier()
@@ -257,16 +269,22 @@ public final class InitPackage {
257269
}
258270

259271
// Package dependencies
272+
var dependencies = [String]()
260273
if packageType == .tool {
261-
pkgParams.append("""
262-
dependencies: [
263-
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
264-
]
265-
""")
274+
dependencies.append(#".package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0")"#)
266275
} else if packageType == .macro {
276+
dependencies.append(#".package(url: "https://github.com/apple/swift-syntax.git", from: "\#(self.installedSwiftPMConfiguration.swiftSyntaxVersionForMacroTemplate.description)")"#)
277+
}
278+
if options.supportedTestingLibraries.contains(.swiftTesting) {
279+
dependencies.append(#".package(url: "https://github.com/apple/swift-testing.git", from: "0.2.0")"#)
280+
}
281+
if !dependencies.isEmpty {
282+
let dependencies = dependencies.map { dependency in
283+
" \(dependency),"
284+
}.joined(separator: "\n")
267285
pkgParams.append("""
268286
dependencies: [
269-
.package(url: "https://github.com/apple/swift-syntax.git", from: "\(self.installedSwiftPMConfiguration.swiftSyntaxVersionForMacroTemplate.description)"),
287+
\(dependencies)
270288
]
271289
""")
272290
}
@@ -317,6 +335,35 @@ public final class InitPackage {
317335
]
318336
"""
319337
} else if packageType == .macro {
338+
let testTarget: String
339+
if options.supportedTestingLibraries.contains(.swiftTesting) {
340+
testTarget = """
341+
342+
// A test target used to develop the macro implementation.
343+
.testTarget(
344+
name: "\(pkgname)Tests",
345+
dependencies: [
346+
"\(pkgname)Macros",
347+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
348+
.product(name: "Testing", package: "swift-testing"),
349+
]
350+
),
351+
"""
352+
} else if options.supportedTestingLibraries.contains(.xctest) {
353+
testTarget = """
354+
355+
// A test target used to develop the macro implementation.
356+
.testTarget(
357+
name: "\(pkgname)Tests",
358+
dependencies: [
359+
"\(pkgname)Macros",
360+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
361+
]
362+
),
363+
"""
364+
} else {
365+
testTarget = ""
366+
}
320367
param += """
321368
// Macro implementation that performs the source transformation of a macro.
322369
.macro(
@@ -332,24 +379,36 @@ public final class InitPackage {
332379
333380
// A client of the library, which is able to use the macro in its own code.
334381
.executableTarget(name: "\(pkgname)Client", dependencies: ["\(pkgname)"]),
335-
336-
// A test target used to develop the macro implementation.
337-
.testTarget(
338-
name: "\(pkgname)Tests",
339-
dependencies: [
340-
"\(pkgname)Macros",
341-
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
342-
]
343-
),
382+
\(testTarget)
344383
]
345384
"""
346385
} else {
386+
let testTarget: String
387+
if options.supportedTestingLibraries.contains(.swiftTesting) {
388+
testTarget = """
389+
.testTarget(
390+
name: "\(pkgname)Tests",
391+
dependencies: [
392+
"\(pkgname)",
393+
.product(name: "Testing", package: "swift-testing"),
394+
]
395+
),
396+
"""
397+
} else if options.supportedTestingLibraries.contains(.xctest) {
398+
testTarget = """
399+
.testTarget(
400+
name: "\(pkgname)Tests",
401+
dependencies: ["\(pkgname)"]
402+
),
403+
"""
404+
} else {
405+
testTarget = ""
406+
}
407+
347408
param += """
348409
.target(
349410
name: "\(pkgname)"),
350-
.testTarget(
351-
name: "\(pkgname)Tests",
352-
dependencies: ["\(pkgname)"]),
411+
\(testTarget)
353412
]
354413
"""
355414
}
@@ -606,6 +665,12 @@ public final class InitPackage {
606665
}
607666

608667
private func writeTests() throws {
668+
if options.supportedTestingLibraries.isEmpty {
669+
// If the developer disabled all testing libraries, do not bother to
670+
// emit any test content.
671+
return
672+
}
673+
609674
switch packageType {
610675
case .empty, .executable, .tool, .buildToolPlugin, .commandPlugin: return
611676
default: break
@@ -620,11 +685,31 @@ public final class InitPackage {
620685
}
621686

622687
private func writeLibraryTestsFile(_ path: AbsolutePath) throws {
623-
try writePackageFile(path) { stream in
624-
stream.send(
688+
var content = ""
689+
690+
if options.supportedTestingLibraries.contains(.swiftTesting) {
691+
content += "import Testing\n"
692+
}
693+
if options.supportedTestingLibraries.contains(.xctest) {
694+
content += "import XCTest\n"
695+
}
696+
content += "@testable import \(moduleName)\n"
697+
698+
// Prefer swift-testing if specified, otherwise XCTest. If both are
699+
// specified, the developer is free to write tests using both
700+
// libraries, but we still only want to present a single library's
701+
// example tests.
702+
if options.supportedTestingLibraries.contains(.swiftTesting) {
703+
content += """
704+
705+
@Test func example() throws {
706+
// swift-testing Documentation
707+
// https://swiftpackageindex.com/apple/swift-testing/main/documentation/testing
708+
}
709+
625710
"""
626-
import XCTest
627-
@testable import \(moduleName)
711+
} else if options.supportedTestingLibraries.contains(.xctest) {
712+
content += """
628713
629714
final class \(moduleName)Tests: XCTestCase {
630715
func testExample() throws {
@@ -637,28 +722,52 @@ public final class InitPackage {
637722
}
638723
639724
"""
640-
)
725+
}
726+
727+
try writePackageFile(path) { stream in
728+
stream.send(content)
641729
}
642730
}
643731

644732
private func writeMacroTestsFile(_ path: AbsolutePath) throws {
645-
try writePackageFile(path) { stream in
646-
stream.send(##"""
647-
import SwiftSyntax
648-
import SwiftSyntaxBuilder
649-
import SwiftSyntaxMacros
650-
import SwiftSyntaxMacrosTestSupport
651-
import XCTest
733+
var content = ""
652734

653-
// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
654-
#if canImport(\##(moduleName)Macros)
655-
import \##(moduleName)Macros
735+
content += ##"""
736+
import SwiftSyntax
737+
import SwiftSyntaxBuilder
738+
import SwiftSyntaxMacros
739+
import SwiftSyntaxMacrosTestSupport
740+
"""##
656741

657-
let testMacros: [String: Macro.Type] = [
658-
"stringify": StringifyMacro.self,
659-
]
660-
#endif
742+
if options.supportedTestingLibraries.contains(.swiftTesting) {
743+
content += "import Testing\n"
744+
}
745+
if options.supportedTestingLibraries.contains(.xctest) {
746+
content += "import XCTest\n"
747+
}
748+
749+
content += ##"""
750+
751+
// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
752+
#if canImport(\##(moduleName)Macros)
753+
import \##(moduleName)Macros
754+
755+
let testMacros: [String: Macro.Type] = [
756+
"stringify": StringifyMacro.self,
757+
]
758+
#endif
661759
760+
761+
"""##
762+
763+
// Prefer swift-testing if specified, otherwise XCTest. If both are
764+
// specified, the developer is free to write tests using both
765+
// libraries, but we still only want to present a single library's
766+
// example tests.
767+
if options.supportedTestingLibraries.contains(.swiftTesting) {
768+
// FIXME: https://github.com/apple/swift-syntax/issues/2400
769+
} else if options.supportedTestingLibraries.contains(.xctest) {
770+
content += ##"""
662771
final class \##(moduleName)Tests: XCTestCase {
663772
func testMacro() throws {
664773
#if canImport(\##(moduleName)Macros)
@@ -694,7 +803,10 @@ public final class InitPackage {
694803
}
695804
696805
"""##
697-
)
806+
}
807+
808+
try writePackageFile(path) { stream in
809+
stream.send(content)
698810
}
699811
}
700812

@@ -783,13 +895,16 @@ public final class InitPackage {
783895

784896
private enum InitError: Swift.Error {
785897
case manifestAlreadyExists
898+
case unsupportedTestingLibraryForPackageType(_ testingLibrary: BuildParameters.Testing.Library, _ packageType: InitPackage.PackageType)
786899
}
787900

788901
extension InitError: CustomStringConvertible {
789902
var description: String {
790903
switch self {
791904
case .manifestAlreadyExists:
792905
return "a manifest file already exists in this directory"
906+
case let .unsupportedTestingLibraryForPackageType(library, packageType):
907+
return "\(library) cannot be used when initializing a \(packageType) package"
793908
}
794909
}
795910
}

0 commit comments

Comments
 (0)