diff --git a/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift b/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift index 3258f8caf6c..b068ecf8444 100644 --- a/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift +++ b/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import Testing import struct SPMBuildCore.BuildSystemProvider import enum PackageModel.BuildConfiguration @@ -28,6 +29,12 @@ public struct BuildData { public let config: BuildConfiguration } +extension BuildData: CustomTestStringConvertible { + public var testDescription: String { + return "\(buildSystem):\(config)" + } +} + public func getBuildData(for buildSystems: [BuildSystemProvider.Kind]) -> [BuildData] { buildSystems.flatMap { buildSystem in BuildConfiguration.allCases.compactMap { config in diff --git a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift index d7e3185b9a4..2c7902968dd 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift @@ -11,106 +11,155 @@ import Basics import Testing +// MARK: File System 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, _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { - let commentPrefix = - if let comment { - "\(comment): " - } else { - "" - } - let msgSuffix: String - do { - msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path.parentDirectory))" - } catch { - msgSuffix = "" - } #expect( localFileSystem.exists(path), - "\(commentPrefix)File '\(path)' does not exist. \(msgSuffix)", + wrapMessage( + "Files '\(path)' does not exist.", + comment: comment, + directoryPath: path.parentDirectory + ), sourceLocation: sourceLocation, ) } -public func expectFileDoesNotExists( +/// Requires 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 requireFileExists( + at path: AbsolutePath, + _ comment: Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, +) throws { + try #require( + localFileSystem.exists(path), + wrapMessage( + "Files '\(path)' does not exist.", + comment: comment, + directoryPath: path.parentDirectory + ), + sourceLocation: sourceLocation, + ) +} + +/// Verifies that a file does not exist at the specified path. +/// +/// - Parameters: +/// - path: The absolute path to check for file non-existence. +/// - comment: An optional comment to include in the failure message. +/// - sourceLocation: The source location where the expectation is made. +public func expectFileDoesNotExist( at path: AbsolutePath, _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { - let commentPrefix = - if let comment { - "\(comment): " - } else { - "" - } - let msgSuffix: String - do { - msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path.parentDirectory))" - } catch { - msgSuffix = "" - } #expect( !localFileSystem.exists(path), - "\(commentPrefix)File: '\(path)' was not expected to exist, but does.\(msgSuffix))", + wrapMessage( + "File: '\(path)' was not expected to exist, but does.", + comment: comment, + directoryPath: path.parentDirectory + ), sourceLocation: sourceLocation, ) } +/// Verifies that a file exists and is executable at the specified path. +/// +/// - Parameters: +/// - fixturePath: The absolute path to check for executable file existence. +/// - comment: An optional comment to include in the failure message. +/// - sourceLocation: The source location where the expectation is made. public func expectFileIsExecutable( at fixturePath: AbsolutePath, _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { - let commentPrefix = - if let comment { - "\(comment): " - } else { - "" - } #expect( localFileSystem.isExecutableFile(fixturePath), - "\(commentPrefix)File '\(fixturePath)' expected to be executable, but is not.", + wrapMessage("File '\(fixturePath)' expected to be executable, but is not.", comment: comment), 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( at path: AbsolutePath, + _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { -let msgSuffix: String - do { - msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path))" - } catch { - msgSuffix = "" - } #expect( localFileSystem.isDirectory(path), - "Expected directory doesn't exist: '\(path)'. \(msgSuffix)", + wrapMessage("Expected directory doesn't exist: '\(path)'", comment: comment, directoryPath: path), sourceLocation: sourceLocation, ) } +/// Verifies that a directory does not exist at the specified path. +/// +/// - Parameters: +/// - path: The absolute path to check for directory non-existence. +/// - sourceLocation: The source location where the expectation is made. public func expectDirectoryDoesNotExist( at path: AbsolutePath, + _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { - let msgSuffix: String - do { - msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path))" - } catch { - msgSuffix = "" - } #expect( !localFileSystem.isDirectory(path), - "Directory exists unexpectedly: '\(path)'.\(msgSuffix)", + wrapMessage("Directory exists unexpectedly: '\(path)'", comment: comment, directoryPath: path), sourceLocation: sourceLocation, ) } +/// Wraps a message with an optional comment prefix and directory contents suffix. +/// +/// - Parameters: +/// - message: The base message to wrap. +/// - comment: An optional comment to prefix the message with. +/// - directoryPath: An optional path to a folder whose contents will be appended to the message. +/// - Returns: The formatted message with prefix and suffix. +private func wrapMessage( + _ message: Comment, + comment: Comment? = nil, + directoryPath: AbsolutePath? = nil +) -> Comment { + let commentPrefix = + if let comment { + "\(comment): " + } else { + "" + } + + var msgSuffix = "" + if let directoryPath { + do { + msgSuffix = try " Directory contents: \(localFileSystem.getDirectoryContents(directoryPath))" + } catch { + // Silently ignore errors when getting directory contents + } + } + + return "\(commentPrefix)\(message)\(msgSuffix)" +} + /// Expects that the expression throws a CommandExecutionError and passes it to the provided throwing error handler. /// - Parameters: /// - expression: The expression expected to throw diff --git a/Sources/_InternalTestSupport/misc.swift b/Sources/_InternalTestSupport/misc.swift index 944a9b1c079..0b114951cc6 100644 --- a/Sources/_InternalTestSupport/misc.swift +++ b/Sources/_InternalTestSupport/misc.swift @@ -97,7 +97,15 @@ public func testWithTemporaryDirectory( .replacing(")", with: "") .replacing(".", with: "") .replacing(":", with: "_") - return try await withTemporaryDirectory(prefix: "spm-tests-\(cleanedFunction)") { tmpDirPath in + + // Use shorter prefix on Windows to avoid MAX_PATH issues + #if os(Windows) + let prefix = "spm-" + #else + let prefix = "spm-tests-\(cleanedFunction)" + #endif + + return try await withTemporaryDirectory(prefix: prefix) { tmpDirPath in defer { // Unblock and remove the tmp dir on deinit. try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index e7b1936c4b5..84879c3bc0a 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -2866,7 +2866,7 @@ struct PackageCommandTests { configuration: data.config, buildSystem: data.buildSystem, ) - expectFileDoesNotExists(at: binFile) + expectFileDoesNotExist(at: binFile) // Clean again to ensure we get no error. _ = try await execute( ["clean"], @@ -2911,7 +2911,7 @@ struct PackageCommandTests { configuration: data.config, buildSystem: data.buildSystem, ) - expectFileDoesNotExists(at: binFile) + expectFileDoesNotExist(at: binFile) try #expect( !localFileSystem.getDirectoryContents(buildPath.appending("repositories")).isEmpty ) @@ -3014,7 +3014,7 @@ struct PackageCommandTests { #expect(!result.stderr.contains("Could not find Package.swift")) // Verify manifest.db was removed (the purge implementation removes this file) - expectFileDoesNotExists(at: manifestDB, "manifest.db should be removed after purge") + expectFileDoesNotExist(at: manifestDB, "manifest.db should be removed after purge") // Note: SQLite auxiliary files (WAL/SHM) may or may not be removed depending on SQLite state // The important check is that the main database file is removed @@ -5341,7 +5341,7 @@ struct PackageCommandTests { buildSystem: buildData.buildSystem, ) expectFileIsExecutable(at: fixturePath.appending(components: debugTarget), "build-target") - expectFileDoesNotExists( + expectFileDoesNotExist( at: fixturePath.appending(components: releaseTarget), "build-target build-inherit" ) @@ -5375,7 +5375,7 @@ struct PackageCommandTests { at: fixturePath.appending(components: debugTarget), "build-target build-debug" ) - expectFileDoesNotExists( + expectFileDoesNotExist( at: fixturePath.appending(components: releaseTarget), "build-target build-inherit" ) @@ -5406,7 +5406,7 @@ struct PackageCommandTests { configuration: buildData.config, buildSystem: buildData.buildSystem, ) - expectFileDoesNotExists( + expectFileDoesNotExist( at: fixturePath.appending(components: debugTarget), "build-target build-inherit" ) @@ -5450,7 +5450,7 @@ struct PackageCommandTests { fileShouldNotExist = fixturePath.appending(components: debugTarget) fileShouldExist = fixturePath.appending(components: releaseTarget) } - expectFileDoesNotExists(at: fileShouldNotExist, "build-target build-inherit") + expectFileDoesNotExist(at: fileShouldNotExist, "build-target build-inherit") expectFileIsExecutable(at: fileShouldExist, "build-target build-inherit") } } when: { diff --git a/Tests/WorkspaceTests/InitTests.swift b/Tests/WorkspaceTests/InitTests.swift index c5a6cc61319..b3d890a913e 100644 --- a/Tests/WorkspaceTests/InitTests.swift +++ b/Tests/WorkspaceTests/InitTests.swift @@ -14,131 +14,75 @@ 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))) +import Testing +import SPMBuildCore + +/// 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. + /// We instantiate this once lazily because it is not thread safe, and multiple tests + /// running in parallel can cause a crash. + static let targetTriple: Triple = { + do { + return try UserToolchain.default.targetTriple + } catch { + fatalError("Failed to determine target triple: \(error)") } - } - - 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) + }() + + // MARK: - Helper Methods + + /// Asserts that the package under test builds successfully. + 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, + configuration: conf, + extraArgs: extraArgs, + Xcc: Xcc, + Xld: Xld, + Xswiftc: Xswiftc, + env: env, + buildSystem: buildSystem + ) } - 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( - path, - buildSystem: .native, - ) - 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, - ) } } - func testInitPackageLibraryWithXCTestOnly() async throws { - try await testWithTemporaryDirectory { tmpPath in + /// Creates a test package with the specified configuration and verifies its structure. + private func createAndVerifyPackage( + packageType: InitPackage.PackageType, + name: String = "Foo", + supportedTestingLibraries: Set = [.xctest], + buildSystem: BuildSystemProvider.Kind? = nil, + buildConfiguration: BuildConfiguration = .debug, + customVerification: ((AbsolutePath, String) throws -> Void)? = nil, + function: StaticString = #function + ) async throws { + return try await testWithTemporaryDirectory(function: function) { 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 ) var progressMessages = [String]() initPackage.progressReporter = { message in @@ -146,298 +90,295 @@ final class InitTests: XCTestCase { } try initPackage.writePackageStructure() - // Not picky about the specific progress messages, just checking that we got some. - XCTAssertGreaterThan(progressMessages.count, 0) + #expect(progressMessages.count > 0, "Expected progress messages during package creation") - // 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) + 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"]) + if !supportedTestingLibraries.isEmpty { + #expect(manifestContents.contains(".testTarget(")) + } - let tests = path.appending("Tests") - XCTAssertEqual(try fs.getDirectoryContents(tests).sorted(), ["FooTests"]) + if let buildSystem = buildSystem { + await expectBuilds(path, buildSystem: buildSystem, configurations: Set([buildConfiguration])) - 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 verifyBuildProducts(for: packageType, at: path, name: packageName, buildSystem: buildSystem, buildConfiguration: buildConfiguration) + } - // 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")) + try customVerification?(path, packageName) } } - func testInitPackageLibraryWithSwiftTestingOnly() 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: [.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 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 + /// Verifies that the expected build products exist for a package. + private func verifyBuildProducts( + for packageType: InitPackage.PackageType, + at path: AbsolutePath, + name: String, + buildSystem: BuildSystemProvider.Kind, + buildConfiguration: BuildConfiguration + ) throws { + let expectedPath = path.appending(components: try buildSystem.binPath(for: buildConfiguration, triple: Self.targetTriple.platformBuildPathComponent)) + + 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: + Issue.record("Only library, executable, and tool packages have specific build products to verify.") + break } } - func testInitPackageLibraryWithBothSwiftTestingAndXCTest() 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: [.swiftTesting, .xctest], - destinationPath: path, - fileSystem: localFileSystem - ) - try initPackage.writePackageStructure() + /// Verifies the test file contents for a package. + 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"#)) + } - // 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 + if hasXCTest { + #expect(testFileContents.contains(#"import XCTest"#)) + #expect(testFileContents.contains("func test")) } } - 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 + /// Verifies plugin package contents. + private func verifyPluginPackage( + at path: AbsolutePath, + name: String, + isCommandPlugin: Bool + ) throws { + let manifest = path.appending("Package.swift") + try requireFileExists(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)\"") ) - 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"#)) + } else { + #expect(manifestContents.contains(".plugin(") && manifestContents.contains("capability: .buildTool()")) + } - XCTAssertNoSuchPath(path.appending("Tests")) + // Verify source file + let source = path.appending("Plugins", "\(name).swift") + try requireFileExists(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")) + } -#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 testInitPackageExecutableWithSwiftTesting() 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: .executable, - 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(".testTarget(")) - 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")) + // 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))) + }) } } - func testInitPackageToolWithSwiftTesting() 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: .tool, - supportedTestingLibraries: [.swiftTesting], - destinationPath: path, - fileSystem: localFileSystem - ) + /// Tests creating an executable package with different build systems. + @Test(arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageExecutable(data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: .executable, + buildSystem: data.buildSystem, + buildConfiguration: data.config, + customVerification: { path, name in + let directoryContents = try localFileSystem.getDirectoryContents(path.appending("Sources").appending(name)) + #expect(directoryContents == ["\(name).swift"]) + } + ) + } - 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(".testTarget(")) - 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")) - } + /// Tests creating an executable package named "main". + @Test(arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageExecutableCalledMain(data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: .executable, + name: "main", + buildSystem: data.buildSystem, + buildConfiguration: data.config, + customVerification: { path, _ in + let directoryContents = try localFileSystem.getDirectoryContents(path.appending("Sources").appending("main")) + #expect(directoryContents == ["MainEntrypoint.swift"]) + } + ) } - 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 XCTest only. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageLibraryWithXCTestOnly(packageType: InitPackage.PackageType, data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.xctest], + buildSystem: data.buildSystem, + buildConfiguration: data.config, + 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) + } + ) + } - // Create the package - try InitPackage( - name: name, - packageType: .commandPlugin, - destinationPath: path, - fileSystem: localFileSystem - ).writePackageStructure() + /// Tests creating packages with Swift Testing only. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackagesWithSwiftTestingOnly(packageType: InitPackage.PackageType, data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.swiftTesting], + buildSystem: data.buildSystem, + buildConfiguration: data.config, + customVerification: { path, name in + try verifyTestFileContents(at: path, name: name, hasSwiftTesting: true, hasXCTest: false) + let binPath = try data.buildSystem.binPath(for: data.config, triple: Self.targetTriple.platformBuildPathComponent) + let swiftModule = "\(name).swiftmodule" + let expectedPath = path + .appending(components: binPath) + .appending(components: data.buildSystem == .native ? ["Modules", swiftModule] : [swiftModule]) + + expectFileExists(at: expectedPath) + } + ) + } - // 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 both Swift Testing and XCTest. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageWithBothSwiftTestingAndXCTest(packageType: InitPackage.PackageType, data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [.swiftTesting, .xctest], + buildSystem: data.buildSystem, + buildConfiguration: data.config, + customVerification: { path, name in + try verifyTestFileContents(at: path, name: name, hasSwiftTesting: true, hasXCTest: true) + let binPath = try data.buildSystem.binPath(for: data.config, triple: Self.targetTriple.platformBuildPathComponent) + let swiftModule = "\(name).swiftmodule" + let expectedPath = path + .appending(components: binPath) + .appending(components: data.buildSystem == .native ? ["Modules", swiftModule] : [swiftModule]) + + expectFileExists(at: expectedPath) + } + ) } - - 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 packages with no testing libraries. + @Test(arguments: [InitPackage.PackageType.library, .executable, .tool], getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageWithNoTests(packageType: InitPackage.PackageType, data: BuildData) async throws { + try await createAndVerifyPackage( + packageType: packageType, + supportedTestingLibraries: [], + buildSystem: data.buildSystem, + buildConfiguration: data.config, + customVerification: { path, name in + let manifestContents: String = try localFileSystem.readFileContents(path.appending("Package.swift")) + #expect(!manifestContents.contains(#".testTarget"#)) + + expectDirectoryDoesNotExist(at: path.appending("Tests")) + + let binPath = try data.buildSystem.binPath(for: data.config, triple: Self.targetTriple.platformBuildPathComponent) + let swiftModule = "\(name).swiftmodule" + let expectedPath = path + .appending(components: binPath) + .appending(components: data.buildSystem == .native ? ["Modules", swiftModule] : [swiftModule]) + + expectFileExists(at: expectedPath) + } + ) + } - // 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 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) + } + ) } - // MARK: Special case testing + /// 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) + } + ) + } - func testInitPackageNonc99Directory() async throws { + // MARK: - Special Case Tests + + /// Tests creating a package in a directory with a non-C99 compliant name. + @Test(arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms)) + func initPackageNonc99Directory(data: BuildData) 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(at: packageRoot) + // Create the package let initPackage = try InitPackage( name: packageName, @@ -445,45 +386,50 @@ 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: data.buildSystem, configurations: [data.config]) + + // Assert that the expected build products exist + let binPath = try data.buildSystem.binPath(for: data.config, triple: Self.targetTriple.platformBuildPathComponent) + let expectedPath = packageRoot.appending(components: binPath) + + // Verify the module name is properly mangled + switch data.buildSystem { + case .native: expectFileExists(at: expectedPath.appending("Modules", "some_package.swiftmodule")) + case .swiftbuild: expectFileExists(at: expectedPath.appending("some_package.swiftmodule")) + case .xcode: Issue.record("Not implemented") + } } } - - 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(at: packageRoot) + // Create package with non c99name. let initPackage = try InitPackage( - name: "package-name", + name: "Foo", packageType: .executable, destinationPath: packageRoot, 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")), @@ -496,6 +442,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, @@ -505,11 +452,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( @@ -518,6 +471,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(