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/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift new file mode 100644 index 00000000000..e3eb90f3a29 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.6 +import PackageDescription +let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary", + plugins: [ + "MyPlugin", + ] + ), + .plugin( + name: "MyPlugin", + capability: .buildTool() + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift new file mode 100644 index 00000000000..550b8c22a65 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift @@ -0,0 +1,15 @@ +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 {} diff --git a/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift new file mode 100644 index 00000000000..ed191a40c0e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift @@ -0,0 +1 @@ +public func Foo() { } diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift new file mode 100644 index 00000000000..820c7a6d89e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift @@ -0,0 +1,34 @@ +// 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") + ) + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift new file mode 100644 index 00000000000..0e674b46988 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift @@ -0,0 +1,9 @@ +import PackagePlugin +@main struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + return [] + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift new file mode 100644 index 00000000000..8704377cba5 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift @@ -0,0 +1,9 @@ +import PackagePlugin +@main struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + this is an error + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift new file mode 100644 index 00000000000..1c9c151f5f6 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift @@ -0,0 +1,2 @@ +import MyLibrary +print("\\(GetGreeting()), World!") diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift new file mode 100644 index 00000000000..4ce3fd140d4 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift @@ -0,0 +1 @@ +public func GetGreeting() -> String { return "Hello" } diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index a6d79c01ca4..19a7c3322f8 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -1126,10 +1126,22 @@ extension SwiftModuleBuildDescription { extension SwiftModuleBuildDescription { package var diagnosticFiles: [AbsolutePath] { - self.sources.compactMap { self.diagnosticFile(sourceFile: $0) } + // WMO builds have a single frontend invocation and produce a single + // diagnostic file named after the module. + if self.useWholeModuleOptimization { + return [ + self.diagnosticFile(name: self.target.name) + ] + } + + return self.sources.map(self.diagnosticFile(sourceFile:)) + } + + private func diagnosticFile(name: String) -> AbsolutePath { + self.tempsPath.appending(component: "\(name).dia") } private func diagnosticFile(sourceFile: AbsolutePath) -> AbsolutePath { - self.tempsPath.appending(component: "\(sourceFile.basenameWithoutExt).dia") + self.diagnosticFile(name: sourceFile.basenameWithoutExt) } } 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..8ffd8b7e7af 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 @@ -43,14 +46,31 @@ extension Tag.Feature.Command { } 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 +83,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/BuildCommandTests.swift b/Tests/CommandsTests/BuildCommandTests.swift index 67203343e9b..e47f1044cc7 100644 --- a/Tests/CommandsTests/BuildCommandTests.swift +++ b/Tests/CommandsTests/BuildCommandTests.swift @@ -223,7 +223,7 @@ struct BuildCommandTestCases { buildSystem: buildSystem, ) } - guard case SwiftPMError.executionFailure(_, _, let stderr) = try #require(error) else { + guard case SwiftPMError.executionFailure(_, let stdout, let stderr) = try #require(error) else { Issue.record("Incorrect error was raised.") return } diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 8469e246e44..6a37494aa78 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -2,7 +2,7 @@ // // 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 @@ -11,1548 +11,2602 @@ //===----------------------------------------------------------------------===// import Basics -@testable import CoreCommands -@testable import Commands import Foundation - -@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) -import PackageGraph - +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) import PackageGraph import PackageLoading import PackageModel import SourceControl -import SPMBuildCore -import _InternalTestSupport +import Testing import Workspace -import XCTest +import _InternalTestSupport -import struct TSCBasic.ByteString +import class Basics.AsyncProcess +import struct SPMBuildCore.BuildSystemProvider +import typealias SPMBuildCore.CLIArguments import class TSCBasic.BufferedOutputByteStream +import struct TSCBasic.ByteString import enum TSCBasic.JSON -import class Basics.AsyncProcess -class PackageCommandTestCase: CommandsBuildProviderTestCase { - override func setUpWithError() throws { - try XCTSkipIf(type(of: self) == PackageCommandTestCase.self, "Skipping this test since it will be run in subclasses that will provide different build systems to test.") +@testable import Commands +@testable import CoreCommands + +@discardableResult +fileprivate func execute( + _ args: [String] = [], + packagePath: AbsolutePath? = nil, + manifest: String? = nil, + env: Environment? = nil, + configuration: BuildConfiguration, + buildSystem: BuildSystemProvider.Kind +) async throws -> (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 PackageCommandTests { + @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-") + } + ) - // 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-") }) - } + // Remove .build folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - 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, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect( + try localFileSystem.getDirectoryContents(repositoriesPath).contains { + $0.hasPrefix("Foo-") + } + ) - try await self.execute(["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + // 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-") + } + ) + } - // 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)) - } + do { + // Remove .build and cache folder + _ = try await execute( + ["reset"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + try await execute( + ["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - let (_, _) = try await self.execute(["resolve", "--enable-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/...`. + #expect( + try localFileSystem.getDirectoryContents(repositoriesPath).contains { + $0.hasPrefix("Foo-") + } + ) + #expect(!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/...`. - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesPath).contains { $0.hasPrefix("Foo-") }) - XCTAssert(try localFileSystem.getDirectoryContents(repositoriesCachePath).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 folder - _ = try await execute(["reset"], packagePath: packageRoot) + let (_, _) = try await execute( + ["resolve", "--enable-dependency-cache", "--cache-path", cachePath.pathString], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) - // 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-") }) + // 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 and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + // 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-") + } + ) - do { - // Remove .build and cache folder - _ = try await execute(["reset"], packagePath: packageRoot) - try localFileSystem.removeFileTree(cachePath) + // 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, + configuration: data.config, + buildSystem: data.buildSystem, + ) + try localFileSystem.removeFileTree(cachePath) - let (_, _) = try await self.execute(["resolve", "--disable-dependency-cache", "--cache-path", cachePath.pathString], packagePath: packageRoot) + let (_, _) = 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)) + } } } - } - - 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( + 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 testUpdate() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") + @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, + ) + + 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 + } } - } + @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)) - 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") + // 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 .dictionary(let contents) = json else { + Issue.record("unexpected result") + return + } + guard case .string(let name)? = contents["name"] else { + Issue.record("unexpected result") + return + } + guard case .array(let 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() + @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 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) + 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 + ) + ) - var symbolGraphURL: URL? - for case let url as URL in enumerator where url.lastPathComponent == "Bar.symbols.json" { - symbolGraphURL = url - break - } + var symbolGraphURLOptional: URL? = nil + while let element = enumerator.nextObject() { + if let url = element as? URL, url.lastPathComponent == "Bar.symbols.json" { + symbolGraphURLOptional = 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 - } + let symbolGraphURL = try #require( + symbolGraphURLOptional, + "Failed to extract symbol graph: \(result.stdout)\n\(result.stderr)" + ) + let symbolGraphData = try Data(contentsOf: symbolGraphURL) - // Double check that it's a valid JSON - XCTAssertNoThrow(try JSONSerialization.jsonObject(with: symbolGraphData), file: file, line: line) + // Double check that it's a valid JSON + #expect(throws: Never.self) { + try JSONSerialization.jsonObject(with: symbolGraphData) + } - return symbolGraphData - } - - 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") - - 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) - } - } - - 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") - - 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 .array(let 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") - if case let .string(package)? = dealer["package"] { - XCTFail("unexpected package for dealer (should be unset): \(package)") + guard case let first = contents.first else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let dealer) = first else { + Issue.record("unexpected result") + return + } + guard case .string(let dealerName)? = dealer["name"] else { + Issue.record("unexpected result") + return + } + #expect(dealerName == "dealer") + if case .string(let package)? = dealer["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") - if case let .string(package)? = deck["package"] { - XCTAssertEqual("deck-of-playing-cards", package) + guard case let last = contents.last else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let deck) = last else { + Issue.record("unexpected result") + return + } + guard case .string(let deckName)? = deck["name"] else { + Issue.record("unexpected result") + return + } + #expect(deckName == "deck") + if case .string(let package)? = deck["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 .dictionary(let contents) = json else { + Issue.record("unexpected result") + return + } + guard case .string(let name)? = contents["name"] else { + Issue.record("unexpected result") + return + } + #expect(name == "Dealer") + guard case .string(let 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 testPackageAddDifferentDependencyWithSameURLTwiceFails() 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" ]) ] + 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, ) - """ - 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 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, ) - """ - 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" ]) ] + @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(path: "../foo")"# - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: depPath, - requirementArgs: ["--type", "path"], - expectedManifestString: expected - ) + try expectManifest(path) { + let components = $0.components(separatedBy: expected) + #expect(components.count == 2) + } + } + } - try assertManifest(path) { - let components = $0.components(separatedBy: expected) - XCTAssertEqual(components.count, 2, "Expected the dependency to be added exactly once.") + 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, + ) } } - } - func testPackageAddSameDependencyRegistryTwiceHasNoEffect() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) + @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, + ) + } + } - let registryId = "foo" - let manifest = """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client", - dependencies: [ - .package(id: "\(registryId)") + @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", ], - targets: [ .target(name: "client", dependencies: [ "library" ]) ] + 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, ) - """ - - 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 - ) - - try assertManifest(path) { - let components = $0.components(separatedBy: expected) - XCTAssertEqual(components.count, 2, "Expected the dependency to be added exactly once.") } } } - 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" ]) ] + @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" + ) + """ ) - """ - - // 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"),"#, - ) - - // 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"),"# - ) - - // 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"),"# - ) - // 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"),"# - ) - - // 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"),"# - ) - - // 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"),"# - ) - - // 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"),"# - ) - } - } - - func testPackageAddPathDependency() 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 execute( + ["add-target", "client", "--dependencies", "MyLib", "OtherLib", "--type", "executable"], + packagePath: path, + configuration: data.config, + buildSystem: data.buildSystem, ) - """ - // Add absolute path dependency - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "/absolute", - requirementArgs: ["--type", "path"], - expectedManifestString: #".package(path: "/absolute"),"# - ) + let manifest = path.appending("Package.swift") + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) - // Add relative path dependency (operates on the modified manifest) - try await executeAddURLDependencyAndAssert( - packagePath: path, - initialManifest: manifest, - url: "../relative", - requirementArgs: ["--type", "path"], - expectedManifestString: #".package(path: "../relative"),"# - ) + #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 testPackageAddRegistryDependency() 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 - 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"),"# - ) - - // Test adding with --from and --to - try await executeAddURLDependencyAndAssert( - 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"),"# - ) - - // 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"),"# - ) - - // 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"),"# - ) - } - } - func testPackageAddTarget() async throws { - try await testWithTemporaryDirectory { tmpPath in - let fs = localFileSystem - let path = tmpPath.appending("PackageB") - try fs.createDirectory(path) + let sourcesFolder = tmpPath.appending("Sources") + try fs.createDirectory(sourcesFolder) - try fs.writeFileContents(path.appending("Package.swift"), string: - """ - // swift-tools-version: 5.9 - import PackageDescription - let package = Package( - name: "client" + try fs.writeFileContents( + sourcesFolder.appending("main.swift"), + string: + """ + print("Hello World") + """ ) - """ - ) - - _ = try await execute(["add-target", "client", "--dependencies", "MyLib", "OtherLib", "--type", "executable"], packagePath: path) - - let manifest = path.appending("Package.swift") - XCTAssertFileExists(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""#)) - } - } - 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"), - ] + _ = try await execute( + ["add-target", "client"], + packagePath: tmpPath, + configuration: data.config, + buildSystem: data.buildSystem, ) - """ - ) - - let sourcesFolder = tmpPath.appending("Sources") - try fs.createDirectory(sourcesFolder) - - try fs.writeFileContents(sourcesFolder.appending("main.swift"), string: - """ - print("Hello World") - """ - ) - _ = try await execute(["add-target", "client"], packagePath: tmpPath) + expectFileExists(at: manifest) + let contents: String = try fs.readFileContents(manifest) - XCTAssertFileExists(manifest) - let contents: String = try fs.readFileContents(manifest) - - XCTAssertMatch(contents, .contains(#"targets:"#)) - XCTAssertMatch(contents, .contains(#".executableTarget"#)) - XCTAssertMatch(contents, .contains(#"name: "client""#)) + #expect(contents.contains(#"targets:"#)) + #expect(contents.contains(#".executableTarget"#)) + #expect(contents.contains(#"name: "client""#)) - 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"]) + 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"]) + } } - } - 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( + 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 testPackageAddTargetDependency() 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) - 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 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 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) + _ = try await execute( + ["add-target-dependency", "--package", "other-package", "other-product", "library"], + 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(#".product(name: "other-product", package: "other-package"#)) + #expect(contents.contains(#".product(name: "other-product", package: "other-package"#)) } } - func testPackageAddProduct() 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) - 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: 5.9 + import PackageDescription + let package = Package( + name: "client" + ) + """ ) - _ = try await execute(["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], packagePath: path) + _ = try await execute( + ["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], + 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(#"products:"#)) - XCTAssertMatch(contents, .contains(#".library"#)) - XCTAssertMatch(contents, .contains(#"name: "MyLib""#)) - XCTAssertMatch(contents, .contains(#"type: .static"#)) - XCTAssertMatch(contents, .contains(#"targets:"#)) - XCTAssertMatch(contents, .contains(#""MyLib""#)) + #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 testPackageAddSetting() 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("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") ] - ) - """ + 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-setting", - "--target", "test", - "--swift", "languageMode=6", - "--swift", "upcomingFeature=ExistentialAny:migratable", - "--swift", "experimentalFeature=TrailingCommas", - "--swift", "StrictMemorySafety" - ], 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(#"swiftSettings:"#)) - XCTAssertMatch(contents, .contains(#".swiftLanguageMode(.v6)"#)) - XCTAssertMatch(contents, .contains(#".enableUpcomingFeature("ExistentialAny:migratable")"#)) - XCTAssertMatch(contents, .contains(#".enableExperimentalFeature("TrailingCommas")"#)) - XCTAssertMatch(contents, .contains(#".strictMemorySafety()"#)) + #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 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) - } + @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, + ) + } + + // 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, + ) - // 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, + ] + data.buildSystem.binPathSuffixes(for: data.config) + ["foo"] + ).pathString + ] - // 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") + expectDirectoryExists(at: editsPath) - // We should see it now in packages directory. - let editsPath = fooPath.appending(components: "Packages", "bar") - XCTAssertDirectoryExists(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) - 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() - // 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") - 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 execute( + ["unedit", "bar"], + packagePath: fooPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + Issue.record("Unexpected unedit success") + } catch {} - // 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() - try editsRepo.stageEverything() - try editsRepo.commit() + // 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, + ) - // 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) + // 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 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) - } } - func testPackageReset() async throws { - try await fixtureXCTest(name: "DependencyResolution/External/Simple") { fixturePath in - let packageRoot = fixturePath.appending("Bar") + @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") - // 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. + // 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) + // Clean again to ensure we get no error. + _ = try await execute( + ["clean"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild + } + } - _ = try await execute(["clean"], packagePath: packageRoot) - XCTAssertNoSuchPath(binFile) - XCTAssertFalse(try localFileSystem.getDirectoryContents(buildPath.appending("repositories")).isEmpty) + @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") - // Fully clean. - _ = try await execute(["reset"], packagePath: packageRoot) - XCTAssertFalse(localFileSystem.isDirectory(buildPath)) + // 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 + ) - // Test that we can successfully run reset again. - _ = try await execute(["reset"], packagePath: packageRoot) + // 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,21 +2616,24 @@ 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, fileSystem: localFileSystem, mirrors: .init() ) - let state = ResolvedPackagesStore.ResolutionState.branch(name: "YOLO", revision: yoloRevision.identifier) + 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,255 +2642,362 @@ 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") - - 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 - } - """ + @Test( + .issue( + "error: Package.resolved file is corrupted or malformed, needs investigation", + relationship: .defect + ), + .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 await withKnownIssue { + 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(components: "Sources", "library", "library.swift"), + string: + """ + public func Foo() { } + """ + ) - 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(components: "Sources", "library", "library.swift"), string: - """ - public func Foo() { } - """ - ) + let depGit = GitRepository(path: packageDir) + try depGit.create() + try depGit.stageEverything() + try depGit.commit() + try depGit.tag(name: "1.0.0") - let depGit = GitRepository(path: packageDir) - try depGit.create() - try depGit.stageEverything() - try depGit.commit() - try depGit.tag(name: "1.0.0") - - let initialRevision = try depGit.revision(forTag: "1.0.0") - 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(components: "Sources", "client", "main.swift"), string: - """ - print("hello") - """ - ) + let initialRevision = try depGit.revision(forTag: "1.0.0") + 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(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)")) - } + // 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, + 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 depGit.stageEverything() - try depGit.commit() - try depGit.tag(name: "1.0.1") - let updatedRevision = try depGit.revision(forTag: "1.0.1") + // 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 depGit.stageEverything() + try depGit.commit() + try depGit.tag(name: "1.0.1") + let updatedRevision = try depGit.revision(forTag: "1.0.1") - // 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)")) + // 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, + 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)")) + // And again + do { + 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)")) + } } + } when: { + ProcessInfo.hostOperatingSystem == .windows } } - 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") @@ -1841,32 +3005,39 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { let depSym = path.appending(components: "depSym") // 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(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"])] + ) + + """ ) // 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 +3050,418 @@ 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 -> Void) 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 + ) - 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", + 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: [ - .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 + ) - 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", + 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: [ - .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 + ) - 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", + 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: [ - .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,75 +3469,138 @@ 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 + 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 fixture(name: "SwiftMigrate/\(featureName)Migration") { fixturePath in let sourcePaths: [AbsolutePath] let fixedSourcePaths: [AbsolutePath] @@ -2188,414 +3613,513 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { }.sorted().map { filename in sourcesPath.appending(filename) } - fixedSourcePaths = try localFileSystem.getDirectoryContents(fixedSourcesPath).filter { filename in + fixedSourcePaths = try localFileSystem.getDirectoryContents(fixedSourcesPath).filter { + filename in filename.hasSuffix(".swift") }.sorted().map { filename in fixedSourcesPath.appending(filename) } } - let (stdout, _) = try await self.execute( + let (stdout, _) = try await execute( ["migrate", "--to-feature", featureName], - packagePath: fixturePath + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) - XCTAssertEqual(sourcePaths.count, fixedSourcePaths.count) + #expect(sourcePaths.count == fixedSourcePaths.count) for (sourcePath, fixedSourcePath) in zip(sourcePaths, fixedSourcePaths) { - try XCTAssertEqual( - localFileSystem.readFileContents(sourcePath), - localFileSystem.readFileContents(fixedSourcePath) - ) + let sourceContent = try localFileSystem.readFileContents(sourcePath) + let fixedSourceContent = try localFileSystem.readFileContents(fixedSourcePath) + #expect(sourceContent == fixedSourceContent) } - XCTAssertMatch(stdout, .regex("> \(expectedSummary)" + #" \([0-9]\.[0-9]{1,3}s\)"#)) + let regexMatch = try Regex("> \(expectedSummary)" + #" \([0-9]\.[0-9]{1,3}s\)"#) + #expect(stdout.contains(regexMatch)) } } - // 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 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)) + } } - } - 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 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)) + } } - } - 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 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) + } } - } - - 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 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) + } } - } - 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 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 + let updatedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.swift") ) - ) { 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 expectedManifest = try localFileSystem.readFileContents( + fixturePath.appending(components: "Package.updated.targets-all.swift") ) + #expect(updatedManifest == expectedManifest) } - - 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)’" - } - - // 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) ] - } - - } - 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", - ] - ), - .plugin( - name: "MyPlugin", - capability: .buildTool() - ), - ] + try localFileSystem.writeFileContents( + packageDir.appending(components: "Sources", "MyLibrary", "library.swift"), + string: + """ + public func Foo() { } + """ ) - """ - ) - 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 [] - } + 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) ] + } - } - extension String : Error {} - """ - ) + } + extension String : Error {} + """ + ) - // Invoke it, and check the results. - await XCTAssertAsyncThrowsError( - try await executeSwiftBuild( + // Invoke it, and check the results. + let args = staticStdlib ? ["--static-swift-stdlib"] : [] + let (stdout, stderr) = try await executeSwiftBuild( packageDir, - extraArgs: ["-v"], - buildSystem: self.buildSystemProvider, + configuration: data.config, + extraArgs: args, + buildSystem: data.buildSystem, ) - ) { 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")) + #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)) } } } - } - - 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)""#) + @Test( + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func buildToolPluginFailure( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/BuildToolPluginCompilationError") { packageDir in + // 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") + ) + } + } } + } + } - // 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 +4128,2707 @@ 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" - ), - ] + // 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")"# ) - """ - ) - 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.")) + let (stdout, _) = try await execute( + testData.packageCommandArgs, + packagePath: packageDir, + configuration: buildData.config, + buildSystem: buildData.buildSystem, + ) + for expected in testData.expectedStdout { + #expect(stdout.contains(expected)) + } } + } + @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 + } - // 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.")) - } + 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)") + } + } + } + """ + ) - // 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’)")) - } + // 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 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)") + // 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’")) } - XCTAssertMatch(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)")) } } + } - // 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")) - } - - // 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)")) - } - - // 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")) + @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!")) } } - } - 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") + // 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) + } - 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!")) - } - } + // 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) + } + } - // 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) - } + // Default verbosity + // - stdout is always printed + // - Diagnostics below 'warning' are suppressed - // 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)") + 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) } - completion(stdout, stderr) - } - } - // Default verbosity - // - stdout is always printed - // - Diagnostics below 'warning' are suppressed + try await runPlugin(flags: [], diagnostics: ["print", "progress"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - 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", "progress", "remark"]) { + stdout, + stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - try await runPlugin(flags: [], diagnostics: ["print", "progress"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + 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 runPlugin(flags: [], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + 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)) + } - try await runPlugin(flags: [], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsWarning) - } + // Quiet Mode + // - stdout is always printed + // - Diagnostics below 'error' are suppressed - 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 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) + } - // Quiet Mode - // - stdout is always printed - // - Diagnostics below 'error' are suppressed + 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"]) { 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", "progress", "remark"]) { + stdout, + stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - 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", "remark", "warning"] + ) { 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 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)) + } - try await runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - } + // Verbose Mode + // - stdout is always printed + // - All diagnostics are printed + // - Substantial amounts of additional compiler output are also printed - 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 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 + } - // 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", "progress"]) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + } - 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", "progress", "remark"]) { + stdout, + stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsRemark)) + } - 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", "remark", "warning"] + ) { stdout, stderr in + #expect(stdout == isOnlyPrint) + #expect(stderr.contains(containsProgress)) + #expect(stderr.contains(containsRemark)) + #expect(stderr.contains(containsWarning)) + } - try await runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in - XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, containsProgress) - XCTAssertMatch(stderr, containsRemark) + 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)) + } + } } + } - 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) + // 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 } + } + + // 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" + ) + } - 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) + } when: { + ProcessInfo.hostOperatingSystem == .windows } } - } - // 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 - ) - } - - func AssertNotExists(_ fixturePath: AbsolutePath, file: StaticString = #filePath, line: UInt = #line) { - XCTAssertFalse( - localFileSystem.exists(fixturePath), - "\(fixturePath) should not exist", - file: file, - line: line - ) - } - - // 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) + } - // 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 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: 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 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: 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)) - } + // 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: 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)) } - // 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)) + struct CommandPluginNetworkingPermissionsTestData { + let permissionsManifestFragment: String + let permissionError: String + let reason: String + let remedy: CLIArguments } - } - - // 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 - - let isEmpty = StringPattern.equal("") - - // result.logText printed by the plugin has a prefix - let containsLogtext = StringPattern.contains("command plugin: packageManager.build logtext: Building for debugging...") - - // Echoed logs have no prefix - let containsLogecho = StringPattern.contains("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. + 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"], - // 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) + ), + 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"], + ), + ] } - // 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( + .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 - // 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) - } + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("hello world") + } + } + """ + ) - // 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) + 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.") + ) + } + } } - } - 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") + @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 - 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))), - ] + @main + struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("hello world") + } + } + """ ) - """ - ) - 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") - } - } - """ - ) - #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)") - } - 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.")) + // 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")) } } - #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")) - } } - } - - 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"]) - } - 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") - - 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")] - : [] + @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"))" + """ + ) + 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 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.") + ) + } } - print("... successfully created it") + #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 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")) } - } - 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 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 + + // 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")) } - 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.")) - } - } - #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 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 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)") + // 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("successfully created it")) - XCTAssertMatch(stderr, .contains("error: Couldn’t create file at path")) } + } when: { + ProcessInfo.processInfo.environment["SWIFTCI_EXHIBITS_GH_8782"] != nil } - #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")) - } + @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") + ) + ), + ] + ) + """ + ) - // 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")) - } + 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 {} + """ + ) - // 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")) + 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 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") + @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() + ) + ), + ] + ) + """ + ) - try await testWithTemporaryDirectory { tmpPath in - // Create a sample package with a library target and a plugin. - let packageDir = tmpPath.appending(components: "MyPackage") + 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 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 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 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" + 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") } - guard arguments.contains("--verbose") else { - throw "expecting argument verbose" + } + + // 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") } - print("success") } } - 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:")) + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - } - 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() + @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 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" }"# - ) + // 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!")) + } + #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") + } + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("executable")) + } - 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!") - """ - ) + // 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")) + } - 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 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!")) } + #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") + } + #expect(stdout.contains("artifact-kind:")) + #expect(stdout.contains("staticLibrary")) } - } - """ - ) - // 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))) + // 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!")) + } + #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")) + } } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } + } - // 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))) + @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") + } + } + """ + ) + + // 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, + ) + + // We'll add checks for various error conditions here in a future commit. } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } - } - 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") + struct PluginAPIsData { + let commandArgs: CLIArguments + let expectedStdout: [String] + let expectedStderr: [String] + } - 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"] - ), + @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: "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!") - """ - ) + 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(isIntermittent: true) { + 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") + ) + ), + ] + ) + """ + ) - // 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"))) - } + let firstTargetDir = packageDir.appending(components: "Sources", "FirstTarget") + try localFileSystem.createDirectory(firstTargetDir, recursive: true) + try localFileSystem.writeFileContents( + firstTargetDir.appending("library.swift"), + string: """ + public func FirstFunc() { } + """ + ) - // 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"))) - } + let secondTargetDir = packageDir.appending(components: "Sources", "SecondTarget") + try localFileSystem.createDirectory(secondTargetDir, recursive: true) + try localFileSystem.writeFileContents( + secondTargetDir.appending("library.swift"), + string: """ + public func SecondFunc() { } + """ + ) - // 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"))) - } + let thirdTargetDir = packageDir.appending(components: "Sources", "ThirdTarget") + try localFileSystem.createDirectory(thirdTargetDir, recursive: true) + try localFileSystem.writeFileContents( + thirdTargetDir.appending("library.swift"), + string: """ + public func ThirdFunc() { } + """ + ) - // 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") - } - XCTAssertMatch(stdout, .and(.contains("artifact-kind:"), .contains("dynamicLibrary"))) - } - } - } + let fourthTargetDir = packageDir.appending(components: "Sources", "FourthTarget") + try localFileSystem.createDirectory(fourthTargetDir, recursive: true) + try localFileSystem.writeFileContents( + fourthTargetDir.appending("library.swift"), + string: """ + public func FourthFunc() { } + """ + ) - 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") + 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 {} + } + """ + ) - // 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") + 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 { + } + """ + ) - 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") + 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 {} + """ + ) + + // 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: ".") + ] ) - ), - .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([] + [], []) + """ + ) + 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)) } - func testImpossibilities() throws { - XCTFail("no can do") + for expected in testData.expectedStderr { + #expect(stderr.contains(expected)) } } - """ - ) - - // 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. + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } - } - - 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") - ], - 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", + @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 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") + ) + ), ] - ), - .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" + 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 [] + } } - guard let target = try? context.package.targets(named: [targetName]).first else { - throw "No target found with the name '\\(targetName)'" + """ + ) + 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 { + } } - 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())") + // 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)...")) + } - if let target = target.sourceModule { - print("Module kind of '\\(target.name)': \\(target.kind)") + try await withKnownIssue { + // 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, + ) + if data.buildSystem == .native { + #expect(!stdout.contains("Compiling plugin MyBuildToolPlugin")) + #expect(stdout.contains("Compiling plugin MyCommandPlugin")) } - - 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 })") + #expect(!stdout.contains("Building for \(data.config.buildFor)...")) } + } when: { + data.buildSystem == .swiftbuild } - 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")) - } - - // 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")) - } - - // 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")) - } - - // 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")) - } - - // 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")) } } - } - - 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") - 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 + ), + .SWBINTTODO("Building sample package causes a backtrace on linux"), + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + // arguments: getBuildData(for: [.native]), + ) + func commandPluginCompilationError( + data: BuildData, + ) async throws { + try await withKnownIssue { + try await fixture(name: "Miscellaneous/Plugins/CommandPluginCompilationError") { packageDir in + // 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, ) - ), - ] - ) - """ - ) - 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 { + ) { error in + let stdout = error.stdout + let stderr = error.stderr + withKnownIssue(isIntermittent: true) { + #expect( + stdout.contains( + "error: consecutive statements on a line must be separated by ';'" + ), + "iteration \(num) failed. stderr: \(stderr)", + ) + } when: { + data.config == .release && data.buildSystem == .native + } + #expect( + !stdout.contains("Building for \(data.config.buildFor)..."), + "iteration \(num) failed. stderr: \(stderr)", + ) + } } } - """ - ) + } when: { + data.buildSystem == .swiftbuild + } + } - // Check that building without options compiles both plugins and that the build proceeds. - do { - let (stdout, _) = try await executeSwiftBuild( - packageDir, - buildSystem: self.buildSystemProvider, + @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: [] + ) + ) + ] + ) + """ ) - 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, + 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] { } + } + """ ) - XCTAssertFalse(stdout.contains("Compiling plugin MyBuildToolPlugin"), "stdout: '\(stdout)'") - XCTAssertTrue(stdout.contains("Compiling plugin MyCommandPlugin"), "stdout: '\(stdout)'") - XCTAssertFalse(stdout.contains("Building for debugging..."), "stdout: '\(stdout)'") - } - } - } - 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") + // 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() + ) - 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") - ) - ), - ] + // Load the root manifest. + let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope ) - """ - ) - 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 - } - } - """ - ) + #expect(rootManifests.count == 1, "Root manifest: \(rootManifests)") - // 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)") - } - XCTAssertTrue(stdout.contains("error: consecutive statements on a line must be separated by ';'"), "stdout: '\(stdout)'") - XCTAssertFalse(stdout.contains("Building for debugging..."), "stdout: '\(stdout)'") - } + // Load the package graph. + let _ = try await workspace.loadPackageGraph( + rootInput: rootInput, + observabilityScope: observability.topScope + ) + expectNoDiagnostics(observability.diagnostics) } } } - - 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, - ) - - try await super.testMigrateCommandWhenDependencyBuildsForHostAndTarget() - } - - 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, - ) - - 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() - } - - 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() - } - - 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() - } - - 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() - } } 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(