Skip to content

Commit

Permalink
Allow specifying testing libraries for swift package init (#7186)
Browse files Browse the repository at this point in the history
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
apple/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.
  • Loading branch information
grynspan committed Dec 12, 2023
1 parent 4bfc7aa commit 81526cf
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 43 deletions.
21 changes: 21 additions & 0 deletions Sources/Commands/PackageTools/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ArgumentParser
import Basics
import CoreCommands
import Workspace
import SPMBuildCore

extension SwiftPackageTool {
struct Init: SwiftCommand {
Expand All @@ -38,6 +39,18 @@ extension SwiftPackageTool {
"""))
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

@Option(name: .customLong("name"), help: "Provide custom package name")
var packageName: String?

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

var testingLibraries: Set<BuildParameters.Testing.Library> = []
if enableXCTestSupport {
testingLibraries.insert(.xctest)
}
if enableSwiftTestingLibrarySupport {
testingLibraries.insert(.swiftTesting)
}
let packageName = self.packageName ?? cwd.basename
let initPackage = try InitPackage(
name: packageName,
packageType: initMode,
supportedTestingLibraries: testingLibraries,
destinationPath: cwd,
installedSwiftPMConfiguration: swiftTool.getHostToolchain().installedSwiftPMConfiguration,
fileSystem: swiftTool.fileSystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extension BuildParameters {
public var testProductStyle: TestProductStyle

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

/// The swift-testing library.
case swiftTesting = "swift-testing"

public var description: String {
rawValue
}
}

/// Which testing library to use for this build.
Expand Down
3 changes: 2 additions & 1 deletion Sources/SPMTestSupport/misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,13 @@ extension InitPackage {
public convenience init(
name: String,
packageType: PackageType,
supportedTestingLibraries: Set<BuildParameters.Testing.Library> = [.xctest],
destinationPath: AbsolutePath,
fileSystem: FileSystem
) throws {
try self.init(
name: name,
options: InitPackageOptions(packageType: packageType),
options: InitPackageOptions(packageType: packageType, supportedTestingLibraries: supportedTestingLibraries),
destinationPath: destinationPath,
installedSwiftPMConfiguration: .default,
fileSystem: fileSystem
Expand Down
193 changes: 154 additions & 39 deletions Sources/Workspace/InitPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import Basics
import PackageModel
import SPMBuildCore

import protocol TSCBasic.OutputByteStream

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

/// The set of supported testing libraries to include in the package.
public var supportedTestingLibraries: Set<BuildParameters.Testing.Library>

/// The list of platforms in the manifest.
///
/// Note: This should only contain Apple platforms right now.
public var platforms: [SupportedPlatform]

public init(
packageType: PackageType,
supportedTestingLibraries: Set<BuildParameters.Testing.Library> = [.xctest],
platforms: [SupportedPlatform] = []
) {
self.packageType = packageType
self.supportedTestingLibraries = supportedTestingLibraries
self.platforms = platforms
}
}
Expand Down Expand Up @@ -87,13 +93,14 @@ public final class InitPackage {
public convenience init(
name: String,
packageType: PackageType,
supportedTestingLibraries: Set<BuildParameters.Testing.Library>,
destinationPath: AbsolutePath,
installedSwiftPMConfiguration: InstalledSwiftPMConfiguration,
fileSystem: FileSystem
) throws {
try self.init(
name: name,
options: InitPackageOptions(packageType: packageType),
options: InitPackageOptions(packageType: packageType, supportedTestingLibraries: supportedTestingLibraries),
destinationPath: destinationPath,
installedSwiftPMConfiguration: installedSwiftPMConfiguration,
fileSystem: fileSystem
Expand All @@ -108,6 +115,11 @@ public final class InitPackage {
installedSwiftPMConfiguration: InstalledSwiftPMConfiguration,
fileSystem: FileSystem
) throws {
if options.packageType == .macro && options.supportedTestingLibraries.contains(.swiftTesting) {
// FIXME: https://github.com/apple/swift-syntax/issues/2400
throw InitError.unsupportedTestingLibraryForPackageType(.swiftTesting, .macro)
}

self.options = options
self.pkgname = name
self.moduleName = name.spm_mangledToC99ExtendedIdentifier()
Expand Down Expand Up @@ -257,16 +269,22 @@ public final class InitPackage {
}

// Package dependencies
var dependencies = [String]()
if packageType == .tool {
pkgParams.append("""
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
]
""")
dependencies.append(#".package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0")"#)
} else if packageType == .macro {
dependencies.append(#".package(url: "https://github.com/apple/swift-syntax.git", from: "\#(self.installedSwiftPMConfiguration.swiftSyntaxVersionForMacroTemplate.description)")"#)
}
if options.supportedTestingLibraries.contains(.swiftTesting) {
dependencies.append(#".package(url: "https://github.com/apple/swift-testing.git", from: "0.2.0")"#)
}
if !dependencies.isEmpty {
let dependencies = dependencies.map { dependency in
" \(dependency),"
}.joined(separator: "\n")
pkgParams.append("""
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "\(self.installedSwiftPMConfiguration.swiftSyntaxVersionForMacroTemplate.description)"),
\(dependencies)
]
""")
}
Expand Down Expand Up @@ -317,6 +335,35 @@ public final class InitPackage {
]
"""
} else if packageType == .macro {
let testTarget: String
if options.supportedTestingLibraries.contains(.swiftTesting) {
testTarget = """
// A test target used to develop the macro implementation.
.testTarget(
name: "\(pkgname)Tests",
dependencies: [
"\(pkgname)Macros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "Testing", package: "swift-testing"),
]
),
"""
} else if options.supportedTestingLibraries.contains(.xctest) {
testTarget = """
// A test target used to develop the macro implementation.
.testTarget(
name: "\(pkgname)Tests",
dependencies: [
"\(pkgname)Macros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
"""
} else {
testTarget = ""
}
param += """
// Macro implementation that performs the source transformation of a macro.
.macro(
Expand All @@ -332,24 +379,36 @@ public final class InitPackage {
// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "\(pkgname)Client", dependencies: ["\(pkgname)"]),
// A test target used to develop the macro implementation.
.testTarget(
name: "\(pkgname)Tests",
dependencies: [
"\(pkgname)Macros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
\(testTarget)
]
"""
} else {
let testTarget: String
if options.supportedTestingLibraries.contains(.swiftTesting) {
testTarget = """
.testTarget(
name: "\(pkgname)Tests",
dependencies: [
"\(pkgname)",
.product(name: "Testing", package: "swift-testing"),
]
),
"""
} else if options.supportedTestingLibraries.contains(.xctest) {
testTarget = """
.testTarget(
name: "\(pkgname)Tests",
dependencies: ["\(pkgname)"]
),
"""
} else {
testTarget = ""
}

param += """
.target(
name: "\(pkgname)"),
.testTarget(
name: "\(pkgname)Tests",
dependencies: ["\(pkgname)"]),
\(testTarget)
]
"""
}
Expand Down Expand Up @@ -606,6 +665,12 @@ public final class InitPackage {
}

private func writeTests() throws {
if options.supportedTestingLibraries.isEmpty {
// If the developer disabled all testing libraries, do not bother to
// emit any test content.
return
}

switch packageType {
case .empty, .executable, .tool, .buildToolPlugin, .commandPlugin: return
default: break
Expand All @@ -620,11 +685,31 @@ public final class InitPackage {
}

private func writeLibraryTestsFile(_ path: AbsolutePath) throws {
try writePackageFile(path) { stream in
stream.send(
var content = ""

if options.supportedTestingLibraries.contains(.swiftTesting) {
content += "import Testing\n"
}
if options.supportedTestingLibraries.contains(.xctest) {
content += "import XCTest\n"
}
content += "@testable import \(moduleName)\n"

// Prefer swift-testing if specified, otherwise XCTest. If both are
// specified, the developer is free to write tests using both
// libraries, but we still only want to present a single library's
// example tests.
if options.supportedTestingLibraries.contains(.swiftTesting) {
content += """
@Test func example() throws {
// swift-testing Documentation
// https://swiftpackageindex.com/apple/swift-testing/main/documentation/testing
}
"""
import XCTest
@testable import \(moduleName)
} else if options.supportedTestingLibraries.contains(.xctest) {
content += """
final class \(moduleName)Tests: XCTestCase {
func testExample() throws {
Expand All @@ -637,28 +722,52 @@ public final class InitPackage {
}
"""
)
}

try writePackageFile(path) { stream in
stream.send(content)
}
}

private func writeMacroTestsFile(_ path: AbsolutePath) throws {
try writePackageFile(path) { stream in
stream.send(##"""
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
var content = ""

// 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.
#if canImport(\##(moduleName)Macros)
import \##(moduleName)Macros
content += ##"""
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
"""##

let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
]
#endif
if options.supportedTestingLibraries.contains(.swiftTesting) {
content += "import Testing\n"
}
if options.supportedTestingLibraries.contains(.xctest) {
content += "import XCTest\n"
}

content += ##"""
// 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.
#if canImport(\##(moduleName)Macros)
import \##(moduleName)Macros
let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
]
#endif
"""##

// Prefer swift-testing if specified, otherwise XCTest. If both are
// specified, the developer is free to write tests using both
// libraries, but we still only want to present a single library's
// example tests.
if options.supportedTestingLibraries.contains(.swiftTesting) {
// FIXME: https://github.com/apple/swift-syntax/issues/2400
} else if options.supportedTestingLibraries.contains(.xctest) {
content += ##"""
final class \##(moduleName)Tests: XCTestCase {
func testMacro() throws {
#if canImport(\##(moduleName)Macros)
Expand Down Expand Up @@ -694,7 +803,10 @@ public final class InitPackage {
}
"""##
)
}

try writePackageFile(path) { stream in
stream.send(content)
}
}

Expand Down Expand Up @@ -783,13 +895,16 @@ public final class InitPackage {

private enum InitError: Swift.Error {
case manifestAlreadyExists
case unsupportedTestingLibraryForPackageType(_ testingLibrary: BuildParameters.Testing.Library, _ packageType: InitPackage.PackageType)
}

extension InitError: CustomStringConvertible {
var description: String {
switch self {
case .manifestAlreadyExists:
return "a manifest file already exists in this directory"
case let .unsupportedTestingLibraryForPackageType(library, packageType):
return "\(library) cannot be used when initializing a \(packageType) package"
}
}
}
Expand Down

0 comments on commit 81526cf

Please sign in to comment.