diff --git a/.gitignore b/.gitignore index 64c51c37757..6a54c5657d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .build +.test .index-build DerivedData /.previous-build diff --git a/Package.swift b/Package.swift index 622ec204e7d..8cba0226e38 100644 --- a/Package.swift +++ b/Package.swift @@ -825,6 +825,7 @@ let package = Package( name: "_InternalTestSupport", dependencies: [ "Basics", + "DriverSupport", "PackageFingerprint", "PackageGraph", "PackageLoading", diff --git a/Sources/PackageModel/Toolchain+SupportedFeatures.swift b/Sources/PackageModel/Toolchain+SupportedFeatures.swift index 78121486425..924fa395737 100644 --- a/Sources/PackageModel/Toolchain+SupportedFeatures.swift +++ b/Sources/PackageModel/Toolchain+SupportedFeatures.swift @@ -70,7 +70,7 @@ public enum SwiftCompilerFeature { } extension Toolchain { - public var supportesSupportedFeatures: Bool { + public var supportsSupportedFeatures: Bool { guard let features = try? swiftCompilerSupportedFeatures else { return false } diff --git a/Sources/_InternalTestSupport/BuildConfiguration+Helpers.swift b/Sources/_InternalTestSupport/BuildConfiguration+Helpers.swift new file mode 100644 index 00000000000..54329f44f9e --- /dev/null +++ b/Sources/_InternalTestSupport/BuildConfiguration+Helpers.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import enum PackageModel.BuildConfiguration + +extension BuildConfiguration { + + public var buildFor: String { + switch self { + case .debug: + return "debugging" + case .release: + return "production" + } + } +} diff --git a/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift b/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift index 16e56ffaf2d..3258f8caf6c 100644 --- a/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift +++ b/Sources/_InternalTestSupport/BuildSystemProvider+Supported.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// import struct SPMBuildCore.BuildSystemProvider - +import enum PackageModel.BuildConfiguration public var SupportedBuildSystemOnAllPlatforms: [BuildSystemProvider.Kind] = BuildSystemProvider.Kind.allCases.filter { $0 != .xcode } @@ -22,3 +22,16 @@ public var SupportedBuildSystemOnPlatform: [BuildSystemProvider.Kind] { SupportedBuildSystemOnAllPlatforms #endif } + +public struct BuildData { + public let buildSystem: BuildSystemProvider.Kind + public let config: BuildConfiguration +} + +public func getBuildData(for buildSystems: [BuildSystemProvider.Kind]) -> [BuildData] { + buildSystems.flatMap { buildSystem in + BuildConfiguration.allCases.compactMap { config in + return BuildData(buildSystem: buildSystem, config: config) + } + } +} diff --git a/Sources/_InternalTestSupport/SwiftPMProduct.swift b/Sources/_InternalTestSupport/SwiftPMProduct.swift index c2f0e957828..0608fe4ca47 100644 --- a/Sources/_InternalTestSupport/SwiftPMProduct.swift +++ b/Sources/_InternalTestSupport/SwiftPMProduct.swift @@ -127,13 +127,12 @@ extension SwiftPM { // Unset the internal env variable that allows skipping certain tests. environment["_SWIFTPM_SKIP_TESTS_LIST"] = nil - environment["SWIFTPM_EXEC_NAME"] = self.executableName for (key, value) in env ?? [:] { environment[key] = value } - var completeArgs = [xctestBinaryPath.pathString] + var completeArgs = [Self.xctestBinaryPath(for: RelativePath(self.executableName)).pathString] if let packagePath = packagePath { completeArgs += ["--package-path", packagePath.pathString] } diff --git a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift index feb3dabec5a..69e8b06fee0 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift @@ -23,6 +23,63 @@ public func expectFileExists( ) } +public func expectFileDoesNotExists( + at fixturePath: AbsolutePath, + _ comment: Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, +) { + let commentPrefix = + if let comment { + "\(comment): " + } else { + "" + } + #expect( + !localFileSystem.exists(fixturePath), + "\(commentPrefix)\(fixturePath) does not exist", + sourceLocation: sourceLocation, + ) +} + +public func expectFileIsExecutable( + at fixturePath: AbsolutePath, + _ comment: Comment? = nil, + sourceLocation: SourceLocation = #_sourceLocation, +) { + let commentPrefix = + if let comment { + "\(comment): " + } else { + "" + } + #expect( + localFileSystem.isExecutableFile(fixturePath), + "\(commentPrefix)\(fixturePath) does not exist", + sourceLocation: sourceLocation, + ) +} + +public func expectDirectoryExists( + at path: AbsolutePath, + sourceLocation: SourceLocation = #_sourceLocation, +) { + #expect( + localFileSystem.isDirectory(path), + "Expected directory doesn't exist: \(path)", + sourceLocation: sourceLocation, + ) +} + +public func expectDirectoryDoesNotExist( + at path: AbsolutePath, + sourceLocation: SourceLocation = #_sourceLocation, +) { + #expect( + !localFileSystem.isDirectory(path), + "Directory exists unexpectedly: \(path)", + sourceLocation: sourceLocation, + ) +} public func expectThrowsCommandExecutionError( _ expression: @autoclosure () async throws -> T, @@ -32,8 +89,9 @@ public func expectThrowsCommandExecutionError( ) async { await expectAsyncThrowsError(try await expression(), message(), sourceLocation: sourceLocation) { error in guard case SwiftPMError.executionFailure(let processError, let stdout, let stderr) = error, - case AsyncProcessResult.Error.nonZeroExit(let processResult) = processError, - processResult.exitStatus != .terminated(code: 0) else { + case AsyncProcessResult.Error.nonZeroExit(let processResult) = processError, + processResult.exitStatus != .terminated(code: 0) + else { Issue.record("Unexpected error type: \(error.interpolationDescription)", sourceLocation: sourceLocation) return } @@ -50,7 +108,10 @@ public func expectAsyncThrowsError( ) async { do { _ = try await expression() - Issue.record(message() ?? "Expected an error, which did not not.", sourceLocation: sourceLocation) + Issue.record( + message() ?? "Expected an error, which did not occur.", + sourceLocation: sourceLocation, + ) } catch { errorHandler(error) } diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index 104cc7041fc..d49995cd172 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -25,9 +25,12 @@ extension Tag.TestSize { extension Tag.Feature { public enum Command {} public enum PackageType {} + public enum ProductType {} + public enum TargetType {} @Tag public static var CodeCoverage: Tag @Tag public static var Mirror: Tag + @Tag public static var NetRc: Tag @Tag public static var Resource: Tag @Tag public static var SpecialCharacters: Tag @Tag public static var Traits: Tag @@ -38,19 +41,37 @@ extension Tag.Feature.Command { public enum Package {} public enum PackageRegistry {} @Tag public static var Build: Tag - @Tag public static var Test: Tag @Tag public static var Run: Tag + @Tag public static var Sdk: Tag + @Tag public static var Test: Tag } extension Tag.Feature.Command.Package { + @Tag public static var General: Tag + @Tag public static var AddDependency: Tag + @Tag public static var AddProduct: Tag + @Tag public static var ArchiveSource: Tag + @Tag public static var AddSetting: Tag + @Tag public static var AddTarget: Tag + @Tag public static var AddTargetDependency: Tag + @Tag public static var BuildPlugin: Tag + @Tag public static var Clean: Tag + @Tag public static var CommandPlugin: Tag + @Tag public static var CompletionTool: Tag @Tag public static var Config: Tag - @Tag public static var Init: Tag + @Tag public static var Describe: Tag @Tag public static var DumpPackage: Tag @Tag public static var DumpSymbolGraph: Tag + @Tag public static var Edit: Tag + @Tag public static var Init: Tag + @Tag public static var Migrate: Tag @Tag public static var Plugin: Tag @Tag public static var Reset: Tag + @Tag public static var Resolve: Tag @Tag public static var ShowDependencies: Tag + @Tag public static var ShowExecutables: Tag @Tag public static var ToolsVersion: Tag + @Tag public static var Unedit: Tag @Tag public static var Update: Tag } @@ -63,6 +84,19 @@ extension Tag.Feature.Command.PackageRegistry { @Tag public static var Unset: Tag } +extension Tag.Feature.TargetType { + @Tag public static var Executable: Tag + @Tag public static var Library: Tag + @Tag public static var Macro: Tag +} + +extension Tag.Feature.ProductType { + @Tag public static var DynamicLibrary: Tag + @Tag public static var Executable: Tag + @Tag public static var Library: Tag + @Tag public static var Plugin: Tag + @Tag public static var StaticLibrary: Tag +} extension Tag.Feature.PackageType { @Tag public static var Library: Tag @Tag public static var Executable: Tag diff --git a/Sources/_InternalTestSupport/SwiftTesting+TraitConditional.swift b/Sources/_InternalTestSupport/SwiftTesting+TraitConditional.swift index 9e6fc1150a3..9f7802e69fa 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+TraitConditional.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+TraitConditional.swift @@ -1,4 +1,3 @@ - /* This source file is part of the Swift.org open source project @@ -15,7 +14,7 @@ import class PackageModel.UserToolchain import DriverSupport import Basics import Testing -import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported +import TSCclibc // for SPM_posix_spawn_file_actions_addchdir_np_supported extension Trait where Self == Testing.ConditionTrait { /// Skip test if the host operating system does not match the running OS. @@ -39,6 +38,72 @@ extension Trait where Self == Testing.ConditionTrait { } } + /// Enabled only if 'llvm-profdata' is available + public static var requiresLLVMProfData: Self { + disabled("skipping test because the `llvm-profdata` tool isn't available") { + let toolPath = try (try? UserToolchain.default)!.getLLVMProf() + return toolPath == nil + } + } + + /// Enabled only if 'llvm-cov' is available + public static var requiresLLVMCov: Self { + disabled("skipping test because the `llvm-cov` tool isn't available") { + let toolPath = try (try? UserToolchain.default)!.getLLVMCov() + return toolPath == nil + } + } + + /// Enabled only if 'swift-symbolgraph-extract' is available + public static var requiresSymbolgraphExtract: Self { + disabled("skipping test because the `swift-symbolgraph-extract` tools isn't available") { + let toolPath = try (try? UserToolchain.default)!.getSymbolGraphExtract() + return toolPath == nil + } + } + + /// Enabled only is stdlib is supported by the toolchain + public static var requiresStdlibSupport: Self { + enabled("skipping because static stdlib is not supported by the toolchain") { + let args = try [ + UserToolchain.default.swiftCompilerPath.pathString, + "-static-stdlib", "-emit-executable", "-o", "/dev/null", "-", + ] + let process = AsyncProcess(arguments: args) + let stdin = try process.launch() + stdin.write(sequence: "".utf8) + try stdin.close() + let result = try await process.waitUntilExit() + + return result.exitStatus == .terminated(code: 0) + } + } + + // Enabled if the toolchain has supported features + public static var supportsSupportedFeatures: Self { + enabled("skipping because test environment compiler doesn't support `-print-supported-features`") { + (try? UserToolchain.default)!.supportsSupportedFeatures + } + } + + /// Skip of the executable is not available + public static func requires(executable: String) -> Self { + let message: Comment? + let isToolAvailable: Bool + do { + try _requiresTools(executable) + isToolAvailable = true + message = nil + } catch (let AsyncProcessResult.Error.nonZeroExit(result)) { + isToolAvailable = false + message = "Skipping as tool \(executable) is not found in the path. (\(result.description))" + } catch { + isToolAvailable = false + message = "Skipping. Unable to determine if tool exists. Error: \(error) " + } + return enabled(if: isToolAvailable, message) + } + /// Enaled only if marcros are built as dylibs public static var requiresBuildingMacrosAsDylibs: Self { enabled("test is only supported if `BUILD_MACROS_AS_DYLIBS` is set") { @@ -71,16 +136,16 @@ extension Trait where Self == Testing.ConditionTrait { /// Ensure platform support working directory public static var requiresWorkingDirectorySupport: Self { enabled("working directory not supported on this platform") { - #if !os(Windows) - // needed for archiving - if SPM_posix_spawn_file_actions_addchdir_np_supported() { + #if !os(Windows) + // needed for archiving + if SPM_posix_spawn_file_actions_addchdir_np_supported() { + return true + } else { + return false + } + #else return true - } else { - return false - } - #else - return true - #endif + #endif } } @@ -118,9 +183,9 @@ extension Trait where Self == Testing.ConditionTrait { public static func skipIfXcodeBuilt() -> Self { disabled("Tests built by Xcode") { #if Xcode - true + true #else - false + false #endif } } @@ -129,9 +194,9 @@ extension Trait where Self == Testing.ConditionTrait { public static var requireSwift6_2: Self { enabled("This test requires Swift 6.2, or newer.") { #if compiler(>=6.2) - true + true #else - false + false #endif } } diff --git a/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift b/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift index c5104b6f4c8..c361312f141 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift @@ -29,18 +29,43 @@ extension Trait where Self == Testing.Bug { ) -> Self { bug(nil, id: 0, "\(relationship): \(issue)") } + + public static var IssueWindowsRelativePathAssert: Self { + // TSCBasic/Path.swift:969: Assertion failed + issue( + "https://github.com/swiftlang/swift-package-manager/issues/8602", + relationship: .defect, + ) + } + + public static var IssueWindowsPathLastConponent: Self { + // $0.path.lastComponent in test code returns fullpaths on Windows + issue( + "https://github.com/swiftlang/swift-package-manager/issues/8554", + relationship: .defect, + ) + } } extension Trait where Self == Testing.Bug { public static var IssueWindowsLongPath: Self { - .issue("https://github.com/swiftlang/swift-tools-support-core/pull/521", relationship: .fixedBy) + .issue( + "https://github.com/swiftlang/swift-tools-support-core/pull/521", + relationship: .fixedBy, + ) } public static var IssueProductTypeForObjectLibraries: Self { - .issue("https://github.com/swiftlang/swift-build/issues/609", relationship: .defect) + .issue( + "https://github.com/swiftlang/swift-build/issues/609", + relationship: .defect, + ) } public static var IssueSwiftBuildLinuxRunnable: Self { - .issue("https://github.com/swiftlang/swift-package-manager/issues/8416", relationship: .defect) + .issue( + "https://github.com/swiftlang/swift-package-manager/issues/8416", + relationship: .defect, + ) } } diff --git a/Sources/_InternalTestSupport/XCTAssertHelpers.swift b/Sources/_InternalTestSupport/XCTAssertHelpers.swift index 58a47f7fc43..e9ffd6b812b 100644 --- a/Sources/_InternalTestSupport/XCTAssertHelpers.swift +++ b/Sources/_InternalTestSupport/XCTAssertHelpers.swift @@ -330,13 +330,3 @@ public struct CommandExecutionError: Error { self.stderr = stderr } } - - -public func XCTExhibitsGitHubIssue(_ number: Int) throws { - let envVar = "SWIFTCI_EXHIBITS_GH_\(number)" - - try XCTSkipIf( - ProcessInfo.processInfo.environment[envVar] != nil, - "https://github.com/swiftlang/swift-package-manager/issues/\(number): \(envVar) environment variable is set" - ) -} diff --git a/Sources/_InternalTestSupport/misc.swift b/Sources/_InternalTestSupport/misc.swift index 4a069fdd5c1..48a41dcdc0b 100644 --- a/Sources/_InternalTestSupport/misc.swift +++ b/Sources/_InternalTestSupport/misc.swift @@ -155,6 +155,7 @@ public func testWithTemporaryDirectory( @discardableResult public func fixture( name: String, createGitRepo: Bool = true, + removeFixturePathOnDeinit: Bool = true, sourceLocation: SourceLocation = #_sourceLocation, body: (AbsolutePath) throws -> T ) throws -> T { @@ -164,12 +165,17 @@ public func testWithTemporaryDirectory( let copyName = fixtureSubpath.components.joined(separator: "_") // Create a temporary directory for the duration of the block. - return try withTemporaryDirectory(prefix: copyName) { tmpDirPath in + return try withTemporaryDirectory( + prefix: copyName, + removeTreeOnDeinit: removeFixturePathOnDeinit, + ) { tmpDirPath in defer { - // Unblock and remove the tmp dir on deinit. - try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) - try? localFileSystem.removeFileTree(tmpDirPath) + if removeFixturePathOnDeinit { + // Unblock and remove the tmp dir on deinit. + try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) + try? localFileSystem.removeFileTree(tmpDirPath) + } } let fixtureDir = try verifyFixtureExists(at: fixtureSubpath, sourceLocation: sourceLocation) @@ -235,6 +241,7 @@ public enum TestError: Error { @discardableResult public func fixture( name: String, createGitRepo: Bool = true, + removeFixturePathOnDeinit: Bool = true, sourceLocation: SourceLocation = #_sourceLocation, body: (AbsolutePath) async throws -> T ) async throws -> T { @@ -244,12 +251,17 @@ public enum TestError: Error { let copyName = fixtureSubpath.components.joined(separator: "_") // Create a temporary directory for the duration of the block. - return try await withTemporaryDirectory(prefix: copyName) { tmpDirPath in + return try await withTemporaryDirectory( + prefix: copyName, + removeTreeOnDeinit: removeFixturePathOnDeinit + ) { tmpDirPath in defer { - // Unblock and remove the tmp dir on deinit. - try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) - try? localFileSystem.removeFileTree(tmpDirPath) + if removeFixturePathOnDeinit { + // Unblock and remove the tmp dir on deinit. + try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) + try? localFileSystem.removeFileTree(tmpDirPath) + } } let fixtureDir = try verifyFixtureExists(at: fixtureSubpath, sourceLocation: sourceLocation) diff --git a/Tests/CommandsTests/CommandsTestCase.swift b/Tests/CommandsTests/CommandsTestCase.swift deleted file mode 100644 index 93549b997ed..00000000000 --- a/Tests/CommandsTests/CommandsTestCase.swift +++ /dev/null @@ -1,54 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import XCTest -import _InternalTestSupport - -class CommandsTestCase: XCTestCase { - - /// Original working directory before the test ran (if known). - private var originalWorkingDirectory: AbsolutePath? = .none - public let duplicateSymbolRegex = StringPattern.regex( - #"objc[83768]: (.*) is implemented in both .* \(.*\) and .* \(.*\) . One of the two will be used. Which one is undefined."# - ) - - override func setUp() { - originalWorkingDirectory = localFileSystem.currentWorkingDirectory - } - - override func tearDown() { - if let originalWorkingDirectory { - try? localFileSystem.changeCurrentWorkingDirectory(to: originalWorkingDirectory) - } - } - - // FIXME: We should also hoist the `execute()` helper function that the various test suites implement, but right now they all seem to have slightly different implementations, so that's a later project. -} - -class CommandsBuildProviderTestCase: BuildSystemProviderTestCase { - /// Original working directory before the test ran (if known). - private var originalWorkingDirectory: AbsolutePath? = .none - let duplicateSymbolRegex = StringPattern.regex(".*One of the duplicates must be removed or renamed.") - - override func setUp() { - originalWorkingDirectory = localFileSystem.currentWorkingDirectory - } - - override func tearDown() { - if let originalWorkingDirectory { - try? localFileSystem.changeCurrentWorkingDirectory(to: originalWorkingDirectory) - } - } - - // FIXME: We should also hoist the `execute()` helper function that the various test suites implement, but right now they all seem to have slightly different implementations, so that's a later project. -} diff --git a/Tests/CommandsTests/MultiRootSupportTests.swift b/Tests/CommandsTests/MultiRootSupportTests.swift index ae317528862..eb67b4374b3 100644 --- a/Tests/CommandsTests/MultiRootSupportTests.swift +++ b/Tests/CommandsTests/MultiRootSupportTests.swift @@ -2,28 +2,37 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +import Foundation import Basics import Commands import _InternalTestSupport import Workspace -import XCTest +import Testing -final class MultiRootSupportTests: CommandsTestCase { - func testWorkspaceLoader() throws { +@Suite( + .tags( + .TestSize.large, + ), +) +struct MultiRootSupportTests { + @Test + func workspaceLoader() throws { let fs = InMemoryFileSystem(emptyFiles: [ "/tmp/test/dep/Package.swift", "/tmp/test/local/Package.swift", ]) let path = AbsolutePath("/tmp/test/Workspace.xcworkspace") - try fs.writeFileContents(path.appending("contents.xcworkspacedata"), string: + try fs.writeFileContents( + path.appending("contents.xcworkspacedata"), + string: """ (stdout: String, stderr: String) { + var environment = env ?? [:] + if let manifest, let packagePath { + try localFileSystem.writeFileContents(packagePath.appending("Package.swift"), string: manifest) } - @discardableResult - private func execute( - _ args: [String] = [], - packagePath: AbsolutePath? = nil, - manifest: String? = nil, - env: Environment? = nil - ) async throws -> (stdout: String, stderr: String) { - var environment = env ?? [:] - if let manifest, let packagePath { - try localFileSystem.writeFileContents(packagePath.appending("Package.swift"), string: manifest) - } + // don't ignore local packages when caching + environment["SWIFTPM_TESTS_PACKAGECACHE"] = "1" + return try await executeSwiftPackage( + packagePath, + configuration: configuration, + extraArgs: args, + env: environment, + buildSystem: buildSystem, + ) +} - // don't ignore local packages when caching - environment["SWIFTPM_TESTS_PACKAGECACHE"] = "1" - return try await executeSwiftPackage( - packagePath, - extraArgs: args, - env: environment, - buildSystem: buildSystemProvider - ) +// Helper function to arbitrarily assert on manifest content +private func expectManifest(_ packagePath: AbsolutePath, _ callback: (String) throws -> Void) throws { + let manifestPath = packagePath.appending("Package.swift") + expectFileExists(at: manifestPath) + let contents: String = try localFileSystem.readFileContents(manifestPath) + try callback(contents) +} + +// Helper function to assert content exists in the manifest +private func expectManifestContains(_ packagePath: AbsolutePath, _ expected: String) throws { + try expectManifest(packagePath) { manifestContents in + #expect(manifestContents.contains(expected)) } +} + +// Helper function to test adding a URL dependency and asserting the result +private func executeAddURLDependencyAndAssert( + packagePath: AbsolutePath, + initialManifest: String? = nil, + url: String, + requirementArgs: [String], + expectedManifestString: String, + buildData: BuildData, +) async throws { + _ = try await execute( + ["add-dependency", url] + requirementArgs, + packagePath: packagePath, + manifest: initialManifest, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + try expectManifestContains(packagePath, expectedManifestString) +} - func testNoParameters() async throws { - let stdout = try await execute().stdout - XCTAssertMatch(stdout, .contains("USAGE: swift package")) +@Suite( + // .serialized, + .tags( + .TestSize.large, + .Feature.Command.Package.General, + ), +) +struct PackageCommandTestCase { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func noParameters( + data: BuildData, + ) async throws { + let stdout = try await executeSwiftPackage( + nil, + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + #expect(stdout.contains("USAGE: swift package")) } - func testUsage() async throws { - throw XCTSkip("rdar://131126477") - do { - _ = try await execute(["-halp"]) - XCTFail("expecting `execute` to fail") - } catch SwiftPMError.executionFailure(_, _, let stderr) { - XCTAssertMatch(stderr, .contains("Usage: swift package")) - } catch { - throw error + @Test( + .issue("rdar://131126477", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func usage( + data: BuildData, + ) async throws { + await expectThrowsCommandExecutionError( + try await executeSwiftPackage( + nil, + configuration: data.config, + extraArgs: ["-halp"], + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("Usage: swift package")) } } - func testSeeAlso() async throws { - // This test fails when `--build-system ` is provided, so directly invoke SwiftPM.Package.execute + @Test + func seeAlso() async throws { let stdout = try await SwiftPM.Package.execute(["--help"]).stdout - XCTAssertMatch(stdout, .contains("SEE ALSO: swift build, swift run, swift test")) + #expect(stdout.contains("SEE ALSO: swift build, swift run, swift test")) } - func testCommandDoesNotEmitDuplicateSymbols() async throws { + @Test + func commandDoesNotEmitDuplicateSymbols() async throws { + let duplicateSymbolRegex = try #require(duplicateSymbolRegex) + let (stdout, stderr) = try await SwiftPM.Package.execute(["--help"]) - XCTAssertNoMatch(stdout, duplicateSymbolRegex) - XCTAssertNoMatch(stderr, duplicateSymbolRegex) - } - func testVersion() async throws { - // This test fails when `--build-system ` is provided, so directly invoke SwiftPM.Package.execute - let stdout = try await SwiftPM.Package.execute(["--version"]).stdout - XCTAssertMatch(stdout, .regex(#"Swift Package Manager -( \w+ )?\d+.\d+.\d+(-\w+)?"#)) + #expect(!stdout.contains(duplicateSymbolRegex)) + #expect(!stderr.contains(duplicateSymbolRegex)) } - func testCompletionTool() async throws { - let stdout = try await execute(["completion-tool", "--help"]).stdout - XCTAssertMatch(stdout, .contains("OVERVIEW: Command to generate shell completions.")) + @Test + func version() async throws { + let stdout = try await SwiftPM.Package.execute(["--version"], ).stdout + let expectedRegex = try Regex(#"Swift Package Manager -( \w+ )?\d+.\d+.\d+(-\w+)?"#) + #expect(stdout.contains(expectedRegex)) } - func testInitOverview() async throws { - let stdout = try await execute(["init", "--help"]).stdout - XCTAssertMatch(stdout, .contains("OVERVIEW: Initialize a new package")) + @Test( + .tags( + .Feature.Command.Package.CompletionTool, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func completionTool( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["completion-tool", "--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + #expect(stdout.contains("OVERVIEW: Command to generate shell completions.")) } - func testInitUsage() async throws { - let stdout = try await execute(["init", "--help"]).stdout - XCTAssertMatch(stdout, .contains("USAGE: swift package init [--type ] ")) - XCTAssertMatch(stdout, .contains(" [--name ]")) - } + @Suite( + .tags( + .Feature.Command.Package.Init, + ), + ) + struct InitHelpUsageTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initOverview( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["init", "--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + #expect(stdout.contains("OVERVIEW: Initialize a new package")) + } - func testInitOptionsHelp() async throws { - let stdout = try await execute(["init", "--help"]).stdout - XCTAssertMatch(stdout, .contains("OPTIONS:")) - } + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initUsage( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["init", "--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + #expect(stdout.contains("USAGE: swift package init [--type ] ")) + #expect(stdout.contains(" [--name ]")) + } - func testPlugin() async throws { - await XCTAssertThrowsCommandExecutionError(try await execute(["plugin"])) { error in - XCTAssertMatch(error.stderr, .contains("error: Missing expected plugin command")) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initOptionsHelp( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["init", "--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + #expect(stdout.contains("OPTIONS:")) } } - func testUnknownOption() async throws { - await XCTAssertThrowsCommandExecutionError(try await execute(["--foo"])) { error in - XCTAssertMatch(error.stderr, .contains("error: Unknown option '--foo'")) + @Test( + .tags( + .Feature.Command.Package.Plugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func plugin( + data: BuildData, + ) async throws { + await expectThrowsCommandExecutionError( + try await execute( + ["plugin"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("error: Missing expected plugin command")) } } - func testUnknownSubcommand() async throws { - try await fixtureXCTest(name: "Miscellaneous/ExeTest") { fixturePath in - await XCTAssertThrowsCommandExecutionError(try await execute(["foo"], packagePath: fixturePath)) { error in - XCTAssertMatch(error.stderr, .contains("Unknown subcommand or plugin name ‘foo’")) - } + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func unknownOption( + data: BuildData, + ) async throws { + await expectThrowsCommandExecutionError( + try await execute( + ["--foo"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("error: Unknown option '--foo'")) } } - func testNetrc() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/XCFramework") { fixturePath in - // --enable-netrc flag - try await self.execute(["resolve", "--enable-netrc"], packagePath: fixturePath) - - // --disable-netrc flag - try await self.execute(["resolve", "--disable-netrc"], packagePath: fixturePath) - - // --enable-netrc and --disable-netrc flags - await XCTAssertAsyncThrowsError( - try await self.execute(["resolve", "--enable-netrc", "--disable-netrc"], packagePath: fixturePath) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func unknownSubcommand( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/ExeTest") { fixturePath in + await expectThrowsCommandExecutionError( + try await execute( + ["foo"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) ) { error in - XCTAssertMatch(String(describing: error), .contains("Value to be set with flag '--disable-netrc' had already been set with flag '--enable-netrc'")) + #expect(error.stderr.contains("Unknown subcommand or plugin name ‘foo’")) } } } - func testNetrcFile() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/XCFramework") { fixturePath in - let fs = localFileSystem - let netrcPath = fixturePath.appending(".netrc") - try fs.writeFileContents( - netrcPath, - string: "machine mymachine.labkey.org login user@labkey.org password mypassword" + @Suite( + .tags( + .Feature.Command.Package.Resolve, + ), + ) + struct ResolveCommandTests { + @Suite( + .tags( + .Feature.NetRc, + ), + ) + struct NetRcTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func netrc( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/XCFramework") { fixturePath in + // --enable-netrc flag + try await execute( + ["resolve", "--enable-netrc"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // valid .netrc file path - try await execute(["resolve", "--netrc-file", netrcPath.pathString], packagePath: fixturePath) + // --disable-netrc flag + try await execute( + ["resolve", "--disable-netrc"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // valid .netrc file path with --disable-netrc option - await XCTAssertAsyncThrowsError( - try await execute(["resolve", "--netrc-file", netrcPath.pathString, "--disable-netrc"], packagePath: fixturePath) - ) { error in - XCTAssertMatch(String(describing: error), .contains("'--disable-netrc' and '--netrc-file' are mutually exclusive")) + // --enable-netrc and --disable-netrc flags + await expectThrowsCommandExecutionError( + try await execute( + ["resolve", "--enable-netrc", "--disable-netrc"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("Value to be set with flag '--disable-netrc' had already been set with flag '--enable-netrc'")) + } + } } - // invalid .netrc file path - await XCTAssertAsyncThrowsError( - try await execute(["resolve", "--netrc-file", "/foo"], packagePath: fixturePath) - ) { error in - XCTAssertMatch(String(describing: error), .regex(#".* Did not find netrc file at ([A-Z]:\\|\/)foo.*"#)) - } + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func netrcFile( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/XCFramework") { fixturePath in + let fs = localFileSystem + let netrcPath = fixturePath.appending(".netrc") + try fs.writeFileContents( + netrcPath, + string: "machine mymachine.labkey.org login user@labkey.org password mypassword" + ) - // invalid .netrc file path with --disable-netrc option - await XCTAssertAsyncThrowsError( - try await execute(["resolve", "--netrc-file", "/foo", "--disable-netrc"], packagePath: fixturePath) - ) { error in - XCTAssertMatch(String(describing: error), .contains("'--disable-netrc' and '--netrc-file' are mutually exclusive")) - } - } - } + // valid .netrc file path + try await execute( + ["resolve", "--netrc-file", netrcPath.pathString], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - func testEnableDisableCache() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - let repositoriesPath = packageRoot.appending(components: ".build", "repositories") - let cachePath = fixturePath.appending("cache") - let repositoriesCachePath = cachePath.appending("repositories") + // valid .netrc file path with --disable-netrc option + await expectThrowsCommandExecutionError( + try await execute( + ["resolve", "--netrc-file", netrcPath.pathString, "--disable-netrc"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("'--disable-netrc' and '--netrc-file' are mutually exclusive")) + } - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + // invalid .netrc file path + let errorRegex = try Regex(#".* Did not find netrc file at ([A-Z]:\\|\/)foo.*"#) + await expectThrowsCommandExecutionError( + try await execute( + ["resolve", "--netrc-file", "/foo"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains(errorRegex)) + } - try await self.execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + // invalid .netrc file path with --disable-netrc option + await expectThrowsCommandExecutionError( + try await execute( + ["resolve", "--netrc-file", "/foo", "--disable-netrc"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("'--disable-netrc' and '--netrc-file' are mutually exclusive")) + } + } + } + } - // we have to check for the prefix here since the hash value changes because spm sees the `prefix` - // directory `/var/...` as `/private/var/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + @Test( + .tags( + .Feature.Command.Package.Reset, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func enableDisableCache( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + let repositoriesPath = packageRoot.appending(components: ".build", "repositories") + let cachePath = fixturePath.appending("cache") + let repositoriesCachePath = cachePath.appending("repositories") - // Remove .build folder - _ = try await execute(["reset"], packagePath: packageRoot) + do { + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - // Perform another cache this time from the cache - _ = try await execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + // we have to check for the prefix here since the hash value changes because spm sees the `prefix` + // directory `/var/...` as `/private/var/...`. + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + + // Remove .build folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Perform another fetch - _ = try await execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) - } + // Perform another cache this time from the cache + _ = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) + + // Perform another fetch + _ = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + } - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + do { + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - try await self.execute(["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + try await execute( + ["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // we have to check for the prefix here since the hash value changes because spm sees the `prefix` - // directory `/var/...` as `/private/var/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssertFalse(localFileSystem.exists(repositoriesCachePath)) - } + // we have to check for the prefix here since the hash value changes because spm sees the `prefix` + // directory `/var/...` as `/private/var/...`. + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(!localFileSystem.exists(repositoriesCachePath)) + } - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + do { + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - let (_, _) = try await self.execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + let (_, _) = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // we have to check for the prefix here since the hash value changes because spm sees the `prefix` - // directory `/var/...` as `/private/var/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + // we have to check for the prefix here since the hash value changes because spm sees the `prefix` + // directory `/var/...` as `/private/var/...`. + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + + // Remove .build folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Remove .build folder - _ = try await execute(["reset"], packagePath: packageRoot) + // Perform another cache this time from the cache + _ = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) + + // Perform another fetch + _ = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + } - // Perform another cache this time from the cache - _ = try await execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + do { + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + let (_, _) = try await execute( + ["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Perform another fetch - _ = try await execute(["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) + // we have to check for the prefix here since the hash value changes because spm sees the `prefix` + // directory `/var/...` as `/private/var/...`. + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(!localFileSystem.exists(repositoriesCachePath)) + } } - - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) - - let (_, _) = try await self.execute(["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) - - // we have to check for the prefix here since the hash value changes because spm sees the `prefix` - // directory `/var/...` as `/private/var/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssertFalse(localFileSystem.exists(repositoriesCachePath)) + } + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func resolve( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + + // Check that `resolve` works. + _ = try await execute( + ["resolve"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let path = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) + #expect(try GitRepository(path: path).getTags() == ["1.2.3"]) } } - } - func testResolve() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - - // Check that `resolve` works. - _ = try await execute(["resolve"], packagePath: packageRoot) - let path = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) - XCTAssertEqual(try GitRepository(path: path).getTags(), ["1.2.3"]) - } - } + @Test( + .tags( + .Feature.Command.Package.Update, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func update( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + + // Perform an initial fetch. + _ = try await execute( + ["resolve"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - func testUpdate() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") + do { + let checkoutPath = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) + let checkoutRepo = GitRepository(path: checkoutPath) + #expect(try checkoutRepo.getTags() == ["1.2.3"]) + _ = try checkoutRepo.revision(forTag: "1.2.3") + } - // Perform an initial fetch. - _ = try await execute(["resolve"], packagePath: packageRoot) + // update and retag the dependency, and update. + let repoPath = fixturePath.appending("Foo") + let repo = GitRepository(path: repoPath) + try localFileSystem.writeFileContents(repoPath.appending("test"), string: "test") + try repo.stageEverything() + try repo.commit() + try repo.tag(name: "1.2.4") + + // we will validate it is there + let revision = try repo.revision(forTag: "1.2.4") + + _ = try await execute( + ["update"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - do { - let checkoutPath = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) - let checkoutRepo = GitRepository(path: checkoutPath) - XCTAssertEqual(try checkoutRepo.getTags(), ["1.2.3"]) - _ = try checkoutRepo.revision(forTag: "1.2.3") + do { + // We shouldn't assume package path will be same after an update so ask again for it. + let checkoutPath = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) + let checkoutRepo = GitRepository(path: checkoutPath) + // tag may not be there, but revision should be after update + #expect(checkoutRepo.exists(revision: .init(identifier: revision))) + } } + } + @Test( + .tags( + .Feature.Command.Package.Reset, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func cache( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + let repositoriesPath = packageRoot.appending(components: ".build", "repositories") + let cachePath = fixturePath.appending("cache") + let repositoriesCachePath = cachePath.appending("repositories") + + // Perform an initial fetch and populate the cache + _ = try await execute( + ["resolve", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + // we have to check for the prefix here since the hash value changes because spm sees the `prefix` + // directory `/var/...` as `/private/var/...`. + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) - // update and retag the dependency, and update. - let repoPath = fixturePath.appending("Foo") - let repo = GitRepository(path: repoPath) - try localFileSystem.writeFileContents(repoPath.appending("test"), string: "test") - try repo.stageEverything() - try repo.commit() - try repo.tag(name: "1.2.4") + // Remove .build folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // we will validate it is there - let revision = try repo.revision(forTag: "1.2.4") + // Perform another cache this time from the cache + _ = try await execute( + ["resolve", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - _ = try await execute(["update"], packagePath: packageRoot) + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - do { - // We shouldn't assume package path will be same after an update so ask again for it. - let checkoutPath = try SwiftPM.packagePath(for: "Foo", packageRoot: packageRoot) - let checkoutRepo = GitRepository(path: checkoutPath) - // tag may not be there, but revision should be after update - XCTAssertTrue(checkoutRepo.exists(revision: .init(identifier: revision))) + // Perform another fetch + _ = try await execute( + ["resolve", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) + #expect(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) } } } - func testCache() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - let repositoriesPath = packageRoot.appending(components: ".build", "repositories") - let cachePath = fixturePath.appending("cache") - let repositoriesCachePath = cachePath.appending("repositories") - - // Perform an initial fetch and populate the cache - _ = try await execute(["resolve", "--cache-path", cachePath.pathString], packagePath: packageRoot) - // we have to check for the prefix here since the hash value changes because spm sees the `prefix` - // directory `/var/...` as `/private/var/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) - - // Remove .build folder - _ = try await execute(["reset"], packagePath: packageRoot) - - // Perform another cache this time from the cache - _ = try await execute(["resolve", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) - - // Perform another fetch - _ = try await execute(["resolve", "--cache-path", cachePath.pathString], packagePath: packageRoot) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).contains { $0.hasPrefix("Foo-") }) - } - } - - func testDescribe() async throws { - try await fixtureXCTest(name: "Miscellaneous/ExeTest") { fixturePath in - // Generate the JSON description. - let (jsonOutput, _) = try await self.execute(["describe", "--type=json"], packagePath: fixturePath) - let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + @Suite( + .tags( + .Feature.Command.Package.Describe, + ), + ) + struct DescribeCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func describe( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/ExeTest") { fixturePath in + // Generate the JSON description. + let (jsonOutput, _) = try await execute( + ["describe", "--type=json"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + + // Check that tests don't appear in the product memberships. + #expect(json["name"]?.string == "ExeTest") + let jsonTarget0 = try #require(json["targets"]?.array?[0]) + #expect(jsonTarget0["product_memberships"] == nil) + let jsonTarget1 = try #require(json["targets"]?.array?[1]) + #expect(jsonTarget1["product_memberships"]?.array?[0].stringValue == "Exe") + } - // Check that tests don't appear in the product memberships. - XCTAssertEqual(json["name"]?.string, "ExeTest") - let jsonTarget0 = try XCTUnwrap(json["targets"]?.array?[0]) - XCTAssertNil(jsonTarget0["product_memberships"]) - let jsonTarget1 = try XCTUnwrap(json["targets"]?.array?[1]) - XCTAssertEqual(jsonTarget1["product_memberships"]?.array?[0].stringValue, "Exe") + try await fixture(name: "CFamilyTargets/SwiftCMixed") { fixturePath in + // Generate the JSON description. + let (jsonOutput, _) = try await execute( + ["describe", "--type=json"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + + // Check that the JSON description contains what we expect it to. + #expect(json["name"]?.string == "SwiftCMixed") + let pathString = try #require(json["path"]?.string) + try #expect(pathString.contains(Regex(#"^([A-Z]:\\|\/).*"#))) + #expect(pathString.hasSuffix(AbsolutePath("/" + fixturePath.basename).pathString)) + #expect(json["targets"]?.array?.count == 3) + let jsonTarget0 = try #require(json["targets"]?.array?[0]) + #expect(jsonTarget0["name"]?.stringValue == "SeaLib") + #expect(jsonTarget0["c99name"]?.stringValue == "SeaLib") + #expect(jsonTarget0["type"]?.stringValue == "library") + #expect(jsonTarget0["module_type"]?.stringValue == "ClangTarget") + let jsonTarget1 = try #require(json["targets"]?.array?[1]) + #expect(jsonTarget1["name"]?.stringValue == "SeaExec") + #expect(jsonTarget1["c99name"]?.stringValue == "SeaExec") + #expect(jsonTarget1["type"]?.stringValue == "executable") + #expect(jsonTarget1["module_type"]?.stringValue == "SwiftTarget") + #expect(jsonTarget1["product_memberships"]?.array?[0].stringValue == "SeaExec") + let jsonTarget2 = try #require(json["targets"]?.array?[2]) + #expect(jsonTarget2["name"]?.stringValue == "CExec") + #expect(jsonTarget2["c99name"]?.stringValue == "CExec") + #expect(jsonTarget2["type"]?.stringValue == "executable") + #expect(jsonTarget2["module_type"]?.stringValue == "ClangTarget") + #expect(jsonTarget2["product_memberships"]?.array?[0].stringValue == "CExec") + + // Generate the text description. + let (textOutput, _) = try await execute( + ["describe", "--type=text"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let textChunks = textOutput.components(separatedBy: "\n").reduce(into: [""]) { chunks, line in + // Split the text into chunks based on presence or absence of leading whitespace. + if line.hasPrefix(" ") == chunks[chunks.count - 1].hasPrefix(" ") { + chunks[chunks.count - 1].append(line + "\n") + } else { + chunks.append(line + "\n") + } + }.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + + // Check that the text description contains what we expect it to. + // FIXME: This is a bit inelegant, but any errors are easy to reason about. + let textChunk0 = textChunks[0] + #expect(textChunk0.contains("Name: SwiftCMixed")) + try #expect(textChunk0.contains(Regex(#"Path: ([A-Z]:\\|\/)"#))) + #expect(textChunk0.contains(AbsolutePath("/" + fixturePath.basename).pathString + "\n")) + #expect(textChunk0.contains("Tools version: 4.2")) + #expect(textChunk0.contains("Products:")) + let textChunk1 = textChunks[1] + #expect(textChunk1.contains("Name: SeaExec")) + #expect(textChunk1.contains("Type:\n Executable")) + #expect(textChunk1.contains("Targets:\n SeaExec")) + let textChunk2 = textChunks[2] + #expect(textChunk2.contains("Name: CExec")) + #expect(textChunk2.contains("Type:\n Executable")) + #expect(textChunk2.contains("Targets:\n CExec")) + let textChunk3 = textChunks[3] + #expect(textChunk3.contains("Targets:")) + let textChunk4 = textChunks[4] + #expect(textChunk4.contains("Name: SeaLib")) + #expect(textChunk4.contains("C99name: SeaLib")) + #expect(textChunk4.contains("Type: library")) + #expect(textChunk4.contains("Module type: ClangTarget")) + #expect(textChunk4.contains("Path: \(RelativePath("Sources/SeaLib").pathString)")) + #expect(textChunk4.contains("Sources:\n Foo.c")) + let textChunk5 = textChunks[5] + #expect(textChunk5.contains("Name: SeaExec")) + #expect(textChunk5.contains("C99name: SeaExec")) + #expect(textChunk5.contains("Type: executable")) + #expect(textChunk5.contains("Module type: SwiftTarget")) + #expect(textChunk5.contains("Path: \(RelativePath("Sources/SeaExec").pathString)")) + #expect(textChunk5.contains("Sources:\n main.swift")) + let textChunk6 = textChunks[6] + #expect(textChunk6.contains("Name: CExec")) + #expect(textChunk6.contains("C99name: CExec")) + #expect(textChunk6.contains("Type: executable")) + #expect(textChunk6.contains("Module type: ClangTarget")) + #expect(textChunk6.contains("Path: \(RelativePath("Sources/CExec").pathString)")) + #expect(textChunk6.contains("Sources:\n main.c")) + } } - try await fixtureXCTest(name: "CFamilyTargets/SwiftCMixed") { fixturePath in - // Generate the JSON description. - let (jsonOutput, _) = try await self.execute(["describe", "--type=json"], packagePath: fixturePath) - let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) - - // Check that the JSON description contains what we expect it to. - XCTAssertEqual(json["name"]?.string, "SwiftCMixed") - XCTAssertMatch(json["path"]?.string, .regex(#"^([A-Z]:\\|\/).*"#)) - XCTAssertMatch(json["path"]?.string, .suffix(AbsolutePath("/" + fixturePath.basename).pathString)) - XCTAssertEqual(json["targets"]?.array?.count, 3) - let jsonTarget0 = try XCTUnwrap(json["targets"]?.array?[0]) - XCTAssertEqual(jsonTarget0["name"]?.stringValue, "SeaLib") - XCTAssertEqual(jsonTarget0["c99name"]?.stringValue, "SeaLib") - XCTAssertEqual(jsonTarget0["type"]?.stringValue, "library") - XCTAssertEqual(jsonTarget0["module_type"]?.stringValue, "ClangTarget") - let jsonTarget1 = try XCTUnwrap(json["targets"]?.array?[1]) - XCTAssertEqual(jsonTarget1["name"]?.stringValue, "SeaExec") - XCTAssertEqual(jsonTarget1["c99name"]?.stringValue, "SeaExec") - XCTAssertEqual(jsonTarget1["type"]?.stringValue, "executable") - XCTAssertEqual(jsonTarget1["module_type"]?.stringValue, "SwiftTarget") - XCTAssertEqual(jsonTarget1["product_memberships"]?.array?[0].stringValue, "SeaExec") - let jsonTarget2 = try XCTUnwrap(json["targets"]?.array?[2]) - XCTAssertEqual(jsonTarget2["name"]?.stringValue, "CExec") - XCTAssertEqual(jsonTarget2["c99name"]?.stringValue, "CExec") - XCTAssertEqual(jsonTarget2["type"]?.stringValue, "executable") - XCTAssertEqual(jsonTarget2["module_type"]?.stringValue, "ClangTarget") - XCTAssertEqual(jsonTarget2["product_memberships"]?.array?[0].stringValue, "CExec") - - // Generate the text description. - let (textOutput, _) = try await self.execute(["describe", "--type=text"], packagePath: fixturePath) - let textChunks = textOutput.components(separatedBy: "\n").reduce(into: [""]) { chunks, line in - // Split the text into chunks based on presence or absence of leading whitespace. - if line.hasPrefix(" ") == chunks[chunks.count-1].hasPrefix(" ") { - chunks[chunks.count-1].append(line + "\n") - } - else { - chunks.append(line + "\n") + @Test( + .IssueWindowsRelativePathAssert, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func describeJson( + data: BuildData, + ) async throws { + try await withKnownIssue(isIntermittent: ProcessInfo.hostOperatingSystem == .windows) { + try await fixture(name: "DependencyResolution/External/Simple/Bar") { fixturePath in + // Generate the JSON description. + let (jsonOutput, _) = try await execute( + ["describe", "--type=json"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + + // Check that product dependencies and memberships are as expected. + #expect(json["name"]?.string == "Bar") + let jsonTarget = try #require(json["targets"]?.array?[0]) + #expect(jsonTarget["product_memberships"]?.array?[0].stringValue == "Bar") + #expect(jsonTarget["product_dependencies"]?.array?[0].stringValue == "Foo") + #expect(jsonTarget["target_dependencies"] == nil) } - }.filter{ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - - // Check that the text description contains what we expect it to. - // FIXME: This is a bit inelegant, but any errors are easy to reason about. - let textChunk0 = try XCTUnwrap(textChunks[0]) - XCTAssertMatch(textChunk0, .contains("Name: SwiftCMixed")) - XCTAssertMatch(textChunk0, .regex(#"Path: ([A-Z]:\\|\/)"#)) - XCTAssertMatch(textChunk0, .contains(AbsolutePath("/" + fixturePath.basename).pathString + "\n")) - XCTAssertMatch(textChunk0, .contains("Tools version: 4.2")) - XCTAssertMatch(textChunk0, .contains("Products:")) - let textChunk1 = try XCTUnwrap(textChunks[1]) - XCTAssertMatch(textChunk1, .contains("Name: SeaExec")) - XCTAssertMatch(textChunk1, .contains("Type:\n Executable")) - XCTAssertMatch(textChunk1, .contains("Targets:\n SeaExec")) - let textChunk2 = try XCTUnwrap(textChunks[2]) - XCTAssertMatch(textChunk2, .contains("Name: CExec")) - XCTAssertMatch(textChunk2, .contains("Type:\n Executable")) - XCTAssertMatch(textChunk2, .contains("Targets:\n CExec")) - let textChunk3 = try XCTUnwrap(textChunks[3]) - XCTAssertMatch(textChunk3, .contains("Targets:")) - let textChunk4 = try XCTUnwrap(textChunks[4]) - XCTAssertMatch(textChunk4, .contains("Name: SeaLib")) - XCTAssertMatch(textChunk4, .contains("C99name: SeaLib")) - XCTAssertMatch(textChunk4, .contains("Type: library")) - XCTAssertMatch(textChunk4, .contains("Module type: ClangTarget")) - XCTAssertMatch(textChunk4, .contains("Path: \(RelativePath("Sources/SeaLib").pathString)")) - XCTAssertMatch(textChunk4, .contains("Sources:\n Foo.c")) - let textChunk5 = try XCTUnwrap(textChunks[5]) - XCTAssertMatch(textChunk5, .contains("Name: SeaExec")) - XCTAssertMatch(textChunk5, .contains("C99name: SeaExec")) - XCTAssertMatch(textChunk5, .contains("Type: executable")) - XCTAssertMatch(textChunk5, .contains("Module type: SwiftTarget")) - XCTAssertMatch(textChunk5, .contains("Path: \(RelativePath("Sources/SeaExec").pathString)")) - XCTAssertMatch(textChunk5, .contains("Sources:\n main.swift")) - let textChunk6 = try XCTUnwrap(textChunks[6]) - XCTAssertMatch(textChunk6, .contains("Name: CExec")) - XCTAssertMatch(textChunk6, .contains("C99name: CExec")) - XCTAssertMatch(textChunk6, .contains("Type: executable")) - XCTAssertMatch(textChunk6, .contains("Module type: ClangTarget")) - XCTAssertMatch(textChunk6, .contains("Path: \(RelativePath("Sources/CExec").pathString)")) - XCTAssertMatch(textChunk6, .contains("Sources:\n main.c")) - } - - } - - func testDescribeJson() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - - try await fixtureXCTest(name: "DependencyResolution/External/Simple/Bar") { fixturePath in - // Generate the JSON description. - let (jsonOutput, _) = try await self.execute(["describe", "--type=json"], packagePath: fixturePath) - let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) - - // Check that product dependencies and memberships are as expected. - XCTAssertEqual(json["name"]?.string, "Bar") - let jsonTarget = try XCTUnwrap(json["targets"]?.array?[0]) - XCTAssertEqual(jsonTarget["product_memberships"]?.array?[0].stringValue, "Bar") - XCTAssertEqual(jsonTarget["product_dependencies"]?.array?[0].stringValue, "Foo") - XCTAssertNil(jsonTarget["target_dependencies"]) + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - } - - func testDescribePackageUsingPlugins() async throws { - try await fixtureXCTest(name: "Miscellaneous/Plugins/MySourceGenPlugin") { fixturePath in - // Generate the JSON description. - let (stdout, _) = try await self.execute(["describe", "--type=json"], packagePath: fixturePath) - let json = try JSON(bytes: ByteString(encodingAsUTF8: stdout)) - - // Check the contents of the JSON. - XCTAssertEqual(try XCTUnwrap(json["name"]).string, "MySourceGenPlugin") - let targetsArray = try XCTUnwrap(json["targets"]?.array) - let buildToolPluginTarget = try XCTUnwrap(targetsArray.first{ $0["name"]?.string == "MySourceGenBuildToolPlugin" }?.dictionary) - XCTAssertEqual(buildToolPluginTarget["module_type"]?.string, "PluginTarget") - XCTAssertEqual(buildToolPluginTarget["plugin_capability"]?.dictionary?["type"]?.string, "buildTool") - let prebuildPluginTarget = try XCTUnwrap(targetsArray.first{ $0["name"]?.string == "MySourceGenPrebuildPlugin" }?.dictionary) - XCTAssertEqual(prebuildPluginTarget["module_type"]?.string, "PluginTarget") - XCTAssertEqual(prebuildPluginTarget["plugin_capability"]?.dictionary?["type"]?.string, "buildTool") + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func describePackageUsingPlugins( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/MySourceGenPlugin") { fixturePath in + // Generate the JSON description. + let (stdout, _) = try await execute( + ["describe", "--type=json"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let json = try JSON(bytes: ByteString(encodingAsUTF8: stdout)) + + // Check the contents of the JSON. + #expect(try #require(json["name"]).string == "MySourceGenPlugin") + let targetsArray = try #require(json["targets"]?.array) + let buildToolPluginTarget = try #require(targetsArray.first { $0["name"]?.string == "MySourceGenBuildToolPlugin" }?.dictionary) + #expect(buildToolPluginTarget["module_type"]?.string == "PluginTarget") + #expect(buildToolPluginTarget["plugin_capability"]?.dictionary?["type"]?.string == "buildTool") + let prebuildPluginTarget = try #require(targetsArray.first { $0["name"]?.string == "MySourceGenPrebuildPlugin" }?.dictionary) + #expect(prebuildPluginTarget["module_type"]?.string == "PluginTarget") + #expect(prebuildPluginTarget["plugin_capability"]?.dictionary?["type"]?.string == "buildTool") + } } } - func testDumpPackage() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Complex") { fixturePath in + @Test( + .tags( + .Feature.Command.Package.DumpPackage, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func dumpPackage( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Complex") { fixturePath in let packageRoot = fixturePath.appending("app") - let (dumpOutput, _) = try await execute(["dump-package"], packagePath: packageRoot) + let (dumpOutput, _) = try await execute( + ["dump-package"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) let json = try JSON(bytes: ByteString(encodingAsUTF8: dumpOutput)) - guard case let .dictionary(contents) = json else { XCTFail("unexpected result"); return } - guard case let .string(name)? = contents["name"] else { XCTFail("unexpected result"); return } - guard case let .array(platforms)? = contents["platforms"] else { XCTFail("unexpected result"); return } - XCTAssertEqual(name, "Dealer") - XCTAssertEqual(platforms, [ - .dictionary([ - "platformName": .string("macos"), - "version": .string("10.12"), - "options": .array([]) - ]), - .dictionary([ - "platformName": .string("ios"), - "version": .string("10.0"), - "options": .array([]) - ]), - .dictionary([ - "platformName": .string("tvos"), - "version": .string("11.0"), - "options": .array([]) - ]), - .dictionary([ - "platformName": .string("watchos"), - "version": .string("5.0"), - "options": .array([]) - ]), - ]) + guard case let .dictionary(contents) = json else { Issue.record("unexpected result"); return } + guard case let .string(name)? = contents["name"] else { Issue.record("unexpected result"); return } + guard case let .array(platforms)? = contents["platforms"] else { Issue.record("unexpected result"); return } + #expect(name == "Dealer") + #expect( + platforms == [ + .dictionary([ + "platformName": .string("macos"), + "version": .string("10.12"), + "options": .array([]), + ]), + .dictionary([ + "platformName": .string("ios"), + "version": .string("10.0"), + "options": .array([]), + ]), + .dictionary([ + "platformName": .string("tvos"), + "version": .string("11.0"), + "options": .array([]), + ]), + .dictionary([ + "platformName": .string("watchos"), + "version": .string("5.0"), + "options": .array([]), + ]), + ] + ) } } - // Returns symbol graph with or without pretty printing. - private func symbolGraph(atPath path: AbsolutePath, withPrettyPrinting: Bool, file: StaticString = #file, line: UInt = #line) async throws -> Data? { - let tool = try SwiftCommandState.makeMockState(options: GlobalOptions.parse(["--package-path", path.pathString])) - let symbolGraphExtractorPath = try tool.getTargetToolchain().getSymbolGraphExtract() - - let arguments = withPrettyPrinting ? ["dump-symbol-graph", "--pretty-print"] : ["dump-symbol-graph"] - - let result = try await self.execute(arguments, packagePath: path, env: ["SWIFT_SYMBOLGRAPH_EXTRACT": symbolGraphExtractorPath.pathString]) - let enumerator = try XCTUnwrap(FileManager.default.enumerator(at: URL(fileURLWithPath: path.pathString), includingPropertiesForKeys: nil), file: file, line: line) - - var symbolGraphURL: URL? - for case let url as URL in enumerator where url.lastPathComponent == "Bar.symbols.json" { - symbolGraphURL = url - break - } - - let symbolGraphData: Data - if let symbolGraphURL { - symbolGraphData = try Data(contentsOf: symbolGraphURL) - } else { - XCTFail("Failed to extract symbol graph: \(result.stdout)\n\(result.stderr)") - return nil - } - - // Double check that it's a valid JSON - XCTAssertNoThrow(try JSONSerialization.jsonObject(with: symbolGraphData), file: file, line: line) - - return symbolGraphData - } + @Test( + .disabled("disabling this suite.. first one to fail. due to \"couldn't determine the current working directory\""), + .tags( + .Feature.Command.Package.DumpSymbolGraph, + ), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8848", relationship: .defect), + .IssueWindowsLongPath, + .requiresSymbolgraphExtract, + arguments: getBuildData(for: [.swiftbuild]), + [ + true, + false, + ], + ) + func dumpSymbolGraphFormatting( + data: BuildData, + withPrettyPrinting: Bool, + ) async throws { + // try XCTSkipIf(buildSystemProvider == .native && (try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") + try await withKnownIssue { + try await fixture(name: "DependencyResolution/Internal/Simple", removeFixturePathOnDeinit: true) { fixturePath in + let tool = try SwiftCommandState.makeMockState(options: GlobalOptions.parse(["--package-path", fixturePath.pathString])) + let symbolGraphExtractorPath = try tool.getTargetToolchain().getSymbolGraphExtract() + + let arguments = withPrettyPrinting ? ["dump-symbol-graph", "--pretty-print"] : ["dump-symbol-graph"] + + let result = try await execute( + arguments, + packagePath: fixturePath, + env: ["SWIFT_SYMBOLGRAPH_EXTRACT": symbolGraphExtractorPath.pathString], + configuration: data.config, + buildSystem: data.buildSystem, + ) + let enumerator = try #require(FileManager.default.enumerator(at: URL(fileURLWithPath: fixturePath.pathString), includingPropertiesForKeys: nil)) - func testDumpSymbolGraphCompactFormatting() async throws { - // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. - try XCTSkipIf(buildSystemProvider == .native && (try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") - try XCTSkipIf(buildSystemProvider == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows, "skipping test for Windows because of long file path issues") + var symbolGraphURLOptional: URL? = nil + while let element = enumerator.nextObject() { + if let url = element as? URL, url.lastPathComponent == "Bar.symbols.json" { + symbolGraphURLOptional = url + break + } + } - try await fixtureXCTest(name: "DependencyResolution/Internal/Simple") { fixturePath in - let compactGraphData = try await XCTAsyncUnwrap(await symbolGraph(atPath: fixturePath, withPrettyPrinting: false)) - let compactJSONText = String(decoding: compactGraphData, as: UTF8.self) - XCTAssertEqual(compactJSONText.components(separatedBy: .newlines).count, 1) - } - } + let symbolGraphURL = try #require(symbolGraphURLOptional, "Failed to extract symbol graph: \(result.stdout)\n\(result.stderr)") + let symbolGraphData = try Data(contentsOf: symbolGraphURL) - func testDumpSymbolGraphPrettyFormatting() async throws { - // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. - try XCTSkipIf((try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") - try XCTSkipIf(buildSystemProvider == .swiftbuild, "skipping test because pretty printing isn't yet supported with swiftbuild build system via swift build and the swift compiler") + // Double check that it's a valid JSON + #expect(throws: Never.self) { + try JSONSerialization.jsonObject(with: symbolGraphData) + } - try await fixtureXCTest(name: "DependencyResolution/Internal/Simple") { fixturePath in - let prettyGraphData = try await XCTAsyncUnwrap(await symbolGraph(atPath: fixturePath, withPrettyPrinting: true)) - let prettyJSONText = String(decoding: prettyGraphData, as: UTF8.self) - XCTAssertGreaterThan(prettyJSONText.components(separatedBy: .newlines).count, 1) + let JSONText = String(decoding: symbolGraphData, as: UTF8.self) + if withPrettyPrinting { + #expect(JSONText.components(separatedBy: .newlines).count > 1) + } else { + #expect(JSONText.components(separatedBy: .newlines).count == 1) + } + } + } when: { + (ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild && !withPrettyPrinting) + || (data.buildSystem == .swiftbuild && withPrettyPrinting) } } - func testCompletionToolListSnippets() async throws { - try await fixtureXCTest(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in - let result = try await execute(["completion-tool", "list-snippets"], packagePath: fixturePath) - XCTAssertEqual(result.stdout, "MySnippet\n") + @Suite( + .tags( + .Feature.Command.Package.CompletionTool, + ), + ) + struct CompletionToolCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func completionToolListSnippets( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in + let result = try await execute( + ["completion-tool", "list-snippets"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(result.stdout == "MySnippet\n") + } } - } - func testCompletionToolListDependencies() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Complex") { fixturePath in - let result = try await execute(["completion-tool", "list-dependencies"], packagePath: fixturePath.appending("deck-of-playing-cards-local")) - XCTAssertEqual(result.stdout, "playingcard\nfisheryates\n") + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func completionToolListDependencies( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Complex") { fixturePath in + let result = try await execute( + ["completion-tool", "list-dependencies"], + packagePath: fixturePath.appending("deck-of-playing-cards-local"), + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(result.stdout == "playingcard\nfisheryates\n") + } } - } - func testCompletionToolListExecutables() async throws { - try await fixtureXCTest(name: "Miscellaneous/MultipleExecutables") { fixturePath in - let result = try await execute(["completion-tool", "list-executables"], packagePath: fixturePath) - XCTAssertEqual(result.stdout, "exec1\nexec2\n") + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func completionToolListExecutables( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in + let result = try await execute( + ["completion-tool", "list-executables"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(result.stdout == "exec1\nexec2\n") + } } - } - func testCompletionToolListExecutablesDifferentNames() async throws { - try await fixtureXCTest(name: "Miscellaneous/DifferentProductTargetName") { fixturePath in - let result = try await execute(["completion-tool", "list-executables"], packagePath: fixturePath) - XCTAssertEqual(result.stdout, "Foo\n") + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func completionToolListExecutablesDifferentNames( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/DifferentProductTargetName") { fixturePath in + let result = try await execute( + ["completion-tool", "list-executables"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(result.stdout == "Foo\n") + } } } - func testShowExecutables() async throws { - try await fixtureXCTest(name: "Miscellaneous/ShowExecutables") { fixturePath in + @Test( + .tags( + .Feature.Command.Package.ShowExecutables, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func showExecutables( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/ShowExecutables") { fixturePath in let packageRoot = fixturePath.appending("app") - let (textOutput, _) = try await self.execute(["show-executables", "--format=flatlist"], packagePath: packageRoot) - XCTAssert(textOutput.contains("dealer\n")) - XCTAssert(textOutput.contains("deck (deck-of-playing-cards)\n")) - - let (jsonOutput, _) = try await self.execute(["show-executables", "--format=json"], packagePath: packageRoot) + let (textOutput, _) = try await execute( + ["show-executables", "--format=flatlist"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(textOutput.contains("dealer\n")) + #expect(textOutput.contains("deck (deck-of-playing-cards)\n")) + + let (jsonOutput, _) = try await execute( + ["show-executables", "--format=json"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) - guard case let .array(contents) = json else { XCTFail("unexpected result"); return } + guard case let .array(contents) = json else { Issue.record("unexpected result"); return } - XCTAssertEqual(2, contents.count) + #expect(2 == contents.count) - guard case let first = contents.first else { XCTFail("unexpected result"); return } - guard case let .dictionary(dealer) = first else { XCTFail("unexpected result"); return } - guard case let .string(dealerName)? = dealer["name"] else { XCTFail("unexpected result"); return } - XCTAssertEqual(dealerName, "dealer") + guard case let first = contents.first else { Issue.record("unexpected result"); return } + guard case let .dictionary(dealer) = first else { Issue.record("unexpected result"); return } + guard case let .string(dealerName)? = dealer["name"] else { Issue.record("unexpected result"); return } + #expect(dealerName == "dealer") if case let .string(package)? = dealer["package"] { - XCTFail("unexpected package for dealer (should be unset): \(package)") + Issue.record("unexpected package for dealer (should be unset): \(package)") return } - guard case let last = contents.last else { XCTFail("unexpected result"); return } - guard case let .dictionary(deck) = last else { XCTFail("unexpected result"); return } - guard case let .string(deckName)? = deck["name"] else { XCTFail("unexpected result"); return } - XCTAssertEqual(deckName, "deck") + guard case let last = contents.last else { Issue.record("unexpected result"); return } + guard case let .dictionary(deck) = last else { Issue.record("unexpected result"); return } + guard case let .string(deckName)? = deck["name"] else { Issue.record("unexpected result"); return } + #expect(deckName == "deck") if case let .string(package)? = deck["package"] { - XCTAssertEqual("deck-of-playing-cards", package) + #expect("deck-of-playing-cards" == package) } else { - XCTFail("missing package for deck") + Issue.record("missing package for deck") return } } } - func testShowDependencies() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Complex") { fixturePath in - let packageRoot = fixturePath.appending("app") - let (textOutput, _) = try await self.execute(["show-dependencies", "--format=text"], packagePath: packageRoot) - XCTAssert(textOutput.contains("FisherYates@1.2.3")) + @Suite( + .tags( + .Feature.Command.Package.ShowDependencies, + ), + ) + struct ShowDependenciesCommandTests { + @Test( + .tags( + .Feature.Command.Package.ShowDependencies, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func showDependencies( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Complex") { fixturePath in + let packageRoot = fixturePath.appending("app") + let (textOutput, _) = try await execute( + ["show-dependencies", "--format=text"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(textOutput.contains("FisherYates@1.2.3")) - let (jsonOutput, _) = try await self.execute(["show-dependencies", "--format=json"], packagePath: packageRoot) - let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) - guard case let .dictionary(contents) = json else { XCTFail("unexpected result"); return } - guard case let .string(name)? = contents["name"] else { XCTFail("unexpected result"); return } - XCTAssertEqual(name, "Dealer") - guard case let .string(path)? = contents["path"] else { XCTFail("unexpected result"); return } - XCTAssertEqual(try resolveSymlinks(try AbsolutePath(validating: path)), try resolveSymlinks(packageRoot)) + let (jsonOutput, _) = try await execute( + ["show-dependencies", "--format=json"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + guard case let .dictionary(contents) = json else { Issue.record("unexpected result"); return } + guard case let .string(name)? = contents["name"] else { Issue.record("unexpected result"); return } + #expect(name == "Dealer") + guard case let .string(path)? = contents["path"] else { Issue.record("unexpected result"); return } + let actual = try resolveSymlinks(try AbsolutePath(validating: path)) + let expected = try resolveSymlinks(packageRoot) + #expect(actual == expected) + } } - } - func testShowDependencies_dotFormat_sr12016() throws { - // Confirm that SR-12016 is resolved. - // See https://bugs.swift.org/browse/SR-12016 - - let fileSystem = InMemoryFileSystem(emptyFiles: [ - "/PackageA/Sources/TargetA/main.swift", - "/PackageB/Sources/TargetB/B.swift", - "/PackageC/Sources/TargetC/C.swift", - "/PackageD/Sources/TargetD/D.swift", - ]) - - let manifestA = Manifest.createRootManifest( - displayName: "PackageA", - path: "/PackageA", - toolsVersion: .v5_3, - dependencies: [ - .fileSystem(path: "/PackageB"), - .fileSystem(path: "/PackageC"), - ], - products: [ - try .init(name: "exe", type: .executable, targets: ["TargetA"]) - ], - targets: [ - try .init(name: "TargetA", dependencies: ["PackageB", "PackageC"]) - ] + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func showDependencies_dotFormat_sr12016( + data: BuildData, + ) throws { + let fileSystem = InMemoryFileSystem(emptyFiles: [ + "/PackageA/Sources/TargetA/main.swift", + "/PackageB/Sources/TargetB/B.swift", + "/PackageC/Sources/TargetC/C.swift", + "/PackageD/Sources/TargetD/D.swift", + ]) - let manifestB = Manifest.createFileSystemManifest( - displayName: "PackageB", - path: "/PackageB", - toolsVersion: .v5_3, - dependencies: [ - .fileSystem(path: "/PackageC"), - .fileSystem(path: "/PackageD"), - ], - products: [ - try .init(name: "PackageB", type: .library(.dynamic), targets: ["TargetB"]) - ], - targets: [ - try .init(name: "TargetB", dependencies: ["PackageC", "PackageD"]) - ] - ) + let manifestA = Manifest.createRootManifest( + displayName: "PackageA", + path: "/PackageA", + toolsVersion: .v5_3, + dependencies: [ + .fileSystem(path: "/PackageB"), + .fileSystem(path: "/PackageC"), + ], + products: [ + try .init(name: "exe", type: .executable, targets: ["TargetA"]) + ], + targets: [ + try .init(name: "TargetA", dependencies: ["PackageB", "PackageC"]) + ] + ) - let manifestC = Manifest.createFileSystemManifest( - displayName: "PackageC", - path: "/PackageC", - toolsVersion: .v5_3, - dependencies: [ - .fileSystem(path: "/PackageD"), - ], - products: [ - try .init(name: "PackageC", type: .library(.dynamic), targets: ["TargetC"]) - ], - targets: [ - try .init(name: "TargetC", dependencies: ["PackageD"]) - ] - ) + let manifestB = Manifest.createFileSystemManifest( + displayName: "PackageB", + path: "/PackageB", + toolsVersion: .v5_3, + dependencies: [ + .fileSystem(path: "/PackageC"), + .fileSystem(path: "/PackageD"), + ], + products: [ + try .init(name: "PackageB", type: .library(.dynamic), targets: ["TargetB"]) + ], + targets: [ + try .init(name: "TargetB", dependencies: ["PackageC", "PackageD"]) + ] + ) - let manifestD = Manifest.createFileSystemManifest( - displayName: "PackageD", - path: "/PackageD", - toolsVersion: .v5_3, - products: [ - try .init(name: "PackageD", type: .library(.dynamic), targets: ["TargetD"]) - ], - targets: [ - try .init(name: "TargetD") - ] - ) + let manifestC = Manifest.createFileSystemManifest( + displayName: "PackageC", + path: "/PackageC", + toolsVersion: .v5_3, + dependencies: [ + .fileSystem(path: "/PackageD") + ], + products: [ + try .init(name: "PackageC", type: .library(.dynamic), targets: ["TargetC"]) + ], + targets: [ + try .init(name: "TargetC", dependencies: ["PackageD"]) + ] + ) - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fileSystem, - manifests: [manifestA, manifestB, manifestC, manifestD], - observabilityScope: observability.topScope - ) - XCTAssertNoDiagnostics(observability.diagnostics) - - let output = BufferedOutputByteStream() - SwiftPackageCommand.ShowDependencies.dumpDependenciesOf( - graph: graph, - rootPackage: graph.rootPackages[graph.rootPackages.startIndex], - mode: .dot, - on: output - ) - let dotFormat = output.bytes.description + let manifestD = Manifest.createFileSystemManifest( + displayName: "PackageD", + path: "/PackageD", + toolsVersion: .v5_3, + products: [ + try .init(name: "PackageD", type: .library(.dynamic), targets: ["TargetD"]) + ], + targets: [ + try .init(name: "TargetD") + ] + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fileSystem, + manifests: [manifestA, manifestB, manifestC, manifestD], + observabilityScope: observability.topScope + ) + expectNoDiagnostics(observability.diagnostics) + + let output = BufferedOutputByteStream() + SwiftPackageCommand.ShowDependencies.dumpDependenciesOf( + graph: graph, + rootPackage: graph.rootPackages[graph.rootPackages.startIndex], + mode: .dot, + on: output + ) + let dotFormat = output.bytes.description - var alreadyPutOut: Set = [] - for line in dotFormat.split(whereSeparator: { $0.isNewline }) { - if alreadyPutOut.contains(line) { - XCTFail("Same line was already put out: \(line)") + var alreadyPutOut: Set = [] + for line in dotFormat.split(whereSeparator: { $0.isNewline }) { + if alreadyPutOut.contains(line) { + Issue.record("Same line was already put out: \(line)") + } + alreadyPutOut.insert(line) } - alreadyPutOut.insert(line) - } -#if os(Windows) - let pathSep = "\\" -#else - let pathSep = "/" -#endif - let expectedLines: [Substring] = [ - "\"\(pathSep)PackageA\" [label=\"packagea\\n\(pathSep)PackageA\\nunspecified\"]", - "\"\(pathSep)PackageB\" [label=\"packageb\\n\(pathSep)PackageB\\nunspecified\"]", - "\"\(pathSep)PackageC\" [label=\"packagec\\n\(pathSep)PackageC\\nunspecified\"]", - "\"\(pathSep)PackageD\" [label=\"packaged\\n\(pathSep)PackageD\\nunspecified\"]", - "\"\(pathSep)PackageA\" -> \"\(pathSep)PackageB\"", - "\"\(pathSep)PackageA\" -> \"\(pathSep)PackageC\"", - "\"\(pathSep)PackageB\" -> \"\(pathSep)PackageC\"", - "\"\(pathSep)PackageB\" -> \"\(pathSep)PackageD\"", - "\"\(pathSep)PackageC\" -> \"\(pathSep)PackageD\"", - ] - for expectedLine in expectedLines { - XCTAssertTrue(alreadyPutOut.contains(expectedLine), - "Expected line is not found: \(expectedLine)") + #if os(Windows) + let pathSep = "\\" + #else + let pathSep = "/" + #endif + let expectedLines: [Substring] = [ + "\"\(pathSep)PackageA\" [label=\"packagea\\n\(pathSep)PackageA\\nunspecified\"]", + "\"\(pathSep)PackageB\" [label=\"packageb\\n\(pathSep)PackageB\\nunspecified\"]", + "\"\(pathSep)PackageC\" [label=\"packagec\\n\(pathSep)PackageC\\nunspecified\"]", + "\"\(pathSep)PackageD\" [label=\"packaged\\n\(pathSep)PackageD\\nunspecified\"]", + "\"\(pathSep)PackageA\" -> \"\(pathSep)PackageB\"", + "\"\(pathSep)PackageA\" -> \"\(pathSep)PackageC\"", + "\"\(pathSep)PackageB\" -> \"\(pathSep)PackageC\"", + "\"\(pathSep)PackageB\" -> \"\(pathSep)PackageD\"", + "\"\(pathSep)PackageC\" -> \"\(pathSep)PackageD\"", + ] + for expectedLine in expectedLines { + #expect( + alreadyPutOut.contains(expectedLine), + "Expected line is not found: \(expectedLine)" + ) + } } - } - func testShowDependencies_redirectJsonOutput() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let root = tmpPath.appending(components: "root") - let dep = tmpPath.appending(components: "dep") - - // Create root package. - let mainFilePath = root.appending(components: "Sources", "root", "main.swift") - try fs.writeFileContents(mainFilePath, string: "") - try fs.writeFileContents(root.appending("Package.swift"), string: - """ - // swift-tools-version:4.2 - import PackageDescription - let package = Package( - name: "root", - dependencies: [.package(url: "../dep", from: "1.0.0")], - targets: [.target(name: "root", dependencies: ["dep"])] - ) - """ - ) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func showDependencies_redirectJsonOutput( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let root = tmpPath.appending(components: "root") + let dep = tmpPath.appending(components: "dep") + + // Create root package. + let mainFilePath = root.appending(components: "Sources", "root", "main.swift") + try fs.writeFileContents(mainFilePath, string: "") + try fs.writeFileContents( + root.appending("Package.swift"), + string: + """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "root", + dependencies: [.package(url: "../dep", from: "1.0.0")], + targets: [.target(name: "root", dependencies: ["dep"])] + ) + """ + ) - // Create dependency. - try fs.writeFileContents(dep.appending(components: "Sources", "dep", "lib.swift"), string: "") - try fs.writeFileContents(dep.appending("Package.swift"), string: - """ - // swift-tools-version:4.2 - import PackageDescription - let package = Package( - name: "dep", - products: [.library(name: "dep", targets: ["dep"])], - targets: [.target(name: "dep")] - ) - """ - ) + // Create dependency. + try fs.writeFileContents(dep.appending(components: "Sources", "dep", "lib.swift"), string: "") + try fs.writeFileContents( + dep.appending("Package.swift"), + string: + """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "dep", + products: [.library(name: "dep", targets: ["dep"])], + targets: [.target(name: "dep")] + ) + """ + ) - do { - let depGit = GitRepository(path: dep) - try depGit.create() - try depGit.stageEverything() - try depGit.commit() - try depGit.tag(name: "1.0.0") - } + do { + let depGit = GitRepository(path: dep) + try depGit.create() + try depGit.stageEverything() + try depGit.commit() + try depGit.tag(name: "1.0.0") + } - let resultPath = root.appending("result.json") - _ = try await execute(["show-dependencies", "--format", "json", "--output-path", resultPath.pathString ], packagePath: root) + let resultPath = root.appending("result.json") + _ = try await execute( + ["show-dependencies", "--format", "json", "--output-path", resultPath.pathString], + packagePath: root, + configuration: data.config, + buildSystem: data.buildSystem, + ) - XCTAssertFileExists(resultPath) - let jsonOutput: Data = try fs.readFileContents(resultPath) - let json = try JSON(data: jsonOutput) + expectFileExists(at: resultPath) + let jsonOutput: Data = try fs.readFileContents(resultPath) + let json = try JSON(data: jsonOutput) - XCTAssertEqual(json["name"]?.string, "root") - XCTAssertEqual(json["dependencies"]?[0]?["name"]?.string, "dep") + #expect(json["name"]?.string == "root") + #expect(json["dependencies"]?[0]?["name"]?.string == "dep") + } } } - func testInitEmpty() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - try fs.createDirectory(path) - _ = try await execute(["init", "--type", "empty"], packagePath: path) + @Suite( + .tags( + .Feature.Command.Package.Init, + ), + ) + struct InitCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initEmpty( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + try fs.createDirectory(path) + _ = try await execute( + ["init", "--type", "empty"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + ) - XCTAssertFileExists(path.appending("Package.swift")) + expectFileExists(at: path.appending("Package.swift")) + } } - } - func testInitExecutable() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - try fs.createDirectory(path) - _ = try await execute(["init", "--type", "executable"], packagePath: path) + @Test( + .tags( + .Feature.Command.Package.Init, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initExecutable( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + try fs.createDirectory(path) + _ = try await execute( + ["init", "--type", "executable"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + ) - let manifest = path.appending("Package.swift") - let contents: String = try localFileSystem.readFileContents(manifest) - let version = InitPackage.newPackageToolsVersion - let versionSpecifier = "\(version.major).\(version.minor)" - XCTAssertMatch(contents, .prefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) + let manifest = path.appending("Package.swift") + let contents: String = try localFileSystem.readFileContents(manifest) + let version = InitPackage.newPackageToolsVersion + let versionSpecifier = "\(version.major).\(version.minor)" + #expect(contents.hasPrefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) - XCTAssertFileExists(manifest) - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending(("Foo"))), ["Foo.swift"]) + expectFileExists(at: manifest) + #expect(try fs.getDirectoryContents(path.appending("Sources").appending(("Foo"))) == ["Foo.swift"]) + } } - } - func testInitLibrary() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - try fs.createDirectory(path) - _ = try await execute(["init"], packagePath: path) + @Test( + .tags( + .Feature.Command.Package.Init, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initLibrary( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + try fs.createDirectory(path) + _ = try await execute( + ["init"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + ) - XCTAssertFileExists(path.appending("Package.swift")) - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending("Foo")), ["Foo.swift"]) - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Tests")).sorted(), ["FooTests"]) + expectFileExists(at: path.appending("Package.swift")) + #expect(try fs.getDirectoryContents(path.appending("Sources").appending("Foo")) == ["Foo.swift"]) + #expect(try fs.getDirectoryContents(path.appending("Tests")).sorted() == ["FooTests"]) + } } - } - func testInitCustomNameExecutable() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("Foo") - try fs.createDirectory(path) - _ = try await execute(["init", "--name", "CustomName", "--type", "executable"], packagePath: path) + @Test( + .tags( + .Feature.Command.Package.Init, + .Feature.PackageType.Executable, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func initCustomNameExecutable( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("Foo") + try fs.createDirectory(path) + _ = try await execute( + ["init", "--name", "CustomName", "--type", "executable"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + ) - let manifest = path.appending("Package.swift") - let contents: String = try localFileSystem.readFileContents(manifest) - let version = InitPackage.newPackageToolsVersion - let versionSpecifier = "\(version.major).\(version.minor)" - XCTAssertMatch(contents, .prefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) + let manifest = path.appending("Package.swift") + let contents: String = try localFileSystem.readFileContents(manifest) + let version = InitPackage.newPackageToolsVersion + let versionSpecifier = "\(version.major).\(version.minor)" + #expect(contents.hasPrefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n")) - XCTAssertFileExists(manifest) - XCTAssertEqual(try fs.getDirectoryContents(path.appending("Sources").appending("CustomName")), ["CustomName.swift"]) + expectFileExists(at: manifest) + #expect(try fs.getDirectoryContents(path.appending("Sources").appending("CustomName")) == ["CustomName.swift"]) + } } } - // Helper function to arbitrarily assert on manifest content - func assertManifest(_ packagePath: AbsolutePath, _ callback: (String) throws -> Void) throws { - let manifestPath = packagePath.appending("Package.swift") - XCTAssertFileExists(manifestPath) - let contents: String = try localFileSystem.readFileContents(manifestPath) - try callback(contents) - } + @Suite( + .tags( + .Feature.Command.Package.AddDependency, + ), + ) + struct AddDependencyCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddDifferentDependencyWithSameURLTwiceFails( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let url = "https://github.com/swiftlang/swift-syntax.git" + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + dependencies: [ + .package(url: "\(url)", exact: "601.0.1") + ], + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + + try localFileSystem.writeFileContents(path.appending("Package.swift"), string: manifest) - // Helper function to assert content exists in the manifest - func assertManifestContains(_ packagePath: AbsolutePath, _ expected: String) throws { - try assertManifest(packagePath) { manifestContents in - XCTAssertMatch(manifestContents, .contains(expected)) + await expectThrowsCommandExecutionError( + try await execute( + ["add-dependency", url, "--revision", "58e9de4e7b79e67c72a46e164158e3542e570ab6"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("error: unable to add dependency 'https://github.com/swiftlang/swift-syntax.git' because it already exists in the list of dependencies")) + } + } } - } - // Helper function to test adding a URL dependency and asserting the result - func executeAddURLDependencyAndAssert( - packagePath: AbsolutePath, - initialManifest: String? = nil, - url: String, - requirementArgs: [String], - expectedManifestString: String, - ) async throws { - _ = try await execute( - ["add-dependency", url] + requirementArgs, - packagePath: packagePath, - manifest: initialManifest + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - try assertManifestContains(packagePath, expectedManifestString) - } + func packageAddSameDependencyURLTwiceHasNoEffect( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let url = "https://github.com/swiftlang/swift-syntax.git" + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + dependencies: [ + .package(url: "\(url)", exact: "601.0.1"), + ], + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + let expected = #".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"),"# + + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: url, + requirementArgs: ["--exact", "601.0.1"], + expectedManifestString: expected, + buildData: data, + ) - func testPackageAddDifferentDependencyWithSameURLTwiceFails() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) + try expectManifest(path) { + let components = $0.components(separatedBy: expected) + #expect(components.count == 2) + } + } + } - let url = "https://github.com/swiftlang/swift-syntax.git" - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ - .package(url: "\(url)", exact: "601.0.1") - ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddSameDependencyPathTwiceHasNoEffect( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let depPath = "../foo" + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + dependencies: [ + .package(path: "\(depPath)") + ], + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + + let expected = #".package(path: "../foo")"# + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: depPath, + requirementArgs: ["--type", "path"], + expectedManifestString: expected, + buildData: data, ) - """ - - try localFileSystem.writeFileContents(path.appending("Package.swift"), string: manifest) - await XCTAssertThrowsCommandExecutionError( - try await execute(["add-dependency", url, "--revision", "58e9de4e7b79e67c72a46e164158e3542e570ab6"], packagePath: path) - ) { error in - XCTAssertMatch(error.stderr, .contains("error: unable to add dependency 'https://github.com/swiftlang/swift-syntax.git' because it already exists in the list of dependencies")) + try expectManifest(path) { + let components = $0.components(separatedBy: expected) + #expect(components.count == 2) + } } } - } - - func testPackageAddSameDependencyURLTwiceHasNoEffect() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) - let url = "https://github.com/swiftlang/swift-syntax.git" - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ - .package(url: "\(url)", exact: "601.0.1"), - ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddSameDependencyRegistryTwiceHasNoEffect( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let registryId = "foo" + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + dependencies: [ + .package(id: "\(registryId)") + ], + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + + let expected = #".package(id: "foo", exact: "1.0.0")"# + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: registryId, + requirementArgs: ["--type", "registry", "--exact", "1.0.0"], + expectedManifestString: expected, + buildData: data, ) - """ - let expected = #".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"),"# - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: url, - requirementArgs: ["--exact", "601.0.1"], - expectedManifestString: expected - ) - - try assertManifest(path) { - let components = $0.components(separatedBy: expected) - XCTAssertEqual(components.count, 2, "Expected the dependency to be added exactly once.") + try expectManifest(path) { + let components = $0.components(separatedBy: expected) + #expect(components.count == 2) + } } } - } - - func testPackageAddSameDependencyPathTwiceHasNoEffect() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) - let depPath = "../foo" - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ - .package(path: "\(depPath)") - ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + struct PackageAddDependencyTestData { + let url: String + let requirementArgs: CLIArguments + let expectedManifestString: String + } + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + PackageAddDependencyTestData( + // Test adding with --exact using the new helper + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--exact", "1.0.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "1.0.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --exact using the new helper + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--exact", "1.0.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "1.0.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --branch + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--branch", "main"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --revision + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--revision", "58e9de4e7b79e67c72a46e164158e3542e570ab6"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", revision: "58e9de4e7b79e67c72a46e164158e3542e570ab6"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --from + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--from", "1.0.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", from: "1.0.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --from and --to + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--from", "2.0.0", "--to", "2.2.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "2.0.0" ..< "2.2.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --up-to-next-minor-from + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--up-to-next-minor-from", "1.0.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "1.0.0" ..< "1.1.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --up-to-next-minor-from and --to + url: "https://github.com/swiftlang/swift-syntax.git", + requirementArgs: ["--up-to-next-minor-from", "3.0.0", "--to", "3.3.0"], + expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "3.0.0" ..< "3.3.0"),"#, + ), + ], + ) + func packageAddURLDependency( + buildData: BuildData, + testData: PackageAddDependencyTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: testData.url, + requirementArgs: testData.requirementArgs, + expectedManifestString: testData.expectedManifestString, + buildData: buildData, ) - """ + } + } - let expected = #".package(path: "../foo")"# - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: depPath, - requirementArgs: ["--type", "path"], - expectedManifestString: expected - ) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + PackageAddDependencyTestData( + // Add absolute path dependency + url: "/absolute", + requirementArgs: ["--type", "path"], + expectedManifestString: #".package(path: "/absolute"),"#, + ), + PackageAddDependencyTestData( + // Add relative path dependency (operates on the modified manifest) + url: "../relative", + requirementArgs: ["--type", "path"], + expectedManifestString: #".package(path: "../relative"),"#, + ), + ], + ) + func packageAddPathDependency( + buildData: BuildData, + testData: PackageAddDependencyTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: testData.url, + requirementArgs: testData.requirementArgs, + expectedManifestString: testData.expectedManifestString, + buildData: buildData, + ) + } + } - try assertManifest(path) { - let components = $0.components(separatedBy: expected) - XCTAssertEqual(components.count, 2, "Expected the dependency to be added exactly once.") + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + PackageAddDependencyTestData( + // Test adding with --exact + url: "scope.name", + requirementArgs: ["--type", "registry", "--exact", "1.0.0"], + expectedManifestString: #".package(id: "scope.name", exact: "1.0.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --from + url: "scope.name", + requirementArgs: ["--type", "registry", "--from", "1.0.0"], + expectedManifestString: #".package(id: "scope.name", from: "1.0.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --from and --to + url: "scope.name", + requirementArgs: ["--type", "registry", "--from", "2.0.0", "--to", "2.2.0"], + expectedManifestString: #".package(id: "scope.name", "2.0.0" ..< "2.2.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --up-to-next-minor-from + url: "scope.name", + requirementArgs: ["--type", "registry", "--up-to-next-minor-from", "1.0.0"], + expectedManifestString: #".package(id: "scope.name", "1.0.0" ..< "1.1.0"),"#, + ), + PackageAddDependencyTestData( + // Test adding with --up-to-next-minor-from and --to + url: "scope.name", + requirementArgs: ["--type", "registry", "--up-to-next-minor-from", "3.0.0", "--to", "3.3.0"], + expectedManifestString: #".package(id: "scope.name", "3.0.0" ..< "3.3.0"),"#, + ), + ], + ) + func packageAddRegistryDependency( + buildData: BuildData, + testData: PackageAddDependencyTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + let manifest = """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ + try await executeAddURLDependencyAndAssert( + packagePath: path, + initialManifest: manifest, + url: testData.url, + requirementArgs: testData.requirementArgs, + expectedManifestString: testData.expectedManifestString, + buildData: buildData, + ) } } } - func testPackageAddSameDependencyRegistryTwiceHasNoEffect() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) + @Suite( + .tags( + .Feature.Command.Package.AddTarget, + ), + ) + struct AddTargetCommandTests { + @Test( + .tags( + .Feature.TargetType.Executable, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddTarget( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let path = tmpPath.appending("PackageB") + try fs.createDirectory(path) + + try fs.writeFileContents( + path.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client" + ) + """ + ) - let registryId = "foo" - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ - .package(id: "\(registryId)") - ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + _ = try await execute( + ["add-target", "client", "--dependencies", "MyLib", "OtherLib", "--type", "executable"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, ) - """ - let expected = #".package(id: "foo", exact: "1.0.0")"# - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: registryId, - requirementArgs: ["--type", "registry", "--exact", "1.0.0"], - expectedManifestString: expected - ) + let manifest = path.appending("Package.swift") + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) - try assertManifest(path) { - let components = $0.components(separatedBy: expected) - XCTAssertEqual(components.count, 2, "Expected the dependency to be added exactly once.") + #expect(contents.contains(#"targets:"#)) + #expect(contents.contains(#".executableTarget"#)) + #expect(contents.contains(#"name: "client""#)) + #expect(contents.contains(#"dependencies:"#)) + #expect(contents.contains(#""MyLib""#)) + #expect(contents.contains(#""OtherLib""#)) } } - } - - func testPackageAddURLDependency() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddTargetWithoutModuleSourcesFolder( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + let fs = localFileSystem + let manifest = tmpPath.appending("Package.swift") + try fs.writeFileContents( + manifest, + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "SimpleExecutable", + targets: [ + .executableTarget(name: "SimpleExecutable"), + ] + ) + """ ) - """ - // Test adding with --exact using the new helper - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--exact", "1.0.0"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "1.0.0"),"#, - ) + let sourcesFolder = tmpPath.appending("Sources") + try fs.createDirectory(sourcesFolder) - // Test adding with --branch - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--branch", "main"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"),"# - ) + try fs.writeFileContents( + sourcesFolder.appending("main.swift"), + string: + """ + print("Hello World") + """ + ) - // Test adding with --revision - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--revision", "58e9de4e7b79e67c72a46e164158e3542e570ab6"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", revision: "58e9de4e7b79e67c72a46e164158e3542e570ab6"),"# - ) + _ = try await execute( + ["add-target", "client"], + packagePath: tmpPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Test adding with --from - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--from", "1.0.0"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", from: "1.0.0"),"# - ) + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) - // Test adding with --from and --to - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--from", "2.0.0", "--to", "2.2.0"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "2.0.0" ..< "2.2.0"),"# - ) + #expect(contents.contains(#"targets:"#)) + #expect(contents.contains(#".executableTarget"#)) + #expect(contents.contains(#"name: "client""#)) - // Test adding with --up-to-next-minor-from - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--up-to-next-minor-from", "1.0.0"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "1.0.0" ..< "1.1.0"),"# - ) + let fileStructure = try fs.getDirectoryContents(sourcesFolder) + #expect(fileStructure.sorted() == ["SimpleExecutable", "client"]) + #expect(fs.isDirectory(sourcesFolder.appending("SimpleExecutable"))) + #expect(fs.isDirectory(sourcesFolder.appending("client"))) + #expect(try fs.getDirectoryContents(sourcesFolder.appending("SimpleExecutable")) == ["main.swift"]) + #expect(try fs.getDirectoryContents(sourcesFolder.appending("client")) == ["client.swift"]) + } + } - // Test adding with --up-to-next-minor-from and --to - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "https://github.com/swiftlang/swift-syntax.git", - requirementArgs: ["--up-to-next-minor-from", "3.0.0", "--to", "3.3.0"], - expectedManifestString: #".package(url: "https://github.com/swiftlang/swift-syntax.git", "3.0.0" ..< "3.3.0"),"# - ) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func addTargetWithoutManifestThrows( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + await expectThrowsCommandExecutionError( + try await execute( + ["add-target", "client"], + packagePath: tmpPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("error: Could not find Package.swift in this directory or any of its parent directories.")) + } + } } } - func testPackageAddPathDependency() async throws { + @Test( + .tags( + .Feature.Command.Package.AddTargetDependency, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddTargetDependency( + data: BuildData, + ) async throws { try await testWithTemporaryDirectory { tmpPath in let fs = localFileSystem let path = tmpPath.appending("PackageB") try fs.createDirectory(path) - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - targets: [ .target(name: "client", dependencies: [ "library" ]) ] - ) - """ - // Add absolute path dependency - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "/absolute", - requirementArgs: ["--type", "path"], - expectedManifestString: #".package(path: "/absolute"),"# + try fs.writeFileContents( + path.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client", + targets: [ .target(name: "library") ] + ) + """ + ) + try localFileSystem.writeFileContents( + path.appending(components: "Sources", "library", "library.swift"), + string: + """ + public func Foo() { } + """ ) - // Add relative path dependency (operates on the modified manifest) - try await executeAddURLDependencyAndAssert( + _ = try await execute( + ["add-target-dependency", "--package", "other-package", "other-product", "library"], packagePath: path, - initialManifest: manifest, - url: "../relative", - requirementArgs: ["--type", "path"], - expectedManifestString: #".package(path: "../relative"),"# + configuration: data.config, + buildSystem: data.buildSystem, ) + + let manifest = path.appending("Package.swift") + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) + + #expect(contents.contains(#".product(name: "other-product", package: "other-package"#)) } } - func testPackageAddRegistryDependency() async throws { + @Test( + .tags( + .Feature.Command.Package.AddProduct, + .Feature.ProductType.StaticLibrary, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddProduct( + data: BuildData, + ) async throws { try await testWithTemporaryDirectory { tmpPath in let fs = localFileSystem let path = tmpPath.appending("PackageB") try fs.createDirectory(path) - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - targets: [ .target(name: "client", dependencies: [ "library" ]) ] - ) - """ - - // Test adding with --exact - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "scope.name", - requirementArgs: ["--type", "registry", "--exact", "1.0.0"], - expectedManifestString: #".package(id: "scope.name", exact: "1.0.0"),"# - ) - - // Test adding with --from - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "scope.name", - requirementArgs: ["--type", "registry", "--from", "1.0.0"], - expectedManifestString: #".package(id: "scope.name", from: "1.0.0"),"# + try fs.writeFileContents( + path.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "client" + ) + """ ) - // Test adding with --from and --to - try await executeAddURLDependencyAndAssert( + _ = try await execute( + ["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], packagePath: path, - initialManifest: manifest, - url: "scope.name", - requirementArgs: ["--type", "registry", "--from", "2.0.0", "--to", "2.2.0"], - expectedManifestString: #".package(id: "scope.name", "2.0.0" ..< "2.2.0"),"# + configuration: data.config, + buildSystem: data.buildSystem, ) - // Test adding with --up-to-next-minor-from - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "scope.name", - requirementArgs: ["--type", "registry", "--up-to-next-minor-from", "1.0.0"], - expectedManifestString: #".package(id: "scope.name", "1.0.0" ..< "1.1.0"),"# - ) + let manifest = path.appending("Package.swift") + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) - // Test adding with --up-to-next-minor-from and --to - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "scope.name", - requirementArgs: ["--type", "registry", "--up-to-next-minor-from", "3.0.0", "--to", "3.3.0"], - expectedManifestString: #".package(id: "scope.name", "3.0.0" ..< "3.3.0"),"# - ) + #expect(contents.contains(#"products:"#)) + #expect(contents.contains(#".library"#)) + #expect(contents.contains(#"name: "MyLib""#)) + #expect(contents.contains(#"type: .static"#)) + #expect(contents.contains(#"targets:"#)) + #expect(contents.contains(#""MyLib""#)) } } - func testPackageAddTarget() async throws { + @Test( + .tags( + .Feature.Command.Package.AddSetting, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageAddSetting( + data: BuildData, + ) async throws { try await testWithTemporaryDirectory { tmpPath in let fs = localFileSystem - let path = tmpPath.appending("PackageB") + let path = tmpPath.appending("PackageA") try fs.createDirectory(path) - try fs.writeFileContents(path.appending("Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client" - ) - """ + try fs.writeFileContents( + path.appending("Package.swift"), + string: + """ + // swift-tools-version: 6.2 + import PackageDescription + let package = Package( + name: "A", + targets: [ .target(name: "test") ] + ) + """ ) - _ = try await execute(["add-target", "client", "--dependencies", "MyLib", "OtherLib", "--type", "executable"], packagePath: path) + _ = try await execute( + [ + "add-setting", + "--target", "test", + "--swift", "languageMode=6", + "--swift", "upcomingFeature=ExistentialAny:migratable", + "--swift", "experimentalFeature=TrailingCommas", + "--swift", "StrictMemorySafety", + ], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, + + ) let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) + expectFileExists(at: manifest) let contents: String = try fs.readFileContents(manifest) - XCTAssertMatch(contents, .contains(#"targets:"#)) - XCTAssertMatch(contents, .contains(#".executableTarget"#)) - XCTAssertMatch(contents, .contains(#"name: "client""#)) - XCTAssertMatch(contents, .contains(#"dependencies:"#)) - XCTAssertMatch(contents, .contains(#""MyLib""#)) - XCTAssertMatch(contents, .contains(#""OtherLib""#)) + #expect(contents.contains(#"swiftSettings:"#)) + #expect(contents.contains(#".swiftLanguageMode(.v6)"#)) + #expect(contents.contains(#".enableUpcomingFeature("ExistentialAny:migratable")"#)) + #expect(contents.contains(#".enableExperimentalFeature("TrailingCommas")"#)) + #expect(contents.contains(#".strictMemorySafety()"#)) } } - func testPackageAddTargetWithoutModuleSourcesFolder() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let manifest = tmpPath.appending("Package.swift") - try fs.writeFileContents(manifest, string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "SimpleExecutable", - targets: [ - .executableTarget(name: "SimpleExecutable"), - ] - ) - """ - ) - - let sourcesFolder = tmpPath.appending("Sources") - try fs.createDirectory(sourcesFolder) + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8774", relationship: .defect), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8380", relationship: .defect), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8416", relationship: .defect), // swift run linux issue with swift build, + .tags( + .Feature.Command.Package.Edit, + .Feature.Command.Package.Unedit, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageEditAndUnedit( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "Miscellaneous/PackageEdit") { fixturePath in + let fooPath = fixturePath.appending("foo") + func build() async throws -> (stdout: String, stderr: String) { + return try await executeSwiftBuild( + fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } - try fs.writeFileContents(sourcesFolder.appending("main.swift"), string: - """ - print("Hello World") - """ - ) + // Put bar and baz in edit mode. + _ = try await execute( + ["edit", "bar", "--branch", "bugfix"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + _ = try await execute( + ["edit", "baz", "--branch", "bugfix"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - _ = try await execute(["add-target", "client"], packagePath: tmpPath) + // Path to the executable. + let exec = [fooPath.appending(components: [".build", try UserToolchain.default.targetTriple.platformBuildPathComponent] + data.buildSystem.binPathSuffixes(for: data.config) + ["foo"]).pathString] + + // We should see it now in packages directory. + let editsPath = fooPath.appending(components: "Packages", "bar") + expectDirectoryExists(at: editsPath) + + let bazEditsPath = fooPath.appending(components: "Packages", "baz") + expectDirectoryExists(at: bazEditsPath) + // Removing baz externally should just emit an warning and not a build failure. + try localFileSystem.removeFileTree(bazEditsPath) + + // Do a modification in bar and build. + try localFileSystem.writeFileContents(editsPath.appending(components: "Sources", "bar.swift"), bytes: "public let theValue = 88888\n") + let (_, stderr) = try await build() + + #expect(stderr.contains("dependency 'baz' was being edited but is missing; falling back to original checkout")) + // We should be able to see that modification now. + try await withKnownIssue { + let processValue = try await AsyncProcess.checkNonZeroExit(arguments: exec) + #expect(processValue == "88888\(ProcessInfo.EOL)") + } when: { + ProcessInfo.hostOperatingSystem == .linux && data.buildSystem == .swiftbuild + } + // The branch of edited package should be the one we provided when putting it in edit mode. + let editsRepo = GitRepository(path: editsPath) + #expect(try editsRepo.currentBranch() == "bugfix") - XCTAssertFileExists(manifest) - let contents: String = try fs.readFileContents(manifest) + // It shouldn't be possible to unedit right now because of uncommitted changes. + do { + _ = try await execute( + ["unedit", "bar"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + Issue.record("Unexpected unedit success") + } catch {} - XCTAssertMatch(contents, .contains(#"targets:"#)) - XCTAssertMatch(contents, .contains(#".executableTarget"#)) - XCTAssertMatch(contents, .contains(#"name: "client""#)) + try editsRepo.stageEverything() + try editsRepo.commit() - let fileStructure = try fs.getDirectoryContents(sourcesFolder) - XCTAssertEqual(fileStructure.sorted(), ["SimpleExecutable", "client"]) - XCTAssertTrue(fs.isDirectory(sourcesFolder.appending("SimpleExecutable"))) - XCTAssertTrue(fs.isDirectory(sourcesFolder.appending("client"))) - XCTAssertEqual(try fs.getDirectoryContents(sourcesFolder.appending("SimpleExecutable")), ["main.swift"]) - XCTAssertEqual(try fs.getDirectoryContents(sourcesFolder.appending("client")), ["client.swift"]) - } - } + // It shouldn't be possible to unedit right now because of unpushed changes. + do { + _ = try await execute( + ["unedit", "bar"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + Issue.record("Unexpected unedit success") + } catch {} + + // Push the changes. + try editsRepo.push(remote: "origin", branch: "bugfix") + + // We should be able to unedit now. + _ = try await execute( + ["unedit", "bar"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) - func testAddTargetWithoutManifestThrows() async throws { - try await testWithTemporaryDirectory { tmpPath in - await XCTAssertThrowsCommandExecutionError(try await execute(["add-target", "client"], packagePath: tmpPath)) { error in - XCTAssertMatch(error.stderr, .contains("error: Could not find Package.swift in this directory or any of its parent directories.")) + // Test editing with a path i.e. ToT development. + let bazTot = fixturePath.appending("tot") + try await execute( + ["edit", "baz", "--path", bazTot.pathString], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(localFileSystem.exists(bazTot)) + #expect(localFileSystem.isSymlink(bazEditsPath)) + + // Edit a file in baz ToT checkout. + let bazTotPackageFile = bazTot.appending("Package.swift") + var content: String = try localFileSystem.readFileContents(bazTotPackageFile) + content += "\n// Edited." + try localFileSystem.writeFileContents(bazTotPackageFile, string: content) + + // Unediting baz will remove the symlink but not the checked out package. + try await execute( + ["unedit", "baz"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(localFileSystem.exists(bazTot)) + #expect(!localFileSystem.isSymlink(bazEditsPath)) + + // Check that on re-editing with path, we don't make a new clone. + try await execute( + ["edit", "baz", "--path", bazTot.pathString], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(localFileSystem.isSymlink(bazEditsPath)) + #expect(try localFileSystem.readFileContents(bazTotPackageFile) == content) } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } - } - - func testPackageAddTargetDependency() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) - - try fs.writeFileContents(path.appending("Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - targets: [ .target(name: "library") ] - ) - """ - ) - try localFileSystem.writeFileContents(path.appending(components: "Sources", "library", "library.swift"), string: - """ - public func Foo() { } - """ - ) - - _ = try await execute(["add-target-dependency", "--package", "other-package", "other-product", "library"], packagePath: path) - - let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - let contents: String = try fs.readFileContents(manifest) - XCTAssertMatch(contents, .contains(#".product(name: "other-product", package: "other-package"#)) - } } - func testPackageAddProduct() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8774", relationship: .defect), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8380", relationship: .defect), + .tags( + .Feature.Command.Package.Clean, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageClean( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") - try fs.writeFileContents(path.appending("Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client" + // Build it. + try await executeSwiftBuild( + packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, ) - """ - ) - - _ = try await execute(["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], packagePath: path) - - let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - let contents: String = try fs.readFileContents(manifest) - - XCTAssertMatch(contents, .contains(#"products:"#)) - XCTAssertMatch(contents, .contains(#".library"#)) - XCTAssertMatch(contents, .contains(#"name: "MyLib""#)) - XCTAssertMatch(contents, .contains(#"type: .static"#)) - XCTAssertMatch(contents, .contains(#"targets:"#)) - XCTAssertMatch(contents, .contains(#""MyLib""#)) - } - } - - func testPackageAddSetting() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageA") - try fs.createDirectory(path) - - try fs.writeFileContents(path.appending("Package.swift"), string: - """ - // swift-tools-version: 6.2 - import PackageDescription - let package = Package( - name: "A", - targets: [ .target(name: "test") ] + let buildPath = packageRoot.appending(".build") + let binFile = buildPath.appending(components: [try UserToolchain.default.targetTriple.platformBuildPathComponent] + data.buildSystem.binPathSuffixes(for: data.config) + [executableName("Bar")]) + expectFileExists(at: binFile) + #expect(localFileSystem.isDirectory(buildPath)) + + // Clean, and check for removal of the build directory but not Packages. + _ = try await execute( + ["clean"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + expectFileDoesNotExists(at: binFile) + // Clean again to ensure we get no error. + _ = try await execute( + ["clean"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, ) - """ - ) - - _ = try await execute([ - "add-setting", - "--target", "test", - "--swift", "languageMode=6", - "--swift", "upcomingFeature=ExistentialAny:migratable", - "--swift", "experimentalFeature=TrailingCommas", - "--swift", "StrictMemorySafety" - ], packagePath: path) - - let manifest = path.appending("Package.swift") - XCTAssertFileExists(manifest) - let contents: String = try fs.readFileContents(manifest) - - XCTAssertMatch(contents, .contains(#"swiftSettings:"#)) - XCTAssertMatch(contents, .contains(#".swiftLanguageMode(.v6)"#)) - XCTAssertMatch(contents, .contains(#".enableUpcomingFeature("ExistentialAny:migratable")"#)) - XCTAssertMatch(contents, .contains(#".enableExperimentalFeature("TrailingCommas")"#)) - XCTAssertMatch(contents, .contains(#".strictMemorySafety()"#)) - } - } - - func testPackageEditAndUnedit() async throws { - try await fixtureXCTest(name: "Miscellaneous/PackageEdit") { fixturePath in - let fooPath = fixturePath.appending("foo") - func build() async throws -> (stdout: String, stderr: String) { - return try await executeSwiftBuild(fooPath, buildSystem: self.buildSystemProvider) } - - // Put bar and baz in edit mode. - _ = try await self.execute(["edit", "bar", "--branch", "bugfix"], packagePath: fooPath) - _ = try await self.execute(["edit", "baz", "--branch", "bugfix"], packagePath: fooPath) - - // Path to the executable. - let exec = [fooPath.appending(components: [".build", try UserToolchain.default.targetTriple.platformBuildPathComponent] + self.buildSystemProvider.binPathSuffixes(for: .debug) + ["foo"]).pathString] - - // We should see it now in packages directory. - let editsPath = fooPath.appending(components: "Packages", "bar") - XCTAssertDirectoryExists(editsPath) - - let bazEditsPath = fooPath.appending(components: "Packages", "baz") - XCTAssertDirectoryExists(bazEditsPath) - // Removing baz externally should just emit an warning and not a build failure. - try localFileSystem.removeFileTree(bazEditsPath) - - // Do a modification in bar and build. - try localFileSystem.writeFileContents(editsPath.appending(components: "Sources", "bar.swift"), bytes: "public let theValue = 88888\n") - let (_, stderr) = try await build() - - XCTAssertMatch(stderr, .contains("dependency 'baz' was being edited but is missing; falling back to original checkout")) - // We should be able to see that modification now. - try await XCTAssertAsyncEqual(try await AsyncProcess.checkNonZeroExit(arguments: exec), "88888\(ProcessInfo.EOL)") - // The branch of edited package should be the one we provided when putting it in edit mode. - let editsRepo = GitRepository(path: editsPath) - XCTAssertEqual(try editsRepo.currentBranch(), "bugfix") - - // It shouldn't be possible to unedit right now because of uncommitted changes. - do { - _ = try await self.execute(["unedit", "bar"], packagePath: fooPath) - XCTFail("Unexpected unedit success") - } catch {} - - try editsRepo.stageEverything() - try editsRepo.commit() - - // It shouldn't be possible to unedit right now because of unpushed changes. - do { - _ = try await self.execute(["unedit", "bar"], packagePath: fooPath) - XCTFail("Unexpected unedit success") - } catch {} - - // Push the changes. - try editsRepo.push(remote: "origin", branch: "bugfix") - - // We should be able to unedit now. - _ = try await self.execute(["unedit", "bar"], packagePath: fooPath) - - // Test editing with a path i.e. ToT development. - let bazTot = fixturePath.appending("tot") - try await self.execute(["edit", "baz", "--path", bazTot.pathString], packagePath: fooPath) - XCTAssertTrue(localFileSystem.exists(bazTot)) - XCTAssertTrue(localFileSystem.isSymlink(bazEditsPath)) - - // Edit a file in baz ToT checkout. - let bazTotPackageFile = bazTot.appending("Package.swift") - var content: String = try localFileSystem.readFileContents(bazTotPackageFile) - content += "\n// Edited." - try localFileSystem.writeFileContents(bazTotPackageFile, string: content) - - // Unediting baz will remove the symlink but not the checked out package. - try await self.execute(["unedit", "baz"], packagePath: fooPath) - XCTAssertTrue(localFileSystem.exists(bazTot)) - XCTAssertFalse(localFileSystem.isSymlink(bazEditsPath)) - - // Check that on re-editing with path, we don't make a new clone. - try await self.execute(["edit", "baz", "--path", bazTot.pathString], packagePath: fooPath) - XCTAssertTrue(localFileSystem.isSymlink(bazEditsPath)) - XCTAssertEqual(try localFileSystem.readFileContents(bazTotPackageFile), content) - } - } - - func testPackageClean() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - - // Build it. - await XCTAssertBuilds(packageRoot, buildSystem: self.buildSystemProvider) - let buildPath = packageRoot.appending(".build") - let debugBinFile = buildPath.appending(components: [try UserToolchain.default.targetTriple.platformBuildPathComponent] + self.buildSystemProvider.binPathSuffixes(for: .debug) + [executableName("Bar")]) - XCTAssertFileExists(debugBinFile) - XCTAssert(localFileSystem.isDirectory(buildPath)) - - // Clean, and check for removal of the build directory but not Packages. - _ = try await execute(["clean"], packagePath: packageRoot) - XCTAssertNoSuchPath(debugBinFile) - // Clean again to ensure we get no error. - _ = try await execute(["clean"], packagePath: packageRoot) + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - func testPackageReset() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - - // Build it. - await XCTAssertBuilds(packageRoot, buildSystem: self.buildSystemProvider) - let buildPath = packageRoot.appending(".build") - let binFile = buildPath.appending(components: [try UserToolchain.default.targetTriple.platformBuildPathComponent] + self.buildSystemProvider.binPathSuffixes(for: .debug) + [executableName("Bar")]) - XCTAssertFileExists(binFile) - XCTAssert(localFileSystem.isDirectory(buildPath)) - // Clean, and check for removal of the build directory but not Packages. - - _ = try await execute(["clean"], packagePath: packageRoot) - XCTAssertNoSuchPath(binFile) - XCTAssertFalse(try localFileSystem.getDirectoryContents(buildPath.appending("repositories")).isEmpty) - - // Fully clean. - _ = try await execute(["reset"], packagePath: packageRoot) - XCTAssertFalse(localFileSystem.isDirectory(buildPath)) + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8774", relationship: .defect), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8380", relationship: .defect), + .tags( + .Feature.Command.Build, + .Feature.Command.Package.Reset, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageReset( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") - // Test that we can successfully run reset again. - _ = try await execute(["reset"], packagePath: packageRoot) + // Build it. + try await executeSwiftBuild( + packageRoot, + configuration: data.config, + buildSystem: data.buildSystem + ) + let buildPath = packageRoot.appending(".build") + let binFile = buildPath.appending(components: [try UserToolchain.default.targetTriple.platformBuildPathComponent] + data.buildSystem.binPathSuffixes(for: data.config) + [executableName("Bar")]) + expectFileExists(at: binFile) + #expect(localFileSystem.isDirectory(buildPath)) + // Clean, and check for removal of the build directory but not Packages. + + _ = try await execute( + ["clean"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + expectFileDoesNotExists(at: binFile) + try #expect(!localFileSystem.getDirectoryContents(buildPath.appending("repositories")).isEmpty) + + // Fully clean. + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(!localFileSystem.isDirectory(buildPath)) + + // Test that we can successfully run reset again. + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - func testResolvingBranchAndRevision() async throws { - try await fixtureXCTest(name: "Miscellaneous/PackageEdit") { fixturePath in + @Test( + .tags( + .Feature.Command.Package.Resolve, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func resolvingBranchAndRevision( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/PackageEdit") { fixturePath in let fooPath = fixturePath.appending("foo") @discardableResult - func execute(_ args: String..., printError: Bool = true) async throws -> String { - return try await self.execute([] + args, packagePath: fooPath).stdout + func localExecute(_ args: String..., printError: Bool = true) async throws -> String { + return try await execute( + [] + args, + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout } - try await execute("update") + try await localExecute("update") let packageResolvedFile = fooPath.appending("Package.resolved") - XCTAssertFileExists(packageResolvedFile) + expectFileExists(at: packageResolvedFile) // Update bar repo. let barPath = fixturePath.appending("bar") @@ -1562,7 +2414,7 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Try to resolve `bar` at a branch. do { - try await execute("resolve", "bar", "--branch", "YOLO") + try await localExecute("resolve", "bar", "--branch", "YOLO") let resolvedPackagesStore = try ResolvedPackagesStore( packageResolvedFile: packageResolvedFile, workingDirectory: fixturePath, @@ -1571,12 +2423,12 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { ) let state = ResolvedPackagesStore.ResolutionState.branch(name: "YOLO", revision: yoloRevision.identifier) let identity = PackageIdentity(path: barPath) - XCTAssertEqual(resolvedPackagesStore.resolvedPackages[identity]?.state, state) + #expect(resolvedPackagesStore.resolvedPackages[identity]?.state == state) } // Try to resolve `bar` at a revision. do { - try await execute("resolve", "bar", "--revision", yoloRevision.identifier) + try await localExecute("resolve", "bar", "--revision", yoloRevision.identifier) let resolvedPackagesStore = try ResolvedPackagesStore( packageResolvedFile: packageResolvedFile, workingDirectory: fixturePath, @@ -1585,189 +2437,231 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { ) let state = ResolvedPackagesStore.ResolutionState.revision(yoloRevision.identifier) let identity = PackageIdentity(path: barPath) - XCTAssertEqual(resolvedPackagesStore.resolvedPackages[identity]?.state, state) + #expect(resolvedPackagesStore.resolvedPackages[identity]?.state == state) } // Try to resolve `bar` at a bad revision. - do { - try await execute("resolve", "bar", "--revision", "xxxxx") - XCTFail() - } catch {} + await #expect(throws: (any Error).self) { + try await localExecute("resolve", "bar", "--revision", "xxxxx") + } } } - func testPackageResolved() async throws { - try await fixtureXCTest(name: "Miscellaneous/PackageEdit") { fixturePath in - let fooPath = fixturePath.appending("foo") - let exec = [fooPath.appending( - components: - [ - ".build", - try UserToolchain.default.targetTriple.platformBuildPathComponent, - ] + - self.buildSystemProvider.binPathSuffixes(for: .debug) + - [ - "foo", + @Test( + // windows long path issue + .issue("https://github.com/swiftlang/swift-package-manager/issues/8774", relationship: .defect), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8380", relationship: .defect), + .tags( + .Feature.Command.Build, + .Feature.Command.Package.Resolve, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageResolved( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "Miscellaneous/PackageEdit") { fixturePath in + let fooPath = fixturePath.appending("foo") + let exec = [ + fooPath.appending( + components: [ + ".build", + try UserToolchain.default.targetTriple.platformBuildPathComponent, + ] + data.buildSystem.binPathSuffixes(for: data.config) + [ + "foo" + ] + ).pathString ] - ).pathString] - // Build and check. - _ = try await executeSwiftBuild( - fooPath, - buildSystem: self.buildSystemProvider, - ) - try await XCTAssertAsyncEqual(try await AsyncProcess.checkNonZeroExit(arguments: exec).spm_chomp(), "\(5)") + // Build and check. + _ = try await executeSwiftBuild( + fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try await withKnownIssue { + let value = try await AsyncProcess.checkNonZeroExit(arguments: exec).spm_chomp() + #expect(value == "\(5)") + } when: { + ProcessInfo.hostOperatingSystem == .linux && data.buildSystem == .swiftbuild + } - // Get path to `bar` checkout. - let barPath = try SwiftPM.packagePath(for: "bar", packageRoot: fooPath) + // Get path to `bar` checkout. + let barPath = try SwiftPM.packagePath(for: "bar", packageRoot: fooPath) - // Checks the content of checked out `bar.swift`. - func checkBar(_ value: Int, file: StaticString = #file, line: UInt = #line) throws { - let contents: String = try localFileSystem.readFileContents(barPath.appending(components: "Sources", "bar.swift")) - XCTAssertTrue(contents.spm_chomp().hasSuffix("\(value)"), "got \(contents)", file: file, line: line) - } + // Checks the content of checked out `bar.swift`. + func checkBar(_ value: Int, sourceLocation: SourceLocation = #_sourceLocation) throws { + let contents: String = try localFileSystem.readFileContents(barPath.appending(components: "Sources", "bar.swift")) + #expect(contents.spm_chomp().hasSuffix("\(value)"), "got \(contents)", sourceLocation: sourceLocation) + } - // We should see a `Package.resolved` file now. - let packageResolvedFile = fooPath.appending("Package.resolved") - XCTAssertFileExists(packageResolvedFile) + // We should see a `Package.resolved` file now. + let packageResolvedFile = fooPath.appending("Package.resolved") + expectFileExists(at: packageResolvedFile) - // Test `Package.resolved` file. - do { - let resolvedPackagesStore = try ResolvedPackagesStore( - packageResolvedFile: packageResolvedFile, - workingDirectory: fixturePath, - fileSystem: localFileSystem, - mirrors: .init() - ) - XCTAssertEqual(resolvedPackagesStore.resolvedPackages.count, 2) - for pkg in ["bar", "baz"] { - let path = try SwiftPM.packagePath(for: pkg, packageRoot: fooPath) - let resolvedPackage = resolvedPackagesStore.resolvedPackages[PackageIdentity(path: path)]! - XCTAssertEqual(resolvedPackage.packageRef.identity, PackageIdentity(path: path)) - guard case .localSourceControl(let path) = resolvedPackage.packageRef.kind, path.pathString.hasSuffix(pkg) else { - return XCTFail("invalid resolved package location \(path)") + // Test `Package.resolved` file. + do { + let resolvedPackagesStore = try ResolvedPackagesStore( + packageResolvedFile: packageResolvedFile, + workingDirectory: fixturePath, + fileSystem: localFileSystem, + mirrors: .init() + ) + #expect(resolvedPackagesStore.resolvedPackages.count == 2) + for pkg in ["bar", "baz"] { + let path = try SwiftPM.packagePath(for: pkg, packageRoot: fooPath) + let resolvedPackage = resolvedPackagesStore.resolvedPackages[PackageIdentity(path: path)]! + #expect(resolvedPackage.packageRef.identity == PackageIdentity(path: path)) + guard case .localSourceControl(let path) = resolvedPackage.packageRef.kind, path.pathString.hasSuffix(pkg) else { + Issue.record("invalid resolved package location \(path)") + return + } + switch resolvedPackage.state { + case .version(let version, revision: _): + #expect(version == "1.2.3") + default: + Issue.record("invalid `Package.resolved` state") + } } - switch resolvedPackage.state { + } + + @discardableResult + func localExecute(_ args: String...) async throws -> String { + return try await execute( + [] + args, + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout + } + + // Try to pin bar. + do { + try await localExecute("resolve", "bar") + let resolvedPackagesStore = try ResolvedPackagesStore( + packageResolvedFile: packageResolvedFile, + workingDirectory: fixturePath, + fileSystem: localFileSystem, + mirrors: .init() + ) + let identity = PackageIdentity(path: barPath) + // let resolvedPackageIdentify = try #require(resolvedPackagesStore.resolvedPackages[identity]) + // switch resolvedPackageIdentify.state { + switch resolvedPackagesStore.resolvedPackages[identity]?.state { case .version(let version, revision: _): - XCTAssertEqual(version, "1.2.3") + #expect(version == "1.2.3") default: - XCTFail("invalid `Package.resolved` state") + Issue.record("invalid resolved package state") } } - } - - @discardableResult - func execute(_ args: String...) async throws -> String { - return try await self.execute([] + args, packagePath: fooPath).stdout - } - // Try to pin bar. - do { - try await execute("resolve", "bar") - let resolvedPackagesStore = try ResolvedPackagesStore( - packageResolvedFile: packageResolvedFile, - workingDirectory: fixturePath, - fileSystem: localFileSystem, - mirrors: .init() - ) - let identity = PackageIdentity(path: barPath) - switch resolvedPackagesStore.resolvedPackages[identity]?.state { - case .version(let version, revision: _): - XCTAssertEqual(version, "1.2.3") - default: - XCTFail("invalid resolved package state") + // Update bar repo. + do { + let barPath = fixturePath.appending("bar") + let barRepo = GitRepository(path: barPath) + try localFileSystem.writeFileContents(barPath.appending(components: "Sources", "bar.swift"), bytes: "public let theValue = 6\n") + try barRepo.stageEverything() + try barRepo.commit() + try barRepo.tag(name: "1.2.4") } - } - - // Update bar repo. - do { - let barPath = fixturePath.appending("bar") - let barRepo = GitRepository(path: barPath) - try localFileSystem.writeFileContents(barPath.appending(components: "Sources", "bar.swift"), bytes: "public let theValue = 6\n") - try barRepo.stageEverything() - try barRepo.commit() - try barRepo.tag(name: "1.2.4") - } - // Running `package update` should update the package. - do { - try await execute("update") - try checkBar(6) - } + // Running `package update` should update the package. + do { + try await localExecute("update") + try checkBar(6) + } - // We should be able to revert to a older version. - do { - try await execute("resolve", "bar", "--version", "1.2.3") - let resolvedPackagesStore = try ResolvedPackagesStore( - packageResolvedFile: packageResolvedFile, - workingDirectory: fixturePath, - fileSystem: localFileSystem, - mirrors: .init() - ) - let identity = PackageIdentity(path: barPath) - switch resolvedPackagesStore.resolvedPackages[identity]?.state { - case .version(let version, revision: _): - XCTAssertEqual(version, "1.2.3") - default: - XCTFail("invalid resolved package state") + // We should be able to revert to a older version. + do { + try await localExecute("resolve", "bar", "--version", "1.2.3") + let resolvedPackagesStore = try ResolvedPackagesStore( + packageResolvedFile: packageResolvedFile, + workingDirectory: fixturePath, + fileSystem: localFileSystem, + mirrors: .init() + ) + let identity = PackageIdentity(path: barPath) + switch resolvedPackagesStore.resolvedPackages[identity]?.state { + case .version(let version, revision: _): + #expect(version == "1.2.3") + default: + Issue.record("invalid resolved package state") + } + try checkBar(5) } - try checkBar(5) - } - // Try resolving a dependency which is in edit mode. - do { - try await execute("edit", "bar", "--branch", "bugfix") - await XCTAssertThrowsCommandExecutionError(try await execute("resolve", "bar")) { error in - XCTAssertMatch(error.stderr, .contains("error: edited dependency 'bar' can't be resolved")) + // Try resolving a dependency which is in edit mode. + do { + try await localExecute("edit", "bar", "--branch", "bugfix") + await expectThrowsCommandExecutionError(try await localExecute("resolve", "bar")) { error in + #expect(error.stderr.contains("error: edited dependency 'bar' can't be resolved")) + } + try await localExecute("unedit", "bar") } - try await execute("unedit", "bar") } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - func testOnlyUseVersionsFromResolvedFileFetchesWithExistingState() async throws { - try XCTSkipOnWindows(because: "error: Package.resolved file is corrupted or malformed, needs investigation") + @Test( + .tags( + .Feature.Command.Package.Resolve, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func onlyUseVersionsFromResolvedFileFetchesWithExistingState( + data: BuildData, + ) async throws { + // try XCTSkipOnWindows(because: "error: Package.resolved file is corrupted or malformed, needs investigation") func writeResolvedFile(packageDir: AbsolutePath, repositoryURL: String, revision: String, version: String) throws { - try localFileSystem.writeFileContents(packageDir.appending("Package.resolved"), string: - """ - { - "object": { - "pins": [ - { - "package": "library", - "repositoryURL": "\(repositoryURL)", - "state": { - "branch": null, - "revision": "\(revision)", - "version": "\(version)" - } - } - ] - }, - "version": 1 - } - """ + try localFileSystem.writeFileContents( + packageDir.appending("Package.resolved"), + string: + """ + { + "object": { + "pins": [ + { + "package": "library", + "repositoryURL": "\(repositoryURL)", + "state": { + "branch": null, + "revision": "\(revision)", + "version": "\(version)" + } + } + ] + }, + "version": 1 + } + """ ) } try await testWithTemporaryDirectory { tmpPath in let packageDir = tmpPath.appending(components: "library") - try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: - """ - // swift-tools-version:5.0 - import PackageDescription - let package = Package( - name: "library", - products: [ .library(name: "library", targets: ["library"]) ], - targets: [ .target(name: "library") ] - ) - """ + try localFileSystem.writeFileContents( + packageDir.appending("Package.swift"), + string: + """ + // swift-tools-version:5.0 + import PackageDescription + let package = Package( + name: "library", + products: [ .library(name: "library", targets: ["library"]) ], + targets: [ .target(name: "library") ] + ) + """ ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "library", "library.swift"), string: - """ - public func Foo() { } - """ + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "library", "library.swift"), + string: + """ + public func Foo() { } + """ ) let depGit = GitRepository(path: packageDir) @@ -1780,35 +2674,46 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { let repositoryURL = "file://\(packageDir.pathString)" let clientDir = tmpPath.appending(components: "client") - try localFileSystem.writeFileContents(clientDir.appending("Package.swift"), string: - """ - // swift-tools-version:5.0 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ .package(url: "\(repositoryURL)", from: "1.0.0") ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] - ) - """ + try localFileSystem.writeFileContents( + clientDir.appending("Package.swift"), + string: + """ + // swift-tools-version:5.0 + import PackageDescription + let package = Package( + name: "client", + dependencies: [ .package(url: "\(repositoryURL)", from: "1.0.0") ], + targets: [ .target(name: "client", dependencies: [ "library" ]) ] + ) + """ ) - try localFileSystem.writeFileContents(clientDir.appending(components: "Sources", "client", "main.swift"), string: - """ - print("hello") - """ + try localFileSystem.writeFileContents( + clientDir.appending(components: "Sources", "client", "main.swift"), + string: + """ + print("hello") + """ ) // Initial resolution with clean state. do { try writeResolvedFile(packageDir: clientDir, repositoryURL: repositoryURL, revision: initialRevision, version: "1.0.0") - let (_, err) = try await execute(["resolve", "--only-use-versions-from-resolved-file"], packagePath: clientDir) - XCTAssertMatch(err, .contains("Fetching \(repositoryURL)")) + let (_, err) = try await execute( + ["resolve", "--only-use-versions-from-resolved-file"], + packagePath: clientDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(err.contains("Fetching \(repositoryURL)")) } // Make a change to the dependency and tag a new version. - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "library", "library.swift"), string: - """ - public func Best() { } - """ + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "library", "library.swift"), + string: + """ + public func Best() { } + """ ) try depGit.stageEverything() try depGit.commit() @@ -1818,22 +2723,41 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Require new version but re-use existing state that hasn't fetched the latest revision, yet. do { try writeResolvedFile(packageDir: clientDir, repositoryURL: repositoryURL, revision: updatedRevision, version: "1.0.1") - let (_, err) = try await execute(["resolve", "--only-use-versions-from-resolved-file"], packagePath: clientDir) - XCTAssertNoMatch(err, .contains("Fetching \(repositoryURL)")) - XCTAssertMatch(err, .contains("Updating \(repositoryURL)")) + let (_, err) = try await execute( + ["resolve", "--only-use-versions-from-resolved-file"], + packagePath: clientDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(!err.contains("Fetching \(repositoryURL)")) + #expect(err.contains("Updating \(repositoryURL)")) } // And again do { - let (_, err) = try await execute(["resolve", "--only-use-versions-from-resolved-file"], packagePath: clientDir) - XCTAssertNoMatch(err, .contains("Updating \(repositoryURL)")) - XCTAssertNoMatch(err, .contains("Fetching \(repositoryURL)")) + let (_, err) = try await execute( + ["resolve", "--only-use-versions-from-resolved-file"], + packagePath: clientDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(!err.contains("Updating \(repositoryURL)")) + #expect(!err.contains("Fetching \(repositoryURL)")) } } } - func testSymlinkedDependency() async throws { + @Test( + .tags( + .Feature.Command.Build, + .Feature.Command.Package.Resolve, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func symlinkedDependency( + data: BuildData, + ) async throws { try await testWithTemporaryDirectory { path in let fs = localFileSystem let root = path.appending(components: "root") @@ -1842,31 +2766,35 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Create root package. try fs.writeFileContents(root.appending(components: "Sources", "root", "main.swift"), string: "") - try fs.writeFileContents(root.appending("Package.swift"), string: - """ - // swift-tools-version:4.2 - import PackageDescription - let package = Package( - name: "root", - dependencies: [.package(url: "../depSym", from: "1.0.0")], - targets: [.target(name: "root", dependencies: ["dep"])] - ) + try fs.writeFileContents( + root.appending("Package.swift"), + string: + """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "root", + dependencies: [.package(url: "../depSym", from: "1.0.0")], + targets: [.target(name: "root", dependencies: ["dep"])] + ) - """ + """ ) // Create dependency. try fs.writeFileContents(dep.appending(components: "Sources", "dep", "lib.swift"), string: "") - try fs.writeFileContents(dep.appending("Package.swift"), string: - """ - // swift-tools-version:4.2 - import PackageDescription - let package = Package( - name: "dep", - products: [.library(name: "dep", targets: ["dep"])], - targets: [.target(name: "dep")] - ) - """ + try fs.writeFileContents( + dep.appending("Package.swift"), + string: + """ + // swift-tools-version:4.2 + import PackageDescription + let package = Package( + name: "dep", + products: [.library(name: "dep", targets: ["dep"])], + targets: [.target(name: "dep")] + ) + """ ) do { let depGit = GitRepository(path: dep) @@ -1879,227 +2807,381 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Create symlink to the dependency. try fs.createSymbolicLink(depSym, pointingAt: dep, relative: false) - _ = try await execute(["resolve"], packagePath: root) + _ = try await execute( + ["resolve"], + packagePath: root, + configuration: data.config, + buildSystem: data.buildSystem, + ) } } - func testMirrorConfigDeprecation() async throws { - try await testWithTemporaryDirectory { fixturePath in - localFileSystem.createEmptyFiles(at: fixturePath, files: - "/Sources/Foo/Foo.swift", - "/Package.swift" - ) + @Suite( + .tags( + .Feature.Command.Package.Config, + ), + ) + struct ConfigCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func mirrorConfigDeprecation( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { fixturePath in + localFileSystem.createEmptyFiles( + at: fixturePath, + files: + "/Sources/Foo/Foo.swift", + "/Package.swift" + ) - let (_, stderr) = try await execute(["config", "set-mirror", "--package-url", "https://github.com/foo/bar", "--mirror-url", "https://mygithub.com/foo/bar"], packagePath: fixturePath) - XCTAssertMatch(stderr, .contains("warning: '--package-url' option is deprecated; use '--original' instead")) - XCTAssertMatch(stderr, .contains("warning: '--mirror-url' option is deprecated; use '--mirror' instead")) + let (_, stderr) = try await execute( + ["config", "set-mirror", "--package-url", "https://github.com/foo/bar", "--mirror-url", "https://mygithub.com/foo/bar"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stderr.contains("warning: '--package-url' option is deprecated; use '--original' instead")) + #expect(stderr.contains("warning: '--mirror-url' option is deprecated; use '--mirror' instead")) + } } - } - func testMirrorConfig() async throws { - try await testWithTemporaryDirectory { fixturePath in - let fs = localFileSystem - let packageRoot = fixturePath.appending("Foo") - let configOverride = fixturePath.appending("configoverride") - let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) - - fs.createEmptyFiles(at: packageRoot, files: - "/Sources/Foo/Foo.swift", - "/Tests/FooTests/FooTests.swift", - "/Package.swift", - "anchor" - ) + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func mirrorConfig( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { fixturePath in + let fs = localFileSystem + let packageRoot = fixturePath.appending("Foo") + let configOverride = fixturePath.appending("configoverride") + let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) + + fs.createEmptyFiles( + at: packageRoot, + files: + "/Sources/Foo/Foo.swift", + "/Tests/FooTests/FooTests.swift", + "/Package.swift", + "anchor" + ) + + // Test writing. + try await execute( + ["config", "set-mirror", "--original", "https://github.com/foo/bar", "--mirror", "https://mygithub.com/foo/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try await execute( + ["config", "set-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git", "--mirror", "git@mygithub.com:foo/swift-package-manager.git"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(fs.isFile(configFile)) + + // Test env override. + try await execute( + ["config", "set-mirror", "--original", "https://github.com/foo/bar", "--mirror", "https://mygithub.com/foo/bar"], + packagePath: packageRoot, + env: ["SWIFTPM_MIRROR_CONFIG": configOverride.pathString], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(fs.isFile(configOverride)) + let content: String = try fs.readFileContents(configOverride) + #expect(content.contains("mygithub")) + + // Test reading. + var (stdout, _) = try await execute( + ["config", "get-mirror", "--original", "https://github.com/foo/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.spm_chomp() == "https://mygithub.com/foo/bar") + (stdout, _) = try await execute( + ["config", "get-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.spm_chomp() == "git@mygithub.com:foo/swift-package-manager.git") - // Test writing. - try await execute(["config", "set-mirror", "--original", "https://github.com/foo/bar", "--mirror", "https://mygithub.com/foo/bar"], packagePath: packageRoot) - try await execute(["config", "set-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git", "--mirror", "git@mygithub.com:foo/swift-package-manager.git"], packagePath: packageRoot) - XCTAssertTrue(fs.isFile(configFile)) - - // Test env override. - try await execute(["config", "set-mirror", "--original", "https://github.com/foo/bar", "--mirror", "https://mygithub.com/foo/bar"], packagePath: packageRoot, env: ["SWIFTPM_MIRROR_CONFIG": configOverride.pathString]) - XCTAssertTrue(fs.isFile(configOverride)) - let content: String = try fs.readFileContents(configOverride) - XCTAssertMatch(content, .contains("mygithub")) - - // Test reading. - var (stdout, _) = try await execute(["config", "get-mirror", "--original", "https://github.com/foo/bar"], packagePath: packageRoot) - XCTAssertEqual(stdout.spm_chomp(), "https://mygithub.com/foo/bar") - (stdout, _) = try await execute(["config", "get-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git"], packagePath: packageRoot) - XCTAssertEqual(stdout.spm_chomp(), "git@mygithub.com:foo/swift-package-manager.git") - - func check(stderr: String, _ block: () async throws -> ()) async { - await XCTAssertThrowsCommandExecutionError(try await block()) { error in - XCTAssertMatch(stderr, .contains(stderr)) + func check(stderr: String, _ block: () async throws -> ()) async { + await expectThrowsCommandExecutionError(try await block()) { error in + #expect(error.stderr.contains(stderr)) + } } - } - await check(stderr: "not found\n") { - try await execute(["config", "get-mirror", "--original", "foo"], packagePath: packageRoot) - } + await check(stderr: "not found\n") { + try await execute( + ["config", "get-mirror", "--original", "foo"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } - // Test deletion. - try await execute(["config", "unset-mirror", "--original", "https://github.com/foo/bar"], packagePath: packageRoot) - try await execute(["config", "unset-mirror", "--original", "git@mygithub.com:foo/swift-package-manager.git"], packagePath: packageRoot) + // Test deletion. + try await execute( + ["config", "unset-mirror", "--original", "https://github.com/foo/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try await execute( + ["config", "unset-mirror", "--original", "git@mygithub.com:foo/swift-package-manager.git"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - await check(stderr: "not found\n") { - try await execute(["config", "get-mirror", "--original", "https://github.com/foo/bar"], packagePath: packageRoot) - } - await check(stderr: "not found\n") { - try await execute(["config", "get-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git"], packagePath: packageRoot) - } + await check(stderr: "not found\n") { + try await execute( + ["config", "get-mirror", "--original", "https://github.com/foo/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + await check(stderr: "not found\n") { + try await execute( + ["config", "get-mirror", "--original", "git@github.com:swiftlang/swift-package-manager.git"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } - await check(stderr: "error: Mirror not found for 'foo'\n") { - try await execute(["config", "unset-mirror", "--original", "foo"], packagePath: packageRoot) + await check(stderr: "error: Mirror not found for 'foo'\n") { + try await execute( + ["config", "unset-mirror", "--original", "foo"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } } } - } - func testMirrorSimple() async throws { - try await testWithTemporaryDirectory { fixturePath in - let fs = localFileSystem - let packageRoot = fixturePath.appending("MyPackage") - let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) - - fs.createEmptyFiles( - at: packageRoot, - files: - "/Sources/Foo/Foo.swift", - "/Tests/FooTests/FooTests.swift", - "/Package.swift" - ) + @Test( + .tags( + .Feature.Command.Package.DumpPackage, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func mirrorSimple( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { fixturePath in + let fs = localFileSystem + let packageRoot = fixturePath.appending("MyPackage") + let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) + + fs.createEmptyFiles( + at: packageRoot, + files: + "/Sources/Foo/Foo.swift", + "/Tests/FooTests/FooTests.swift", + "/Package.swift" + ) - try fs.writeFileContents(packageRoot.appending("Package.swift"), string: - """ - // swift-tools-version: 5.7 - import PackageDescription - let package = Package( - name: "MyPackage", - dependencies: [ - .package(url: "https://scm.com/org/foo", from: "1.0.0") - ], - targets: [ - .executableTarget( - name: "MyTarget", + try fs.writeFileContents( + packageRoot.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyPackage", dependencies: [ - .product(name: "Foo", package: "foo") - ]) - ] + .package(url: "https://scm.com/org/foo", from: "1.0.0") + ], + targets: [ + .executableTarget( + name: "MyTarget", + dependencies: [ + .product(name: "Foo", package: "foo") + ]) + ] + ) + """ ) - """ - ) - try await execute(["config", "set-mirror", "--original", "https://scm.com/org/foo", "--mirror", "https://scm.com/org/bar"], packagePath: packageRoot) - XCTAssertTrue(fs.isFile(configFile)) + try await execute( + ["config", "set-mirror", "--original", "https://scm.com/org/foo", "--mirror", "https://scm.com/org/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(fs.isFile(configFile)) - let (stdout, _) = try await self.execute(["dump-package"], packagePath: packageRoot) - XCTAssertMatch(stdout, .contains("https://scm.com/org/bar")) - XCTAssertNoMatch(stdout, .contains("https://scm.com/org/foo")) + let (stdout, _) = try await execute( + ["dump-package"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("https://scm.com/org/bar")) + #expect(!stdout.contains("https://scm.com/org/foo")) + } } - } - func testMirrorURLToRegistry() async throws { - try await testWithTemporaryDirectory { fixturePath in - let fs = localFileSystem - let packageRoot = fixturePath.appending("MyPackage") - let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) - - fs.createEmptyFiles( - at: packageRoot, - files: - "/Sources/Foo/Foo.swift", - "/Tests/FooTests/FooTests.swift", - "/Package.swift" - ) + @Test( + .tags( + .Feature.Command.Package.DumpPackage, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func mirrorURLToRegistry( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { fixturePath in + let fs = localFileSystem + let packageRoot = fixturePath.appending("MyPackage") + let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) + + fs.createEmptyFiles( + at: packageRoot, + files: + "/Sources/Foo/Foo.swift", + "/Tests/FooTests/FooTests.swift", + "/Package.swift" + ) - try fs.writeFileContents(packageRoot.appending("Package.swift"), string: - """ - // swift-tools-version: 5.7 - import PackageDescription - let package = Package( - name: "MyPackage", - dependencies: [ - .package(url: "https://scm.com/org/foo", from: "1.0.0") - ], - targets: [ - .executableTarget( - name: "MyTarget", + try fs.writeFileContents( + packageRoot.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyPackage", dependencies: [ - .product(name: "Foo", package: "foo") - ]) - ] + .package(url: "https://scm.com/org/foo", from: "1.0.0") + ], + targets: [ + .executableTarget( + name: "MyTarget", + dependencies: [ + .product(name: "Foo", package: "foo") + ]) + ] + ) + """ ) - """ - ) - try await execute(["config", "set-mirror", "--original", "https://scm.com/org/foo", "--mirror", "org.bar"], packagePath: packageRoot) - XCTAssertTrue(fs.isFile(configFile)) + try await execute( + ["config", "set-mirror", "--original", "https://scm.com/org/foo", "--mirror", "org.bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(fs.isFile(configFile)) - let (stdout, _) = try await self.execute(["dump-package"], packagePath: packageRoot) - XCTAssertMatch(stdout, .contains("org.bar")) - XCTAssertNoMatch(stdout, .contains("https://scm.com/org/foo")) + let (stdout, _) = try await execute( + ["dump-package"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("org.bar")) + #expect(!stdout.contains("https://scm.com/org/foo")) + } } - } - func testMirrorRegistryToURL() async throws { - try await testWithTemporaryDirectory { fixturePath in - let fs = localFileSystem - let packageRoot = fixturePath.appending("MyPackage") - let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) - - fs.createEmptyFiles( - at: packageRoot, - files: - "/Sources/Foo/Foo.swift", - "/Tests/FooTests/FooTests.swift", - "/Package.swift" - ) + @Test( + .tags( + .Feature.Command.Package.DumpPackage, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func mirrorRegistryToURL( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { fixturePath in + let fs = localFileSystem + let packageRoot = fixturePath.appending("MyPackage") + let configFile = Workspace.DefaultLocations.mirrorsConfigurationFile(forRootPackage: packageRoot) + + fs.createEmptyFiles( + at: packageRoot, + files: + "/Sources/Foo/Foo.swift", + "/Tests/FooTests/FooTests.swift", + "/Package.swift" + ) - try fs.writeFileContents(packageRoot.appending("Package.swift"), string: - """ - // swift-tools-version: 5.7 - import PackageDescription - let package = Package( - name: "MyPackage", - dependencies: [ - .package(id: "org.foo", from: "1.0.0") - ], - targets: [ - .executableTarget( - name: "MyTarget", + try fs.writeFileContents( + packageRoot.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyPackage", dependencies: [ - .product(name: "Foo", package: "org.foo") - ]) - ] + .package(id: "org.foo", from: "1.0.0") + ], + targets: [ + .executableTarget( + name: "MyTarget", + dependencies: [ + .product(name: "Foo", package: "org.foo") + ]) + ] + ) + """ ) - """ - ) - try await execute(["config", "set-mirror", "--original", "org.foo", "--mirror", "https://scm.com/org/bar"], packagePath: packageRoot) - XCTAssertTrue(fs.isFile(configFile)) + try await execute( + ["config", "set-mirror", "--original", "org.foo", "--mirror", "https://scm.com/org/bar"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(fs.isFile(configFile)) - let (stdout, _) = try await self.execute(["dump-package"], packagePath: packageRoot) - XCTAssertMatch(stdout, .contains("https://scm.com/org/bar")) - XCTAssertNoMatch(stdout, .contains("org.foo")) + let (stdout, _) = try await execute( + ["dump-package"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("https://scm.com/org/bar")) + #expect(!stdout.contains("org.foo")) + } } } - func testPackageLoadingCommandPathResilience() async throws { - #if !os(macOS) - try XCTSkipIf(true, "skipping on non-macOS") - #endif - - try await fixtureXCTest(name: "ValidLayouts/SingleModule") { fixturePath in + @Test( + .requireHostOS(.macOS), + .tags( + .Feature.Command.Package.DumpPackage, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func packageLoadingCommandPathResilience( + data: BuildData, + ) async throws { + try await fixture(name: "ValidLayouts/SingleModule") { fixturePath in try await testWithTemporaryDirectory { tmpdir in // Create fake `xcrun` and `sandbox-exec` commands. let fakeBinDir = tmpdir for fakeCmdName in ["xcrun", "sandbox-exec"] { let fakeCmdPath = fakeBinDir.appending(component: fakeCmdName) - try localFileSystem.writeFileContents(fakeCmdPath, string: - """ - #!/bin/sh - echo "wrong \(fakeCmdName) invoked" - exit 1 - """ + try localFileSystem.writeFileContents( + fakeCmdPath, + string: + """ + #!/bin/sh + echo "wrong \(fakeCmdName) invoked" + exit 1 + """ ) try localFileSystem.chmod(.executable, path: fakeCmdPath) } @@ -2107,495 +3189,697 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Invoke `swift-package`, passing in the overriding `PATH` environment variable. let packageRoot = fixturePath.appending("Library") let patchedPATH = fakeBinDir.pathString + ":" + ProcessInfo.processInfo.environment["PATH"]! - let (stdout, _) = try await self.execute(["dump-package"], packagePath: packageRoot, env: ["PATH": patchedPATH]) + let (stdout, _) = try await execute( + ["dump-package"], + packagePath: packageRoot, + env: ["PATH": patchedPATH], + configuration: data.config, + buildSystem: data.buildSystem, + ) // Check that the wrong tools weren't invoked. We can't just check the exit code because of fallbacks. - XCTAssertNoMatch(stdout, .contains("wrong xcrun invoked")) - XCTAssertNoMatch(stdout, .contains("wrong sandbox-exec invoked")) + #expect(!stdout.contains("wrong xcrun invoked")) + #expect(!stdout.contains("wrong sandbox-exec invoked")) } } } - func testMigrateCommandHelp() async throws { - let (stdout, _) = try await self.execute( - ["migrate", "--help"], + @Suite( + .tags( + .Feature.Command.Package.Migrate, + ), + ) + struct MigrateCommandTests { + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - - // Global options are hidden. - XCTAssertNoMatch(stdout, .contains("--package-path")) - } - - func testMigrateCommandNoFeatures() async throws { - try await XCTAssertThrowsCommandExecutionError( - await self.execute(["migrate"]) - ) { error in - XCTAssertMatch( - error.stderr, - .contains("error: Missing expected argument '--to-feature '") + func migrateCommandHelp( + data: BuildData, + ) async throws { + let (stdout, _) = try await execute( + ["migrate", "--help"], + configuration: data.config, + buildSystem: data.buildSystem, ) + + // Global options are hidden. + #expect(!stdout.contains("--package-path")) } - } - func testMigrateCommandUnknownFeature() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - - try await XCTAssertThrowsCommandExecutionError( - await self.execute(["migrate", "--to-feature", "X"]) - ) { error in - XCTAssertMatch( - error.stderr, - .contains("error: Unsupported feature 'X'. Available features:") - ) + func migrateCommandNoFeatures( + data: BuildData, + ) async throws { + try await expectThrowsCommandExecutionError( + await execute( + ["migrate"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect( + error.stderr.contains("error: Missing expected argument '--to-feature '") + ) + } } - } - func testMigrateCommandNonMigratableFeature() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - - try await XCTAssertThrowsCommandExecutionError( - await self.execute(["migrate", "--to-feature", "StrictConcurrency"]) - ) { error in - XCTAssertMatch( - error.stderr, - .contains("error: Feature 'StrictConcurrency' is not migratable") - ) + func migrateCommandUnknownFeature( + data: BuildData, + ) async throws { + try await expectThrowsCommandExecutionError( + await execute( + ["migrate", "--to-feature", "X"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect( + error.stderr.contains("error: Unsupported feature 'X'. Available features:") + ) + } } - } - func testMigrateCommand() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandNonMigratableFeature( + data: BuildData, + ) async throws { + try await expectThrowsCommandExecutionError( + await execute( + ["migrate", "--to-feature", "StrictConcurrency"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect( + error.stderr.contains("error: Feature 'StrictConcurrency' is not migratable") + ) + } + } - func doMigration(featureName: String, expectedSummary: String) async throws { - try await fixtureXCTest(name: "SwiftMigrate/\(featureName)Migration") { fixturePath in - let sourcePaths: [AbsolutePath] - let fixedSourcePaths: [AbsolutePath] + struct MigrateCommandTestData { + let featureName: String + let expectedSummary: String + } + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + // When updating these, make sure we keep testing both the singular and + // plural forms of the nouns in the summary. + MigrateCommandTestData( + featureName: "ExistentialAny", + expectedSummary: "Applied 5 fix-its in 1 file", + ), + MigrateCommandTestData( + featureName: "StrictMemorySafety", + expectedSummary: "Applied 1 fix-it in 1 file", + ), + MigrateCommandTestData( + featureName: "InferIsolatedConformances", + expectedSummary: "Applied 3 fix-its in 2 files", + ), + ], + ) + func migrateCommand( + buildData: BuildData, + testData: MigrateCommandTestData, + ) async throws { + let featureName = testData.featureName + let expectedSummary = testData.expectedSummary + + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/\(featureName)Migration") { fixturePath in + let sourcePaths: [AbsolutePath] + let fixedSourcePaths: [AbsolutePath] + + do { + let sourcesPath = fixturePath.appending(components: "Sources") + let fixedSourcesPath = sourcesPath.appending("Fixed") + + sourcePaths = try localFileSystem.getDirectoryContents(sourcesPath).filter { filename in + filename.hasSuffix(".swift") + }.sorted().map { filename in + sourcesPath.appending(filename) + } + fixedSourcePaths = try localFileSystem.getDirectoryContents(fixedSourcesPath).filter { filename in + filename.hasSuffix(".swift") + }.sorted().map { filename in + fixedSourcesPath.appending(filename) + } + } - do { - let sourcesPath = fixturePath.appending(components: "Sources") - let fixedSourcesPath = sourcesPath.appending("Fixed") + let (stdout, _) = try await execute( + ["migrate", "--to-feature", featureName], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, - sourcePaths = try localFileSystem.getDirectoryContents(sourcesPath).filter { filename in - filename.hasSuffix(".swift") - }.sorted().map { filename in - sourcesPath.appending(filename) - } - fixedSourcePaths = try localFileSystem.getDirectoryContents(fixedSourcesPath).filter { filename in - filename.hasSuffix(".swift") - }.sorted().map { filename in - fixedSourcesPath.appending(filename) - } - } + ) - let (stdout, _) = try await self.execute( - ["migrate", "--to-feature", featureName], - packagePath: fixturePath - ) + #expect(sourcePaths.count == fixedSourcePaths.count) - XCTAssertEqual(sourcePaths.count, fixedSourcePaths.count) + for (sourcePath, fixedSourcePath) in zip(sourcePaths, fixedSourcePaths) { + let sourceContent = try localFileSystem.readFileContents(sourcePath) + let fixedSourceContent = try localFileSystem.readFileContents(fixedSourcePath) + #expect(sourceContent == fixedSourceContent) + } - for (sourcePath, fixedSourcePath) in zip(sourcePaths, fixedSourcePaths) { - try XCTAssertEqual( - localFileSystem.readFileContents(sourcePath), - localFileSystem.readFileContents(fixedSourcePath) - ) + let regexMatch = try Regex("> \(expectedSummary)" + #" \([0-9]\.[0-9]{1,3}s\)"#) + #expect(stdout.contains(regexMatch)) } - - XCTAssertMatch(stdout, .regex("> \(expectedSummary)" + #" \([0-9]\.[0-9]{1,3}s\)"#)) + } when: { + buildData.buildSystem == .native && buildData.config == .release } } - // When updating these, make sure we keep testing both the singular and - // plural forms of the nouns in the summary. - try await doMigration(featureName: "ExistentialAny", expectedSummary: "Applied 5 fix-its in 1 file") - try await doMigration(featureName: "StrictMemorySafety", expectedSummary: "Applied 1 fix-it in 1 file") - try await doMigration(featureName: "InferIsolatedConformances", expectedSummary: "Applied 3 fix-its in 2 files") - } - - func testMigrateCommandWithBuildToolPlugins() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandWithBuildToolPlugins( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/ExistentialAnyWithPluginMigration") { fixturePath in + let (stdout, _) = try await execute( + ["migrate", "--to-feature", "ExistentialAny"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, - try await fixtureXCTest(name: "SwiftMigrate/ExistentialAnyWithPluginMigration") { fixturePath in - let (stdout, _) = try await self.execute( - ["migrate", "--to-feature", "ExistentialAny"], - packagePath: fixturePath - ) + ) - // Check the plugin target in the manifest wasn't updated - let manifestContent = try localFileSystem.readFileContents(fixturePath.appending(component: "Package.swift")).description - XCTAssertTrue(manifestContent.contains(".plugin(name: \"Plugin\", capability: .buildTool, dependencies: [\"Tool\"]),")) + // Check the plugin target in the manifest wasn't updated + let manifestContent = try localFileSystem.readFileContents(fixturePath.appending(component: "Package.swift")).description + #expect(manifestContent.contains(".plugin(name: \"Plugin\", capability: .buildTool, dependencies: [\"Tool\"]),")) - // Building the package produces migration fix-its in both an authored and generated source file. Check we only applied fix-its to the hand-authored one. - XCTAssertMatch(stdout, .regex("> \("Applied 3 fix-its in 1 file")" + #" \([0-9]\.[0-9]{1,3}s\)"#)) + // Building the package produces migration fix-its in both an authored and generated source file. Check we only applied fix-its to the hand-authored one. + let regexMatch = try Regex("> \("Applied 3 fix-its in 1 file")" + #" \([0-9]\.[0-9]{1,3}s\)"#) + #expect(stdout.contains(regexMatch)) + } + } when: { + data.buildSystem == .native && data.config == .release + } } - } - func testMigrateCommandWhenDependencyBuildsForHostAndTarget() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandWhenDependencyBuildsForHostAndTarget( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration") { fixturePath in + let (stdout, _) = try await execute( + ["migrate", "--to-feature", "ExistentialAny"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, - try await fixtureXCTest(name: "SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration") { fixturePath in - let (stdout, _) = try await self.execute( - ["migrate", "--to-feature", "ExistentialAny"], - packagePath: fixturePath - ) + ) - // Even though the CommonLibrary dependency built for both the host and destination, we should only apply a single fix-it once to its sources. - XCTAssertMatch(stdout, .regex("> \("Applied 1 fix-it in 1 file")" + #" \([0-9]\.[0-9]{1,3}s\)"#)) + // Even though the CommonLibrary dependency built for both the host and destination, we should only apply a single fix-it once to its sources. + let regexMatch = try Regex("> \("Applied 1 fix-it in 1 file")" + #" \([0-9]\.[0-9]{1,3}s\)"#) + #expect(stdout.contains(regexMatch)) + } + } when: { + data.buildSystem == .native && data.config == .release + } } - } - func testMigrateCommandUpdateManifestSingleTarget() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandUpdateManifestSingleTarget( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/UpdateManifest") { fixturePath in + _ = try await execute( + [ + "migrate", + "--to-feature", + "ExistentialAny,InferIsolatedConformances", + "--target", + "A", + ], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, - try await fixtureXCTest(name: "SwiftMigrate/UpdateManifest") { fixturePath in - _ = try await self.execute( - [ - "migrate", - "--to-feature", - "ExistentialAny,InferIsolatedConformances", - "--target", - "A", - ], - packagePath: fixturePath - ) + ) - let updatedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.swift") - ) - let expectedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.updated.targets-A.swift") - ) - XCTAssertEqual(updatedManifest, expectedManifest) + let updatedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.swift") + ) + let expectedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.updated.targets-A.swift") + ) + #expect(updatedManifest == expectedManifest) + } + } when: { + data.buildSystem == .native && data.config == .release + } } - } - - func testMigrateCommandUpdateManifest2Targets() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandUpdateManifest2Targets( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/UpdateManifest") { fixturePath in + _ = try await execute( + [ + "migrate", + "--to-feature", + "ExistentialAny,InferIsolatedConformances", + "--target", + "A,B", + ], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, - try await fixtureXCTest(name: "SwiftMigrate/UpdateManifest") { fixturePath in - _ = try await self.execute( - [ - "migrate", - "--to-feature", - "ExistentialAny,InferIsolatedConformances", - "--target", - "A,B", - ], - packagePath: fixturePath - ) + ) - let updatedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.swift") - ) - let expectedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.updated.targets-A-B.swift") - ) - XCTAssertEqual(updatedManifest, expectedManifest) + let updatedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.swift") + ) + let expectedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.updated.targets-A-B.swift") + ) + #expect(updatedManifest == expectedManifest) + } + } when: { + data.buildSystem == .native && data.config == .release + } } - } - func testMigrateCommandUpdateManifestWithErrors() async throws { - try XCTSkipIf( - !UserToolchain.default.supportesSupportedFeatures, - "skipping because test environment compiler doesn't support `-print-supported-features`" + @Test( + .supportsSupportedFeatures, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9006", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func migrateCommandUpdateManifestWithErrors( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "SwiftMigrate/UpdateManifest") { fixturePath in + try await expectThrowsCommandExecutionError( + await execute( + ["migrate", "--to-feature", "ExistentialAny,InferIsolatedConformances,StrictMemorySafety"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + // 'SwiftMemorySafety.strictMemorySafety' was introduced in 6.2. + #expect( + error.stderr.contains( + """ + error: Could not update manifest to enable requested features for target 'A' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' + error: Could not update manifest to enable requested features for target 'B' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' + error: Could not update manifest to enable requested features for target 'CannotFindSettings' (unable to find array literal for 'swiftSettings' argument). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' + error: Could not update manifest to enable requested features for target 'CannotFindTarget' (unable to find target named 'CannotFindTarget' in package). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' + """ + ) + ) + } - try await fixtureXCTest(name: "SwiftMigrate/UpdateManifest") { fixturePath in - try await XCTAssertThrowsCommandExecutionError( - await self.execute( - ["migrate", "--to-feature", "ExistentialAny,InferIsolatedConformances,StrictMemorySafety"], - packagePath: fixturePath - ) - ) { error in - // 'SwiftMemorySafety.strictMemorySafety' was introduced in 6.2. - XCTAssertMatch( - error.stderr, - .contains( - """ - error: Could not update manifest to enable requested features for target 'A' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' - error: Could not update manifest to enable requested features for target 'B' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' - error: Could not update manifest to enable requested features for target 'CannotFindSettings' (unable to find array literal for 'swiftSettings' argument). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' - error: Could not update manifest to enable requested features for target 'CannotFindTarget' (unable to find target named 'CannotFindTarget' in package). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' - """ + let updatedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.swift") ) - ) + let expectedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.updated.targets-all.swift") + ) + #expect(updatedManifest == expectedManifest) + } + } when: { + data.buildSystem == .native && data.config == .release } - - let updatedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.swift") - ) - let expectedManifest = try localFileSystem.readFileContents( - fixturePath.appending(components: "Package.updated.targets-all.swift") - ) - XCTAssertEqual(updatedManifest, expectedManifest) } } - func testBuildToolPlugin() async throws { - try await testBuildToolPlugin(staticStdlib: false) - } - - func testBuildToolPluginWithStaticStdlib() async throws { - // Skip if the toolchain cannot compile a simple program with static stdlib. - do { - let args = try [ - UserToolchain.default.swiftCompilerPath.pathString, - "-static-stdlib", "-emit-executable", "-o", "/dev/null", "-" - ] - let process = AsyncProcess(arguments: args) - let stdin = try process.launch() - stdin.write(sequence: "".utf8) - try stdin.close() - let result = try await process.waitUntilExit() - try XCTSkipIf( - result.exitStatus != .terminated(code: 0), - "skipping because static stdlib is not supported by the toolchain" - ) + @Suite( + .tags( + .Feature.Command.Package.BuildPlugin, + ), + ) + struct BuildPluginTests { + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func buildToolPlugin( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testBuildToolPlugin(data: data, staticStdlib: false) + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild + } } - try await testBuildToolPlugin(staticStdlib: true) - } - func testBuildToolPlugin(staticStdlib: Bool) async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") + @Test( + .requiresStdlibSupport, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func buildToolPluginWithStaticStdlib( + data: BuildData, + ) async throws { + try await testBuildToolPlugin(data: data, staticStdlib: true) + } - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "MyPackage", - targets: [ - .target( - name: "MyLibrary", - plugins: [ - "MyPlugin", + func testBuildToolPlugin(data: BuildData, staticStdlib: Bool) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.writeFileContents( + packageDir.appending("Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary", + plugins: [ + "MyPlugin", + ] + ), + .plugin( + name: "MyPlugin", + capability: .buildTool() + ), ] - ), - .plugin( - name: "MyPlugin", - capability: .buildTool() - ), - ] - ) - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), string: - """ - public func Foo() { } - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.foo"), string: - """ - a file with a filename suffix handled by the plugin - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.bar"), string: - """ - a file with a filename suffix not handled by the plugin - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), string: - """ - import PackagePlugin - import Foundation - @main - struct MyBuildToolPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) throws -> [Command] { - // Expect the initial working directory for build tool plugins is the package directory. - guard FileManager.default.currentDirectoryPath == context.package.directory.string else { - throw "expected initial working directory ‘\\(FileManager.default.currentDirectoryPath)’" - } + ) + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), + string: + """ + public func Foo() { } + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.foo"), + string: + """ + a file with a filename suffix handled by the plugin + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.bar"), + string: + """ + a file with a filename suffix not handled by the plugin + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), + string: + """ + import PackagePlugin + import Foundation + @main + struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + // Expect the initial working directory for build tool plugins is the package directory. + guard FileManager.default.currentDirectoryPath == context.package.directory.string else { + throw "expected initial working directory ‘\\(FileManager.default.currentDirectoryPath)’" + } + + // Check that the package display name is what we expect. + guard context.package.displayName == "MyPackage" else { + throw "expected display name to be ‘MyPackage’ but found ‘\\(context.package.displayName)’" + } + + // Create and return a build command that uses all the `.foo` files in the target as inputs, so they get counted as having been handled. + let fooFiles = target.sourceModule?.sourceFiles.compactMap{ $0.path.extension == "foo" ? $0.path : nil } ?? [] + #if os(Windows) + let exec = "echo" + #else + let exec = "/bin/echo" + #endif + return [ .buildCommand(displayName: "A command", executable: Path(exec), arguments: fooFiles, inputFiles: fooFiles) ] + } - // Check that the package display name is what we expect. - guard context.package.displayName == "MyPackage" else { - throw "expected display name to be ‘MyPackage’ but found ‘\\(context.package.displayName)’" } + extension String : Error {} + """ + ) - // Create and return a build command that uses all the `.foo` files in the target as inputs, so they get counted as having been handled. - let fooFiles = target.sourceModule?.sourceFiles.compactMap{ $0.path.extension == "foo" ? $0.path : nil } ?? [] - #if os(Windows) - let exec = "echo" - #else - let exec = "/bin/echo" - #endif - return [ .buildCommand(displayName: "A command", executable: Path(exec), arguments: fooFiles, inputFiles: fooFiles) ] - } + // Invoke it, and check the results. + let args = staticStdlib ? ["--static-swift-stdlib"] : [] + let (stdout, stderr) = try await executeSwiftBuild( + packageDir, + configuration: data.config, + extraArgs: args, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Build complete!")) + // We expect a warning about `library.bar` but not about `library.foo`. + #expect(!stderr.contains(RelativePath("Sources/MyLibrary/library.foo").pathString)) + if data.buildSystem == .native { + #expect(stderr.contains("found 1 file(s) which are unhandled")) + #expect(stderr.contains(RelativePath("Sources/MyLibrary/library.bar").pathString)) } - extension String : Error {} - """ - ) - - // Invoke it, and check the results. - let args = staticStdlib ? ["--static-swift-stdlib"] : [] - let (stdout, stderr) = try await executeSwiftBuild( - packageDir, - extraArgs: args, - buildSystem: self.buildSystemProvider, - ) - XCTAssert(stdout.contains("Build complete!")) - - // We expect a warning about `library.bar` but not about `library.foo`. - XCTAssertNoMatch(stderr, .contains(RelativePath("Sources/MyLibrary/library.foo").pathString)) - if self.buildSystemProvider == .native { - XCTAssertMatch(stderr, .contains("found 1 file(s) which are unhandled")) - XCTAssertMatch(stderr, .contains(RelativePath("Sources/MyLibrary/library.bar").pathString)) } } - } - func testBuildToolPluginFailure() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - targets: [ - .target( - name: "MyLibrary", - plugins: [ - "MyPlugin", + @Test( + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func buildToolPluginFailure( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending("Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary", + plugins: [ + "MyPlugin", + ] + ), + .plugin( + name: "MyPlugin", + capability: .buildTool() + ), ] - ), - .plugin( - name: "MyPlugin", - capability: .buildTool() - ), - ] - ) - """ - ) - let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) - try localFileSystem.writeFileContents(myLibraryTargetDir.appending("library.swift"), string: """ - public func Foo() { } - """ - ) - let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") - try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - import Foundation - @main - struct MyBuildToolPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) throws -> [Command] { - print("This is text from the plugin") - throw "This is an error from the plugin" - return [] - } - - } - extension String : Error {} - """ - ) + ) + """ + ) + let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myLibraryTargetDir.appending("library.swift"), + string: """ + public func Foo() { } + """ + ) + let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") + try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + import Foundation + @main + struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + print("This is text from the plugin") + throw "This is an error from the plugin" + return [] + } - // Invoke it, and check the results. - await XCTAssertAsyncThrowsError( - try await executeSwiftBuild( - packageDir, - extraArgs: ["-v"], - buildSystem: self.buildSystemProvider, + } + extension String : Error {} + """ ) - ) { error in - guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { - return XCTFail("invalid error \(error)") - } - XCTAssertMatch(stderr, .contains("This is text from the plugin")) - XCTAssertMatch(stderr, .contains("error: This is an error from the plugin")) - if self.buildSystemProvider == .native { - XCTAssertMatch(stderr, .contains("build planning stopped due to build-tool plugin failures")) + + // Invoke it, and check the results. + await expectThrowsCommandExecutionError( + try await executeSwiftBuild( + packageDir, + configuration: data.config, + extraArgs: ["-v"], + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("This is text from the plugin")) + #expect(error.stderr.contains("error: This is an error from the plugin")) + if data.buildSystem == .native { + #expect(error.stderr.contains("build planning stopped due to build-tool plugin failures")) + } } } } } - func testArchiveSource() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") - - // Running without arguments or options - do { - let (stdout, _) = try await self.execute(["archive-source"], packagePath: packageRoot) - XCTAssert(stdout.contains("Created Bar.zip"), #"actual: "\#(stdout)""#) - } - - // Running without arguments or options again, overwriting existing archive - do { - let (stdout, _) = try await self.execute(["archive-source"], packagePath: packageRoot) - XCTAssert(stdout.contains("Created Bar.zip"), #"actual: "\#(stdout)""#) + @Suite( + .tags( + .Feature.Command.Package.ArchiveSource, + ), + ) + struct ArchiveSourceTests { + @Test( + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild]), + [1, 2, 5] + // arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), [1, 2, 5] + ) + func archiveSourceWithoutArguments( + data: BuildData, + numberOfExecutions: Int, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + + // Running without arguments or options, overwriting existing archive + for num in 1...numberOfExecutions { + let (stdout, _) = try await execute( + ["archive-source"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect( + stdout.contains("Created Bar.zip"), + #"Iteration \#(num) of \#(numberOfExecutions) failed --> stdout: "\#(stdout)""#, + ) + } } + } - // Running with output as absolute path within package root - do { + @Test( + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild]), + // arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func archiveSourceRunningWithOutputAsAbsolutePathWithingThePackageRoot( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + // Running with output as absolute path within package root let destination = packageRoot.appending("Bar-1.2.3.zip") - let (stdout, _) = try await self.execute(["archive-source", "--output", destination.pathString], packagePath: packageRoot) - XCTAssert(stdout.contains("Created Bar-1.2.3.zip"), #"actual: "\#(stdout)""#) + let (stdout, _) = try await execute( + ["archive-source", "--output", destination.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Created Bar-1.2.3.zip"), #"actual: "\#(stdout)""#) } + } - // Running with output is outside the package root - try await withTemporaryDirectory { tempDirectory in - let destination = tempDirectory.appending("Bar-1.2.3.zip") - let (stdout, _) = try await self.execute(["archive-source", "--output", destination.pathString], packagePath: packageRoot) - XCTAssert(stdout.hasPrefix("Created "), #"actual: "\#(stdout)""#) - XCTAssert(stdout.contains("Bar-1.2.3.zip"), #"actual: "\#(stdout)""#) + @Test( + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild]), + // arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func archiveSourceRunningWithoutArgumentsOutsideThePackageRoot( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + // Running with output is outside the package root + try await withTemporaryDirectory { tempDirectory in + let destination = tempDirectory.appending("Bar-1.2.3.zip") + let (stdout, _) = try await execute( + ["archive-source", "--output", destination.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.hasPrefix("Created "), #"actual: "\#(stdout)""#) + #expect(stdout.contains("Bar-1.2.3.zip"), #"actual: "\#(stdout)""#) + } } + } - // Running without arguments or options in non-package directory - do { - await XCTAssertAsyncThrowsError(try await self.execute(["archive-source"], packagePath: fixturePath)) { error in - guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { - return XCTFail("invalid error \(error)") - } - XCTAssert(stderr.contains("error: Could not find Package.swift in this directory or any of its parent directories."), #"actual: "\#(stderr)""#) + @Test( + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild]), + // arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func archiveSourceRunningWithoutArgumentsInNonPackageDirectoryProducesAnError( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + // Running without arguments or options in non-package directory + await expectThrowsCommandExecutionError( + try await execute( + ["archive-source"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + #expect(error.stderr.contains("error: Could not find Package.swift in this directory or any of its parent directories."), #"actual: "\#(stderr)""#) } } + } - // Running with output as absolute path to existing directory - do { + @Test( + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild]), + // arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func archiveSourceRunningWithOuboutAsAbsolutePathToExistingDirectory( + data: BuildData, + ) async throws { + try await fixture(name: "DependencyResolution/External/Simple") { fixturePath in + let packageRoot = fixturePath.appending("Bar") + // Running with output as absolute path to existing directory let destination = AbsolutePath.root - await XCTAssertAsyncThrowsError(try await self.execute(["archive-source", "--output", destination.pathString], packagePath: packageRoot)) { error in - guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { - return XCTFail("invalid error \(error)") - } - XCTAssert( + await expectThrowsCommandExecutionError( + try await execute( + ["archive-source", "--output", destination.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + let stderr = error.stderr + #expect( stderr.contains("error: Couldn’t create an archive:"), #"actual: "\#(stderr)""# ) @@ -2604,1969 +3888,2640 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { } } - func testCommandPlugin() async throws { - try XCTRequires(executable: "sed") - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") + @Suite( + .tags( + .Feature.Command.Package.CommandPlugin, + ), + ) + struct CommandPluginTests { + struct CommandPluginTestData { + let packageCommandArgs: CLIArguments + let expectedStdout: [String] + } + @Test( + .requiresSwiftConcurrencySupport, + .requires(executable: "sed"), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + CommandPluginTestData( + // Check that we can invoke the plugin with the "plugin" subcommand. + packageCommandArgs: ["plugin", "mycmd"], + expectedStdout: [ + "This is MyCommandPlugin." + ], + ), - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target, a plugin, and a local tool. It depends on a sample package which also has a tool. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "MyPackage", - dependencies: [ - .package(name: "HelperPackage", path: "VendoredDependencies/HelperPackage") + CommandPluginTestData( + // Check that we can also invoke it without the "plugin" subcommand. + packageCommandArgs: ["mycmd"], + expectedStdout: [ + "This is MyCommandPlugin." ], - targets: [ - .target( - name: "MyLibrary", - dependencies: [ - .product(name: "HelperLibrary", package: "HelperPackage") - ] - ), - .plugin( - name: "MyPlugin", - capability: .command( - intent: .custom(verb: "mycmd", description: "What is mycmd anyway?") - ), + ), + CommandPluginTestData( + // Testing listing the available command plugins. + packageCommandArgs: ["plugin", "--list"], + expectedStdout: [ + "‘mycmd’ (plugin ‘MyPlugin’ in package ‘MyPackage’)" + ], + ), + + CommandPluginTestData( + // Check that the .docc file was properly vended to the plugin. + packageCommandArgs: ["mycmd", "--target", "MyLibrary"], + expectedStdout: [ + "Sources/MyLibrary/library.swift: source", + "Sources/MyLibrary/test.docc: unknown", + ], + ), + CommandPluginTestData( + // Check that the .docc file was properly vended to the plugin. + packageCommandArgs: ["mycmd", "--target", "MyLibrary"], + expectedStdout: [ + "Sources/MyLibrary/library.swift: source", + "Sources/MyLibrary/test.docc: unknown", + ], + ), + CommandPluginTestData( + // Check that information about the dependencies was properly sent to the plugin. + packageCommandArgs: ["mycmd", "--target", "MyLibrary"], + expectedStdout: [ + "dependency HelperPackage: local" + ], + ), + ] + ) + func commandPlugin( + buildData: BuildData, + testData: CommandPluginTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target, a plugin, and a local tool. It depends on a sample package which also has a tool. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", dependencies: [ - .target(name: "LocalBuiltTool"), - .target(name: "LocalBinaryTool"), - .product(name: "RemoteBuiltTool", package: "HelperPackage") + .package(name: "HelperPackage", path: "VendoredDependencies/HelperPackage") + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [ + .product(name: "HelperLibrary", package: "HelperPackage") + ] + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "mycmd", description: "What is mycmd anyway?") + ), + dependencies: [ + .target(name: "LocalBuiltTool"), + .target(name: "LocalBinaryTool"), + .product(name: "RemoteBuiltTool", package: "HelperPackage") + ] + ), + .binaryTarget( + name: "LocalBinaryTool", + path: "Binaries/LocalBinaryTool.artifactbundle" + ), + .executableTarget( + name: "LocalBuiltTool" + ) ] - ), - .binaryTarget( - name: "LocalBinaryTool", - path: "Binaries/LocalBinaryTool.artifactbundle" - ), - .executableTarget( - name: "LocalBuiltTool" ) - ] + """ ) - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), string: - """ - public func Foo() { } - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "test.docc"), string: - """ - - - - - CFBundleName - sample - - """ - ) - let environment = Environment.current - let hostTriple = try UserToolchain( - swiftSDK: .hostSwiftSDK(environment: environment), - environment: environment - ).targetTriple - let hostTripleString = if hostTriple.isDarwin() { - hostTriple.tripleString(forPlatformVersion: "") - } else { - hostTriple.tripleString - } - - try localFileSystem.writeFileContents( - packageDir.appending(components: "Binaries", "LocalBinaryTool.artifactbundle", "info.json"), - string: """ - { "schemaVersion": "1.0", - "artifacts": { - "LocalBinaryTool": { - "type": "executable", - "version": "1.2.3", - "variants": [ - { "path": "LocalBinaryTool.sh", - "supportedTriples": ["\(hostTripleString)"] - }, - ] - } + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), + string: + """ + public func Foo() { } + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "test.docc"), + string: + """ + + + + + CFBundleName + sample + + """ + ) + let environment = Environment.current + let hostTriple = try UserToolchain( + swiftSDK: .hostSwiftSDK(environment: environment), + environment: environment + ).targetTriple + let hostTripleString = + if hostTriple.isDarwin() { + hostTriple.tripleString(forPlatformVersion: "") + } else { + hostTriple.tripleString } - } - """ - ) - try localFileSystem.writeFileContents( - packageDir.appending(components: "Sources", "LocalBuiltTool", "main.swift"), - string: #"print("Hello")"# - ) - try localFileSystem.writeFileContents( - packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), - string: """ - import PackagePlugin - import Foundation - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - print("This is MyCommandPlugin.") - - // Print out the initial working directory so we can check it in the test. - print("Initial working directory: \\(FileManager.default.currentDirectoryPath)") - - // Check that we can find a binary-provided tool in the same package. - print("Looking for LocalBinaryTool...") - let localBinaryTool = try context.tool(named: "LocalBinaryTool") - print("... found it at \\(localBinaryTool.path)") - - // Check that we can find a source-built tool in the same package. - print("Looking for LocalBuiltTool...") - let localBuiltTool = try context.tool(named: "LocalBuiltTool") - print("... found it at \\(localBuiltTool.path)") - - // Check that we can find a source-built tool in another package. - print("Looking for RemoteBuiltTool...") - let remoteBuiltTool = try context.tool(named: "RemoteBuiltTool") - print("... found it at \\(remoteBuiltTool.path)") - - // Check that we can find a tool in the toolchain. - print("Looking for swiftc...") - let swiftc = try context.tool(named: "swiftc") - print("... found it at \\(swiftc.path)") - - // Check that we can find a standard tool. - print("Looking for sed...") - let sed = try context.tool(named: "sed") - print("... found it at \\(sed.path)") - - // Extract the `--target` arguments. - var argExtractor = ArgumentExtractor(arguments) - let targetNames = argExtractor.extractOption(named: "target") - let targets = try context.package.targets(named: targetNames) - - // Print out the source files so that we can check them. - if let sourceFiles = targets.first(where: { $0.name == "MyLibrary" })?.sourceModule?.sourceFiles { - for file in sourceFiles { - print(" \\(file.path): \\(file.type)") + + try localFileSystem.writeFileContents( + packageDir.appending(components: "Binaries", "LocalBinaryTool.artifactbundle", "info.json"), + string: """ + { "schemaVersion": "1.0", + "artifacts": { + "LocalBinaryTool": { + "type": "executable", + "version": "1.2.3", + "variants": [ + { "path": "LocalBinaryTool.sh", + "supportedTriples": ["\(hostTripleString)"] + }, + ] + } } } - - // Print out the dependencies so that we can check them. - for dependency in context.package.dependencies { - print(" dependency \\(dependency.package.displayName): \\(dependency.package.origin)") + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "LocalBuiltTool", "main.swift"), + string: #"print("Hello")"# + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), + string: """ + import PackagePlugin + import Foundation + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + print("This is MyCommandPlugin.") + + // Print out the initial working directory so we can check it in the test. + print("Initial working directory: \\(FileManager.default.currentDirectoryPath)") + + // Check that we can find a binary-provided tool in the same package. + print("Looking for LocalBinaryTool...") + let localBinaryTool = try context.tool(named: "LocalBinaryTool") + print("... found it at \\(localBinaryTool.path)") + + // Check that we can find a source-built tool in the same package. + print("Looking for LocalBuiltTool...") + let localBuiltTool = try context.tool(named: "LocalBuiltTool") + print("... found it at \\(localBuiltTool.path)") + + // Check that we can find a source-built tool in another package. + print("Looking for RemoteBuiltTool...") + let remoteBuiltTool = try context.tool(named: "RemoteBuiltTool") + print("... found it at \\(remoteBuiltTool.path)") + + // Check that we can find a tool in the toolchain. + print("Looking for swiftc...") + let swiftc = try context.tool(named: "swiftc") + print("... found it at \\(swiftc.path)") + + // Check that we can find a standard tool. + print("Looking for sed...") + let sed = try context.tool(named: "sed") + print("... found it at \\(sed.path)") + + // Extract the `--target` arguments. + var argExtractor = ArgumentExtractor(arguments) + let targetNames = argExtractor.extractOption(named: "target") + let targets = try context.package.targets(named: targetNames) + + // Print out the source files so that we can check them. + if let sourceFiles = targets.first(where: { $0.name == "MyLibrary" })?.sourceModule?.sourceFiles { + for file in sourceFiles { + print(" \\(file.path): \\(file.type)") + } + } + + // Print out the dependencies so that we can check them. + for dependency in context.package.dependencies { + print(" dependency \\(dependency.package.displayName): \\(dependency.package.origin)") + } + } } - } - } - """ - ) - - // Create the sample vendored dependency package. - try localFileSystem.writeFileContents( - packageDir.appending(components: "VendoredDependencies", "HelperPackage", "Package.swift"), - string: """ - // swift-tools-version: 5.5 - import PackageDescription - let package = Package( - name: "HelperPackage", - products: [ - .library( - name: "HelperLibrary", - targets: ["HelperLibrary"] - ), - .executable( - name: "RemoteBuiltTool", - targets: ["RemoteBuiltTool"] - ), - ], - targets: [ - .target( - name: "HelperLibrary" - ), - .executableTarget( - name: "RemoteBuiltTool" - ), - ] - ) - """ - ) - try localFileSystem.writeFileContents( - packageDir.appending( - components: "VendoredDependencies", - "HelperPackage", - "Sources", - "HelperLibrary", - "library.swift" - ), - string: "public func Bar() { }" - ) - try localFileSystem.writeFileContents( - packageDir.appending( - components: "VendoredDependencies", - "HelperPackage", - "Sources", - "RemoteBuiltTool", - "main.swift" - ), - string: #"print("Hello")"# - ) + """ + ) - // Check that we can invoke the plugin with the "plugin" subcommand. - do { - let (stdout, _) = try await self.execute(["plugin", "mycmd"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("This is MyCommandPlugin.")) - } + // Create the sample vendored dependency package. + try localFileSystem.writeFileContents( + packageDir.appending(components: "VendoredDependencies", "HelperPackage", "Package.swift"), + string: """ + // swift-tools-version: 5.5 + import PackageDescription + let package = Package( + name: "HelperPackage", + products: [ + .library( + name: "HelperLibrary", + targets: ["HelperLibrary"] + ), + .executable( + name: "RemoteBuiltTool", + targets: ["RemoteBuiltTool"] + ), + ], + targets: [ + .target( + name: "HelperLibrary" + ), + .executableTarget( + name: "RemoteBuiltTool" + ), + ] + ) + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending( + components: "VendoredDependencies", + "HelperPackage", + "Sources", + "HelperLibrary", + "library.swift" + ), + string: "public func Bar() { }" + ) + try localFileSystem.writeFileContents( + packageDir.appending( + components: "VendoredDependencies", + "HelperPackage", + "Sources", + "RemoteBuiltTool", + "main.swift" + ), + string: #"print("Hello")"# + ) - // Check that we can also invoke it without the "plugin" subcommand. - do { - let (stdout, _) = try await self.execute(["mycmd"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("This is MyCommandPlugin.")) + let (stdout, _) = try await execute( + testData.packageCommandArgs, + packagePath: packageDir, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + for expected in testData.expectedStdout { + #expect(stdout.contains(expected)) + } } - - // Testing listing the available command plugins. - do { - let (stdout, _) = try await self.execute(["plugin", "--list"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("‘mycmd’ (plugin ‘MyPlugin’ in package ‘MyPackage’)")) - } - - // Check that we get the expected error if trying to invoke a plugin with the wrong name. - do { - await XCTAssertAsyncThrowsError(try await self.execute(["my-nonexistent-cmd"], packagePath: packageDir)) { error in - guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { - return XCTFail("invalid error \(error)") + } + @Test( + .requiresSwiftConcurrencySupport, + .requires(executable: "sed"), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginSpecialCases( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target, a plugin, and a local tool. It depends on a sample package which also has a tool. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", + dependencies: [ + .package(name: "HelperPackage", path: "VendoredDependencies/HelperPackage") + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [ + .product(name: "HelperLibrary", package: "HelperPackage") + ] + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "mycmd", description: "What is mycmd anyway?") + ), + dependencies: [ + .target(name: "LocalBuiltTool"), + .target(name: "LocalBinaryTool"), + .product(name: "RemoteBuiltTool", package: "HelperPackage") + ] + ), + .binaryTarget( + name: "LocalBinaryTool", + path: "Binaries/LocalBinaryTool.artifactbundle" + ), + .executableTarget( + name: "LocalBuiltTool" + ) + ] + ) + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), + string: + """ + public func Foo() { } + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "test.docc"), + string: + """ + + + + + CFBundleName + sample + + """ + ) + let environment = Environment.current + let hostTriple = try UserToolchain( + swiftSDK: .hostSwiftSDK(environment: environment), + environment: environment + ).targetTriple + let hostTripleString = + if hostTriple.isDarwin() { + hostTriple.tripleString(forPlatformVersion: "") + } else { + hostTriple.tripleString } - XCTAssertMatch(stderr, .contains("Unknown subcommand or plugin name ‘my-nonexistent-cmd’")) - } - } - // Check that the .docc file was properly vended to the plugin. - do { - let (stdout, _) = try await self.execute(["mycmd", "--target", "MyLibrary"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Sources/MyLibrary/library.swift: source")) - XCTAssertMatch(stdout, .contains("Sources/MyLibrary/test.docc: unknown")) - } + try localFileSystem.writeFileContents( + packageDir.appending(components: "Binaries", "LocalBinaryTool.artifactbundle", "info.json"), + string: """ + { "schemaVersion": "1.0", + "artifacts": { + "LocalBinaryTool": { + "type": "executable", + "version": "1.2.3", + "variants": [ + { "path": "LocalBinaryTool.sh", + "supportedTriples": ["\(hostTripleString)"] + }, + ] + } + } + } + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "LocalBuiltTool", "main.swift"), + string: #"print("Hello")"# + ) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), + string: """ + import PackagePlugin + import Foundation + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + print("This is MyCommandPlugin.") + + // Print out the initial working directory so we can check it in the test. + print("Initial working directory: \\(FileManager.default.currentDirectoryPath)") + + // Check that we can find a binary-provided tool in the same package. + print("Looking for LocalBinaryTool...") + let localBinaryTool = try context.tool(named: "LocalBinaryTool") + print("... found it at \\(localBinaryTool.path)") + + // Check that we can find a source-built tool in the same package. + print("Looking for LocalBuiltTool...") + let localBuiltTool = try context.tool(named: "LocalBuiltTool") + print("... found it at \\(localBuiltTool.path)") + + // Check that we can find a source-built tool in another package. + print("Looking for RemoteBuiltTool...") + let remoteBuiltTool = try context.tool(named: "RemoteBuiltTool") + print("... found it at \\(remoteBuiltTool.path)") + + // Check that we can find a tool in the toolchain. + print("Looking for swiftc...") + let swiftc = try context.tool(named: "swiftc") + print("... found it at \\(swiftc.path)") + + // Check that we can find a standard tool. + print("Looking for sed...") + let sed = try context.tool(named: "sed") + print("... found it at \\(sed.path)") + + // Extract the `--target` arguments. + var argExtractor = ArgumentExtractor(arguments) + let targetNames = argExtractor.extractOption(named: "target") + let targets = try context.package.targets(named: targetNames) + + // Print out the source files so that we can check them. + if let sourceFiles = targets.first(where: { $0.name == "MyLibrary" })?.sourceModule?.sourceFiles { + for file in sourceFiles { + print(" \\(file.path): \\(file.type)") + } + } + + // Print out the dependencies so that we can check them. + for dependency in context.package.dependencies { + print(" dependency \\(dependency.package.displayName): \\(dependency.package.origin)") + } + } + } + """ + ) - // Check that the initial working directory is what we expected. - do { - let workingDirectory = FileManager.default.currentDirectoryPath - let (stdout, _) = try await self.execute(["mycmd"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Initial working directory: \(workingDirectory)")) - } + // Create the sample vendored dependency package. + try localFileSystem.writeFileContents( + packageDir.appending(components: "VendoredDependencies", "HelperPackage", "Package.swift"), + string: """ + // swift-tools-version: 5.5 + import PackageDescription + let package = Package( + name: "HelperPackage", + products: [ + .library( + name: "HelperLibrary", + targets: ["HelperLibrary"] + ), + .executable( + name: "RemoteBuiltTool", + targets: ["RemoteBuiltTool"] + ), + ], + targets: [ + .target( + name: "HelperLibrary" + ), + .executableTarget( + name: "RemoteBuiltTool" + ), + ] + ) + """ + ) + try localFileSystem.writeFileContents( + packageDir.appending( + components: "VendoredDependencies", + "HelperPackage", + "Sources", + "HelperLibrary", + "library.swift" + ), + string: "public func Bar() { }" + ) + try localFileSystem.writeFileContents( + packageDir.appending( + components: "VendoredDependencies", + "HelperPackage", + "Sources", + "RemoteBuiltTool", + "main.swift" + ), + string: #"print("Hello")"# + ) - // Check that information about the dependencies was properly sent to the plugin. - do { - let (stdout, _) = try await self.execute(["mycmd", "--target", "MyLibrary"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("dependency HelperPackage: local")) + // Check that we get the expected error if trying to invoke a plugin with the wrong name. + do { + await expectThrowsCommandExecutionError( + try await execute( + ["my-nonexistent-cmd"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + // guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { + // Issue.record("invalid error \(error)") + // return + // } + #expect(error.stderr.contains("Unknown subcommand or plugin name ‘my-nonexistent-cmd’")) + } + } + do { + // Check that the initial working directory is what we expected. + let workingDirectory = FileManager.default.currentDirectoryPath + let (stdout, _) = try await execute( + ["mycmd"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Initial working directory: \(workingDirectory)")) + } } } - } - - func testAmbiguousCommandPlugin() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try await fixtureXCTest(name: "Miscellaneous/Plugins/AmbiguousCommands") { fixturePath in - let (stdout, _) = try await self.execute(["plugin", "--package", "A", "A"], packagePath: fixturePath) - XCTAssertMatch(stdout, .contains("Hello A!")) + @Test( + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func ambiguousCommandPlugin( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/AmbiguousCommands") { fixturePath in + let (stdout, _) = try await execute( + ["plugin", "--package", "A", "A"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Hello A!")) + } } - } - // Test reporting of plugin diagnostic messages at different verbosity levels - func testCommandPluginDiagnostics() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - throw XCTSkip("Skipping test due to flakiness, see https://github.com/swiftlang/swift-package-manager/issues/8180") - - // Match patterns for expected messages - let isEmpty = StringPattern.equal("") - let isOnlyPrint = StringPattern.equal("command plugin: print\n") - let containsProgress = StringPattern.contains("[diagnostics-stub] command plugin: Diagnostics.progress") - let containsRemark = StringPattern.contains("command plugin: Diagnostics.remark") - let containsWarning = StringPattern.contains("command plugin: Diagnostics.warning") - let containsError = StringPattern.contains("command plugin: Diagnostics.error") - - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - func runPlugin(flags: [String], diagnostics: [String], completion: (String, String) -> Void) async throws { - let (stdout, stderr) = try await self.execute(flags + ["print-diagnostics"] + diagnostics, packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - completion(stdout, stderr) - } + // Test reporting of plugin diagnostic messages at different verbosity levels + @Test( + .requiresSwiftConcurrencySupport, + .issue("https://github.com/swiftlang/swift-package-manager/issues/8180", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginDiagnostics( + data: BuildData, + ) async throws { + + // Match patterns for expected messages + let isEmpty = "" + let isOnlyPrint = "command plugin: print\n" + let containsProgress = "[diagnostics-stub] command plugin: Diagnostics.progress" + let containsRemark = "command plugin: Diagnostics.remark" + let containsWarning = "command plugin: Diagnostics.warning" + let containsError = "command plugin: Diagnostics.error" + + await withKnownIssue(isIntermittent: true) { + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + func runPlugin(flags: [String], diagnostics: [String], completion: (String, String) -> Void) async throws { + let (stdout, stderr) = try await execute( + flags + ["print-diagnostics"] + diagnostics, + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + completion(stdout, stderr) + } - // Diagnostics.error causes SwiftPM to return a non-zero exit code, but we still need to check stdout and stderr - func runPluginWithError(flags: [String], diagnostics: [String], completion: (String, String) -> Void) async throws { - await XCTAssertAsyncThrowsError(try await self.execute(flags + ["print-diagnostics"] + diagnostics, packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"])) { error in - guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { - return XCTFail("invalid error \(error)") + // Diagnostics.error causes SwiftPM to return a non-zero exit code, but we still need to check stdout and stderr + func runPluginWithError(flags: [String], diagnostics: [String], completion: (String, String) -> Void) async throws { + await expectThrowsCommandExecutionError( + try await execute( + flags + ["print-diagnostics"] + diagnostics, + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + // guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { + // Issue.record("invalid error \(error)") + // return + // } + completion(error.stdout, error.stderr) + } } - completion(stdout, stderr) - } - } - // Default verbosity - // - stdout is always printed - // - Diagnostics below 'warning' are suppressed + // Default verbosity + // - stdout is always printed + // - Diagnostics below 'warning' are suppressed - try await runPlugin(flags: [], diagnostics: ["print"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - let filteredStderr = stderr.components(separatedBy: "\n") - .filter { !$0.contains("Unable to locate libSwiftScan") }.joined(separator: "\n") - XCTAssertMatch(filteredStderr, isEmpty) - } + try await runPlugin(flags: [], diagnostics: ["print"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + let filteredStderr = stderr.components(separatedBy: "\n") + .filter { !$0.contains("Unable to locate libSwiftScan") }.joined(separator: "\n") + #expect(filteredStderr == isEmpty) + } - try await runPlugin(flags: [], diagnostics: ["print", "progress"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: [], diagnostics: ["print", "progress"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: [], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: [], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: [], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsWarning) - } + try await runPlugin(flags: [], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsWarning)) + } - try await runPluginWithError(flags: [], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsWarning) - XCTAssertMatch(stderr, containsError) - } + try await runPluginWithError(flags: [], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsWarning)) + #expect(stderr.contains(containsError)) + } - // Quiet Mode - // - stdout is always printed - // - Diagnostics below 'error' are suppressed + // Quiet Mode + // - stdout is always printed + // - Diagnostics below 'error' are suppressed - try await runPlugin(flags: ["-q"], diagnostics: ["print"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - let filteredStderr = stderr.components(separatedBy: "\n") - .filter { !$0.contains("Unable to locate libSwiftScan") }.joined(separator: "\n") - XCTAssertMatch(filteredStderr, isEmpty) - } + try await runPlugin(flags: ["-q"], diagnostics: ["print"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + let filteredStderr = stderr.components(separatedBy: "\n") + .filter { !$0.contains("Unable to locate libSwiftScan") }.joined(separator: "\n") + #expect(filteredStderr == isEmpty) + } - try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPluginWithError(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertNoMatch(stderr, containsRemark) - XCTAssertNoMatch(stderr, containsWarning) - XCTAssertMatch(stderr, containsError) - } + try await runPluginWithError(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(!stderr.contains(containsRemark)) + #expect(!stderr.contains(containsWarning)) + #expect(stderr.contains(containsError)) + } - // Verbose Mode - // - stdout is always printed - // - All diagnostics are printed - // - Substantial amounts of additional compiler output are also printed + // Verbose Mode + // - stdout is always printed + // - All diagnostics are printed + // - Substantial amounts of additional compiler output are also printed - try await runPlugin(flags: ["-v"], diagnostics: ["print"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - // At this level stderr contains extra compiler output even if the plugin does not print diagnostics - } + try await runPlugin(flags: ["-v"], diagnostics: ["print"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + // At this level stderr contains extra compiler output even if the plugin does not print diagnostics + } - try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsRemark) - } + try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsRemark)) + } - try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsRemark) - XCTAssertMatch(stderr, containsWarning) - } + try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsRemark)) + #expect(stderr.contains(containsWarning)) + } - try await runPluginWithError(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsRemark) - XCTAssertMatch(stderr, containsWarning) - XCTAssertMatch(stderr, containsError) + try await runPluginWithError(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsRemark)) + #expect(stderr.contains(containsWarning)) + #expect(stderr.contains(containsError)) + } + } } } - } - // Test target builds requested by a command plugin - func testCommandPluginTargetBuilds() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - #if os(Linux) - let osSuffix = "-linux" - #elseif os(Windows) - let osSuffix = "-windows" - #else - let osSuffix = "" - #endif - - let tripleString = try UserToolchain.default.targetTriple.platformBuildPathComponent - let debugTarget = [".build", tripleString ] + self.buildSystemProvider.binPathSuffixes(for: .debug) + [executableName("placeholder")] - let releaseTarget = [".build", tripleString ] + self.buildSystemProvider.binPathSuffixes(for: .release) + [executableName("placeholder")] - - func AssertIsExecutableFile(_ fixturePath: AbsolutePath, file: StaticString = #filePath, line: UInt = #line) { - XCTAssert( - localFileSystem.isExecutableFile(fixturePath), - "\(fixturePath) does not exist", - file: file, - line: line - ) + // Test target builds requested by a command plugin + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginTargetBuilds_BinaryIsBuildinDebugByDefault( + buildData: BuildData, + ) async throws { + let tripleString = try UserToolchain.default.targetTriple.platformBuildPathComponent + let debugTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .debug) + [executableName("placeholder")] + let releaseTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .release) + [executableName("placeholder")] + try await withKnownIssue { + // By default, a plugin-requested build produces a debug binary + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = try await execute( + ["build-target"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + expectFileIsExecutable(at: fixturePath.appending(components: debugTarget), "build-target") + expectFileDoesNotExists(at: fixturePath.appending(components: releaseTarget), "build-target build-inherit") + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - func AssertNotExists(_ fixturePath: AbsolutePath, file: StaticString = #filePath, line: UInt = #line) { - XCTAssertFalse( - localFileSystem.exists(fixturePath), - "\(fixturePath) should not exist", - file: file, - line: line - ) + // Test target builds requested by a command plugin + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginTargetBuilds_BinaryWillBeBuiltInDebugIfPluginSpecifiesDebugBuild( + buildData: BuildData, + ) async throws { + let tripleString = try UserToolchain.default.targetTriple.platformBuildPathComponent + let debugTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .debug) + [executableName("placeholder")] + let releaseTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .release) + [executableName("placeholder")] + try await withKnownIssue { + // If the plugin specifies a debug binary, that is what will be built, regardless of overall configuration + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = try await execute( + ["build-target", "build-debug"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + expectFileIsExecutable(at: fixturePath.appending(components: debugTarget), "build-target build-debug") + expectFileDoesNotExists(at: fixturePath.appending(components: releaseTarget), "build-target build-inherit") + } + + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - // By default, a plugin-requested build produces a debug binary - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let _ = try await self.execute(["-c", "release", "build-target"], packagePath: fixturePath) - AssertIsExecutableFile(fixturePath.appending(components: debugTarget)) - AssertNotExists(fixturePath.appending(components: releaseTarget)) + // Test target builds requested by a command plugin + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginTargetBuilds_BinaryWillBeBuiltInReleaseIfPluginSpecifiesReleaseBuild( + buildData: BuildData, + ) async throws { + let tripleString = try UserToolchain.default.targetTriple.platformBuildPathComponent + let debugTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .debug) + [executableName("placeholder")] + let releaseTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .release) + [executableName("placeholder")] + try await withKnownIssue { + + // If the plugin requests a release binary, that is what will be built, regardless of overall configuration + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = try await execute( + ["build-target", "build-release"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + expectFileDoesNotExists(at: fixturePath.appending(components: debugTarget), "build-target build-inherit") + expectFileIsExecutable(at: fixturePath.appending(components: releaseTarget), "build-target build-release") + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - // If the plugin specifies a debug binary, that is what will be built, regardless of overall configuration - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let _ = try await self.execute(["-c", "release", "build-target", "build-debug"], packagePath: fixturePath) - AssertIsExecutableFile(fixturePath.appending(components: debugTarget)) - AssertNotExists(fixturePath.appending(components: releaseTarget)) + // Test target builds requested by a command plugin + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginTargetBuilds_BinaryWillBeBuiltCorrectlyIfPluginSpecifiesInheritBuild( + buildData: BuildData, + ) async throws { + let tripleString = try UserToolchain.default.targetTriple.platformBuildPathComponent + let debugTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .debug) + [executableName("placeholder")] + let releaseTarget = [".build", tripleString] + buildData.buildSystem.binPathSuffixes(for: .release) + [executableName("placeholder")] + try await withKnownIssue { + // If the plugin inherits the overall build configuration, that is what will be built + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = try await execute( + ["build-target", "build-inherit"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + let fileShouldNotExist: AbsolutePath + let fileShouldExist: AbsolutePath + switch buildData.config { + case .debug: + fileShouldExist = fixturePath.appending(components: debugTarget) + fileShouldNotExist = fixturePath.appending(components: releaseTarget) + case .release: + fileShouldNotExist = fixturePath.appending(components: debugTarget) + fileShouldExist = fixturePath.appending(components: releaseTarget) + } + expectFileDoesNotExists(at: fileShouldNotExist, "build-target build-inherit") + expectFileIsExecutable(at: fileShouldExist, "build-target build-inherit") + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - // If the plugin requests a release binary, that is what will be built, regardless of overall configuration - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let _ = try await self.execute(["-c", "debug", "build-target", "build-release"], packagePath: fixturePath) - AssertNotExists(fixturePath.appending(components: debugTarget)) - AssertIsExecutableFile(fixturePath.appending(components: releaseTarget)) + @Test( + .IssueWindowsRelativePathAssert, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginBuildTestabilityInternal_ModuleDebug_True( + data: BuildData, + ) async throws { + // Plugin arguments: check-testability + try await withKnownIssue { + // Overall configuration: debug, plugin build request: debug -> without testability + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = await #expect(throws: Never.self) { + try await execute( + ["check-testability", "InternalModule", "debug", "true"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - // If the plugin inherits the overall build configuration, that is what will be built - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let _ = try await self.execute(["-c", "debug", "build-target", "build-inherit"], packagePath: fixturePath) - AssertIsExecutableFile(fixturePath.appending(components: debugTarget)) - AssertNotExists(fixturePath.appending(components: releaseTarget)) + @Test( + .IssueWindowsRelativePathAssert, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginBuildTestabilityInternalModule_Release_False( + data: BuildData, + ) async throws { + try await withKnownIssue { + // Overall configuration: debug, plugin build request: release -> without testability + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = await #expect(throws: Never.self) { + try await execute( + ["check-testability", "InternalModule", "release", "false"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - // If the plugin inherits the overall build configuration, that is what will be built - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let _ = try await self.execute(["-c", "release", "build-target", "build-inherit"], packagePath: fixturePath) - AssertNotExists(fixturePath.appending(components: debugTarget)) - AssertIsExecutableFile(fixturePath.appending(components: releaseTarget)) + @Test( + .IssueWindowsRelativePathAssert, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginBuildTestabilityAllWithTests_Release_True( + data: BuildData, + ) async throws { + try await withKnownIssue { + // Overall configuration: release, plugin build request: release including tests -> with testability + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let _ = await #expect(throws: Never.self) { + try await execute( + ["check-testability", "all-with-tests", "release", "true"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - } - func testCommandPluginBuildTestability() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - // Plugin arguments: check-testability + // Test logging of builds initiated by a command plugin + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginBuildLogs( + data: BuildData, + ) async throws { + + // Match patterns for expected messages + let isEmpty = "" + + // result.logText printed by the plugin has a prefix + let containsLogtext = "command plugin: packageManager.build logtext: Building for debugging..." + + // Echoed logs have no prefix + let containsLogecho = "Building for debugging...\n" + + // These tests involve building a target, so each test must run with a fresh copy of the fixture + // otherwise the logs may be different in subsequent tests. + + // Check than nothing is echoed when echoLogs is false + try await withKnownIssue(isIntermittent: ProcessInfo.hostOperatingSystem == .windows) { + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try await execute( //got here + ["print-diagnostics", "build"], + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout == isEmpty) + // Filter some unrelated output that could show up on stderr. + let filteredStderr = stderr.components(separatedBy: "\n") + .filter { !$0.contains("Unable to locate libSwiftScan") } + .filter { !($0.contains("warning: ") && $0.contains("unable to find libclang")) }.joined(separator: "\n") + #expect(filteredStderr == isEmpty) + } + + // Check that logs are returned to the plugin when echoLogs is false + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try await execute( // got here + ["print-diagnostics", "build", "printlogs"], + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains(containsLogtext)) + // Filter some unrelated output that could show up on stderr. + let filteredStderr = stderr.components(separatedBy: "\n") + .filter { !$0.contains("Unable to locate libSwiftScan") } + .filter { !($0.contains("warning: ") && $0.contains("unable to find libclang")) }.joined(separator: "\n") + #expect(filteredStderr == isEmpty) + } - // Overall configuration: debug, plugin build request: debug -> without testability - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - await XCTAssertAsyncNoThrow(try await self.execute(["-c", "debug", "check-testability", "InternalModule", "debug", "true"], packagePath: fixturePath)) - } + // Check that logs echoed to the console (on stderr) when echoLogs is true + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try await execute( + ["print-diagnostics", "build", "echologs"], + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout == isEmpty) + #expect(stderr.contains(containsLogecho)) + } - // Overall configuration: debug, plugin build request: release -> without testability - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - await XCTAssertAsyncNoThrow(try await self.execute(["-c", "debug", "check-testability", "InternalModule", "release", "false"], packagePath: fixturePath)) - } + // Check that logs are returned to the plugin and echoed to the console (on stderr) when echoLogs is true + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try await execute( + ["print-diagnostics", "build", "printlogs", "echologs"], + packagePath: fixturePath, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains(containsLogtext)) + #expect(stderr.contains(containsLogecho)) + } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } - // Overall configuration: release, plugin build request: debug -> with testability - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - await XCTAssertAsyncNoThrow(try await self.execute(["-c", "release", "check-testability", "InternalModule", "debug", "true"], packagePath: fixturePath)) } - // Overall configuration: release, plugin build request: release -> with testability - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - await XCTAssertAsyncNoThrow(try await self.execute(["-c", "release", "check-testability", "InternalModule", "release", "false"], packagePath: fixturePath)) + struct CommandPluginNetworkingPermissionsTestData { + let permissionsManifestFragment: String + let permissionError: String + let reason: String + let remedy: CLIArguments } + fileprivate static func getCommandPluginNetworkingPermissionTestData() -> [CommandPluginNetworkingPermissionsTestData] { + [ + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(), reason: \"internet good\")]", + permissionError: "all network connections on all ports", + reason: "internet good", + remedy: ["--allow-network-connections", "all"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(ports: [23, 42, 443, 8080]), reason: \"internet good\")]", + permissionError: "all network connections on ports: 23, 42, 443, 8080", + reason: "internet good", + remedy: ["--allow-network-connections", "all:23,42,443,8080"], - // Overall configuration: release, plugin build request: release including tests -> with testability - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - await XCTAssertAsyncNoThrow(try await self.execute(["-c", "release", "check-testability", "all-with-tests", "release", "true"], packagePath: fixturePath)) + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(ports: 1..<4), reason: \"internet good\")]", + permissionError: "all network connections on ports: 1, 2, 3", + reason: "internet good", + remedy: ["--allow-network-connections", "all:1,2,3"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(), reason: \"localhost good\")]", + permissionError: "local network connections on all ports", + reason: "localhost good", + remedy: ["--allow-network-connections", "local"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(ports: [23, 42, 443, 8080]), reason: \"localhost good\")]", + permissionError: "local network connections on ports: 23, 42, 443, 8080", + reason: "localhost good", + remedy: ["--allow-network-connections", "local:23,42,443,8080"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(ports: 1..<4), reason: \"localhost good\")]", + permissionError: "local network connections on ports: 1, 2, 3", + reason: "localhost good", + remedy: ["--allow-network-connections", "local:1,2,3"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .docker, reason: \"docker good\")]", + permissionError: "docker unix domain socket connections", + reason: "docker good", + remedy: ["--allow-network-connections", "docker"], + ), + CommandPluginNetworkingPermissionsTestData( + permissionsManifestFragment: "[.allowNetworkConnections(scope: .unixDomainSocket, reason: \"unix sockets good\")]", + permissionError: "unix domain socket connections", + reason: "unix sockets good", + remedy: ["--allow-network-connections", "unixDomainSocket"], + ), + ] } - } - // Test logging of builds initiated by a command plugin - func testCommandPluginBuildLogs() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - // Match patterns for expected messages + @Test( + .requiresSwiftConcurrencySupport, + .requireHostOS(.macOS), + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + Self.getCommandPluginNetworkingPermissionTestData() + ) + func commandPluginNetworkingPermissionsWithoutUsingRemedy( + buildData: BuildData, + testData: CommandPluginNetworkingPermissionsTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target(name: "MyLibrary"), + .plugin(name: "MyPlugin", capability: .command(intent: .custom(verb: "Network", description: "Help description"), permissions: \(testData.permissionsManifestFragment))), + ] + ) + """ + ) + try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), string: "public func Foo() { }") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), + string: + """ + import PackagePlugin - let isEmpty = StringPattern.equal("") + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("hello world") + } + } + """ + ) - // result.logText printed by the plugin has a prefix - let containsLogtext = StringPattern.contains("command plugin: packageManager.build logtext: Building for debugging...") + await expectThrowsCommandExecutionError( + try await execute( + ["plugin", "Network"], + packagePath: packageDir, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + ) { error in + #expect(!error.stdout.contains("hello world")) + #expect(error.stderr.contains("error: Plugin ‘MyPlugin’ wants permission to allow \(testData.permissionError).")) + #expect(error.stderr.contains("Stated reason: “\(testData.reason)”.")) + #expect(error.stderr.contains("Use `\(testData.remedy.joined(separator: " "))` to allow this.")) + } + } + } - // Echoed logs have no prefix - let containsLogecho = StringPattern.contains("Building for debugging...\n") + @Test( + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + Self.getCommandPluginNetworkingPermissionTestData() + ) + func commandPluginNetworkingPermissionsUsingRemedy( + buildData: BuildData, + testData: CommandPluginNetworkingPermissionsTestData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: + """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target(name: "MyLibrary"), + .plugin(name: "MyPlugin", capability: .command(intent: .custom(verb: "Network", description: "Help description"), permissions: \(testData.permissionsManifestFragment))), + ] + ) + """ + ) + try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), string: "public func Foo() { }") + try localFileSystem.writeFileContents( + packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), + string: + """ + import PackagePlugin - // These tests involve building a target, so each test must run with a fresh copy of the fixture - // otherwise the logs may be different in subsequent tests. + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("hello world") + } + } + """ + ) - // Check than nothing is echoed when echoLogs is false - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let (stdout, stderr) = try await self.execute(["print-diagnostics", "build"], packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, isEmpty) - // Filter some unrelated output that could show up on stderr. - let filteredStderr = stderr.components(separatedBy: "\n") - .filter { !$0.contains("Unable to locate libSwiftScan") } - .filter { !($0.contains("warning: ") && $0.contains("unable to find libclang")) }.joined(separator: "\n") - XCTAssertMatch(filteredStderr, isEmpty) + // Check that we don't get an error (and also are allowed to write to the package directory) if we pass `--allow-writing-to-package-directory`. + do { + let (stdout, _) = try await execute( + ["plugin"] + testData.remedy + ["Network"], + packagePath: packageDir, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + #expect(stdout.contains("hello world")) + } + } } - // Check that logs are returned to the plugin when echoLogs is false - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let (stdout, stderr) = try await self.execute(["print-diagnostics", "build", "printlogs"], packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, containsLogtext) - // Filter some unrelated output that could show up on stderr. - let filteredStderr = stderr.components(separatedBy: "\n") - .filter { !$0.contains("Unable to locate libSwiftScan") } - .filter { !($0.contains("warning: ") && $0.contains("unable to find libclang")) }.joined(separator: "\n") - XCTAssertMatch(filteredStderr, isEmpty) - } + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8782", relationship: .defect), + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginPermissions( + data: BuildData, + ) async throws { + try await withKnownIssue(isIntermittent: true) { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: + """ + // swift-tools-version: 5.6 + import PackageDescription + import Foundation + let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary" + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "PackageScribbler", description: "Help description"), + // We use an environment here so we can control whether we declare the permission. + permissions: ProcessInfo.processInfo.environment["DECLARE_PACKAGE_WRITING_PERMISSION"] == "1" + ? [.writeToPackageDirectory(reason: "For testing purposes")] + : [] + ) + ), + ] + ) + """ + ) + let libPath = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(libPath, recursive: true) + try localFileSystem.writeFileContents( + libPath.appending("library.swift"), + string: + "public func Foo() { }" + ) + let pluginPath = packageDir.appending(components: "Plugins", "MyPlugin") + try localFileSystem.createDirectory(pluginPath, recursive: true) + try localFileSystem.writeFileContents( + pluginPath.appending("plugin.swift"), + string: + """ + import PackagePlugin + import Foundation + + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + // Check that we can write to the package directory. + print("Trying to write to the package directory...") + guard FileManager.default.createFile(atPath: context.package.directory.appending("Foo").string, contents: Data("Hello".utf8)) else { + throw "Couldn’t create file at path \\(context.package.directory.appending("Foo"))" + } + print("... successfully created it") + } + } + extension String: Error {} + """ + ) - // Check that logs echoed to the console (on stderr) when echoLogs is true - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let (stdout, stderr) = try await self.execute(["print-diagnostics", "build", "echologs"], packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, isEmpty) - XCTAssertMatch(stderr, containsLogecho) - } + // Check that we get an error if the plugin needs permission but if we don't give it to them. Note that sandboxing is only currently supported on macOS. + #if os(macOS) + do { + await expectThrowsCommandExecutionError( + try await execute( + ["plugin", "PackageScribbler"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + // guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { + // return Issue.record("invalid error \(error)") + // } + #expect(!error.stdout.contains("successfully created it")) + #expect(error.stderr.contains("error: Plugin ‘MyPlugin’ wants permission to write to the package directory.")) + #expect(error.stderr.contains("Stated reason: “For testing purposes”.")) + #expect(error.stderr.contains("Use `--allow-writing-to-package-directory` to allow this.")) + } + } + #endif - // Check that logs are returned to the plugin and echoed to the console (on stderr) when echoLogs is true - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let (stdout, stderr) = try await self.execute(["print-diagnostics", "build", "printlogs", "echologs"], packagePath: fixturePath, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, containsLogtext) - XCTAssertMatch(stderr, containsLogecho) - } - } + // Check that we don't get an error (and also are allowed to write to the package directory) if we pass `--allow-writing-to-package-directory`. + do { + let (stdout, stderr) = try await execute( + ["plugin", "--allow-writing-to-package-directory", "PackageScribbler"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("successfully created it")) + #expect(!stderr.contains("error: Couldn’t create file at path")) + } - func testCommandPluginNetworkingPermissions(permissionsManifestFragment: String, permissionError: String, reason: String, remedy: [String]) async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") + // Check that we get an error if the plugin doesn't declare permission but tries to write anyway. Note that sandboxing is only currently supported on macOS. + #if os(macOS) + do { + await expectThrowsCommandExecutionError( + try await execute( + ["plugin", "PackageScribbler"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "0"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + // guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { + // Issue.record("invalid error \(error)") + // return + // } + #expect(!error.stdout.contains("successfully created it")) + #expect(error.stderr.contains("error: Couldn’t create file at path")) + } + } + #endif - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "MyPackage", - targets: [ - .target(name: "MyLibrary"), - .plugin(name: "MyPlugin", capability: .command(intent: .custom(verb: "Network", description: "Help description"), permissions: \(permissionsManifestFragment))), - ] - ) - """ - ) - try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), string: "public func Foo() { }") - try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift"), string: - """ - import PackagePlugin - - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand(context: PluginContext, arguments: [String]) throws { - print("hello world") + // Check default command with arguments + do { + let (stdout, stderr) = try await execute( + ["--allow-writing-to-package-directory", "PackageScribbler"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("successfully created it")) + #expect(!stderr.contains("error: Couldn’t create file at path")) } - } - """ - ) - #if os(macOS) - do { - await XCTAssertAsyncThrowsError(try await self.execute(["plugin", "Network"], packagePath: packageDir)) { error in - guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { - return XCTFail("invalid error \(error)") + // Check plugin arguments after plugin name + do { + let (stdout, stderr) = try await execute( + ["plugin", "PackageScribbler", "--allow-writing-to-package-directory"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("successfully created it")) + #expect(!stderr.contains("error: Couldn’t create file at path")) + } + + // Check default command with arguments after plugin name + do { + let (stdout, stderr) = try await execute( + ["PackageScribbler", "--allow-writing-to-package-directory"], + packagePath: packageDir, + env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("successfully created it")) + #expect(!stderr.contains("error: Couldn’t create file at path")) } - XCTAssertNoMatch(stdout, .contains("hello world")) - XCTAssertMatch(stderr, .contains("error: Plugin ‘MyPlugin’ wants permission to allow \(permissionError).")) - XCTAssertMatch(stderr, .contains("Stated reason: “\(reason)”.")) - XCTAssertMatch(stderr, .contains("Use `\(remedy.joined(separator: " "))` to allow this.")) } + } when: { + ProcessInfo.processInfo.environment["SWIFTCI_EXHIBITS_GH_8782"] != nil } - #endif + } - // Check that we don't get an error (and also are allowed to write to the package directory) if we pass `--allow-writing-to-package-directory`. - do { - let (stdout, _) = try await self.execute(["plugin"] + remedy + ["Network"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("hello world")) + @Test( + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + true, // check argument + false, // check default argument + ] + ) + func commandPluginArgumentsNotSwallowed( + data: BuildData, + _ checkArgument: Bool, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + + try localFileSystem.createDirectory(packageDir) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + import Foundation + let package = Package( + name: "MyPackage", + targets: [ + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "MyPlugin", description: "Help description") + ) + ), + ] + ) + """ + ) + + let pluginDir = packageDir.appending(components: "Plugins", "MyPlugin") + try localFileSystem.createDirectory(pluginDir, recursive: true) + try localFileSystem.writeFileContents( + pluginDir.appending("plugin.swift"), + string: """ + import PackagePlugin + import Foundation + + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + print (arguments) + guard arguments.contains("--foo") else { + throw "expecting argument foo" + } + guard arguments.contains("--help") else { + throw "expecting argument help" + } + guard arguments.contains("--version") else { + throw "expecting argument version" + } + guard arguments.contains("--verbose") else { + throw "expecting argument verbose" + } + print("success") + } + } + extension String: Error {} + """ + ) + + let commandPrefix = checkArgument ? ["plugin"] : [] + let (stdout, stderr) = try await execute( + commandPrefix + ["MyPlugin", "--foo", "--help", "--version", "--verbose"], + packagePath: packageDir, + env: ["SWIFT_DRIVER_SWIFTSCAN_LIB": "/this/is/a/bad/path"], + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("success")) + #expect(!stderr.contains("error:")) } } - } - func testCommandPluginNetworkingPermissions() async throws { - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(), reason: \"internet good\")]", - permissionError: "all network connections on all ports", - reason: "internet good", - remedy: ["--allow-network-connections", "all"]) - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(ports: [23, 42, 443, 8080]), reason: \"internet good\")]", - permissionError: "all network connections on ports: 23, 42, 443, 8080", - reason: "internet good", - remedy: ["--allow-network-connections", "all:23,42,443,8080"]) - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .all(ports: 1..<4), reason: \"internet good\")]", - permissionError: "all network connections on ports: 1, 2, 3", - reason: "internet good", - remedy: ["--allow-network-connections", "all:1,2,3"]) - - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(), reason: \"localhost good\")]", - permissionError: "local network connections on all ports", - reason: "localhost good", - remedy: ["--allow-network-connections", "local"]) - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(ports: [23, 42, 443, 8080]), reason: \"localhost good\")]", - permissionError: "local network connections on ports: 23, 42, 443, 8080", - reason: "localhost good", - remedy: ["--allow-network-connections", "local:23,42,443,8080"]) - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .local(ports: 1..<4), reason: \"localhost good\")]", - permissionError: "local network connections on ports: 1, 2, 3", - reason: "localhost good", - remedy: ["--allow-network-connections", "local:1,2,3"]) - - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .docker, reason: \"docker good\")]", - permissionError: "docker unix domain socket connections", - reason: "docker good", - remedy: ["--allow-network-connections", "docker"]) - try await testCommandPluginNetworkingPermissions( - permissionsManifestFragment: "[.allowNetworkConnections(scope: .unixDomainSocket, reason: \"unix sockets good\")]", - permissionError: "unix domain socket connections", - reason: "unix sockets good", - remedy: ["--allow-network-connections", "unixDomainSocket"]) - } + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. + .requiresSymbolgraphExtract, + .issue("https://github.com/swiftlang/swift-package-manager/issues/8848", relationship: .defect), + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginSymbolGraphCallbacks( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library, and executable, and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary" + ), + .executableTarget( + name: "MyCommand", + dependencies: ["MyLibrary"] + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .documentationGeneration() + ) + ), + ] + ) + """ + ) - func testCommandPluginPermissions() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") + let libraryPath = packageDir.appending(components: "Sources", "MyLibrary", "library.swift") + try localFileSystem.createDirectory(libraryPath.parentDirectory, recursive: true) + try localFileSystem.writeFileContents( + libraryPath, + string: #"public func GetGreeting() -> String { return "Hello" }"# + ) - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: - """ - // swift-tools-version: 5.6 - import PackageDescription - import Foundation - let package = Package( - name: "MyPackage", - targets: [ - .target( - name: "MyLibrary" - ), - .plugin( - name: "MyPlugin", - capability: .command( - intent: .custom(verb: "PackageScribbler", description: "Help description"), - // We use an environment here so we can control whether we declare the permission. - permissions: ProcessInfo.processInfo.environment["DECLARE_PACKAGE_WRITING_PERMISSION"] == "1" - ? [.writeToPackageDirectory(reason: "For testing purposes")] - : [] - ) - ), - ] - ) - """ - ) - let libPath = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(libPath, recursive: true) - try localFileSystem.writeFileContents(libPath.appending("library.swift"), string: - "public func Foo() { }" - ) - let pluginPath = packageDir.appending(components: "Plugins", "MyPlugin") - try localFileSystem.createDirectory(pluginPath, recursive: true) - try localFileSystem.writeFileContents(pluginPath.appending("plugin.swift"), string: - """ - import PackagePlugin - import Foundation - - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - // Check that we can write to the package directory. - print("Trying to write to the package directory...") - guard FileManager.default.createFile(atPath: context.package.directory.appending("Foo").string, contents: Data("Hello".utf8)) else { - throw "Couldn’t create file at path \\(context.package.directory.appending("Foo"))" + let commandPath = packageDir.appending(components: "Sources", "MyCommand", "main.swift") + try localFileSystem.createDirectory(commandPath.parentDirectory, recursive: true) + try localFileSystem.writeFileContents( + commandPath, + string: """ + import MyLibrary + print("\\(GetGreeting()), World!") + """ + ) + + let pluginPath = packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift") + try localFileSystem.createDirectory(pluginPath.parentDirectory, recursive: true) + try localFileSystem.writeFileContents( + pluginPath, + string: """ + import PackagePlugin + import Foundation + + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + // Ask for and print out the symbol graph directory for each target. + var argExtractor = ArgumentExtractor(arguments) + let targetNames = argExtractor.extractOption(named: "target") + let targets = targetNames.isEmpty + ? context.package.targets + : try context.package.targets(named: targetNames) + for target in targets { + let symbolGraph = try packageManager.getSymbolGraph(for: target, + options: .init(minimumAccessLevel: .public)) + print("\\(target.name): \\(symbolGraph.directoryPath)") + } + } + } + """ + ) + + // Check that if we don't pass any target, we successfully get symbol graph information for all targets in the package, and at different paths. + do { + let (stdout, _) = try await execute( + ["generate-documentation"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + switch data.buildSystem { + case .native: + #expect(stdout.contains("MyLibrary:")) + #expect(stdout.contains(AbsolutePath("/mypackage/MyLibrary").pathString)) + #expect(stdout.contains("MyCommand:")) + #expect(stdout.contains(AbsolutePath("/mypackage/MyCommand").pathString)) + case .swiftbuild: + #expect(stdout.contains("MyLibrary:")) + #expect(stdout.contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString)) + #expect(stdout.contains("MyCommand:")) + #expect(stdout.contains(AbsolutePath("/MyCommand.symbolgraphs").pathString)) + case .xcode: + Issue.record("Test expectations are not defined") } - print("... successfully created it") } - } - extension String: Error {} - """ - ) - // Check that we get an error if the plugin needs permission but if we don't give it to them. Note that sandboxing is only currently supported on macOS. - #if os(macOS) - do { - await XCTAssertAsyncThrowsError(try await self.execute(["plugin", "PackageScribbler"], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"])) { error in - guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { - return XCTFail("invalid error \(error)") + // Check that if we pass a target, we successfully get symbol graph information for just the target we asked for. + do { + let (stdout, _) = try await execute( + ["generate-documentation", "--target", "MyLibrary"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + switch data.buildSystem { + case .native: + #expect(stdout.contains("MyLibrary:")) + #expect(stdout.contains(AbsolutePath("/mypackage/MyLibrary").pathString)) + #expect(!stdout.contains("MyCommand:")) + #expect(!stdout.contains(AbsolutePath("/mypackage/MyCommand").pathString)) + case .swiftbuild: + #expect(stdout.contains("MyLibrary:")) + #expect(stdout.contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString)) + #expect(!stdout.contains("MyCommand:")) + #expect(!stdout.contains(AbsolutePath("/MyCommand.symbolgraphs").pathString)) + case .xcode: + Issue.record("Test expectations are not defined") + } } - XCTAssertNoMatch(stdout, .contains("successfully created it")) - XCTAssertMatch(stderr, .contains("error: Plugin ‘MyPlugin’ wants permission to write to the package directory.")) - XCTAssertMatch(stderr, .contains("Stated reason: “For testing purposes”.")) - XCTAssertMatch(stderr, .contains("Use `--allow-writing-to-package-directory` to allow this.")) } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } - #endif - - // Check that we don't get an error (and also are allowed to write to the package directory) if we pass `--allow-writing-to-package-directory`. - do { - let (stdout, stderr) = try await self.execute(["plugin", "--allow-writing-to-package-directory", "PackageScribbler"], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"]) - XCTAssertMatch(stdout, .contains("successfully created it")) - XCTAssertNoMatch(stderr, .contains("error: Couldn’t create file at path")) - } + } - // Check that we get an error if the plugin doesn't declare permission but tries to write anyway. Note that sandboxing is only currently supported on macOS. - #if os(macOS) - do { - await XCTAssertAsyncThrowsError(try await self.execute(["plugin", "PackageScribbler"], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "0"])) { error in - guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = error else { - return XCTFail("invalid error \(error)") - } - XCTAssertNoMatch(stdout, .contains("successfully created it")) - XCTAssertMatch(stderr, .contains("error: Couldn’t create file at path")) - } - } - #endif - - // Check default command with arguments - do { - let (stdout, stderr) = try await self.execute(["--allow-writing-to-package-directory", "PackageScribbler"], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"]) - XCTAssertMatch(stdout, .contains("successfully created it")) - XCTAssertNoMatch(stderr, .contains("error: Couldn’t create file at path")) - } - - // Check plugin arguments after plugin name - do { - let (stdout, stderr) = try await self.execute(["plugin", "PackageScribbler", "--allow-writing-to-package-directory"], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"]) - XCTAssertMatch(stdout, .contains("successfully created it")) - XCTAssertNoMatch(stderr, .contains("error: Couldn’t create file at path")) - } - - // Check default command with arguments after plugin name - do { - let (stdout, stderr) = try await self.execute(["PackageScribbler", "--allow-writing-to-package-directory", ], packagePath: packageDir, env: ["DECLARE_PACKAGE_WRITING_PERMISSION": "1"]) - XCTAssertMatch(stdout, .contains("successfully created it")) - XCTAssertNoMatch(stderr, .contains("error: Couldn’t create file at path")) - } - } - } - - func testCommandPluginArgumentsNotSwallowed() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - - try localFileSystem.createDirectory(packageDir) - try localFileSystem.writeFileContents( - packageDir.appending(components: "Package.swift"), - string: """ - // swift-tools-version: 5.6 - import PackageDescription - import Foundation - let package = Package( - name: "MyPackage", - targets: [ - .plugin( - name: "MyPlugin", - capability: .command( - intent: .custom(verb: "MyPlugin", description: "Help description") + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginBuildingCallbacks( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + let buildSystemProvider = data.buildSystem + // Create a sample package with a library, an executable, and a command plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + products: [ + .library( + name: "MyAutomaticLibrary", + targets: ["MyLibrary"] + ), + .library( + name: "MyStaticLibrary", + type: .static, + targets: ["MyLibrary"] + ), + .library( + name: "MyDynamicLibrary", + type: .dynamic, + targets: ["MyLibrary"] + ), + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + targets: [ + .target( + name: "MyLibrary" + ), + .executableTarget( + name: "MyExecutable", + dependencies: ["MyLibrary"] + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "my-build-tester", description: "Help description") + ) + ), + ] ) - ), - ] - ) - """ - ) + """ + ) + let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") + try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + // Extract the plugin arguments. + var argExtractor = ArgumentExtractor(arguments) + let productNames = argExtractor.extractOption(named: "product") + if productNames.count != 1 { + throw "Expected exactly one product name, but had: \\(productNames.joined(separator: ", "))" + } + let products = try context.package.products(named: productNames) + let printCommands = (argExtractor.extractFlag(named: "print-commands") > 0) + let release = (argExtractor.extractFlag(named: "release") > 0) + if let unextractedArgs = argExtractor.unextractedOptionsOrFlags.first { + throw "Unknown option: \\(unextractedArgs)" + } + let positionalArgs = argExtractor.remainingArguments + if !positionalArgs.isEmpty { + throw "Unexpected extra arguments: \\(positionalArgs)" + } + do { + var parameters = PackageManager.BuildParameters() + parameters.configuration = release ? .release : .debug + parameters.logging = printCommands ? .verbose : .concise + parameters.otherSwiftcFlags = ["-DEXTRA_SWIFT_FLAG"] + let result = try packageManager.build(.product(products[0].name), parameters: parameters) + print("succeeded: \\(result.succeeded)") + for artifact in result.builtArtifacts { + print("artifact-path: \\(artifact.path.string)") + print("artifact-kind: \\(artifact.kind)") + } + print("log:\\n\\(result.logText)") + } + catch { + print("error from the plugin host: \\(error)") + } + } + } + extension String: Error {} + """ + ) + let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myLibraryTargetDir.appending("library.swift"), + string: """ + public func GetGreeting() -> String { return "Hello" } + """ + ) + let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") + try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myExecutableTargetDir.appending("main.swift"), + string: """ + import MyLibrary + print("\\(GetGreeting()), World!") + """ + ) - let pluginDir = packageDir.appending(components: "Plugins", "MyPlugin") - try localFileSystem.createDirectory(pluginDir, recursive: true) - try localFileSystem.writeFileContents( - pluginDir.appending("plugin.swift"), - string: """ - import PackagePlugin - import Foundation - - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - print (arguments) - guard arguments.contains("--foo") else { - throw "expecting argument foo" - } - guard arguments.contains("--help") else { - throw "expecting argument help" - } - guard arguments.contains("--version") else { - throw "expecting argument version" + // Invoke the plugin with parameters choosing a verbose build of MyExecutable for debugging. + do { + let (stdout, _) = try await execute( + ["my-build-tester", "--product", "MyExecutable", "--print-commands"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Building for debugging...")) + if buildSystemProvider == .native { + #expect(stdout.contains("-module-name MyExecutable")) + #expect(stdout.contains("-DEXTRA_SWIFT_FLAG")) + #expect(stdout.contains("Build of product 'MyExecutable' complete!")) } - guard arguments.contains("--verbose") else { - throw "expecting argument verbose" + #expect(stdout.contains("succeeded: true")) + switch buildSystemProvider { + case .native: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("debug/MyExecutable").pathString)) + case .swiftbuild: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("MyExecutable").pathString)) + case .xcode: + Issue.record("unimplemented assertion for --build-system xcode") } - print("success") + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("executable")) } - } - extension String: Error {} - """ - ) - - // Check arguments - do { - let (stdout, stderr) = try await self.execute(["plugin", "MyPlugin", "--foo", "--help", "--version", "--verbose"], packagePath: packageDir, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, .contains("success")) - XCTAssertFalse(stderr.contains("error:")) - } - - // Check default command arguments - do { - let (stdout, stderr) = try await self.execute(["MyPlugin", "--foo", "--help", "--version", "--verbose"], packagePath: packageDir, env: ["SWIFT_DRIVER_SWIFTSCAN_LIB" : "/this/is/a/bad/path"]) - XCTAssertMatch(stdout, .contains("success")) - XCTAssertFalse(stderr.contains("error:")) - } - } - } - - func testCommandPluginSymbolGraphCallbacks() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. - try XCTSkipIf((try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library, and executable, and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir) - try localFileSystem.writeFileContents( - packageDir.appending(components: "Package.swift"), - string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - targets: [ - .target( - name: "MyLibrary" - ), - .executableTarget( - name: "MyCommand", - dependencies: ["MyLibrary"] - ), - .plugin( - name: "MyPlugin", - capability: .command( - intent: .documentationGeneration() - ) - ), - ] - ) - """ - ) - let libraryPath = packageDir.appending(components: "Sources", "MyLibrary", "library.swift") - try localFileSystem.createDirectory(libraryPath.parentDirectory, recursive: true) - try localFileSystem.writeFileContents( - libraryPath, - string: #"public func GetGreeting() -> String { return "Hello" }"# - ) - - let commandPath = packageDir.appending(components: "Sources", "MyCommand", "main.swift") - try localFileSystem.createDirectory(commandPath.parentDirectory, recursive: true) - try localFileSystem.writeFileContents( - commandPath, - string: """ - import MyLibrary - print("\\(GetGreeting()), World!") - """ - ) - - let pluginPath = packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift") - try localFileSystem.createDirectory(pluginPath.parentDirectory, recursive: true) - try localFileSystem.writeFileContents( - pluginPath, - string: """ - import PackagePlugin - import Foundation - - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - // Ask for and print out the symbol graph directory for each target. - var argExtractor = ArgumentExtractor(arguments) - let targetNames = argExtractor.extractOption(named: "target") - let targets = targetNames.isEmpty - ? context.package.targets - : try context.package.targets(named: targetNames) - for target in targets { - let symbolGraph = try packageManager.getSymbolGraph(for: target, - options: .init(minimumAccessLevel: .public)) - print("\\(target.name): \\(symbolGraph.directoryPath)") + // Invoke the plugin with parameters choosing a concise build of MyExecutable for release. + do { + let (stdout, _) = try await execute( + ["my-build-tester", "--product", "MyExecutable", "--release"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Building for production...")) + #expect(!stdout.contains("-module-name MyExecutable")) + if buildSystemProvider == .native { + #expect(stdout.contains("Build of product 'MyExecutable' complete!")) + } + #expect(stdout.contains("succeeded: true")) + switch buildSystemProvider { + case .native: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("release/MyExecutable").pathString)) + case .swiftbuild: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("MyExecutable").pathString)) + case .xcode: + Issue.record("unimplemented assertion for --build-system xcode") } + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("executable")) } - } - """ - ) - - // Check that if we don't pass any target, we successfully get symbol graph information for all targets in the package, and at different paths. - do { - let (stdout, _) = try await self.execute(["generate-documentation"], packagePath: packageDir) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) - XCTAssertMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) - } else if buildSystemProvider == .swiftbuild { - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString))) - XCTAssertMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/MyCommand.symbolgraphs").pathString))) - } - } - - // Check that if we pass a target, we successfully get symbol graph information for just the target we asked for. - do { - let (stdout, _) = try await self.execute(["generate-documentation", "--target", "MyLibrary"], packagePath: packageDir) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) - XCTAssertNoMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) - } else if buildSystemProvider == .swiftbuild { - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString))) - XCTAssertNoMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/MyCommand.symbolgraphs").pathString))) - } - } - } - } - func testCommandPluginBuildingCallbacks() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library, an executable, and a command plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - products: [ - .library( - name: "MyAutomaticLibrary", - targets: ["MyLibrary"] - ), - .library( - name: "MyStaticLibrary", - type: .static, - targets: ["MyLibrary"] - ), - .library( - name: "MyDynamicLibrary", - type: .dynamic, - targets: ["MyLibrary"] - ), - .executable( - name: "MyExecutable", - targets: ["MyExecutable"] - ), - ], - targets: [ - .target( - name: "MyLibrary" - ), - .executableTarget( - name: "MyExecutable", - dependencies: ["MyLibrary"] - ), - .plugin( - name: "MyPlugin", - capability: .command( - intent: .custom(verb: "my-build-tester", description: "Help description") - ) - ), - ] - ) - """ - ) - let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") - try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - // Extract the plugin arguments. - var argExtractor = ArgumentExtractor(arguments) - let productNames = argExtractor.extractOption(named: "product") - if productNames.count != 1 { - throw "Expected exactly one product name, but had: \\(productNames.joined(separator: ", "))" - } - let products = try context.package.products(named: productNames) - let printCommands = (argExtractor.extractFlag(named: "print-commands") > 0) - let release = (argExtractor.extractFlag(named: "release") > 0) - if let unextractedArgs = argExtractor.unextractedOptionsOrFlags.first { - throw "Unknown option: \\(unextractedArgs)" + // Invoke the plugin with parameters choosing a verbose build of MyStaticLibrary for release. + do { + let (stdout, _) = try await execute( + ["my-build-tester", "--product", "MyStaticLibrary", "--print-commands", "--release"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Building for production...")) + #expect(!stdout.contains("Building for debug...")) + #expect(!stdout.contains("-module-name MyLibrary")) + if buildSystemProvider == .native { + #expect(stdout.contains("Build of product 'MyStaticLibrary' complete!")) } - let positionalArgs = argExtractor.remainingArguments - if !positionalArgs.isEmpty { - throw "Unexpected extra arguments: \\(positionalArgs)" + #expect(stdout.contains("succeeded: true")) + switch buildSystemProvider { + case .native: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("release/libMyStaticLibrary").pathString)) + case .swiftbuild: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("MyStaticLibrary").pathString)) + case .xcode: + Issue.record("unimplemented assertion for --build-system xcode") } - do { - var parameters = PackageManager.BuildParameters() - parameters.configuration = release ? .release : .debug - parameters.logging = printCommands ? .verbose : .concise - parameters.otherSwiftcFlags = ["-DEXTRA_SWIFT_FLAG"] - let result = try packageManager.build(.product(products[0].name), parameters: parameters) - print("succeeded: \\(result.succeeded)") - for artifact in result.builtArtifacts { - print("artifact-path: \\(artifact.path.string)") - print("artifact-kind: \\(artifact.kind)") - } - print("log:\\n\\(result.logText)") + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("staticLibrary")) + } + + // Invoke the plugin with parameters choosing a verbose build of MyDynamicLibrary for release. + do { + let (stdout, _) = try await execute( + ["my-build-tester", "--product", "MyDynamicLibrary", "--print-commands", "--release"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(stdout.contains("Building for production...")) + #expect(!stdout.contains("Building for debug...")) + #expect(!stdout.contains("-module-name MyLibrary")) + if buildSystemProvider == .native { + #expect(stdout.contains("Build of product 'MyDynamicLibrary' complete!")) } - catch { - print("error from the plugin host: \\(error)") + #expect(stdout.contains("succeeded: true")) + switch buildSystemProvider { + case .native: + #if os(Windows) + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("release/MyDynamicLibrary.dll").pathString)) + #else + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("release/libMyDynamicLibrary").pathString)) + #endif + case .swiftbuild: + #expect(stdout.contains("artifact-path:")) + #expect(stdout.contains(RelativePath("MyDynamicLibrary").pathString)) + case .xcode: + Issue.record("unimplemented assertion for --build-system xcode") } + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("dynamicLibrary")) } } - extension String: Error {} - """ - ) - let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) - try localFileSystem.writeFileContents(myLibraryTargetDir.appending("library.swift"), string: """ - public func GetGreeting() -> String { return "Hello" } - """ - ) - let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") - try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) - try localFileSystem.writeFileContents(myExecutableTargetDir.appending("main.swift"), string: """ - import MyLibrary - print("\\(GetGreeting()), World!") - """ - ) - - // Invoke the plugin with parameters choosing a verbose build of MyExecutable for debugging. - do { - let (stdout, _) = try await self.execute(["my-build-tester", "--product", "MyExecutable", "--print-commands"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Building for debugging...")) - XCTAssertNoMatch(stdout, .contains("Building for production...")) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .contains("-module-name MyExecutable")) - XCTAssertMatch(stdout, .contains("-DEXTRA_SWIFT_FLAG")) - XCTAssertMatch(stdout, .contains("Build of product 'MyExecutable' complete!")) - } - XCTAssertMatch(stdout, .contains("succeeded: true")) - switch buildSystemProvider { - case .native: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("debug/MyExecutable").pathString))) - case .swiftbuild: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("MyExecutable").pathString))) - case .xcode: - XCTFail("unimplemented assertion for --build-system xcode") - } - XCTAssertMatch(stdout, .and(.contains("artifact-kind:"), .contains("executable"))) + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } + } - // Invoke the plugin with parameters choosing a concise build of MyExecutable for release. - do { - let (stdout, _) = try await self.execute(["my-build-tester", "--product", "MyExecutable", "--release"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Building for production...")) - XCTAssertNoMatch(stdout, .contains("Building for debug...")) - XCTAssertNoMatch(stdout, .contains("-module-name MyExecutable")) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .contains("Build of product 'MyExecutable' complete!")) - } - XCTAssertMatch(stdout, .contains("succeeded: true")) - switch buildSystemProvider { - case .native: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("release/MyExecutable").pathString))) - case .swiftbuild: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("MyExecutable").pathString))) - case .xcode: - XCTFail("unimplemented assertion for --build-system xcode") - } - XCTAssertMatch(stdout, .and(.contains("artifact-kind:"), .contains("executable"))) - } + @Test( + .IssueWindowsRelativePathAssert, + .requiresSwiftConcurrencySupport, + // Depending on how the test is running, the `llvm-profdata` and `llvm-cov` tool might be unavailable. + .requiresLLVMProfData, + .requiresLLVMCov, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginTestingCallbacks( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library, a command plugin, and a couple of tests. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary" + ), + .plugin( + name: "MyPlugin", + capability: .command( + intent: .custom(verb: "my-test-tester", description: "Help description") + ) + ), + .testTarget( + name: "MyBasicTests" + ), + .testTarget( + name: "MyExtendedTests" + ), + ] + ) + """ + ) + let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") + try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + do { + let result = try packageManager.test(.filtered(["MyBasicTests"]), parameters: .init(enableCodeCoverage: true)) + assert(result.succeeded == true) + assert(result.testTargets.count == 1) + assert(result.testTargets[0].name == "MyBasicTests") + assert(result.testTargets[0].testCases.count == 2) + assert(result.testTargets[0].testCases[0].name == "MyBasicTests.TestSuite1") + assert(result.testTargets[0].testCases[0].tests.count == 2) + assert(result.testTargets[0].testCases[0].tests[0].name == "testBooleanInvariants") + assert(result.testTargets[0].testCases[0].tests[1].result == .succeeded) + assert(result.testTargets[0].testCases[0].tests[1].name == "testNumericalInvariants") + assert(result.testTargets[0].testCases[0].tests[1].result == .succeeded) + assert(result.testTargets[0].testCases[1].name == "MyBasicTests.TestSuite2") + assert(result.testTargets[0].testCases[1].tests.count == 1) + assert(result.testTargets[0].testCases[1].tests[0].name == "testStringInvariants") + assert(result.testTargets[0].testCases[1].tests[0].result == .succeeded) + assert(result.codeCoverageDataFile?.extension == "json") + } + catch { + print("error from the plugin host: \\(error)") + } + } + } + """ + ) + let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myLibraryTargetDir.appending("library.swift"), + string: """ + public func Foo() { } + """ + ) + let myBasicTestsTargetDir = packageDir.appending(components: "Tests", "MyBasicTests") + try localFileSystem.createDirectory(myBasicTestsTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myBasicTestsTargetDir.appending("Test1.swift"), + string: """ + import XCTest + class TestSuite1: XCTestCase { + func testBooleanInvariants() throws { + XCTAssertEqual(true || true, true) + } + func testNumericalInvariants() throws { + XCTAssertEqual(1 + 1, 2) + } + } + """ + ) + try localFileSystem.writeFileContents( + myBasicTestsTargetDir.appending("Test2.swift"), + string: """ + import XCTest + class TestSuite2: XCTestCase { + func testStringInvariants() throws { + XCTAssertEqual("" + "", "") + } + } + """ + ) + let myExtendedTestsTargetDir = packageDir.appending(components: "Tests", "MyExtendedTests") + try localFileSystem.createDirectory(myExtendedTestsTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myExtendedTestsTargetDir.appending("Test3.swift"), + string: """ + import XCTest + class TestSuite3: XCTestCase { + func testArrayInvariants() throws { + XCTAssertEqual([] + [], []) + } + func testImpossibilities() throws { + XCTFail("no can do") + } + } + """ + ) - // Invoke the plugin with parameters choosing a verbose build of MyStaticLibrary for release. - do { - let (stdout, _) = try await self.execute(["my-build-tester", "--product", "MyStaticLibrary", "--print-commands", "--release"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Building for production...")) - XCTAssertNoMatch(stdout, .contains("Building for debug...")) - XCTAssertNoMatch(stdout, .contains("-module-name MyLibrary")) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .contains("Build of product 'MyStaticLibrary' complete!")) - } - XCTAssertMatch(stdout, .contains("succeeded: true")) - switch buildSystemProvider { - case .native: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("release/libMyStaticLibrary").pathString))) - case .swiftbuild: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("MyStaticLibrary").pathString))) - case .xcode: - XCTFail("unimplemented assertion for --build-system xcode") - } - XCTAssertMatch(stdout, .and(.contains("artifact-kind:"), .contains("staticLibrary"))) - } + // Check basic usage with filtering and code coverage. The plugin itself asserts a bunch of values. + try await execute( + ["my-test-tester"], + packagePath: packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // Invoke the plugin with parameters choosing a verbose build of MyDynamicLibrary for release. - do { - let (stdout, _) = try await self.execute(["my-build-tester", "--product", "MyDynamicLibrary", "--print-commands", "--release"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Building for production...")) - XCTAssertNoMatch(stdout, .contains("Building for debug...")) - XCTAssertNoMatch(stdout, .contains("-module-name MyLibrary")) - if buildSystemProvider == .native { - XCTAssertMatch(stdout, .contains("Build of product 'MyDynamicLibrary' complete!")) - } - XCTAssertMatch(stdout, .contains("succeeded: true")) - switch buildSystemProvider { - case .native: - #if os(Windows) - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("release/MyDynamicLibrary.dll").pathString))) - #else - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("release/libMyDynamicLibrary").pathString))) - #endif - case .swiftbuild: - XCTAssertMatch(stdout, .and(.contains("artifact-path:"), .contains(RelativePath("MyDynamicLibrary").pathString))) - case .xcode: - XCTFail("unimplemented assertion for --build-system xcode") + // We'll add checks for various error conditions here in a future commit. } - XCTAssertMatch(stdout, .and(.contains("artifact-kind:"), .contains("dynamicLibrary"))) + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - } - - func testCommandPluginTestingCallbacks() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - // Depending on how the test is running, the `llvm-profdata` and `llvm-cov` tool might be unavailable. - try XCTSkipIf((try? UserToolchain.default.getLLVMProf()) == nil, "skipping test because the `llvm-profdata` tool isn't available") - try XCTSkipIf((try? UserToolchain.default.getLLVMCov()) == nil, "skipping test because the `llvm-cov` tool isn't available") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library, a command plugin, and a couple of tests. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - targets: [ - .target( - name: "MyLibrary" - ), - .plugin( - name: "MyPlugin", - capability: .command( - intent: .custom(verb: "my-test-tester", description: "Help description") - ) - ), - .testTarget( - name: "MyBasicTests" - ), - .testTarget( - name: "MyExtendedTests" - ), - ] - ) - """ - ) - let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin") - try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main - struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - do { - let result = try packageManager.test(.filtered(["MyBasicTests"]), parameters: .init(enableCodeCoverage: true)) - assert(result.succeeded == true) - assert(result.testTargets.count == 1) - assert(result.testTargets[0].name == "MyBasicTests") - assert(result.testTargets[0].testCases.count == 2) - assert(result.testTargets[0].testCases[0].name == "MyBasicTests.TestSuite1") - assert(result.testTargets[0].testCases[0].tests.count == 2) - assert(result.testTargets[0].testCases[0].tests[0].name == "testBooleanInvariants") - assert(result.testTargets[0].testCases[0].tests[1].result == .succeeded) - assert(result.testTargets[0].testCases[0].tests[1].name == "testNumericalInvariants") - assert(result.testTargets[0].testCases[0].tests[1].result == .succeeded) - assert(result.testTargets[0].testCases[1].name == "MyBasicTests.TestSuite2") - assert(result.testTargets[0].testCases[1].tests.count == 1) - assert(result.testTargets[0].testCases[1].tests[0].name == "testStringInvariants") - assert(result.testTargets[0].testCases[1].tests[0].result == .succeeded) - assert(result.codeCoverageDataFile?.extension == "json") - } - catch { - print("error from the plugin host: \\(error)") - } - } - } - """ - ) - let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) - try localFileSystem.writeFileContents(myLibraryTargetDir.appending("library.swift"), string: """ - public func Foo() { } - """ - ) - let myBasicTestsTargetDir = packageDir.appending(components: "Tests", "MyBasicTests") - try localFileSystem.createDirectory(myBasicTestsTargetDir, recursive: true) - try localFileSystem.writeFileContents(myBasicTestsTargetDir.appending("Test1.swift"), string: """ - import XCTest - class TestSuite1: XCTestCase { - func testBooleanInvariants() throws { - XCTAssertEqual(true || true, true) - } - func testNumericalInvariants() throws { - XCTAssertEqual(1 + 1, 2) - } - } - """ - ) - try localFileSystem.writeFileContents(myBasicTestsTargetDir.appending("Test2.swift"), string: """ - import XCTest - class TestSuite2: XCTestCase { - func testStringInvariants() throws { - XCTAssertEqual("" + "", "") - } - } - """ - ) - let myExtendedTestsTargetDir = packageDir.appending(components: "Tests", "MyExtendedTests") - try localFileSystem.createDirectory(myExtendedTestsTargetDir, recursive: true) - try localFileSystem.writeFileContents(myExtendedTestsTargetDir.appending("Test3.swift"), string: """ - import XCTest - class TestSuite3: XCTestCase { - func testArrayInvariants() throws { - XCTAssertEqual([] + [], []) - } - func testImpossibilities() throws { - XCTFail("no can do") - } - } - """ - ) - - // Check basic usage with filtering and code coverage. The plugin itself asserts a bunch of values. - try await self.execute(["my-test-tester"], packagePath: packageDir) - // We'll add checks for various error conditions here in a future commit. + struct PluginAPIsData { + let commandArgs: CLIArguments + let expectedStdout: [String] + let expectedStderr: [String] } - } - - func testPluginAPIs() async throws { - try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8554, $0.path.lastComponent in test code returns fullpaths on Windows") - - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a plugin to test various parts of the API. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "MyPackage", - dependencies: [ - .package(name: "HelperPackage", path: "VendoredDependencies/HelperPackage") + @Test( + .IssueWindowsPathLastConponent, + // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.Plugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + [ + PluginAPIsData( + // Check that a target doesn't include itself in its recursive dependencies. + commandArgs: ["print-target-dependencies", "--target", "SecondTarget"], + expectedStdout: [ + "Recursive dependencies of 'SecondTarget': [\"FirstTarget\"]", + "Module kind of 'SecondTarget': generic", ], - targets: [ - .target( - name: "FirstTarget", - dependencies: [ - ] - ), - .target( - name: "SecondTarget", - dependencies: [ - "FirstTarget", - ] - ), - .target( - name: "ThirdTarget", - dependencies: [ - "FirstTarget", - ] - ), - .target( - name: "FourthTarget", - dependencies: [ - "SecondTarget", - "ThirdTarget", - .product(name: "HelperLibrary", package: "HelperPackage"), - ] - ), - .executableTarget( - name: "FifthTarget", - dependencies: [ - "FirstTarget", - "ThirdTarget", - ] - ), - .testTarget( - name: "TestTarget", - dependencies: [ - "SecondTarget", - ] - ), - .plugin( - name: "PrintTargetDependencies", - capability: .command( - intent: .custom(verb: "print-target-dependencies", description: "Plugin that prints target dependencies; argument is name of target") - ) - ), - ] - ) - """) - - let firstTargetDir = packageDir.appending(components: "Sources", "FirstTarget") - try localFileSystem.createDirectory(firstTargetDir, recursive: true) - try localFileSystem.writeFileContents(firstTargetDir.appending("library.swift"), string: """ - public func FirstFunc() { } - """) - - let secondTargetDir = packageDir.appending(components: "Sources", "SecondTarget") - try localFileSystem.createDirectory(secondTargetDir, recursive: true) - try localFileSystem.writeFileContents(secondTargetDir.appending("library.swift"), string: """ - public func SecondFunc() { } - """) - - let thirdTargetDir = packageDir.appending(components: "Sources", "ThirdTarget") - try localFileSystem.createDirectory(thirdTargetDir, recursive: true) - try localFileSystem.writeFileContents(thirdTargetDir.appending("library.swift"), string: """ - public func ThirdFunc() { } - """) - - let fourthTargetDir = packageDir.appending(components: "Sources", "FourthTarget") - try localFileSystem.createDirectory(fourthTargetDir, recursive: true) - try localFileSystem.writeFileContents(fourthTargetDir.appending("library.swift"), string: """ - public func FourthFunc() { } - """) - - let fifthTargetDir = packageDir.appending(components: "Sources", "FifthTarget") - try localFileSystem.createDirectory(fifthTargetDir, recursive: true) - try localFileSystem.writeFileContents(fifthTargetDir.appending("main.swift"), string: """ - @main struct MyExec { - func run() throws {} - } - """) - - let testTargetDir = packageDir.appending(components: "Tests", "TestTarget") - try localFileSystem.createDirectory(testTargetDir, recursive: true) - try localFileSystem.writeFileContents(testTargetDir.appending("tests.swift"), string: """ - import XCTest - class MyTestCase: XCTestCase { - } - """) - - let pluginTargetTargetDir = packageDir.appending(components: "Plugins", "PrintTargetDependencies") - try localFileSystem.createDirectory(pluginTargetTargetDir, recursive: true) - try localFileSystem.writeFileContents(pluginTargetTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct PrintTargetDependencies: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - // Print names of the recursive dependencies of the given target. - var argExtractor = ArgumentExtractor(arguments) - guard let targetName = argExtractor.extractOption(named: "target").first else { - throw "No target argument provided" - } - guard let target = try? context.package.targets(named: [targetName]).first else { - throw "No target found with the name '\\(targetName)'" - } - print("Recursive dependencies of '\\(target.name)': \\(target.recursiveTargetDependencies.map(\\.name))") - - let execProducts = context.package.products(ofType: ExecutableProduct.self) - print("execProducts: \\(execProducts.map{ $0.name })") - let swiftTargets = context.package.targets(ofType: SwiftSourceModuleTarget.self) - print("swiftTargets: \\(swiftTargets.map{ $0.name }.sorted())") - let swiftSources = swiftTargets.flatMap{ $0.sourceFiles(withSuffix: ".swift") } - print("swiftSources: \\(swiftSources.map{ $0.path.lastComponent }.sorted())") + expectedStderr: [], + ), + PluginAPIsData( + // Check that targets are not included twice in recursive dependencies. + commandArgs: ["print-target-dependencies", "--target", "ThirdTarget"], + expectedStdout: [ + "Recursive dependencies of 'ThirdTarget': [\"FirstTarget\"]", + "Module kind of 'ThirdTarget': generic", + ], + expectedStderr: [], + ), + PluginAPIsData( + // Check that product dependencies work in recursive dependencies. + commandArgs: ["print-target-dependencies", "--target", "FourthTarget"], + expectedStdout: [ + "Recursive dependencies of 'FourthTarget': [\"FirstTarget\", \"SecondTarget\", \"ThirdTarget\", \"HelperLibrary\"]", + "Module kind of 'FourthTarget': generic", + ], + expectedStderr: [], + ), + PluginAPIsData( + // Check some of the other utility APIs. + commandArgs: ["print-target-dependencies", "--target", "FifthTarget"], + expectedStdout: [ + "execProducts: [\"FifthTarget\"]", + "swiftTargets: [\"FifthTarget\", \"FirstTarget\", \"FourthTarget\", \"SecondTarget\", \"TestTarget\", \"ThirdTarget\"]", + "swiftSources: [\"library.swift\", \"library.swift\", \"library.swift\", \"library.swift\", \"main.swift\", \"tests.swift\"]", + "Module kind of 'FifthTarget': executable", + ], + expectedStderr: [], + ), + PluginAPIsData( + // Check a test target. + commandArgs: ["print-target-dependencies", "--target", "TestTarget"], + expectedStdout: [ + "Recursive dependencies of 'TestTarget': [\"FirstTarget\", \"SecondTarget\"]", + "Module kind of 'TestTarget': test", + ], + expectedStderr: [], + ), + ], + ) + func pluginAPIs( + buildData: BuildData, + testData: PluginAPIsData + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a plugin to test various parts of the API. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending("Package.swift"), + string: """ + // swift-tools-version: 5.9 + import PackageDescription + let package = Package( + name: "MyPackage", + dependencies: [ + .package(name: "HelperPackage", path: "VendoredDependencies/HelperPackage") + ], + targets: [ + .target( + name: "FirstTarget", + dependencies: [ + ] + ), + .target( + name: "SecondTarget", + dependencies: [ + "FirstTarget", + ] + ), + .target( + name: "ThirdTarget", + dependencies: [ + "FirstTarget", + ] + ), + .target( + name: "FourthTarget", + dependencies: [ + "SecondTarget", + "ThirdTarget", + .product(name: "HelperLibrary", package: "HelperPackage"), + ] + ), + .executableTarget( + name: "FifthTarget", + dependencies: [ + "FirstTarget", + "ThirdTarget", + ] + ), + .testTarget( + name: "TestTarget", + dependencies: [ + "SecondTarget", + ] + ), + .plugin( + name: "PrintTargetDependencies", + capability: .command( + intent: .custom(verb: "print-target-dependencies", description: "Plugin that prints target dependencies; argument is name of target") + ) + ), + ] + ) + """ + ) - if let target = target.sourceModule { - print("Module kind of '\\(target.name)': \\(target.kind)") - } + let firstTargetDir = packageDir.appending(components: "Sources", "FirstTarget") + try localFileSystem.createDirectory(firstTargetDir, recursive: true) + try localFileSystem.writeFileContents( + firstTargetDir.appending("library.swift"), + string: """ + public func FirstFunc() { } + """ + ) - var sourceModules = context.package.sourceModules - print("sourceModules in package: \\(sourceModules.map { $0.name })") - sourceModules = context.package.products.first?.sourceModules ?? [] - print("sourceModules in first product: \\(sourceModules.map { $0.name })") - } - } - extension String: Error {} - """) - - // Create a separate vendored package so that we can test dependencies across products in other packages. - let helperPackageDir = packageDir.appending(components: "VendoredDependencies", "HelperPackage") - try localFileSystem.createDirectory(helperPackageDir, recursive: true) - try localFileSystem.writeFileContents(helperPackageDir.appending("Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "HelperPackage", - products: [ - .library( - name: "HelperLibrary", - targets: ["HelperLibrary"]) - ], - targets: [ - .target( - name: "HelperLibrary", - path: ".") - ] - ) - """) - try localFileSystem.writeFileContents(helperPackageDir.appending("library.swift"), string: """ - public func Foo() { } - """) - - // Check that a target doesn't include itself in its recursive dependencies. - do { - let (stdout, _) = try await self.execute(["print-target-dependencies", "--target", "SecondTarget"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Recursive dependencies of 'SecondTarget': [\"FirstTarget\"]")) - XCTAssertMatch(stdout, .contains("Module kind of 'SecondTarget': generic")) - } + let secondTargetDir = packageDir.appending(components: "Sources", "SecondTarget") + try localFileSystem.createDirectory(secondTargetDir, recursive: true) + try localFileSystem.writeFileContents( + secondTargetDir.appending("library.swift"), + string: """ + public func SecondFunc() { } + """ + ) - // Check that targets are not included twice in recursive dependencies. - do { - let (stdout, _) = try await self.execute(["print-target-dependencies", "--target", "ThirdTarget"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Recursive dependencies of 'ThirdTarget': [\"FirstTarget\"]")) - XCTAssertMatch(stdout, .contains("Module kind of 'ThirdTarget': generic")) - } + let thirdTargetDir = packageDir.appending(components: "Sources", "ThirdTarget") + try localFileSystem.createDirectory(thirdTargetDir, recursive: true) + try localFileSystem.writeFileContents( + thirdTargetDir.appending("library.swift"), + string: """ + public func ThirdFunc() { } + """ + ) - // Check that product dependencies work in recursive dependencies. - do { - let (stdout, _) = try await self.execute(["print-target-dependencies", "--target", "FourthTarget"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Recursive dependencies of 'FourthTarget': [\"FirstTarget\", \"SecondTarget\", \"ThirdTarget\", \"HelperLibrary\"]")) - XCTAssertMatch(stdout, .contains("Module kind of 'FourthTarget': generic")) - } + let fourthTargetDir = packageDir.appending(components: "Sources", "FourthTarget") + try localFileSystem.createDirectory(fourthTargetDir, recursive: true) + try localFileSystem.writeFileContents( + fourthTargetDir.appending("library.swift"), + string: """ + public func FourthFunc() { } + """ + ) - // Check some of the other utility APIs. - do { - let (stdout, _) = try await self.execute(["print-target-dependencies", "--target", "FifthTarget"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("execProducts: [\"FifthTarget\"]")) - XCTAssertMatch(stdout, .contains("swiftTargets: [\"FifthTarget\", \"FirstTarget\", \"FourthTarget\", \"SecondTarget\", \"TestTarget\", \"ThirdTarget\"]")) - XCTAssertMatch(stdout, .contains("swiftSources: [\"library.swift\", \"library.swift\", \"library.swift\", \"library.swift\", \"main.swift\", \"tests.swift\"]")) - XCTAssertMatch(stdout, .contains("Module kind of 'FifthTarget': executable")) - } + let fifthTargetDir = packageDir.appending(components: "Sources", "FifthTarget") + try localFileSystem.createDirectory(fifthTargetDir, recursive: true) + try localFileSystem.writeFileContents( + fifthTargetDir.appending("main.swift"), + string: """ + @main struct MyExec { + func run() throws {} + } + """ + ) - // Check a test target. - do { - let (stdout, _) = try await self.execute(["print-target-dependencies", "--target", "TestTarget"], packagePath: packageDir) - XCTAssertMatch(stdout, .contains("Recursive dependencies of 'TestTarget': [\"FirstTarget\", \"SecondTarget\"]")) - XCTAssertMatch(stdout, .contains("Module kind of 'TestTarget': test")) - } - } - } + let testTargetDir = packageDir.appending(components: "Tests", "TestTarget") + try localFileSystem.createDirectory(testTargetDir, recursive: true) + try localFileSystem.writeFileContents( + testTargetDir.appending("tests.swift"), + string: """ + import XCTest + class MyTestCase: XCTestCase { + } + """ + ) - func testPluginCompilationBeforeBuilding() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") + let pluginTargetTargetDir = packageDir.appending(components: "Plugins", "PrintTargetDependencies") + try localFileSystem.createDirectory(pluginTargetTargetDir, recursive: true) + try localFileSystem.writeFileContents( + pluginTargetTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct PrintTargetDependencies: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + // Print names of the recursive dependencies of the given target. + var argExtractor = ArgumentExtractor(arguments) + guard let targetName = argExtractor.extractOption(named: "target").first else { + throw "No target argument provided" + } + guard let target = try? context.package.targets(named: [targetName]).first else { + throw "No target found with the name '\\(targetName)'" + } + print("Recursive dependencies of '\\(target.name)': \\(target.recursiveTargetDependencies.map(\\.name))") + + let execProducts = context.package.products(ofType: ExecutableProduct.self) + print("execProducts: \\(execProducts.map{ $0.name })") + let swiftTargets = context.package.targets(ofType: SwiftSourceModuleTarget.self) + print("swiftTargets: \\(swiftTargets.map{ $0.name }.sorted())") + let swiftSources = swiftTargets.flatMap{ $0.sourceFiles(withSuffix: ".swift") } + print("swiftSources: \\(swiftSources.map{ $0.path.lastComponent }.sorted())") + + if let target = target.sourceModule { + print("Module kind of '\\(target.name)': \\(target.kind)") + } + + var sourceModules = context.package.sourceModules + print("sourceModules in package: \\(sourceModules.map { $0.name })") + sourceModules = context.package.products.first?.sourceModules ?? [] + print("sourceModules in first product: \\(sourceModules.map { $0.name })") + } + } + extension String: Error {} + """ + ) - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a couple of plugins a other targets and products. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - products: [ - .library( - name: "MyLibrary", - targets: ["MyLibrary"] - ), - .executable( - name: "MyExecutable", - targets: ["MyExecutable"] - ), - ], - targets: [ - .target( - name: "MyLibrary" - ), - .executableTarget( - name: "MyExecutable", - dependencies: ["MyLibrary"] - ), - .plugin( - name: "MyBuildToolPlugin", - capability: .buildTool() - ), - .plugin( - name: "MyCommandPlugin", - capability: .command( - intent: .custom(verb: "my-build-tester", description: "Help description") + // Create a separate vendored package so that we can test dependencies across products in other packages. + let helperPackageDir = packageDir.appending(components: "VendoredDependencies", "HelperPackage") + try localFileSystem.createDirectory(helperPackageDir, recursive: true) + try localFileSystem.writeFileContents( + helperPackageDir.appending("Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "HelperPackage", + products: [ + .library( + name: "HelperLibrary", + targets: ["HelperLibrary"]) + ], + targets: [ + .target( + name: "HelperLibrary", + path: ".") + ] ) - ), - ] - ) - """ - ) - let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) - try localFileSystem.writeFileContents(myLibraryTargetDir.appending("library.swift"), string: """ - public func GetGreeting() -> String { return "Hello" } - """ - ) - let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") - try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) - try localFileSystem.writeFileContents(myExecutableTargetDir.appending("main.swift"), string: """ - import MyLibrary - print("\\(GetGreeting()), World!") - """ - ) - let myBuildToolPluginTargetDir = packageDir.appending(components: "Plugins", "MyBuildToolPlugin") - try localFileSystem.createDirectory(myBuildToolPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myBuildToolPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct MyBuildToolPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) throws -> [Command] { - return [] + """ + ) + try localFileSystem.writeFileContents( + helperPackageDir.appending("library.swift"), + string: """ + public func Foo() { } + """ + ) + + let (stdout, stderr) = try await execute( + testData.commandArgs, + packagePath: packageDir, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + for expected in testData.expectedStdout { + #expect(stdout.contains(expected)) } - } - """ - ) - let myCommandPluginTargetDir = packageDir.appending(components: "Plugins", "MyCommandPlugin") - try localFileSystem.createDirectory(myCommandPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myCommandPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { + for expected in testData.expectedStderr { + #expect(stderr.contains(expected)) } } - """ - ) - - // Check that building without options compiles both plugins and that the build proceeds. - do { - let (stdout, _) = try await executeSwiftBuild( - packageDir, - buildSystem: self.buildSystemProvider, - ) - if (self.buildSystemProvider == .native) { - XCTAssertMatch(stdout, .contains("Compiling plugin MyBuildToolPlugin")) - XCTAssertMatch(stdout, .contains("Compiling plugin MyCommandPlugin")) - } - XCTAssertMatch(stdout, .contains("Building for debugging...")) - } - - // Check that building just one of them just compiles that plugin and doesn't build anything else. - do { - let (stdout, _) = try await executeSwiftBuild( - packageDir, - extraArgs: ["--target", "MyCommandPlugin"], - buildSystem: self.buildSystemProvider, - ) - XCTAssertFalse(stdout.contains("Compiling plugin MyBuildToolPlugin"), "stdout: '\(stdout)'") - XCTAssertTrue(stdout.contains("Compiling plugin MyCommandPlugin"), "stdout: '\(stdout)'") - XCTAssertFalse(stdout.contains("Building for debugging..."), "stdout: '\(stdout)'") + } when: { + ProcessInfo.hostOperatingSystem == .windows } } - } - - func testCommandPluginCompilationError() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a couple of plugins a other targets and products. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift"), string: """ - // swift-tools-version: 5.6 - import PackageDescription - let package = Package( - name: "MyPackage", - products: [ - .library( - name: "MyLibrary", - targets: ["MyLibrary"] - ), - .executable( - name: "MyExecutable", - targets: ["MyExecutable"] - ), - ], - targets: [ - .target( - name: "MyLibrary" - ), - .executableTarget( - name: "MyExecutable", - dependencies: ["MyLibrary"] - ), - .plugin( - name: "MyBuildToolPlugin", - capability: .buildTool() - ), - .plugin( - name: "MyCommandPlugin", - capability: .command( - intent: .custom(verb: "my-build-tester", description: "Help description") + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8977", relationship: .defect), + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.Plugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func pluginCompilationBeforeBuilding( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a couple of plugins a other targets and products. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + products: [ + .library( + name: "MyLibrary", + targets: ["MyLibrary"] + ), + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + targets: [ + .target( + name: "MyLibrary" + ), + .executableTarget( + name: "MyExecutable", + dependencies: ["MyLibrary"] + ), + .plugin( + name: "MyBuildToolPlugin", + capability: .buildTool() + ), + .plugin( + name: "MyCommandPlugin", + capability: .command( + intent: .custom(verb: "my-build-tester", description: "Help description") + ) + ), + ] ) - ), - ] - ) - """ - ) - let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") - try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) - try localFileSystem.writeFileContents(myLibraryTargetDir.appending("library.swift"), string: """ - public func GetGreeting() -> String { return "Hello" } - """ - ) - let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") - try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) - try localFileSystem.writeFileContents(myExecutableTargetDir.appending("main.swift"), string: """ - import MyLibrary - print("\\(GetGreeting()), World!") - """ - ) - let myBuildToolPluginTargetDir = packageDir.appending(components: "Plugins", "MyBuildToolPlugin") - try localFileSystem.createDirectory(myBuildToolPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myBuildToolPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct MyBuildToolPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) throws -> [Command] { - return [] - } - } - """ - ) - // Deliberately break the command plugin. - let myCommandPluginTargetDir = packageDir.appending(components: "Plugins", "MyCommandPlugin") - try localFileSystem.createDirectory(myCommandPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myCommandPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct MyCommandPlugin: CommandPlugin { - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - this is an error + """ + ) + let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myLibraryTargetDir.appending("library.swift"), + string: """ + public func GetGreeting() -> String { return "Hello" } + """ + ) + let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") + try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myExecutableTargetDir.appending("main.swift"), + string: """ + import MyLibrary + print("\\(GetGreeting()), World!") + """ + ) + let myBuildToolPluginTargetDir = packageDir.appending(components: "Plugins", "MyBuildToolPlugin") + try localFileSystem.createDirectory(myBuildToolPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myBuildToolPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + return [] + } + } + """ + ) + let myCommandPluginTargetDir = packageDir.appending(components: "Plugins", "MyCommandPlugin") + try localFileSystem.createDirectory(myCommandPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myCommandPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + } + } + """ + ) + + // Check that building without options compiles both plugins and that the build proceeds. + do { + let (stdout, _) = try await executeSwiftBuild( + packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + if (data.buildSystem == .native) { + #expect(stdout.contains("Compiling plugin MyBuildToolPlugin")) + #expect(stdout.contains("Compiling plugin MyCommandPlugin")) + } + #expect(stdout.contains("Building for \(data.config.buildFor)...")) } - } - """ - ) - // Check that building stops after compiling the plugin and doesn't proceed. - // Run this test a number of times to try to catch any race conditions. - for _ in 1...5 { - await XCTAssertAsyncThrowsError( - try await executeSwiftBuild( - packageDir, - buildSystem: self.buildSystemProvider, - ) - ) { error in - guard case SwiftPMError.executionFailure(_, let stdout, _) = error else { - return XCTFail("invalid error \(error)") + // Check that building just one of them just compiles that plugin and doesn't build anything else. + do { + let (stdout, _) = try await executeSwiftBuild( + packageDir, + configuration: data.config, + extraArgs: ["--target", "MyCommandPlugin"], + buildSystem: data.buildSystem, + ) + #expect(!stdout.contains("Compiling plugin MyBuildToolPlugin")) + #expect(stdout.contains("Compiling plugin MyCommandPlugin")) + #expect(!stdout.contains("Building for \(data.config.buildFor)...")) } - XCTAssertTrue(stdout.contains("error: consecutive statements on a line must be separated by ';'"), "stdout: '\(stdout)'") - XCTAssertFalse(stdout.contains("Building for debugging..."), "stdout: '\(stdout)'") } + } when: { + data.buildSystem == .swiftbuild } } - } - - func testSinglePluginTarget() async throws { - // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). - try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") - try localFileSystem.createDirectory(packageDir, recursive: true) - try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: """ - // swift-tools-version: 5.7 - import PackageDescription - let package = Package( - name: "MyPackage", - products: [ - .plugin(name: "Foo", targets: ["Foo"]) - ], - dependencies: [ - ], - targets: [ - .plugin( - name: "Foo", - capability: .command( - intent: .custom(verb: "Foo", description: "Plugin example"), - permissions: [] - ) - ) - ] - ) - """) - - let myPluginTargetDir = packageDir.appending(components: "Plugins", "Foo") - try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) - try localFileSystem.writeFileContents(myPluginTargetDir.appending("plugin.swift"), string: """ - import PackagePlugin - @main struct FooPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) throws -> [Command] { } - } - """) - - // Load a workspace from the package. - let observability = ObservabilitySystem.makeForTesting() - let workspace = try Workspace( - fileSystem: localFileSystem, - forRootPackage: packageDir, - customManifestLoader: ManifestLoader(toolchain: UserToolchain.default), - delegate: MockWorkspaceDelegate() - ) - - // Load the root manifest. - let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try await workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope - ) - XCTAssert(rootManifests.count == 1, "\(rootManifests)") - - // Load the package graph. - let _ = try await workspace.loadPackageGraph( - rootInput: rootInput, - observabilityScope: observability.topScope - ) - XCTAssertNoDiagnostics(observability.diagnostics) - } - } -} - - -class PackageCommandNativeTests: PackageCommandTestCase { - - override open var buildSystemProvider: BuildSystemProvider.Kind { - return .native - } - - override func testNoParameters() async throws { - try await super.testNoParameters() - } - - override func testMigrateCommandWhenDependencyBuildsForHostAndTarget() async throws { - try XCTSkipOnWindows( - because: "error: build planning stopped due to build-tool plugin failures", - skipPlatformCi: true, - ) - - try await super.testMigrateCommandWhenDependencyBuildsForHostAndTarget() - } -} - -class PackageCommandSwiftBuildTests: PackageCommandTestCase { - - override open var buildSystemProvider: BuildSystemProvider.Kind { - return .swiftbuild - } - - override func testNoParameters() async throws { - try await super.testNoParameters() - } - - override func testMigrateCommand() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'AllIncludingTests' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, - ) - try await super.testMigrateCommand() - } - - override func testMigrateCommandUpdateManifest2Targets() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'A' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, - ) - - try await super.testMigrateCommandUpdateManifest2Targets() - } - - override func testMigrateCommandUpdateManifestSingleTarget() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'A' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, - ) - - try await super.testMigrateCommandUpdateManifestSingleTarget() - } - - override func testMigrateCommandUpdateManifestWithErrors() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'A' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, - ) - - try await super.testMigrateCommandUpdateManifestWithErrors() - } - - override func testMigrateCommandWhenDependencyBuildsForHostAndTarget() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'A' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8977", relationship: .defect), + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func commandPluginCompilationError( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a couple of plugins a other targets and products. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending(components: "Package.swift"), + string: """ + // swift-tools-version: 5.6 + import PackageDescription + let package = Package( + name: "MyPackage", + products: [ + .library( + name: "MyLibrary", + targets: ["MyLibrary"] + ), + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + targets: [ + .target( + name: "MyLibrary" + ), + .executableTarget( + name: "MyExecutable", + dependencies: ["MyLibrary"] + ), + .plugin( + name: "MyBuildToolPlugin", + capability: .buildTool() + ), + .plugin( + name: "MyCommandPlugin", + capability: .command( + intent: .custom(verb: "my-build-tester", description: "Help description") + ) + ), + ] + ) + """ + ) + let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary") + try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myLibraryTargetDir.appending("library.swift"), + string: """ + public func GetGreeting() -> String { return "Hello" } + """ + ) + let myExecutableTargetDir = packageDir.appending(components: "Sources", "MyExecutable") + try localFileSystem.createDirectory(myExecutableTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myExecutableTargetDir.appending("main.swift"), + string: """ + import MyLibrary + print("\\(GetGreeting()), World!") + """ + ) + let myBuildToolPluginTargetDir = packageDir.appending(components: "Plugins", "MyBuildToolPlugin") + try localFileSystem.createDirectory(myBuildToolPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myBuildToolPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + return [] + } + } + """ + ) + // Deliberately break the command plugin. + let myCommandPluginTargetDir = packageDir.appending(components: "Plugins", "MyCommandPlugin") + try localFileSystem.createDirectory(myCommandPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myCommandPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + this is an error + } + } + """ + ) - try await super.testMigrateCommandWhenDependencyBuildsForHostAndTarget() - } + // Check that building stops after compiling the plugin and doesn't proceed. + // Run this test a number of times to try to catch any race conditions. + for num in 1...5 { + await expectThrowsCommandExecutionError( + try await executeSwiftBuild( + packageDir, + configuration: data.config, + buildSystem: data.buildSystem, + ) + ) { error in + let stdout = error.stdout + withKnownIssue(isIntermittent: true) { + #expect( + stdout.contains("error: consecutive statements on a line must be separated by ';'"), + "iteration \(num) failed", + ) + } when: { + data.config == .release && data.buildSystem == .native + } + #expect( + !stdout.contains("Building for \(data.config.buildFor)..."), + "iteration \(num) failed", + ) + } + } + } + } when: { + data.buildSystem == .swiftbuild + } + } - override func testMigrateCommandWithBuildToolPlugins() async throws { - try XCTSkipOnWindows( - because: """ - Possibly https://github.com/swiftlang/swift-package-manager/issues/8602: - error: Could not choose a single platform for target 'A' from the supported platforms 'android qnx webassembly'. Specialization parameters imposed by workspace: platform 'nil' sdkVariant 'nil' supportedPlatforms: 'nil' toolchain: 'nil' - """, - skipPlatformCi: true, + @Test( + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.Plugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) + func singlePluginTarget( + data: BuildData, + ) async throws { + try await testWithTemporaryDirectory { tmpPath in + // Create a sample package with a library target and a plugin. + let packageDir = tmpPath.appending(components: "MyPackage") + try localFileSystem.createDirectory(packageDir, recursive: true) + try localFileSystem.writeFileContents( + packageDir.appending("Package.swift"), + string: """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyPackage", + products: [ + .plugin(name: "Foo", targets: ["Foo"]) + ], + dependencies: [ + ], + targets: [ + .plugin( + name: "Foo", + capability: .command( + intent: .custom(verb: "Foo", description: "Plugin example"), + permissions: [] + ) + ) + ] + ) + """ + ) - try await super.testMigrateCommandWithBuildToolPlugins() - } - - override func testCommandPluginSymbolGraphCallbacks() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - try await super.testCommandPluginSymbolGraphCallbacks() - } - - override func testCommandPluginBuildingCallbacks() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - try await super.testCommandPluginBuildingCallbacks() - } - - override func testCommandPluginTestingCallbacks() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - try await super.testCommandPluginTestingCallbacks() - } - - override func testCommandPluginTargetBuilds() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - try await super.testCommandPluginTargetBuilds() - } - - override func testCommandPluginPermissions() async throws { - try XCTExhibitsGitHubIssue(8782) - try await super.testCommandPluginPermissions() - } - - override func testBuildToolPlugin() async throws { - try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") - try await super.testBuildToolPlugin() - } - - override func testCommandPluginCompilationError() async throws { - throw XCTSkip("SWBINTTODO: https://github.com/swiftlang/swift-package-manager/issues/8977: does not throw expected error") - try await super.testCommandPluginCompilationError() - } - - override func testPluginCompilationBeforeBuilding() async throws { - throw XCTSkip("SWBINTTODO: https:4//github.com/swiftlang/swift-package-manager/issues/8977") - try await super.testPluginCompilationBeforeBuilding() - } - - override func testPackageEditAndUnedit() async throws { - #if os(Linux) - throw XCTSkip("SWBINTTODO: https://github.com/swiftlang/swift-package-manager/issues/8416: /tmp/Miscellaneous_PackageEdit.H5ku8Q/foo/.build/aarch64-unknown-linux-gnu/Products/Debug-linux/foo: error while loading shared libraries: libswiftCore.so: cannot open shared object file: No such file or directory") - #endif - try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8774: Unable to write file. and https://github.com/swiftlang/swift-package-manager/issues/8380: linker issue") - try await super.testPackageEditAndUnedit() - } + let myPluginTargetDir = packageDir.appending(components: "Plugins", "Foo") + try localFileSystem.createDirectory(myPluginTargetDir, recursive: true) + try localFileSystem.writeFileContents( + myPluginTargetDir.appending("plugin.swift"), + string: """ + import PackagePlugin + @main struct FooPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { } + } + """ + ) - override func testPackageReset() async throws { - try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8774: Unable to write file. and https://github.com/swiftlang/swift-package-manager/issues/8380: linker issue") - try await super.testPackageReset() - } + // Load a workspace from the package. + let observability = ObservabilitySystem.makeForTesting() + let workspace = try Workspace( + fileSystem: localFileSystem, + forRootPackage: packageDir, + customManifestLoader: ManifestLoader(toolchain: UserToolchain.default), + delegate: MockWorkspaceDelegate() + ) - override func testPackageClean() async throws { - try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8774: Unable to write file. and https://github.com/swiftlang/swift-package-manager/issues/8380: linker issue") - try await super.testPackageClean() - } + // Load the root manifest. + let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) + #expect(rootManifests.count == 1, "Root manifest: \(rootManifests)") - override func testPackageResolved() async throws { - #if os(Linux) - throw XCTSkip("SWBINTTODO: https://github.com/swiftlang/swift-package-manager/issues/8416: /tmp/Miscellaneous_PackageEdit.H5ku8Q/foo/.build/aarch64-unknown-linux-gnu/Products/Debug-linux/foo: error while loading shared libraries: libswiftCore.so: cannot open shared object file: No such file or directory") - #endif - try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8774: Unable to write file. and https://github.com/swiftlang/swift-package-manager/issues/8380: linker issue") - try await super.testPackageResolved() + // Load the package graph. + let _ = try await workspace.loadPackageGraph( + rootInput: rootInput, + observabilityScope: observability.topScope + ) + expectNoDiagnostics(observability.diagnostics) + } + } } } diff --git a/Tests/CommandsTests/PackageRegistryCommandTests.swift b/Tests/CommandsTests/PackageRegistryCommandTests.swift index 4ed21677e7f..4add7133a3f 100644 --- a/Tests/CommandsTests/PackageRegistryCommandTests.swift +++ b/Tests/CommandsTests/PackageRegistryCommandTests.swift @@ -30,6 +30,11 @@ import struct Basics.AsyncProcessResult let defaultRegistryBaseURL = URL("https://packages.example.com") let customRegistryBaseURL = URL("https://custom.packages.example.com") +@Suite( + .tags( + .Feature.Command.PackageRegistry.General, + ), +) struct PackageRegistryCommandTests { @discardableResult private func execute( diff --git a/Tests/CommandsTests/SwiftCommandStateTests.swift b/Tests/CommandsTests/SwiftCommandStateTests.swift index 266f8faebfe..d97538a0523 100644 --- a/Tests/CommandsTests/SwiftCommandStateTests.swift +++ b/Tests/CommandsTests/SwiftCommandStateTests.swift @@ -2,25 +2,25 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2021-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2021-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +import Foundation @testable import Basics @testable import Build @testable import Commands @testable import CoreCommands -@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) -import func PackageGraph.loadModulesGraph +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) import func PackageGraph.loadModulesGraph import _InternalTestSupport @testable import PackageModel -import XCTest +import Testing import ArgumentParser import class TSCBasic.BufferedOutputByteStream @@ -28,9 +28,17 @@ import protocol TSCBasic.OutputByteStream import enum TSCBasic.SystemError import var TSCBasic.stderrStream -final class SwiftCommandStateTests: CommandsTestCase { - func testSeverityEnum() async throws { - try fixtureXCTest(name: "Miscellaneous/Simple") { _ in +@Suite( + .serialized, +) +struct SwiftCommandStateTests { + @Test( + .tags( + .TestSize.small, + ) + ) + func severityEnum() async throws { + try fixture(name: "Miscellaneous/Simple") { _ in do { let info = Diagnostic(severity: .info, message: "info-string", metadata: nil) @@ -38,33 +46,34 @@ final class SwiftCommandStateTests: CommandsTestCase { let warning = Diagnostic(severity: .warning, message: "warning-string", metadata: nil) let error = Diagnostic(severity: .error, message: "error-string", metadata: nil) // testing color - XCTAssertEqual(info.severity.color, .white) - XCTAssertEqual(debug.severity.color, .white) - XCTAssertEqual(warning.severity.color, .yellow) - XCTAssertEqual(error.severity.color, .red) + #expect(info.severity.color == .white) + #expect(debug.severity.color == .white) + #expect(warning.severity.color == .yellow) + #expect(error.severity.color == .red) // testing prefix - XCTAssertEqual(info.severity.logLabel, "info: ") - XCTAssertEqual(debug.severity.logLabel, "debug: ") - XCTAssertEqual(warning.severity.logLabel, "warning: ") - XCTAssertEqual(error.severity.logLabel, "error: ") + #expect(info.severity.logLabel == "info: ") + #expect(debug.severity.logLabel == "debug: ") + #expect(warning.severity.logLabel == "warning: ") + #expect(error.severity.logLabel == "error: ") // testing boldness - XCTAssertTrue(info.severity.isBold) - XCTAssertTrue(debug.severity.isBold) - XCTAssertTrue(warning.severity.isBold) - XCTAssertTrue(error.severity.isBold) + #expect(info.severity.isBold) + #expect(debug.severity.isBold) + #expect(warning.severity.isBold) + #expect(error.severity.isBold) } } } - func testVerbosityLogLevel() async throws { - try fixtureXCTest(name: "Miscellaneous/Simple") { fixturePath in + @Test + func verbosityLogLevel() async throws { + try fixture(name: "Miscellaneous/Simple") { fixturePath in do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .warning) + #expect(tool.logLevel == .warning) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -73,17 +82,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(description.contains("warning: warning")) + #expect(!description.contains("info: info")) + #expect(!description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--verbose"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .info) + #expect(tool.logLevel == .info) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -92,17 +102,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(description.contains("warning: warning")) + #expect(description.contains("info: info")) + #expect(!description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "-v"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .info) + #expect(tool.logLevel == .info) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -111,17 +122,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(description.contains("warning: warning")) + #expect(description.contains("info: info")) + #expect(!description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--very-verbose"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .debug) + #expect(tool.logLevel == .debug) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -130,17 +142,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(description.contains("warning: warning")) + #expect(description.contains("info: info")) + #expect(description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--vv"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .debug) + #expect(tool.logLevel == .debug) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -149,17 +162,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(description.contains("warning: warning")) + #expect(description.contains("info: info")) + #expect(description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--quiet"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .error) + #expect(tool.logLevel == .error) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -168,17 +182,18 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(!description.contains("warning: warning")) + #expect(!description.contains("info: info")) + #expect(!description.contains("debug: debug")) } do { let outputStream = BufferedOutputByteStream() let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "-q"]) let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) - XCTAssertEqual(tool.logLevel, .error) + #expect(tool.logLevel == .error) tool.observabilityScope.emit(error: "error") tool.observabilityScope.emit(warning: "warning") @@ -187,42 +202,46 @@ final class SwiftCommandStateTests: CommandsTestCase { tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: error")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("warning: warning")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("info: info")) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("debug: debug")) + let description = try #require(outputStream.bytes.validDescription) + #expect(description.contains("error: error")) + #expect(!description.contains("warning: warning")) + #expect(!description.contains("info: info")) + #expect(!description.contains("debug: debug")) } } } - func testAuthorizationProviders() async throws { - try fixtureXCTest(name: "DependencyResolution/External/XCFramework") { fixturePath in + @Test + func authorizationProviders() async throws { + try fixture(name: "DependencyResolution/External/XCFramework") { fixturePath in let fs = localFileSystem // custom .netrc file do { - let customPath = try fs.tempDirectory.appending(component: UUID().uuidString) + let netrcFile = try fs.tempDirectory.appending(component: UUID().uuidString) try fs.writeFileContents( - customPath, + netrcFile, string: "machine mymachine.labkey.org login custom@labkey.org password custom" ) - let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--netrc-file", customPath.pathString]) + let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--netrc-file", netrcFile.pathString]) let tool = try SwiftCommandState.makeMockState(options: options) - let authorizationProvider = try tool.getAuthorizationProvider() as? CompositeAuthorizationProvider - let netrcProviders = authorizationProvider?.providers.compactMap { $0 as? NetrcAuthorizationProvider } ?? [] - XCTAssertEqual(netrcProviders.count, 1) - XCTAssertEqual(try netrcProviders.first.map { try resolveSymlinks($0.path) }, try resolveSymlinks(customPath)) + let authorizationProvider = try #require(tool.getAuthorizationProvider() as? CompositeAuthorizationProvider) + let netrcProviders = authorizationProvider.providers.compactMap { $0 as? NetrcAuthorizationProvider } + try #require(netrcProviders.count == 1) + let expectedPath = try resolveSymlinks(netrcFile) + let actualPath = try netrcProviders.first.map { try resolveSymlinks($0.path) } + #expect(actualPath == expectedPath) - let auth = try tool.getAuthorizationProvider()?.authentication(for: "https://mymachine.labkey.org") - XCTAssertEqual(auth?.user, "custom@labkey.org") - XCTAssertEqual(auth?.password, "custom") + let auth = try #require(tool.getAuthorizationProvider()?.authentication(for: "https://mymachine.labkey.org")) + #expect(auth.user == "custom@labkey.org") + #expect(auth.password == "custom") // delete it - try localFileSystem.removeFileTree(customPath) - XCTAssertThrowsError(try tool.getAuthorizationProvider(), "error expected") { error in - XCTAssertEqual(error as? StringError, StringError("Did not find netrc file at \(customPath).")) + try localFileSystem.removeFileTree(netrcFile) + #expect(throws: StringError("Did not find netrc file at \(netrcFile).")) { + try tool.getAuthorizationProvider() } } @@ -230,57 +249,65 @@ final class SwiftCommandStateTests: CommandsTestCase { } } - func testRegistryAuthorizationProviders() async throws { - try fixtureXCTest(name: "DependencyResolution/External/XCFramework") { fixturePath in + @Test + func registryAuthorizationProviders() async throws { + try fixture(name: "DependencyResolution/External/XCFramework") { fixturePath in let fs = localFileSystem // custom .netrc file do { - let customPath = try fs.tempDirectory.appending(component: UUID().uuidString) + let netrcFile = try fs.tempDirectory.appending(component: UUID().uuidString) try fs.writeFileContents( - customPath, + netrcFile, string: "machine mymachine.labkey.org login custom@labkey.org password custom" ) - let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--netrc-file", customPath.pathString]) + let options = try GlobalOptions.parse(["--package-path", fixturePath.pathString, "--netrc-file", netrcFile.pathString]) let tool = try SwiftCommandState.makeMockState(options: options) // There is only one AuthorizationProvider depending on platform -#if canImport(Security) - let keychainProvider = try tool.getRegistryAuthorizationProvider() as? KeychainAuthorizationProvider - XCTAssertNotNil(keychainProvider) -#else - let netrcProvider = try tool.getRegistryAuthorizationProvider() as? NetrcAuthorizationProvider - XCTAssertNotNil(netrcProvider) - XCTAssertEqual(try netrcProvider.map { try resolveSymlinks($0.path) }, try resolveSymlinks(customPath)) - - let auth = try tool.getRegistryAuthorizationProvider()?.authentication(for: "https://mymachine.labkey.org") - XCTAssertEqual(auth?.user, "custom@labkey.org") - XCTAssertEqual(auth?.password, "custom") - - // delete it - try localFileSystem.removeFileTree(customPath) - XCTAssertThrowsError(try tool.getRegistryAuthorizationProvider(), "error expected") { error in - XCTAssertEqual(error as? StringError, StringError("did not find netrc file at \(customPath)")) - } -#endif + #if canImport(Security) + let _ = try #require(tool.getRegistryAuthorizationProvider() as? KeychainAuthorizationProvider) + #else + let netrcProvider = try #require(tool.getRegistryAuthorizationProvider() as? NetrcAuthorizationProvider) + let expectedPath = try resolveSymlinks(netrcFile) + #expect(try netrcProvider.map { try resolveSymlinks($0.path) } == expectedPath) + + let authorizationProvider = try #require(tool.getRegistryAuthorizationProvider()) + let auth = authorizationProvider.authentication(for: "https://mymachine.labkey.org") + #expect(auth.user == "custom@labkey.org") + #expect(auth.password == "custom") + + // delete it + try localFileSystem.removeFileTree(netrcFile) + #expect(throws: (any Error).self, "error expected") { error in + #expect(error as? StringError == StringError("did not find netrc file at \(netrcFile)")) + } + #endif } // Tests should not modify user's home dir .netrc so leaving that out intentionally } } - func testDebugFormatFlags() async throws { + @Test + func debugFormatFlags() async throws { let fs = InMemoryFileSystem(emptyFiles: [ - "/Pkg/Sources/exe/main.swift", + "/Pkg/Sources/exe/main.swift" ]) let observer = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph(fileSystem: fs, manifests: [ - Manifest.createRootManifest(displayName: "Pkg", - path: "/Pkg", - targets: [TargetDescription(name: "exe")]) - ], observabilityScope: observer.topScope) + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Pkg", + path: "/Pkg", + targets: [TargetDescription(name: "exe")] + ) + ], + observabilityScope: observer.topScope + ) var plan: BuildPlan @@ -294,8 +321,10 @@ final class SwiftCommandStateTests: CommandsTestCase { fileSystem: fs, observabilityScope: observer.topScope ) - try XCTAssertMatch(plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], - [.anySequence, "-g", "-use-ld=lld", "-Xlinker", "-debug:dwarf"]) + try XCTAssertMatch( + plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], + [.anySequence, "-g", "-use-ld=lld", "-Xlinker", "-debug:dwarf"] + ) /* -debug-info-format codeview */ let explicitCodeViewOptions = try GlobalOptions.parse(["--triple", "x86_64-unknown-windows-msvc", "-debug-info-format", "codeview"]) @@ -308,8 +337,10 @@ final class SwiftCommandStateTests: CommandsTestCase { fileSystem: fs, observabilityScope: observer.topScope ) - try XCTAssertMatch(plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], - [.anySequence, "-g", "-debug-info-format=codeview", "-Xlinker", "-debug"]) + try XCTAssertMatch( + plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], + [.anySequence, "-g", "-debug-info-format=codeview", "-Xlinker", "-debug"] + ) // Explicitly pass Linux as when the `SwiftCommandState` tests are enabled on // Windows, this would fail otherwise as CodeView is supported on the @@ -317,8 +348,8 @@ final class SwiftCommandStateTests: CommandsTestCase { let unsupportedCodeViewOptions = try GlobalOptions.parse(["--triple", "x86_64-unknown-linux-gnu", "-debug-info-format", "codeview"]) let unsupportedCodeView = try SwiftCommandState.makeMockState(options: unsupportedCodeViewOptions) - XCTAssertThrowsError(try unsupportedCodeView.productsBuildParameters) { - XCTAssertEqual($0 as? StringError, StringError("CodeView debug information is currently not supported on linux")) + #expect(throws: StringError("CodeView debug information is currently not supported on linux")) { + try unsupportedCodeView.productsBuildParameters } /* <> */ @@ -331,8 +362,10 @@ final class SwiftCommandStateTests: CommandsTestCase { fileSystem: fs, observabilityScope: observer.topScope ) - try XCTAssertMatch(plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], - [.anySequence, "-g", "-use-ld=lld", "-Xlinker", "-debug:dwarf"]) + try XCTAssertMatch( + plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], + [.anySequence, "-g", "-use-ld=lld", "-Xlinker", "-debug:dwarf"] + ) /* -debug-info-format none */ let explicitNoDebugInfoOptions = try GlobalOptions.parse(["--triple", "x86_64-unknown-windows-msvc", "-debug-info-format", "none"]) @@ -344,195 +377,218 @@ final class SwiftCommandStateTests: CommandsTestCase { fileSystem: fs, observabilityScope: observer.topScope ) - try XCTAssertMatch(plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], - [.anySequence, "-gnone", .anySequence]) - } - - func testToolchainOption() async throws { - try XCTSkipOnWindows(because: #"https://github.com/swiftlang/swift-package-manager/issues/8660, threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation"#) - let customTargetToolchain = AbsolutePath("/path/to/toolchain") - let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") - let hostArPath = AbsolutePath("/usr/bin/ar") - let targetSwiftcPath = customTargetToolchain.appending(components: ["usr", "bin", "swiftc"]) - let targetArPath = customTargetToolchain.appending(components: ["usr", "bin", "llvm-ar"]) - - let fs = InMemoryFileSystem(emptyFiles: [ - "/Pkg/Sources/exe/main.swift", - hostSwiftcPath.pathString, - hostArPath.pathString, - targetSwiftcPath.pathString, - targetArPath.pathString - ]) - - for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath,] { - try fs.updatePermissions(path, isExecutable: true) - } - - let observer = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: [ - Manifest.createRootManifest( - displayName: "Pkg", - path: "/Pkg", - targets: [TargetDescription(name: "exe")] - ) - ], - observabilityScope: observer.topScope + try XCTAssertMatch( + plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [], + [.anySequence, "-gnone", .anySequence] ) - - let options = try GlobalOptions.parse([ - "--toolchain", customTargetToolchain.pathString, - "--triple", "x86_64-unknown-linux-gnu", - ]) - let swiftCommandState = try SwiftCommandState.makeMockState( - options: options, - fileSystem: fs, - environment: ["PATH": "/usr/bin"] - ) - - XCTAssertEqual(swiftCommandState.originalWorkingDirectory, fs.currentWorkingDirectory) - XCTAssertEqual( - try swiftCommandState.getTargetToolchain().swiftCompilerPath, - targetSwiftcPath - ) - XCTAssertEqual( - try swiftCommandState.getTargetToolchain().swiftSDK.toolset.knownTools[.swiftCompiler]?.path, - nil - ) - - let plan = try await BuildPlan( - destinationBuildParameters: swiftCommandState.productsBuildParameters, - toolsBuildParameters: swiftCommandState.toolsBuildParameters, - graph: graph, - fileSystem: fs, - observabilityScope: observer.topScope - ) - - let arguments = try plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first?.linkArguments() ?? [] - - XCTAssertMatch(arguments, [.contains("/path/to/toolchain")]) } - func testToolsetOption() throws { - try XCTSkipOnWindows(because: #"https://github.com/swiftlang/swift-package-manager/issues/8660. threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation"#) - let targetToolchainPath = "/path/to/toolchain" - let customTargetToolchain = AbsolutePath(targetToolchainPath) - let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") - let hostArPath = AbsolutePath("/usr/bin/ar") - let targetSwiftcPath = customTargetToolchain.appending(components: ["swiftc"]) - let targetArPath = customTargetToolchain.appending(components: ["llvm-ar"]) - - let fs = InMemoryFileSystem(emptyFiles: [ - hostSwiftcPath.pathString, - hostArPath.pathString, - targetSwiftcPath.pathString, - targetArPath.pathString - ]) - - for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath,] { - try fs.updatePermissions(path, isExecutable: true) - } + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8660", relationship: .defect), // threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation + ) + func toolchainOption() async throws { + try await withKnownIssue { + let customTargetToolchain = AbsolutePath("/path/to/toolchain") + let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") + let hostArPath = AbsolutePath("/usr/bin/ar") + let targetSwiftcPath = customTargetToolchain.appending(components: ["usr", "bin", "swiftc"]) + let targetArPath = customTargetToolchain.appending(components: ["usr", "bin", "llvm-ar"]) + + let fs = InMemoryFileSystem(emptyFiles: [ + "/Pkg/Sources/exe/main.swift", + hostSwiftcPath.pathString, + hostArPath.pathString, + targetSwiftcPath.pathString, + targetArPath.pathString, + ]) + + for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath] { + try fs.updatePermissions(path, isExecutable: true) + } - try fs.writeFileContents("/toolset.json", string: """ - { - "schemaVersion": "1.0", - "rootPath": "\(targetToolchainPath)" + let observer = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Pkg", + path: "/Pkg", + targets: [TargetDescription(name: "exe")] + ) + ], + observabilityScope: observer.topScope + ) + + let options = try GlobalOptions.parse([ + "--toolchain", customTargetToolchain.pathString, + "--triple", "x86_64-unknown-linux-gnu", + ]) + let swiftCommandState = try SwiftCommandState.makeMockState( + options: options, + fileSystem: fs, + environment: ["PATH": "/usr/bin"] + ) + + #expect(swiftCommandState.originalWorkingDirectory == fs.currentWorkingDirectory) + #expect(try swiftCommandState.getTargetToolchain().swiftCompilerPath == targetSwiftcPath) + let compilerToolProperties = try #require(try swiftCommandState.getTargetToolchain().swiftSDK.toolset.knownTools[.swiftCompiler]) + #expect(compilerToolProperties.path == nil) + + let plan = try await BuildPlan( + destinationBuildParameters: swiftCommandState.productsBuildParameters, + toolsBuildParameters: swiftCommandState.toolsBuildParameters, + graph: graph, + fileSystem: fs, + observabilityScope: observer.topScope + ) + + let buildProduct = try #require(try plan.buildProducts.compactMap { $0 as? Build.ProductBuildDescription }.first) + let arguments = try buildProduct.linkArguments() + + #expect(arguments.contains("/path/to/toolchain")) + } when: { + ProcessInfo.hostOperatingSystem == .windows } - """) - - let options = try GlobalOptions.parse(["--toolset", "/toolset.json"]) - let swiftCommandState = try SwiftCommandState.makeMockState( - options: options, - fileSystem: fs, - environment: ["PATH": "/usr/bin"] - ) - - let hostToolchain = try swiftCommandState.getHostToolchain() - let targetToolchain = try swiftCommandState.getTargetToolchain() - - XCTAssertEqual( - targetToolchain.swiftSDK.toolset.rootPaths, - [customTargetToolchain] + hostToolchain.swiftSDK.toolset.rootPaths - ) - XCTAssertEqual(targetToolchain.swiftCompilerPath, targetSwiftcPath) - XCTAssertEqual(targetToolchain.librarianPath, targetArPath) } - func testMultipleToolsets() throws { - try XCTSkipOnWindows(because: #"https://github.com/swiftlang/swift-package-manager/issues/8660, threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation"#) - let targetToolchainPath1 = "/path/to/toolchain1" - let customTargetToolchain1 = AbsolutePath(targetToolchainPath1) - let targetToolchainPath2 = "/path/to/toolchain2" - let customTargetToolchain2 = AbsolutePath(targetToolchainPath2) - let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") - let hostArPath = AbsolutePath("/usr/bin/ar") - let targetSwiftcPath = customTargetToolchain1.appending(components: ["swiftc"]) - let targetArPath = customTargetToolchain1.appending(components: ["llvm-ar"]) - let targetClangPath = customTargetToolchain2.appending(components: ["clang"]) - - let fs = InMemoryFileSystem(emptyFiles: [ - hostSwiftcPath.pathString, - hostArPath.pathString, - targetSwiftcPath.pathString, - targetArPath.pathString, - targetClangPath.pathString - ]) + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8660", relationship: .defect), // threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation + ) + func toolsetOption() async throws { + try withKnownIssue { + let targetToolchainPath = "/path/to/toolchain" + let customTargetToolchain = AbsolutePath(targetToolchainPath) + let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") + let hostArPath = AbsolutePath("/usr/bin/ar") + let targetSwiftcPath = customTargetToolchain.appending(components: ["swiftc"]) + let targetArPath = customTargetToolchain.appending(components: ["llvm-ar"]) + + let fs = InMemoryFileSystem(emptyFiles: [ + hostSwiftcPath.pathString, + hostArPath.pathString, + targetSwiftcPath.pathString, + targetArPath.pathString, + ]) + + for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath] { + try fs.updatePermissions(path, isExecutable: true) + } - for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath, targetClangPath,] { - try fs.updatePermissions(path, isExecutable: true) + try fs.writeFileContents( + "/toolset.json", + string: """ + { + "schemaVersion": "1.0", + "rootPath": "\(targetToolchainPath)" + } + """ + ) + + let options = try GlobalOptions.parse(["--toolset", "/toolset.json"]) + let swiftCommandState = try SwiftCommandState.makeMockState( + options: options, + fileSystem: fs, + environment: ["PATH": "/usr/bin"] + ) + + let hostToolchain = try swiftCommandState.getHostToolchain() + let targetToolchain = try swiftCommandState.getTargetToolchain() + + #expect(targetToolchain.swiftSDK.toolset.rootPaths == [customTargetToolchain] + hostToolchain.swiftSDK.toolset.rootPaths) + #expect(targetToolchain.swiftCompilerPath == targetSwiftcPath) + #expect(targetToolchain.librarianPath == targetArPath) + } when: { + ProcessInfo.hostOperatingSystem == .windows } + } - try fs.writeFileContents("/toolset1.json", string: """ - { - "schemaVersion": "1.0", - "rootPath": "\(targetToolchainPath1)" - } - """) + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/8660", relationship: .defect), // threw error \"toolchain is invalid: could not find CLI tool `swiftc` at any of these directories: []\", needs investigation + ) + func multipleToolsets() async throws { + try withKnownIssue { + let targetToolchainPath1 = "/path/to/toolchain1" + let customTargetToolchain1 = AbsolutePath(targetToolchainPath1) + let targetToolchainPath2 = "/path/to/toolchain2" + let customTargetToolchain2 = AbsolutePath(targetToolchainPath2) + let hostSwiftcPath = AbsolutePath("/usr/bin/swiftc") + let hostArPath = AbsolutePath("/usr/bin/ar") + let targetSwiftcPath = customTargetToolchain1.appending(components: ["swiftc"]) + let targetArPath = customTargetToolchain1.appending(components: ["llvm-ar"]) + let targetClangPath = customTargetToolchain2.appending(components: ["clang"]) + + let fs = InMemoryFileSystem(emptyFiles: [ + hostSwiftcPath.pathString, + hostArPath.pathString, + targetSwiftcPath.pathString, + targetArPath.pathString, + targetClangPath.pathString, + ]) + + for path in [hostSwiftcPath, hostArPath, targetSwiftcPath, targetArPath, targetClangPath] { + try fs.updatePermissions(path, isExecutable: true) + } - try fs.writeFileContents("/toolset2.json", string: """ - { - "schemaVersion": "1.0", - "rootPath": "\(targetToolchainPath2)" + try fs.writeFileContents( + "/toolset1.json", + string: """ + { + "schemaVersion": "1.0", + "rootPath": "\(targetToolchainPath1)" + } + """ + ) + + try fs.writeFileContents( + "/toolset2.json", + string: """ + { + "schemaVersion": "1.0", + "rootPath": "\(targetToolchainPath2)" + } + """ + ) + + let options = try GlobalOptions.parse([ + "--toolset", "/toolset1.json", "--toolset", "/toolset2.json", + ]) + let swiftCommandState = try SwiftCommandState.makeMockState( + options: options, + fileSystem: fs, + environment: ["PATH": "/usr/bin"] + ) + + let hostToolchain = try swiftCommandState.getHostToolchain() + let targetToolchain = try swiftCommandState.getTargetToolchain() + + #expect(targetToolchain.swiftSDK.toolset.rootPaths == [customTargetToolchain2, customTargetToolchain1] + hostToolchain.swiftSDK.toolset.rootPaths) + #expect(targetToolchain.swiftCompilerPath == targetSwiftcPath) + try #expect(targetToolchain.getClangCompiler() == targetClangPath) + #expect(targetToolchain.librarianPath == targetArPath) + } when: { + ProcessInfo.hostOperatingSystem == .windows } - """) - - let options = try GlobalOptions.parse([ - "--toolset", "/toolset1.json", "--toolset", "/toolset2.json" - ]) - let swiftCommandState = try SwiftCommandState.makeMockState( - options: options, - fileSystem: fs, - environment: ["PATH": "/usr/bin"] - ) - - let hostToolchain = try swiftCommandState.getHostToolchain() - let targetToolchain = try swiftCommandState.getTargetToolchain() - - XCTAssertEqual( - targetToolchain.swiftSDK.toolset.rootPaths, - [customTargetToolchain2, customTargetToolchain1] + hostToolchain.swiftSDK.toolset.rootPaths - ) - XCTAssertEqual(targetToolchain.swiftCompilerPath, targetSwiftcPath) - XCTAssertEqual(try targetToolchain.getClangCompiler(), targetClangPath) - XCTAssertEqual(targetToolchain.librarianPath, targetArPath) } - func testPackagePathWithMissingFolder() async throws { + @Test + func packagePathWithMissingFolder() async throws { try withTemporaryDirectory { fixturePath in let packagePath = fixturePath.appending(component: "Foo") let options = try GlobalOptions.parse(["--package-path", packagePath.pathString]) do { let outputStream = BufferedOutputByteStream() - XCTAssertThrowsError(try SwiftCommandState.makeMockState(outputStream: outputStream, options: options), "error expected") + #expect(throws: (any Error).self) { + try SwiftCommandState.makeMockState(outputStream: outputStream, options: options) + } } do { let outputStream = BufferedOutputByteStream() let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options, createPackagePath: true) tool.waitForObservabilityEvents(timeout: .now() + .seconds(1)) - XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("error:")) + let description = try #require(outputStream.bytes.validDescription) + #expect(!description.contains("error:")) } } } diff --git a/Tests/CommandsTests/SwiftSDKCommandTests.swift b/Tests/CommandsTests/SwiftSDKCommandTests.swift index a8a19433491..202dc3bbe1a 100644 --- a/Tests/CommandsTests/SwiftSDKCommandTests.swift +++ b/Tests/CommandsTests/SwiftSDKCommandTests.swift @@ -2,18 +2,19 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +// import Foundation import Basics import Commands import _InternalTestSupport -import XCTest +import Testing import class Basics.AsyncProcess @@ -23,158 +24,188 @@ private let sdkCommandDeprecationWarning = """ """ - -final class SwiftSDKCommandTests: CommandsTestCase { - func testUsage() async throws { - for command in [SwiftPM.sdk, SwiftPM.experimentalSDK] { - let stdout = try await command.execute(["-help"]).stdout - XCTAssert(stdout.contains("USAGE: swift sdk ") || stdout.contains("USAGE: swift sdk []"), "got stdout:\n" + stdout) - } +@Suite( + .serialized, + .tags( + .Feature.Command.Sdk, + .TestSize.large, + ), +) +struct SwiftSDKCommandTests { + @Test( + arguments: [SwiftPM.sdk, SwiftPM.experimentalSDK], + ) + func usage( + command: SwiftPM, + ) async throws { + + let stdout = try await command.execute(["-help"]).stdout + #expect( + stdout.contains("USAGE: swift sdk ") || stdout.contains("USAGE: swift sdk []"), + "got stdout:\n\(stdout)", + ) } - func testCommandDoesNotEmitDuplicateSymbols() async throws { - for command in [SwiftPM.sdk, SwiftPM.experimentalSDK] { - let (stdout, stderr) = try await command.execute(["--help"]) - XCTAssertNoMatch(stdout, duplicateSymbolRegex) - XCTAssertNoMatch(stderr, duplicateSymbolRegex) - } + @Test( + arguments: [SwiftPM.sdk, SwiftPM.experimentalSDK], + ) + func commandDoesNotEmitDuplicateSymbols( + command: SwiftPM, + ) async throws { + let (stdout, stderr) = try await command.execute(["--help"]) + let duplicateSymbolRegex = try Regex(#"objc[83768]: (.*) is implemented in both .* \(.*\) and .* \(.*\) . One of the two will be used. Which one is undefined."#) + #expect(!stdout.contains(duplicateSymbolRegex)) + #expect(!stderr.contains(duplicateSymbolRegex)) + } - func testVersionS() async throws { + @Test( + arguments: [SwiftPM.sdk, SwiftPM.experimentalSDK], + ) + func version( + command: SwiftPM, + ) async throws { for command in [SwiftPM.sdk, SwiftPM.experimentalSDK] { let stdout = try await command.execute(["--version"]).stdout - XCTAssertMatch(stdout, .regex(#"Swift Package Manager -( \w+ )?\d+.\d+.\d+(-\w+)?"#)) + let versionRegex = try Regex(#"Swift Package Manager -( \w+ )?\d+.\d+.\d+(-\w+)?"#) + #expect(stdout.contains(versionRegex)) } } - func testInstallSubcommand() async throws { - for command in [SwiftPM.sdk, SwiftPM.experimentalSDK] { - try await fixtureXCTest(name: "SwiftSDKs") { fixturePath in - for bundle in ["test-sdk.artifactbundle.tar.gz", "test-sdk.artifactbundle.zip"] { - var (stdout, stderr) = try await command.execute( - [ - "install", - "--swift-sdks-path", fixturePath.pathString, - fixturePath.appending(bundle).pathString - ] - ) - - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - XCTAssertNoMatch(stdout, .contains(sdkCommandDeprecationWarning)) - } + @Test( + arguments: [SwiftPM.sdk, SwiftPM.experimentalSDK], + ["test-sdk.artifactbundle.tar.gz", "test-sdk.artifactbundle.zip"], + ) + func installSubcommand( + command: SwiftPM, + bundle: String, + ) async throws { + try await fixture(name: "SwiftSDKs") { fixturePath in + let bundlePath = fixturePath.appending(bundle) + expectFileExists(at: bundlePath) + var (stdout, stderr) = try await command.execute( + [ + "install", + "--swift-sdks-path", fixturePath.pathString, + bundlePath.pathString, + ] + ) - // We only expect tool's output on the stdout stream. - XCTAssertMatch( - stdout + "\nstderr:\n" + stderr, - .contains("\(bundle)` successfully installed as test-sdk.artifactbundle.") - ) + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + #expect(!stdout.contains(sdkCommandDeprecationWarning)) + } - (stdout, stderr) = try await command.execute( - ["list", "--swift-sdks-path", fixturePath.pathString]) + // We only expect tool's output on the stdout stream. + #expect( + (stdout + "\nstderr:\n" + stderr).contains("\(bundle)` successfully installed as test-sdk.artifactbundle.") + ) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - XCTAssertNoMatch(stdout, .contains(sdkCommandDeprecationWarning)) - } + (stdout, stderr) = try await command.execute( + ["list", "--swift-sdks-path", fixturePath.pathString]) - // We only expect tool's output on the stdout stream. - XCTAssertMatch(stdout, .contains("test-artifact")) - - await XCTAssertAsyncThrowsError(try await command.execute( - [ - "install", - "--swift-sdks-path", fixturePath.pathString, - fixturePath.appending(bundle).pathString - ] - )) { error in - guard case SwiftPMError.executionFailure(_, _, let stderr) = error else { - XCTFail() - return - } + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + #expect(!stdout.contains(sdkCommandDeprecationWarning)) + } - XCTAssertMatch( - stderr, .contains( - "Error: Swift SDK bundle with name `test-sdk.artifactbundle` is already installed. Can't install a new bundle with the same name." - ), - ) - } + // We only expect tool's output on the stdout stream. + #expect(stdout.contains("test-artifact")) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - } + await expectThrowsCommandExecutionError( + try await command.execute( + [ + "install", + "--swift-sdks-path", fixturePath.pathString, + bundlePath.pathString, + ] + ) + ) { error in + let stderr = error.stderr + #expect( + stderr.contains( + "Error: Swift SDK bundle with name `test-sdk.artifactbundle` is already installed. Can't install a new bundle with the same name." + ), + ) + } - (stdout, stderr) = try await command.execute( - ["remove", "--swift-sdks-path", fixturePath.pathString, "test-artifact"]) + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + } - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - XCTAssertNoMatch(stdout, .contains(sdkCommandDeprecationWarning)) - } + (stdout, stderr) = try await command.execute( + ["remove", "--swift-sdks-path", fixturePath.pathString, "test-artifact"]) - // We only expect tool's output on the stdout stream. - XCTAssertMatch(stdout, .contains("test-sdk.artifactbundle` was successfully removed from the file system.")) + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + #expect(!stdout.contains(sdkCommandDeprecationWarning)) + } - (stdout, stderr) = try await command.execute( - ["list", "--swift-sdks-path", fixturePath.pathString]) + // We only expect tool's output on the stdout stream. + #expect(stdout.contains("test-sdk.artifactbundle` was successfully removed from the file system.")) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - XCTAssertNoMatch(stdout, .contains(sdkCommandDeprecationWarning)) - } + (stdout, stderr) = try await command.execute( + ["list", "--swift-sdks-path", fixturePath.pathString]) - // We only expect tool's output on the stdout stream. - XCTAssertNoMatch(stdout, .contains("test-artifact")) - } + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + #expect(!stdout.contains(sdkCommandDeprecationWarning)) } + + // We only expect tool's output on the stdout stream. + #expect(!stdout.contains("test-artifact")) } } - func testConfigureSubcommand() async throws { + @Test( + arguments: [SwiftPM.sdk, SwiftPM.experimentalSDK], + ) + func configureSubcommand( + command: SwiftPM, + ) async throws { let deprecationWarning = """ warning: `swift sdk configuration` command is deprecated and will be removed in a future version of \ SwiftPM. Use `swift sdk configure` instead. """ - for command in [SwiftPM.sdk, SwiftPM.experimentalSDK] { - try await fixtureXCTest(name: "SwiftSDKs") { fixturePath in - let bundle = "test-sdk.artifactbundle.zip" + try await fixture(name: "SwiftSDKs") { fixturePath in + let bundle = "test-sdk.artifactbundle.zip" - var (stdout, stderr) = try await command.execute([ - "install", - "--swift-sdks-path", fixturePath.pathString, - fixturePath.appending(bundle).pathString - ]) + var (stdout, stderr) = try await command.execute([ + "install", + "--swift-sdks-path", fixturePath.pathString, + fixturePath.appending(bundle).pathString, + ]) - // We only expect tool's output on the stdout stream. - XCTAssertMatch( - stdout, - .contains("\(bundle)` successfully installed as test-sdk.artifactbundle.") - ) + // We only expect tool's output on the stdout stream. + #expect( + stdout.contains("\(bundle)` successfully installed as test-sdk.artifactbundle.") + ) - let deprecatedShowSubcommand = ["configuration", "show"] + let deprecatedShowSubcommand = ["configuration", "show"] - for showSubcommand in [deprecatedShowSubcommand, ["configure", "--show-configuration"]] { - let invocation = showSubcommand + [ + for showSubcommand in [deprecatedShowSubcommand, ["configure", "--show-configuration"]] { + let invocation = + showSubcommand + [ "--swift-sdks-path", fixturePath.pathString, "test-artifact", "aarch64-unknown-linux-gnu", ] - (stdout, stderr) = try await command.execute(invocation) + (stdout, stderr) = try await command.execute(invocation) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - } + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + } - if showSubcommand == deprecatedShowSubcommand { - XCTAssertMatch(stderr, .contains(deprecationWarning)) - } + if showSubcommand == deprecatedShowSubcommand { + #expect(stderr.contains(deprecationWarning)) + } - let sdkSubpath = ["test-sdk.artifactbundle", "sdk" ,"sdk"] + let sdkSubpath = ["test-sdk.artifactbundle", "sdk", "sdk"] - XCTAssertEqual(stdout, - """ + #expect( + stdout == """ sdkRootPath: \(fixturePath.appending(components: sdkSubpath)) swiftResourcesPath: not set swiftStaticResourcesPath: not set @@ -182,47 +213,48 @@ final class SwiftSDKCommandTests: CommandsTestCase { librarySearchPaths: not set toolsetPaths: not set - """, - invocation.joined(separator: " ") - ) + """ + ) - let deprecatedSetSubcommand = ["configuration", "set"] - let deprecatedResetSubcommand = ["configuration", "reset"] - for setSubcommand in [deprecatedSetSubcommand, ["configure"]] { - for resetSubcommand in [deprecatedResetSubcommand, ["configure", "--reset"]] { - var invocation = setSubcommand + [ + let deprecatedSetSubcommand = ["configuration", "set"] + let deprecatedResetSubcommand = ["configuration", "reset"] + for setSubcommand in [deprecatedSetSubcommand, ["configure"]] { + for resetSubcommand in [deprecatedResetSubcommand, ["configure", "--reset"]] { + var invocation = + setSubcommand + [ "--swift-resources-path", fixturePath.appending("foo").pathString, "--swift-sdks-path", fixturePath.pathString, "test-artifact", "aarch64-unknown-linux-gnu", ] - (stdout, stderr) = try await command.execute(invocation) + (stdout, stderr) = try await command.execute(invocation) - XCTAssertEqual(stdout, """ + #expect( + stdout == """ info: These properties of Swift SDK `test-artifact` for target triple `aarch64-unknown-linux-gnu` \ were successfully updated: swiftResourcesPath. - """, - invocation.joined(separator: " ") - ) + """ + ) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - } + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + } - if setSubcommand == deprecatedSetSubcommand { - XCTAssertMatch(stderr, .contains(deprecationWarning)) - } + if setSubcommand == deprecatedSetSubcommand { + #expect(stderr.contains(deprecationWarning)) + } - invocation = showSubcommand + [ + invocation = + showSubcommand + [ "--swift-sdks-path", fixturePath.pathString, "test-artifact", "aarch64-unknown-linux-gnu", ] - (stdout, stderr) = try await command.execute(invocation) + (stdout, stderr) = try await command.execute(invocation) - XCTAssertEqual(stdout, - """ + #expect( + stdout == """ sdkRootPath: \(fixturePath.appending(components: sdkSubpath).pathString) swiftResourcesPath: \(fixturePath.appending("foo")) swiftStaticResourcesPath: not set @@ -230,42 +262,40 @@ final class SwiftSDKCommandTests: CommandsTestCase { librarySearchPaths: not set toolsetPaths: not set - """, - invocation.joined(separator: " ") - ) + """ + ) - invocation = resetSubcommand + [ + invocation = + resetSubcommand + [ "--swift-sdks-path", fixturePath.pathString, "test-artifact", "aarch64-unknown-linux-gnu", ] - (stdout, stderr) = try await command.execute(invocation) + (stdout, stderr) = try await command.execute(invocation) - if command == .experimentalSDK { - XCTAssertMatch(stderr, .contains(sdkCommandDeprecationWarning)) - } + if command == .experimentalSDK { + #expect(stderr.contains(sdkCommandDeprecationWarning)) + } - if resetSubcommand == deprecatedResetSubcommand { - XCTAssertMatch(stderr, .contains(deprecationWarning)) - } + if resetSubcommand == deprecatedResetSubcommand { + #expect(stderr.contains(deprecationWarning)) + } - XCTAssertEqual(stdout, - """ + #expect( + stdout == """ info: All configuration properties of Swift SDK `test-artifact` for target triple `aarch64-unknown-linux-gnu` were successfully reset. - """, - invocation.joined(separator: " ") - ) - } + """ + ) } } + } - (stdout, stderr) = try await command.execute( - ["remove", "--swift-sdks-path", fixturePath.pathString, "test-artifact"]) + (stdout, stderr) = try await command.execute( + ["remove", "--swift-sdks-path", fixturePath.pathString, "test-artifact"]) - // We only expect tool's output on the stdout stream. - XCTAssertMatch(stdout, .contains("test-sdk.artifactbundle` was successfully removed from the file system.")) - } + // We only expect tool's output on the stdout stream. + #expect(stdout.contains("test-sdk.artifactbundle` was successfully removed from the file system.")) } } }