From a90b91674bc945fe910a05d42ba198cde8369428 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 8 May 2024 01:32:02 -0700 Subject: [PATCH] Teach swift package add-target --type test about swift-testing (#7481) Introduce a command-line argument `--testing-library` to the add-target command to specify which test library to generate the test for. This can be 'xctest' (the prior XCTest behavior), 'swift-testing' (to use the new swift-testing library), or 'none' (for no test library at all). For the new swift-testing generation, also add the appropriate package and test target dependency, along with a stub testsuite to start from. Fixes https://github.com/apple/swift-package-manager/issues/7478 (cherry picked from commit a37631a1cb1d449a04f07ddd9d2cd41666ce771f) --- .../Commands/PackageCommands/AddTarget.swift | 6 + .../InstalledSwiftPMConfiguration.swift | 54 ++++++- Sources/PackageModelSyntax/AddTarget.swift | 134 ++++++++++++++++-- .../ManifestEditTests.swift | 47 ++++++ Utilities/config.json | 4 +- 5 files changed, 232 insertions(+), 13 deletions(-) diff --git a/Sources/Commands/PackageCommands/AddTarget.swift b/Sources/Commands/PackageCommands/AddTarget.swift index 0bd46584f57..5f3f30ad806 100644 --- a/Sources/Commands/PackageCommands/AddTarget.swift +++ b/Sources/Commands/PackageCommands/AddTarget.swift @@ -21,6 +21,8 @@ import TSCBasic import TSCUtility import Workspace +extension AddTarget.TestHarness: ExpressibleByArgument { } + extension SwiftPackageCommand { struct AddTarget: SwiftCommand { /// The type of target that can be specified on the command line. @@ -58,6 +60,9 @@ extension SwiftPackageCommand { @Option(help: "The checksum for a remote binary target") var checksum: String? + @Option(help: "The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'") + var testingLibrary: PackageModelSyntax.AddTarget.TestHarness = .default + func run(_ swiftCommandState: SwiftCommandState) throws { let workspace = try swiftCommandState.getActiveWorkspace() @@ -110,6 +115,7 @@ extension SwiftPackageCommand { let editResult = try PackageModelSyntax.AddTarget.addTarget( target, to: manifestSyntax, + configuration: .init(testHarness: testingLibrary), installedSwiftPMConfiguration: swiftCommandState .getHostToolchain() .installedSwiftPMConfiguration diff --git a/Sources/PackageModel/InstalledSwiftPMConfiguration.swift b/Sources/PackageModel/InstalledSwiftPMConfiguration.swift index a34a3d22dad..00c637289d6 100644 --- a/Sources/PackageModel/InstalledSwiftPMConfiguration.swift +++ b/Sources/PackageModel/InstalledSwiftPMConfiguration.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -public struct InstalledSwiftPMConfiguration: Codable { +public struct InstalledSwiftPMConfiguration { public struct Version: Codable, CustomStringConvertible { let major: Int let minor: Int @@ -31,6 +31,7 @@ public struct InstalledSwiftPMConfiguration: Codable { let version: Int public let swiftSyntaxVersionForMacroTemplate: Version + public let swiftTestingVersionForTestTemplate: Version public static var `default`: InstalledSwiftPMConfiguration { return .init( @@ -40,7 +41,56 @@ public struct InstalledSwiftPMConfiguration: Codable { minor: 0, patch: 0, prereleaseIdentifier: "latest" - ) + ), + swiftTestingVersionForTestTemplate: defaultSwiftTestingVersionForTestTemplate ) } + + private static var defaultSwiftTestingVersionForTestTemplate: Version { + .init( + major: 0, + minor: 8, + patch: 0, + prereleaseIdentifier: nil + ) + } +} + +extension InstalledSwiftPMConfiguration: Codable { + enum CodingKeys: CodingKey { + case version + case swiftSyntaxVersionForMacroTemplate + case swiftTestingVersionForTestTemplate + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.version = try container.decode( + Int.self, + forKey: CodingKeys.version + ) + self.swiftSyntaxVersionForMacroTemplate = try container.decode( + Version.self, + forKey: CodingKeys.swiftSyntaxVersionForMacroTemplate + ) + self.swiftTestingVersionForTestTemplate = try container.decodeIfPresent( + Version.self, + forKey: CodingKeys.swiftTestingVersionForTestTemplate + ) ?? InstalledSwiftPMConfiguration.defaultSwiftTestingVersionForTestTemplate + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.version, forKey: CodingKeys.version) + try container.encode( + self.swiftSyntaxVersionForMacroTemplate, + forKey: CodingKeys.swiftSyntaxVersionForMacroTemplate + ) + try container.encode( + self.swiftTestingVersionForTestTemplate, + forKey: CodingKeys.swiftTestingVersionForTestTemplate + ) + } } diff --git a/Sources/PackageModelSyntax/AddTarget.swift b/Sources/PackageModelSyntax/AddTarget.swift index 0ae83b78e0c..2f60c1a840d 100644 --- a/Sources/PackageModelSyntax/AddTarget.swift +++ b/Sources/PackageModelSyntax/AddTarget.swift @@ -30,12 +30,40 @@ public struct AddTarget { "cxxLanguageStandard" ] + /// The kind of test harness to use. This isn't part of the manifest + /// itself, but is used to guide the generation process. + public enum TestHarness: String, Codable { + /// Don't use any library + case none + + /// Create a test using the XCTest library. + case xctest + + /// Create a test using the swift-testing package. + case swiftTesting = "swift-testing" + + /// The default testing library to use. + public static var `default`: TestHarness = .xctest + } + + /// Additional configuration information to guide the package editing + /// process. + public struct Configuration { + /// The test harness to use. + public var testHarness: TestHarness + + public init(testHarness: TestHarness = .default) { + self.testHarness = testHarness + } + } + /// Add the given target to the manifest, producing a set of edit results /// that updates the manifest and adds some source files to stub out the /// new target. public static func addTarget( _ target: TargetDescription, to manifest: SourceFileSyntax, + configuration: Configuration = .init(), installedSwiftPMConfiguration: InstalledSwiftPMConfiguration = .default ) throws -> PackageEditResult { // Make sure we have a suitable tools version in the manifest. @@ -49,10 +77,20 @@ public struct AddTarget { // content when needed. var target = target - // Macro targets need to depend on a couple of libraries from - // SwiftSyntax. - if target.type == .macro { + // Add dependencies needed for various targets. + switch target.type { + case .macro: + // Macro targets need to depend on a couple of libraries from + // SwiftSyntax. target.dependencies.append(contentsOf: macroTargetDependencies) + + case .test where configuration.testHarness == .swiftTesting: + // Testing targets using swift-testing need to depend on + // SwiftTesting from the swift-testing package. + target.dependencies.append(contentsOf: swiftTestingTestTargetDependencies) + + default: + break; } var newPackageCall = try packageCall.appendingToArrayArgument( @@ -84,6 +122,7 @@ public struct AddTarget { addPrimarySourceFile( outerPath: outerPath, target: target, + configuration: configuration, to: &auxiliaryFiles ) @@ -124,6 +163,17 @@ public struct AddTarget { } } + case .test where configuration.testHarness == .swiftTesting: + if !manifest.description.contains("swift-testing") { + newPackageCall = try AddPackageDependency + .addPackageDependencyLocal( + .swiftTesting( + configuration: installedSwiftPMConfiguration + ), + to: newPackageCall + ) + } + default: break; } @@ -140,6 +190,7 @@ public struct AddTarget { fileprivate static func addPrimarySourceFile( outerPath: RelativePath, target: TargetDescription, + configuration: Configuration, to auxiliaryFiles: inout AuxiliaryFiles ) { let sourceFilePath = outerPath.appending( @@ -153,7 +204,17 @@ public struct AddTarget { // Add appropriate test module dependencies. if target.type == .test { - importModuleNames.append("XCTest") + switch configuration.testHarness { + case .none: + break + + case .xctest: + importModuleNames.append("XCTest") + + case .swiftTesting: + // Import is handled by the added dependency. + break + } } let importDecls = importModuleNames.lazy.sorted().map { name in @@ -184,14 +245,35 @@ public struct AddTarget { """ case .test: - """ - \(imports) - class \(raw: target.name): XCTestCase { - func test\(raw: target.name)() { - XCTAssertEqual(42, 17 + 25) + switch configuration.testHarness { + case .none: + """ + \(imports) + // Test code here + """ + + case .xctest: + """ + \(imports) + class \(raw: target.name): XCTestCase { + func test\(raw: target.name)() { + XCTAssertEqual(42, 17 + 25) + } + } + """ + + case .swiftTesting: + """ + \(imports) + @Suite + struct \(raw: target.name)Tests { + @Test("\(raw: target.name) tests") + func example() { + #expect(42 == 17 + 25) + } } + """ } - """ case .regular: """ @@ -298,3 +380,35 @@ fileprivate extension PackageDependency { ) } } + +/// The set of dependencies we need to introduce to a newly-created macro +/// target. +fileprivate let swiftTestingTestTargetDependencies: [TargetDescription.Dependency] = [ + .product(name: "Testing", package: "swift-testing"), +] + + +/// The package dependency for swift-testing, for use in test files. +fileprivate extension PackageDependency { + /// Source control URL for the swift-syntax package. + static var swiftTestingURL: SourceControlURL { + "https://github.com/apple/swift-testing.git" + } + + /// Package dependency on the swift-testing package. + static func swiftTesting( + configuration: InstalledSwiftPMConfiguration + ) -> PackageDependency { + let swiftTestingVersionDefault = + configuration.swiftTestingVersionForTestTemplate + let swiftTestingVersion = Version(swiftTestingVersionDefault.description)! + + return .sourceControl( + identity: PackageIdentity(url: swiftTestingURL), + nameForTargetDependencyResolutionOnly: nil, + location: .remote(swiftTestingURL), + requirement: .range(.upToNextMajor(from: swiftTestingVersion)), + productFilter: .everything + ) + } +} diff --git a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift b/Tests/PackageModelSyntaxTests/ManifestEditTests.swift index 2acb6debd62..812b72bfe84 100644 --- a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift +++ b/Tests/PackageModelSyntaxTests/ManifestEditTests.swift @@ -564,6 +564,53 @@ class ManifestEditTests: XCTestCase { } } + func testAddSwiftTestingTestTarget() throws { + try assertManifestRefactor(""" + // swift-tools-version: 5.5 + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-testing.git", from: "0.8.0"), + ], + targets: [ + .testTarget( + name: "MyTest", + dependencies: [ .product(name: "Testing", package: "swift-testing") ] + ), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Tests/MyTest/MyTest.swift") : """ + import Testing + + @Suite + struct MyTestTests { + @Test("MyTest tests") + func example() { + #expect(42 == 17 + 25) + } + } + """ + ]) { manifest in + try AddTarget.addTarget( + TargetDescription( + name: "MyTest", + type: .test + ), + to: manifest, + configuration: .init( + testHarness: .swiftTesting + ) + ) + } + } } diff --git a/Utilities/config.json b/Utilities/config.json index dc8ec6cafe9..b31399a6276 100644 --- a/Utilities/config.json +++ b/Utilities/config.json @@ -1 +1,3 @@ -{"version":1,"swiftSyntaxVersionForMacroTemplate":{"major":600,"minor":0,"patch":0, "prereleaseIdentifier":"latest"}} +{"version":1, + "swiftSyntaxVersionForMacroTemplate":{"major":600,"minor":0,"patch":0, "prereleaseIdentifier":"latest"}, + "swiftTestingVersionForTestTemplate":{"major":0,"minor":8,"patch":0}}