diff --git a/Sources/Commands/PackageTools/Init.swift b/Sources/Commands/PackageTools/Init.swift index 221840107ce..42e492aa67a 100644 --- a/Sources/Commands/PackageTools/Init.swift +++ b/Sources/Commands/PackageTools/Init.swift @@ -14,6 +14,7 @@ import ArgumentParser import Basics import CoreCommands import Workspace +import SPMBuildCore extension SwiftPackageTool { struct Init: SwiftCommand { @@ -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? @@ -46,10 +59,18 @@ extension SwiftPackageTool { throw InternalError("Could not find the current working directory") } + var testingLibraries: Set = [] + 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 diff --git a/Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift b/Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift index 36b0c5660e2..bdfb66bb6b8 100644 --- a/Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift +++ b/Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift @@ -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 @@ -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. diff --git a/Sources/SPMTestSupport/misc.swift b/Sources/SPMTestSupport/misc.swift index 8164a923b6c..f4abd9a2004 100644 --- a/Sources/SPMTestSupport/misc.swift +++ b/Sources/SPMTestSupport/misc.swift @@ -444,12 +444,13 @@ extension InitPackage { public convenience init( name: String, packageType: PackageType, + supportedTestingLibraries: Set = [.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 diff --git a/Sources/Workspace/InitPackage.swift b/Sources/Workspace/InitPackage.swift index 1f24c2ac11c..cc75e6223d9 100644 --- a/Sources/Workspace/InitPackage.swift +++ b/Sources/Workspace/InitPackage.swift @@ -12,6 +12,7 @@ import Basics import PackageModel +import SPMBuildCore import protocol TSCBasic.OutputByteStream @@ -25,6 +26,9 @@ 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 + /// The list of platforms in the manifest. /// /// Note: This should only contain Apple platforms right now. @@ -32,9 +36,11 @@ public final class InitPackage { public init( packageType: PackageType, + supportedTestingLibraries: Set = [.xctest], platforms: [SupportedPlatform] = [] ) { self.packageType = packageType + self.supportedTestingLibraries = supportedTestingLibraries self.platforms = platforms } } @@ -87,13 +93,14 @@ public final class InitPackage { public convenience init( name: String, packageType: PackageType, + supportedTestingLibraries: Set, 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 @@ -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() @@ -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) ] """) } @@ -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( @@ -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) ] """ } @@ -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 @@ -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 { @@ -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) @@ -694,7 +803,10 @@ public final class InitPackage { } """## - ) + } + + try writePackageFile(path) { stream in + stream.send(content) } } @@ -783,6 +895,7 @@ public final class InitPackage { private enum InitError: Swift.Error { case manifestAlreadyExists + case unsupportedTestingLibraryForPackageType(_ testingLibrary: BuildParameters.Testing.Library, _ packageType: InitPackage.PackageType) } extension InitError: CustomStringConvertible { @@ -790,6 +903,8 @@ extension InitError: CustomStringConvertible { 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" } } } diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 82ddc313da9..31bad5f1b44 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -74,7 +74,8 @@ final class PackageToolTests: CommandsTestCase { func testInitUsage() throws { let stdout = try execute(["init", "--help"]).stdout - XCTAssertMatch(stdout, .contains("USAGE: swift package init [--type ] [--name ]")) + XCTAssertMatch(stdout, .contains("USAGE: swift package init [--type ] ")) + XCTAssertMatch(stdout, .contains(" [--name ]")) } func testInitOptionsHelp() throws { diff --git a/Tests/WorkspaceTests/InitTests.swift b/Tests/WorkspaceTests/InitTests.swift index 6522b362b2e..687f7f1ed01 100644 --- a/Tests/WorkspaceTests/InitTests.swift +++ b/Tests/WorkspaceTests/InitTests.swift @@ -98,7 +98,7 @@ class InitTests: XCTestCase { } } - func testInitPackageLibrary() throws { + func testInitPackageLibraryWithXCTestOnly() throws { try testWithTemporaryDirectory { tmpPath in let fs = localFileSystem let path = tmpPath.appending("Foo") @@ -149,6 +149,116 @@ class InitTests: XCTestCase { } } + func testInitPackageLibraryWithSwiftTestingOnly() throws { + try testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + let name = path.basename + try fs.createDirectory(path) + + // Create the package + let initPackage = try InitPackage( + name: name, + packageType: .library, + supportedTestingLibraries: [.swiftTesting], + destinationPath: path, + fileSystem: localFileSystem + ) + try initPackage.writePackageStructure() + + // Verify basic file system content that we expect in the package + let manifest = path.appending("Package.swift") + XCTAssertFileExists(manifest) + let manifestContents: String = try localFileSystem.readFileContents(manifest) + XCTAssertMatch(manifestContents, .contains(#"swift-testing.git", from: "0.2.0""#)) + XCTAssertMatch(manifestContents, .contains(#".product(name: "Testing", package: "swift-testing")"#)) + + let testFile = path.appending("Tests").appending("FooTests").appending("FooTests.swift") + let testFileContents: String = try localFileSystem.readFileContents(testFile) + XCTAssertMatch(testFileContents, .contains(#"import Testing"#)) + XCTAssertNoMatch(testFileContents, .contains(#"import XCTest"#)) + XCTAssertMatch(testFileContents, .contains(#"@Test func example() throws"#)) + XCTAssertNoMatch(testFileContents, .contains("func testExample() throws")) + + // Try building it -- DISABLED because we cannot pull the swift-testing repository from CI. +// XCTAssertBuilds(path) +// let triple = try UserToolchain.default.targetTriple +// XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) + } + } + + func testInitPackageLibraryWithBothSwiftTestingAndXCTest() throws { + try testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + let name = path.basename + try fs.createDirectory(path) + + // Create the package + let initPackage = try InitPackage( + name: name, + packageType: .library, + supportedTestingLibraries: [.swiftTesting, .xctest], + destinationPath: path, + fileSystem: localFileSystem + ) + try initPackage.writePackageStructure() + + // Verify basic file system content that we expect in the package + let manifest = path.appending("Package.swift") + XCTAssertFileExists(manifest) + let manifestContents: String = try localFileSystem.readFileContents(manifest) + XCTAssertMatch(manifestContents, .contains(#"swift-testing.git", from: "0.2.0""#)) + XCTAssertMatch(manifestContents, .contains(#".product(name: "Testing", package: "swift-testing")"#)) + + let testFile = path.appending("Tests").appending("FooTests").appending("FooTests.swift") + let testFileContents: String = try localFileSystem.readFileContents(testFile) + XCTAssertMatch(testFileContents, .contains(#"import Testing"#)) + XCTAssertMatch(testFileContents, .contains(#"import XCTest"#)) + XCTAssertMatch(testFileContents, .contains(#"@Test func example() throws"#)) + XCTAssertNoMatch(testFileContents, .contains("func testExample() throws")) + + // Try building it -- DISABLED because we cannot pull the swift-testing repository from CI. + // XCTAssertBuilds(path) + // let triple = try UserToolchain.default.targetTriple + // XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) + } + } + + func testInitPackageLibraryWithNoTests() throws { + try testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + let name = path.basename + try fs.createDirectory(path) + + // Create the package + let initPackage = try InitPackage( + name: name, + packageType: .library, + supportedTestingLibraries: [], + destinationPath: path, + fileSystem: localFileSystem + ) + try initPackage.writePackageStructure() + + // Verify basic file system content that we expect in the package + let manifest = path.appending("Package.swift") + XCTAssertFileExists(manifest) + let manifestContents: String = try localFileSystem.readFileContents(manifest) + XCTAssertNoMatch(manifestContents, .contains(#"swift-testing.git", from: "0.2.0""#)) + XCTAssertNoMatch(manifestContents, .contains(#".product(name: "Testing", package: "swift-testing")"#)) + XCTAssertNoMatch(manifestContents, .contains(#".testTarget"#)) + + XCTAssertNoSuchPath(path.appending("Tests")) + + // Try building it + XCTAssertBuilds(path) + let triple = try UserToolchain.default.targetTriple + XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) + } + } + func testInitPackageCommandPlugin() throws { try testWithTemporaryDirectory { tmpPath in let fs = localFileSystem