diff --git a/Sources/Workspace/InitPackage.swift b/Sources/Workspace/InitPackage.swift index f66d72c5985..88e7f1c4d66 100644 --- a/Sources/Workspace/InitPackage.swift +++ b/Sources/Workspace/InitPackage.swift @@ -300,13 +300,36 @@ public final class InitPackage { """ if packageType == .executable { + let testTarget: String + if !options.supportedTestingLibraries.isEmpty { + testTarget = """ + .testTarget( + name: "\(pkgname)Tests", + dependencies: ["\(pkgname)"] + ), + """ + } else { + testTarget = "" + } param += """ .executableTarget( name: "\(pkgname)" ), + \(testTarget) ] """ } else if packageType == .tool { + let testTarget: String + if !options.supportedTestingLibraries.isEmpty { + testTarget = """ + .testTarget( + name: "\(pkgname)Tests", + dependencies: ["\(pkgname)"] + ), + """ + } else { + testTarget = "" + } param += """ .executableTarget( name: "\(pkgname)", @@ -314,6 +337,7 @@ public final class InitPackage { .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), + \(testTarget) ] """ } else if packageType == .buildToolPlugin { @@ -337,22 +361,8 @@ 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) { + if !options.supportedTestingLibraries.isEmpty { testTarget = """ - // A test target used to develop the macro implementation. .testTarget( name: "\(pkgname)Tests", @@ -661,7 +671,7 @@ public final class InitPackage { } switch packageType { - case .empty, .executable, .tool, .buildToolPlugin, .commandPlugin: return + case .empty, .buildToolPlugin, .commandPlugin: return default: break } let tests = destinationPath.appending("Tests") @@ -874,8 +884,8 @@ public final class InitPackage { let testClassFile = try AbsolutePath(validating: "\(moduleName)Tests.swift", relativeTo: testModule) switch packageType { - case .empty, .buildToolPlugin, .commandPlugin, .executable, .tool: break - case .library: + case .empty, .buildToolPlugin, .commandPlugin: break + case .library, .executable, .tool: try writeLibraryTestsFile(testClassFile) case .macro: try writeMacroTestsFile(testClassFile) diff --git a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift index feb3dabec5a..3a8603dbfb0 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift @@ -10,12 +10,19 @@ import Basics import Testing +import TSCTestSupport +// MARK: File Helpers + +/// Verifies that a file exists at the specified path. +/// +/// - Parameters: +/// - path: The absolute path to check for file existence. +/// - sourceLocation: The source location where the expectation is made. public func expectFileExists( at path: AbsolutePath, sourceLocation: SourceLocation = #_sourceLocation, ) { - #expect( localFileSystem.exists(path), "Files '\(path)' does not exist.", @@ -23,7 +30,47 @@ public func expectFileExists( ) } +/// Verifies that no file or directory exists at the specified path. +/// +/// - Parameters: +/// - path: The absolute path to check for non-existence. +/// - sourceLocation: The source location where the expectation is made. +public func expectNoSuchPath( + _ path: AbsolutePath, + sourceLocation: SourceLocation = #_sourceLocation +) { + #expect( + !localFileSystem.exists(path), + "Expected no such path '\(path)'", + sourceLocation: sourceLocation + ) +} +/// Verifies that a directory exists at the specified path. +/// +/// - Parameters: +/// - path: The absolute path to check for directory existence. +/// - sourceLocation: The source location where the expectation is made. +public func expectDirectoryExists( + _ path: AbsolutePath, + sourceLocation: SourceLocation = #_sourceLocation +) { + #expect( + localFileSystem.isDirectory(path), + "Expected directory at '\(path)'", + sourceLocation: sourceLocation + ) +} + +// MARK: Error Helpers + +/// Verifies that an expression throws a `CommandExecutionError`. +/// +/// - Parameters: +/// - expression: The expression to evaluate. +/// - message: An optional description of the failure. +/// - sourceLocation: The source location where the expectation is made. +/// - errorHandler: A closure that's called with the error if the expression throws. public func expectThrowsCommandExecutionError( _ expression: @autoclosure () async throws -> T, _ message: @autoclosure () -> Comment = "", @@ -42,6 +89,12 @@ public func expectThrowsCommandExecutionError( } /// An `async`-friendly replacement for `XCTAssertThrowsError`. +/// +/// - Parameters: +/// - expression: The expression to evaluate. +/// - message: An optional description of the failure. +/// - sourceLocation: The source location where the expectation is made. +/// - errorHandler: A closure that's called with the error if the expression throws. public func expectAsyncThrowsError( _ expression: @autoclosure () async throws -> T, _ message: @autoclosure () -> Comment? = nil, @@ -55,3 +108,4 @@ public func expectAsyncThrowsError( errorHandler(error) } } + diff --git a/Tests/IntegrationTests/BasicTests.swift b/Tests/IntegrationTests/BasicTests.swift index be2d128648a..67b36ee04d1 100644 --- a/Tests/IntegrationTests/BasicTests.swift +++ b/Tests/IntegrationTests/BasicTests.swift @@ -160,27 +160,23 @@ private struct BasicTests { // Create a new package with an executable target. let packagePath = tempDir.appending(component: "Project") try localFileSystem.createDirectory(packagePath) - await withKnownIssue("error: no tests found; create a target in the 'Tests' directory") { - try await executeSwiftPackage( - packagePath, - extraArgs: ["init", "--type", "executable"], - buildSystem: .native, - ) - let packageOutput = try await executeSwiftTest( - packagePath, - extraArgs: ["--vv"], - buildSystem: .native, - ) + try await executeSwiftPackage( + packagePath, + extraArgs: ["init", "--type", "executable"], + buildSystem: .native, + ) + let packageOutput = try await executeSwiftTest( + packagePath, + extraArgs: ["--vv"], + buildSystem: .native, + ) - // Check the test log. - let compilingRegex = try Regex("Compiling .*ProjectTests.*") - #expect(packageOutput.stdout.contains(compilingRegex), "stdout: '\(packageOutput.stdout)'\n stderr:'\(packageOutput.stderr)'") - #expect(packageOutput.stdout.contains("Executed 1 test"), "stdout: '\(packageOutput.stdout)'\n stderr:'\(packageOutput.stderr)'") + // Check the test log. + #expect(packageOutput.stdout.contains("Test run with 1 test"), "stdout: '\(packageOutput.stdout)'\n stderr:'\(packageOutput.stderr)'") - // Check there were no compile errors or warnings. - #expect(packageOutput.stdout.contains("error") == false) - #expect(packageOutput.stdout.contains("warning") == false) - } + // Check there were no compile errors or warnings. + #expect(packageOutput.stdout.contains("error") == false) + #expect(packageOutput.stdout.contains("warning") == false) } } diff --git a/Tests/WorkspaceTests/InitTests.swift b/Tests/WorkspaceTests/InitTests.swift index d09e117d1a5..67d3f854ad1 100644 --- a/Tests/WorkspaceTests/InitTests.swift +++ b/Tests/WorkspaceTests/InitTests.swift @@ -14,370 +14,392 @@ import Basics import _InternalTestSupport import PackageModel import Workspace -import XCTest - -final class InitTests: XCTestCase { - - // MARK: TSCBasic package creation for each package type. - - func testInitPackageEmpty() 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: .empty, - destinationPath: path, - fileSystem: localFileSystem - ) - var progressMessages = [String]() - initPackage.progressReporter = { message in - progressMessages.append(message) - } - try initPackage.writePackageStructure() - - // Not picky about the specific progress messages, just checking that we got some. - XCTAssertGreaterThan(progressMessages.count, 0) - - // 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) - let version = InitPackage.newPackageToolsVersion - let versionSpecifier = "\(version.major).\(version.minor)" - XCTAssertMatch(manifestContents, .prefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) - XCTAssertMatch(manifestContents, .contains(packageWithNameOnly(named: name))) - } - } - - func testInitPackageExecutable() async throws { - try await 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: .executable, - destinationPath: path, - fileSystem: localFileSystem - ) - var progressMessages = [String]() - initPackage.progressReporter = { message in - progressMessages.append(message) - } - try initPackage.writePackageStructure() - - // Not picky about the specific progress messages, just checking that we got some. - XCTAssertGreaterThan(progressMessages.count, 0) - - // 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) - let version = InitPackage.newPackageToolsVersion - let versionSpecifier = "\(version.major).\(version.minor)" - XCTAssertMatch(manifestContents, .prefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) - - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending("Foo")), ["Foo.swift"]) - await XCTAssertBuilds( +import Testing +import SPMBuildCore + +public func expectBuilds( + _ path: AbsolutePath, + buildSystem: BuildSystemProvider.Kind, + configurations: Set = [.debug, .release], + extraArgs: [String] = [], + Xcc: [String] = [], + Xld: [String] = [], + Xswiftc: [String] = [], + env: Environment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, +) async { + for conf in configurations { + await #expect(throws: Never.self, sourceLocation: sourceLocation) { + try await executeSwiftBuild( path, - buildSystem: .native, + configuration: conf, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + env: env, + buildSystem: buildSystem ) - let triple = try UserToolchain.default.targetTriple - let binPath = path.appending(components: ".build", triple.platformBuildPathComponent, "debug") -#if os(Windows) - XCTAssertFileExists(binPath.appending("Foo.exe")) -#else - XCTAssertFileExists(binPath.appending("Foo")) -#endif - XCTAssertFileExists(binPath.appending(components: "Modules", "Foo.swiftmodule")) } } +} - func testInitPackageExecutableCalledMain() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("main") - let name = path.basename - try fs.createDirectory(path) - - // Create the package - let initPackage = try InitPackage( - name: name, - packageType: .executable, - destinationPath: path, - fileSystem: localFileSystem - ) - try initPackage.writePackageStructure() - - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending("main")), ["MainEntrypoint.swift"]) - await XCTAssertBuilds( - path, - buildSystem: .native, - ) +/// Tests for the `InitPackage` functionality, which creates new Swift packages with different configurations. +struct InitTests { + /// The target triple for the current platform, used to locate build products. + static let targetTriple: Triple = { + do { + return try UserToolchain.default.targetTriple + } catch { + fatalError("Failed to determine target triple: \(error)") } - } - - func testInitPackageLibraryWithXCTestOnly() async throws { - try await testWithTemporaryDirectory { tmpPath in + }() + + // MARK: - Helper Methods + + /// Creates a test package with the specified configuration and verifies its structure. + /// - Parameters: + /// - packageType: The type of package to create + /// - name: The name of the package (defaults to "Foo") + /// - supportedTestingLibraries: The testing libraries to include + /// - buildSystem: The build system to use for verification + /// - customVerification: Additional verification steps to perform + /// - Returns: The path to the created package + private func createAndVerifyPackage( + packageType: InitPackage.PackageType, + name: String = "Foo", + supportedTestingLibraries: Set = [.xctest], + buildSystem: BuildSystemProvider.Kind? = nil, + customVerification: ((AbsolutePath, String) throws -> Void)? = nil + ) async throws { + return try await testWithTemporaryDirectory { tmpPath in let fs = localFileSystem - let path = tmpPath.appending("Foo") - let name = path.basename + let path = tmpPath.appending(name) + let packageName = path.basename try fs.createDirectory(path) // Create the package let initPackage = try InitPackage( - name: name, - packageType: .library, - supportedTestingLibraries: [.xctest], + name: packageName, + packageType: packageType, + supportedTestingLibraries: supportedTestingLibraries, destinationPath: path, - fileSystem: localFileSystem + fileSystem: fs ) + + // Capture progress messages var progressMessages = [String]() initPackage.progressReporter = { message in progressMessages.append(message) } + + // Write the package structure try initPackage.writePackageStructure() - // Not picky about the specific progress messages, just checking that we got some. - XCTAssertGreaterThan(progressMessages.count, 0) + // Verify progress messages were generated + #expect(progressMessages.count > 0, "Expected progress messages during package creation") - // Verify basic file system content that we expect in the package + // Verify basic file system content let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - let manifestContents: String = try localFileSystem.readFileContents(manifest) + expectFileExists(at: manifest) + + let manifestContents: String = try fs.readFileContents(manifest) let version = InitPackage.newPackageToolsVersion let versionSpecifier = "\(version.major).\(version.minor)" - XCTAssertMatch(manifestContents, .prefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) + #expect(manifestContents.hasPrefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending("Foo")), ["Foo.swift"]) + // Run custom verification if provided + try customVerification?(path, packageName) - let tests = path.appending("Tests") - XCTAssertEqual(try fs.getDirectoryContents(tests).sorted(), ["FooTests"]) + // Build the package if a build system was specified + if let buildSystem = buildSystem { + await expectBuilds(path, buildSystem: buildSystem) - let testFile = tests.appending("FooTests").appending("FooTests.swift") - let testFileContents: String = try localFileSystem.readFileContents(testFile) - XCTAssertTrue(testFileContents.hasPrefix("import XCTest"), """ - Validates formatting of XCTest source file, in particular that it does not contain leading whitespace: - \(testFileContents) - """) - XCTAssertMatch(testFileContents, .contains("func testExample() throws")) - - // Try building it - await XCTAssertBuilds( - path, - buildSystem: .native, - ) - let triple = try UserToolchain.default.targetTriple - XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) + // Verify build products + verifyBuildProducts(for: packageType, at: path, name: packageName, buildSystem: buildSystem) + } } } - func testInitPackageLibraryWithSwiftTestingOnly() async throws { - try testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - let name = path.basename - try fs.createDirectory(path) + /// Verifies that the expected build products exist for a package. + /// - Parameters: + /// - packageType: The type of package + /// - path: The path to the package + /// - name: The name of the package + /// - buildSystem: The build system used + private func verifyBuildProducts( + for packageType: InitPackage.PackageType, + at path: AbsolutePath, + name: String, + buildSystem: BuildSystemProvider.Kind + ) { + let expectedPath = path + .appending(components: ".build", Self.targetTriple.platformBuildPathComponent) + .appending(components: buildSystem.binPathSuffixes(for: BuildConfiguration.debug)) + + switch packageType { + case .library: + if buildSystem == .native { + expectFileExists(at: expectedPath.appending("Modules", "\(name).swiftmodule")) + } else { + expectFileExists(at: expectedPath.appending("\(name).swiftmodule")) + } + case .executable, .tool: + expectFileExists(at: expectedPath.appending(executableName(name))) + case .empty, .buildToolPlugin, .commandPlugin, .macro: + // These types don't have specific build products to verify or are verified separately + break + } + } - // Create the package - let initPackage = try InitPackage( - name: name, - packageType: .library, - supportedTestingLibraries: [.swiftTesting], - destinationPath: path, - fileSystem: localFileSystem - ) - try initPackage.writePackageStructure() + /// Verifies the test file contents for a package. + /// - Parameters: + /// - path: The path to the package + /// - name: The name of the package + /// - hasSwiftTesting: Whether Swift Testing should be present + /// - hasXCTest: Whether XCTest should be present + private func verifyTestFileContents( + at path: AbsolutePath, + name: String, + hasSwiftTesting: Bool, + hasXCTest: Bool + ) throws { + let testFile = path.appending("Tests").appending("\(name)Tests").appending("\(name)Tests.swift") + let testFileContents: String = try localFileSystem.readFileContents(testFile) + + if hasSwiftTesting { + #expect(testFileContents.contains(#"import Testing"#)) + #expect(testFileContents.contains(#"@Test func example() async throws"#)) + } else { + #expect(!testFileContents.contains(#"import Testing"#)) + #expect(!testFileContents.contains(#"@Test func example() async throws"#)) + } - // Verify basic file system content that we expect in the package - let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - - 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() async throws"#)) - XCTAssertNoMatch(testFileContents, .contains("func testExample() throws")) - -#if canImport(TestingDisabled) - // Try building it - await XCTAssertBuilds( - path, - buildSystem: .native, - ) - let triple = try UserToolchain.default.targetTriple - XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) -#endif + if hasXCTest { + #expect(testFileContents.contains(#"import XCTest"#)) + #expect(testFileContents.contains("func testExample() throws")) + + if hasSwiftTesting { + // When both are present, ensure XCTest content is properly formatted + #expect(testFileContents.contains("import XCTest"), "XCTest import should be present") + } else { + // When only XCTest is present, ensure it's at the beginning of the file + #expect(testFileContents.hasPrefix("import XCTest"), """ + Validates formatting of XCTest source file, in particular that it does not contain leading whitespace: + \(testFileContents) + """) + } + } else { + #expect(!testFileContents.contains(#"import XCTest"#)) + #expect(!testFileContents.contains("func testExample() throws")) } } - func testInitPackageLibraryWithBothSwiftTestingAndXCTest() async throws { - try testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - let name = path.basename - try fs.createDirectory(path) + /// Helper method to verify plugin package contents + /// - Parameters: + /// - path: Path to the package + /// - name: Name of the package + /// - isCommandPlugin: Whether this is a command plugin (vs build tool plugin) + private func verifyPluginPackage( + at path: AbsolutePath, + name: String, + isCommandPlugin: Bool + ) throws { + let manifest = path.appending("Package.swift") + expectFileExists(at: manifest) + let manifestContents: String = try localFileSystem.readFileContents(manifest) + + // Verify manifest contents + #expect(manifestContents.contains(".plugin(") && manifestContents.contains("targets: [\"\(name)\"]")) + + if isCommandPlugin { + #expect(manifestContents.contains(".plugin(") && + manifestContents.contains("capability: .command(intent: .custom(") && + manifestContents.contains("verb: \"\(name)\"")) + } else { + #expect(manifestContents.contains(".plugin(") && manifestContents.contains("capability: .buildTool()")) + } - // Create the package - let initPackage = try InitPackage( - name: name, - packageType: .library, - supportedTestingLibraries: [.swiftTesting, .xctest], - destinationPath: path, - fileSystem: localFileSystem - ) - try initPackage.writePackageStructure() + // Verify source file + let source = path.appending("Plugins", "\(name).swift") + expectFileExists(at: source) + let sourceContents: String = try localFileSystem.readFileContents(source) + + if isCommandPlugin { + #expect(sourceContents.contains("struct \(name): CommandPlugin")) + #expect(sourceContents.contains("performCommand(context: PluginContext")) + } else { + #expect(sourceContents.contains("struct \(name): BuildToolPlugin")) + #expect(sourceContents.contains("createBuildCommands(context: PluginContext")) + } - // Verify basic file system content that we expect in the package - let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - - 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() async throws"#)) - XCTAssertMatch(testFileContents, .contains("func testExample() throws")) - -#if canImport(TestingDisabled) - // Try building it - await XCTAssertBuilds( - path, - buildSystem: .native, - ) - let triple = try UserToolchain.default.targetTriple - XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) -#endif + // Both plugin types should have Xcode extensions + #expect(sourceContents.contains("import XcodeProjectPlugin")) + if isCommandPlugin { + #expect(sourceContents.contains("extension \(name): XcodeCommandPlugin")) + #expect(sourceContents.contains("performCommand(context: XcodePluginContext")) + } else { + #expect(sourceContents.contains("extension \(name): XcodeBuildToolPlugin")) + #expect(sourceContents.contains("createBuildCommands(context: XcodePluginContext")) } } - func testInitPackageLibraryWithNoTests() async 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 + // MARK: - Package Type Tests + + /// Tests creating an empty package. + @Test func initPackageEmpty() throws { + Task { + try await createAndVerifyPackage( + packageType: .empty, + supportedTestingLibraries: [], + customVerification: { path, name in + let manifestContents: String = try localFileSystem.readFileContents(path.appending("Package.swift")) + #expect(manifestContents.contains(packageWithNameOnly(named: name))) + } ) - 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(#".testTarget"#)) + /// Tests creating an executable package with different build systems. + @Test(arguments: [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageExecutable(buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: .executable, + buildSystem: buildSystem, + customVerification: { path, name in + let directoryContents = try localFileSystem.getDirectoryContents(path.appending("Sources").appending(name)) + #expect(directoryContents == ["\(name).swift"]) + } + ) + } - XCTAssertNoSuchPath(path.appending("Tests")) + /// Tests creating an executable package named "main". + @Test(arguments: [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageExecutableCalledMain(buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: .executable, + name: "main", + buildSystem: buildSystem, + customVerification: { path, _ in + let directoryContents = try localFileSystem.getDirectoryContents(path.appending("Sources").appending("main")) + #expect(directoryContents == ["MainEntrypoint.swift"]) + } + ) + } -#if canImport(TestingDisabled) - // Try building it - await XCTAssertBuilds( - path, - buildSystem: .native, - ) - let triple = try UserToolchain.default.targetTriple - XCTAssertFileExists(path.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "Foo.swiftmodule")) -#endif - } + /// Tests creating packages with XCTest only. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageLibraryWithXCTestOnly(packageType: InitPackage.PackageType, buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.xctest], + buildSystem: buildSystem, + customVerification: { path, name in + #expect(try localFileSystem.getDirectoryContents(path.appending("Sources").appending(name)) == ["\(name).swift"], + "Expected single source file in Sources/\(name) directory") + + let tests = path.appending("Tests") + #expect(try localFileSystem.getDirectoryContents(tests).sorted() == ["\(name)Tests"], + "Expected single test directory") + + try verifyTestFileContents(at: path, name: name, hasSwiftTesting: false, hasXCTest: true) + } + ) } - func testInitPackageCommandPlugin() throws { - try testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("MyCommandPlugin") - let name = path.basename - try fs.createDirectory(path) + /// Tests creating packages with Swift Testing only. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackagesWithSwiftTestingOnly(packageType: InitPackage.PackageType, buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.swiftTesting], + buildSystem: buildSystem, + customVerification: { path, name in + try verifyTestFileContents(at: path, name: name, hasSwiftTesting: true, hasXCTest: false) + + #if canImport(TestingDisabled) + let expectedPath = path.appending(components: ".build", Self.targetTriple.platformBuildPathComponent, "debug", "Modules", "\(name).swiftmodule") + expectFileExists(at: expectedPath) + #endif + } + ) + } - // Create the package - try InitPackage( - name: name, - packageType: .commandPlugin, - destinationPath: path, - fileSystem: localFileSystem - ).writePackageStructure() + /// Tests creating packages with both Swift Testing and XCTest. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageWithBothSwiftTestingAndXCTest(packageType: InitPackage.PackageType, buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.swiftTesting, .xctest], + buildSystem: buildSystem, + customVerification: { path, name in + try verifyTestFileContents(at: path, name: name, hasSwiftTesting: true, hasXCTest: true) + + #if canImport(TestingDisabled) + let expectedPath = path.appending(components: ".build", Self.targetTriple.platformBuildPathComponent, "debug", "Modules", "\(name).swiftmodule") + expectFileExists(at: expectedPath) + #endif + } + ) + } - // 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, .and(.contains(".plugin("), .contains("targets: [\"MyCommandPlugin\"]"))) - XCTAssertMatch(manifestContents, .and(.contains(".plugin("), - .and(.contains("capability: .command(intent: .custom("), .contains("verb: \"MyCommandPlugin\"")))) - - // Check basic content that we expect in the plugin source file - let source = path.appending("Plugins", "MyCommandPlugin.swift") - XCTAssertFileExists(source) - let sourceContents: String = try localFileSystem.readFileContents(source) - XCTAssertMatch(sourceContents, .contains("struct MyCommandPlugin: CommandPlugin")) - XCTAssertMatch(sourceContents, .contains("performCommand(context: PluginContext")) - XCTAssertMatch(sourceContents, .contains("import XcodeProjectPlugin")) - XCTAssertMatch(sourceContents, .contains("extension MyCommandPlugin: XcodeCommandPlugin")) - XCTAssertMatch(sourceContents, .contains("performCommand(context: XcodePluginContext")) - } + /// Tests creating packages with no testing libraries. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageWithNoTests(packageType: InitPackage.PackageType, buildSystem: BuildSystemProvider.Kind) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [], + buildSystem: buildSystem, + customVerification: { path, name in + let manifestContents: String = try localFileSystem.readFileContents(path.appending("Package.swift")) + #expect(!manifestContents.contains(#".testTarget"#)) + + expectNoSuchPath(path.appending("Tests")) + + #if canImport(TestingDisabled) + let expectedPath = path.appending(components: ".build", Self.targetTriple.platformBuildPathComponent, "debug", "Modules", "\(name).swiftmodule") + expectFileExists(at: expectedPath) + #endif + } + ) } - - func testInitPackageBuildToolPlugin() throws { - try testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("MyBuildToolPlugin") - let name = path.basename - try fs.createDirectory(path) - // Create the package - try InitPackage( - name: name, - packageType: .buildToolPlugin, - destinationPath: path, - fileSystem: localFileSystem - ).writePackageStructure() + /// Tests creating a command plugin package. + @Test func initPackageCommandPlugin() async throws { + try await createAndVerifyPackage( + packageType: .commandPlugin, + name: "MyCommandPlugin", + supportedTestingLibraries: [], + customVerification: { path, name in + try verifyPluginPackage(at: path, name: name, isCommandPlugin: true) + } + ) + } - // 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, .and(.contains(".plugin("), .contains("targets: [\"MyBuildToolPlugin\"]"))) - XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("capability: .buildTool()"))) - - // Check basic content that we expect in the plugin source file - let source = path.appending("Plugins", "MyBuildToolPlugin.swift") - XCTAssertFileExists(source) - let sourceContents: String = try localFileSystem.readFileContents(source) - XCTAssertMatch(sourceContents, .contains("struct MyBuildToolPlugin: BuildToolPlugin")) - XCTAssertMatch(sourceContents, .contains("createBuildCommands(context: PluginContext")) - XCTAssertMatch(sourceContents, .contains("import XcodeProjectPlugin")) - XCTAssertMatch(sourceContents, .contains("extension MyBuildToolPlugin: XcodeBuildToolPlugin")) - XCTAssertMatch(sourceContents, .contains("createBuildCommands(context: XcodePluginContext")) - } + /// Tests creating a build tool plugin package. + @Test func initPackageBuildToolPlugin() async throws { + try await createAndVerifyPackage( + packageType: .buildToolPlugin, + name: "MyBuildToolPlugin", + supportedTestingLibraries: [], + customVerification: { path, name in + try verifyPluginPackage(at: path, name: name, isCommandPlugin: false) + } + ) } - // MARK: Special case testing + // MARK: - Special Case Tests - func testInitPackageNonc99Directory() async throws { + /// Tests creating a package in a directory with a non-C99 compliant name. + @Test(arguments: [BuildSystemProvider.Kind.native, .swiftbuild]) + func initPackageNonc99Directory(buildSystem: BuildSystemProvider.Kind) async throws { try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in - XCTAssertDirectoryExists(tempDirPath) - // Create a directory with non c99name. let packageRoot = tempDirPath.appending("some-package") let packageName = packageRoot.basename try localFileSystem.createDirectory(packageRoot) - XCTAssertDirectoryExists(packageRoot) - + expectDirectoryExists(packageRoot) + // Create the package let initPackage = try InitPackage( name: packageName, @@ -385,27 +407,34 @@ final class InitTests: XCTestCase { destinationPath: packageRoot, fileSystem: localFileSystem ) - initPackage.progressReporter = { message in } + initPackage.progressReporter = { _ in } try initPackage.writePackageStructure() // Try building it. - await XCTAssertBuilds( - packageRoot, - buildSystem: .native, - ) - let triple = try UserToolchain.default.targetTriple - XCTAssertFileExists(packageRoot.appending(components: ".build", triple.platformBuildPathComponent, "debug", "Modules", "some_package.swiftmodule")) + await expectBuilds(packageRoot, buildSystem: buildSystem) + + // Assert that the expected build products exist + let expectedPath = packageRoot + .appending(components: ".build", Self.targetTriple.platformBuildPathComponent) + .appending(components: buildSystem.binPathSuffixes(for: BuildConfiguration.debug)) + + // Verify the module name is properly mangled + if buildSystem == .native { + expectFileExists(at: expectedPath.appending("Modules", "some_package.swiftmodule")) + } else { + expectFileExists(at: expectedPath.appending("some_package.swiftmodule")) + } } } - - func testNonC99NameExecutablePackage() async throws { + + /// Tests creating a package with a non-C99 compliant name. + @Test(arguments: [BuildSystemProvider.Kind.native, .swiftbuild]) + func nonC99NameExecutablePackage(buildSystem: BuildSystemProvider.Kind) async throws { try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in - XCTAssertDirectoryExists(tempDirPath) - let packageRoot = tempDirPath.appending("Foo") try localFileSystem.createDirectory(packageRoot) - XCTAssertDirectoryExists(packageRoot) - + expectDirectoryExists(packageRoot) + // Create package with non c99name. let initPackage = try InitPackage( name: "package-name", @@ -414,16 +443,15 @@ final class InitTests: XCTestCase { fileSystem: localFileSystem ) try initPackage.writePackageStructure() - - await XCTAssertBuilds( - packageRoot, - buildSystem: .native, - ) + + await expectBuilds(packageRoot, buildSystem: buildSystem) } } - func testPlatforms() throws { + /// Tests creating a package with custom platform requirements. + @Test func platforms() throws { try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in + // Define custom platform requirements var options = InitPackage.InitPackageOptions(packageType: .library, supportedTestingLibraries: []) options.platforms = [ .init(platform: .macOS, version: PlatformVersion("10.15")), @@ -436,6 +464,7 @@ final class InitTests: XCTestCase { try localFileSystem.removeFileTree(packageRoot) try localFileSystem.createDirectory(packageRoot) + // Create the package with custom options let initPackage = try InitPackage( name: "Foo", options: options, @@ -445,11 +474,17 @@ final class InitTests: XCTestCase { ) try initPackage.writePackageStructure() + // Verify platform requirements are correctly included in the manifest let contents: String = try localFileSystem.readFileContents(packageRoot.appending("Package.swift")) - XCTAssertMatch(contents, .contains(#"platforms: [.macOS(.v10_15), .iOS(.v12), .watchOS("2.1"), .tvOS("999.0")],"#)) + #expect(contents.contains(#"platforms: [.macOS(.v10_15), .iOS(.v12), .watchOS("2.1"), .tvOS("999.0")],"#)) } } + // MARK: - Helper Methods for Package Content + + /// Creates a simple package manifest with just the name. + /// - Parameter name: The name of the package + /// - Returns: A string containing the package manifest private func packageWithNameOnly(named name: String) -> String { return """ let package = Package( @@ -458,6 +493,9 @@ final class InitTests: XCTestCase { """ } + /// Creates a package manifest with name and dependencies section. + /// - Parameter name: The name of the package + /// - Returns: A string containing the package manifest private func packageWithNameAndDependencies(with name: String) -> String { return """ let package = Package(