From c4b01ffc4042aadcf8f57533ecda80878ae73a6e Mon Sep 17 00:00:00 2001 From: Sam Khouri Date: Tue, 12 Aug 2025 16:35:14 -0400 Subject: [PATCH] Tests: Migrate PluginsBuildPlanTests to SwiftTesting and augment Migrate the `PluginsBuildPlanTests` suite to Swift Testing and, where applicable, augment the test to run against both the Native and SwiftBuild build system, in addition to the `debug` and `release` build configurations. Depends on: #9013 Relates to: #8997 issue: rdar://157669245 --- .../Plugins/PluginScriptTarget/Script.swift | 4 +- .../SwiftTesting+Helpers.swift | 46 +++- Tests/BuildTests/PluginsBuildPlanTests.swift | 204 +++++++++++++----- Tests/FunctionalTests/PluginTests.swift | 22 +- 4 files changed, 195 insertions(+), 81 deletions(-) diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift index 01a0d6c302f..9d835415424 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift @@ -4,7 +4,9 @@ import PackagePlugin struct PluginScript: CommandPlugin { func performCommand(context: PluginContext, arguments: [String]) async throws { - dump(context) + if !arguments.contains("--skip-dump") { + dump(context) + } if let target = try context.package.targets(named: ["MySnippet"]).first as? SourceModuleTarget { print("type of snippet target: \(target.kind)") } diff --git a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift index 69e8b06fee0..a3f96ff686c 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Helpers.swift @@ -13,18 +13,30 @@ import Testing public func expectFileExists( at path: AbsolutePath, + _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { - + let commentPrefix = + if let comment { + "\(comment): " + } else { + "" + } + let msgSuffix: String + do { + msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path.parentDirectory))" + } catch { + msgSuffix = "" + } #expect( localFileSystem.exists(path), - "Files '\(path)' does not exist.", + "\(commentPrefix)File '\(path)' does not exist. \(msgSuffix)", sourceLocation: sourceLocation, ) } public func expectFileDoesNotExists( - at fixturePath: AbsolutePath, + at path: AbsolutePath, _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation, ) { @@ -34,9 +46,15 @@ public func expectFileDoesNotExists( } else { "" } + let msgSuffix: String + do { + msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path.parentDirectory))" + } catch { + msgSuffix = "" + } #expect( - !localFileSystem.exists(fixturePath), - "\(commentPrefix)\(fixturePath) does not exist", + !localFileSystem.exists(path), + "\(commentPrefix)File: '\(path)' was not expected to exist, but does.\(msgSuffix))", sourceLocation: sourceLocation, ) } @@ -54,7 +72,7 @@ public func expectFileIsExecutable( } #expect( localFileSystem.isExecutableFile(fixturePath), - "\(commentPrefix)\(fixturePath) does not exist", + "\(commentPrefix)File '\(fixturePath)' expected to be executable, but is not.", sourceLocation: sourceLocation, ) } @@ -63,9 +81,15 @@ public func expectDirectoryExists( at path: AbsolutePath, sourceLocation: SourceLocation = #_sourceLocation, ) { +let msgSuffix: String + do { + msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path))" + } catch { + msgSuffix = "" + } #expect( localFileSystem.isDirectory(path), - "Expected directory doesn't exist: \(path)", + "Expected directory doesn't exist: '\(path)'. \(msgSuffix)", sourceLocation: sourceLocation, ) } @@ -74,9 +98,15 @@ public func expectDirectoryDoesNotExist( at path: AbsolutePath, sourceLocation: SourceLocation = #_sourceLocation, ) { + let msgSuffix: String + do { + msgSuffix = try "Directory contents: \(localFileSystem.getDirectoryContents(path))" + } catch { + msgSuffix = "" + } #expect( !localFileSystem.isDirectory(path), - "Directory exists unexpectedly: \(path)", + "Directory exists unexpectedly: '\(path)'.\(msgSuffix)", sourceLocation: sourceLocation, ) } diff --git a/Tests/BuildTests/PluginsBuildPlanTests.swift b/Tests/BuildTests/PluginsBuildPlanTests.swift index 3a3630de7bf..86158adfc16 100644 --- a/Tests/BuildTests/PluginsBuildPlanTests.swift +++ b/Tests/BuildTests/PluginsBuildPlanTests.swift @@ -2,91 +2,181 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - +import Foundation import Basics -import _InternalTestSupport -@testable import SPMBuildCore -import XCTest import PackageModel +import Testing +import _InternalTestSupport -final class PluginsBuildPlanTests: XCTestCase { - func testBuildToolsDatabasePath() async throws { - try XCTSkipOnWindows(because: "Fails to build the project to due to incorrect Path handling. Possibly related to https://github.com/swiftlang/swift-package-manager/issues/8511") +@testable import SPMBuildCore - try await fixtureXCTest(name: "Miscellaneous/Plugins/MySourceGenPlugin") { fixturePath in - let (stdout, _) = try await executeSwiftBuild(fixturePath, buildSystem: .native) - XCTAssertMatch(stdout, .contains("Build complete!")) +@Suite( + .serialized, + .tags( + .TestSize.large, + ), +) +struct PluginsBuildPlanTests { + @Test( + .tags( + .Feature.Command.Build, + ), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8511", relationship: .defect), // Fails to build the project to due to incorrect Path handling + arguments: BuildConfiguration.allCases, + ) + func buildToolsDatabasePath( + config: BuildConfiguration, + ) async throws { + try await withKnownIssue(isIntermittent: true) { + try await fixture(name: "Miscellaneous/Plugins/MySourceGenPlugin") { fixturePath in + let (stdout, _) = try await executeSwiftBuild( + fixturePath, + configuration: config, + buildSystem: .native + ) + #expect(stdout.contains("Build complete!")) // FIXME: This is temporary until build of plugin tools is extracted into its own command. - XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/plugin-tools.db")))) - XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/build.db")))) + #expect(localFileSystem.exists(fixturePath.appending(RelativePath(".build/plugin-tools.db")))) + #expect(localFileSystem.exists(fixturePath.appending(RelativePath(".build/build.db")))) + } + } when: { + ProcessInfo.hostOperatingSystem == .windows } } - func testCommandPluginDependenciesWhenCrossCompiling() async throws { - // Command Plugin dependencies must be built for the host. - // This test is only supported on macOS because that is the only - // platform on which we can currently be sure of having a viable - // cross-compilation environment (arm64->x86_64 or vice versa). - // On Linux it is typically only possible to build for the host - // environment unless cross-compilation SDKs are being used. - #if !os(macOS) - try XCTSkipIf(true, "test is only supported on macOS") - #endif - - let hostToolchain = try UserToolchain(swiftSDK: .hostSwiftSDK(environment: [:]), environment: [:]) + @Test( + .serialized, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + .requireHostOS(.macOS), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginDependenciesWhenNotCrossCompiling( + buildData: BuildData, + ) async throws { + let hostToolchain = try UserToolchain( + swiftSDK: .hostSwiftSDK(environment: [:]), + environment: [:] + ) let hostTriple = try! hostToolchain.targetTriple.withoutVersion().tripleString - let x86Triple = "x86_64-apple-macosx" - let armTriple = "arm64-apple-macosx" - let targetTriple = hostToolchain.targetTriple.arch == .aarch64 ? x86Triple : armTriple - + let hostBinPathSegments = try buildData.buildSystem.binPath( + for: buildData.config, + triple: hostTriple, + ) + let hostDebugBinPathSegments = try buildData.buildSystem.binPath( + for: .debug, + triple: hostTriple, + ) // By default, plugin dependencies are built for the host platform - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let hostBinPath: AbsolutePath = fixturePath.appending(components: hostBinPathSegments) + let hostDebugBinPath: AbsolutePath = fixturePath.appending(components: hostDebugBinPathSegments) let (stdout, stderr) = try await executeSwiftPackage( fixturePath, + configuration: buildData.config, extraArgs: ["-v", "build-plugin-dependency"], - buildSystem: .native, - ) - XCTAssertMatch(stdout, .contains("Hello from dependencies-stub")) - XCTAssertMatch(stderr, .contains("Build of product 'plugintool' complete!")) - XCTAssertTrue( - localFileSystem.exists( - fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/plugintool-tool")) - ) - ) - XCTAssertTrue( - localFileSystem.exists( - fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/placeholder")) - ) + buildSystem: buildData.buildSystem, ) + #expect(stdout.contains("Hello from dependencies-stub")) + if buildData.buildSystem == .native { + #expect(stderr.contains("Build of product 'plugintool' complete!")) + } + let pluginToolName: String + switch buildData.buildSystem { + case .native: + pluginToolName = "plugintool-tool" + case .swiftbuild: + pluginToolName = "plugintool" + case .xcode: + pluginToolName = "" + Issue.record("Test has not been updated for this build system") + } + expectFileExists(at: hostBinPath.appending(pluginToolName)) + expectFileExists(at: hostDebugBinPath.appending("placeholder")) } + } + + @Test( + .serialized, + .tags( + .Feature.Command.Package.CommandPlugin, + ), + .requireHostOS(.macOS), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func commandPluginDependenciesWhenCrossCompiling( + buildData: BuildData, + ) async throws { + let hostToolchain = try UserToolchain( + swiftSDK: .hostSwiftSDK(environment: [:]), + environment: [:] + ) + // let hostTriple = try! hostToolchain.targetTriple.withoutVersion().tripleString + + let x86Triple = "x86_64-apple-macosx" + let armTriple = "arm64-apple-macosx" + let targetTriple = hostToolchain.targetTriple.arch == .aarch64 ? x86Triple : armTriple + + let hostBinPathSegments = try buildData.buildSystem.binPath( + for: buildData.config, + ) + let targetDebugBinPathSegments = try buildData.buildSystem.binPath( + for: .debug, + triple: targetTriple, + ) // When cross compiling the final product, plugin dependencies should still be built for the host - try await fixtureXCTest(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in - let (stdout, stderr) = try await executeSwiftPackage( - fixturePath, - extraArgs: ["--triple", targetTriple, "-v", "build-plugin-dependency"], - buildSystem: .native, - ) - XCTAssertMatch(stdout, .contains("Hello from dependencies-stub")) - XCTAssertMatch(stderr, .contains("Build of product 'plugintool' complete!")) - XCTAssertTrue( - localFileSystem.exists( - fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/plugintool-tool")) + try await fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + // let hostBinPath: AbsolutePath = fixturePath.appending(components: hostBinPathSegments) + let targetDebugBinPath: AbsolutePath = fixturePath.appending(components: targetDebugBinPathSegments) + let hostBinPath = try fixturePath.appending( + components: buildData.buildSystem.binPath( + for: buildData.config, ) ) - XCTAssertTrue( - localFileSystem.exists( - fixturePath.appending(RelativePath(".build/\(targetTriple)/debug/placeholder")) + let targetBinPath = try fixturePath.appending( + components: buildData.buildSystem.binPath( + for: buildData.config, + triple: targetTriple, ) ) + let (stdout, stderr) = try await executeSwiftPackage( + fixturePath, + configuration: buildData.config, + extraArgs: ["-v", "--triple", targetTriple, "build-plugin-dependency"], + buildSystem: buildData.buildSystem, + ) + #expect(stdout.contains("Hello from dependencies-stub")) + if buildData.buildSystem == .native { + #expect(stderr.contains("Build of product 'plugintool' complete!")) + } + let pluginToolName: String + let pluginToolBinPath: AbsolutePath + switch buildData.buildSystem { + case .native: + pluginToolName = "plugintool-tool" + pluginToolBinPath = hostBinPath + case .swiftbuild: + pluginToolName = "plugintool" + pluginToolBinPath = targetBinPath + case .xcode: + pluginToolName = "" + pluginToolBinPath = AbsolutePath("/") + Issue.record("Test has not been updated for this build system") + } + + expectFileExists(at: targetDebugBinPath.appending("placeholder")) + expectFileExists(at: pluginToolBinPath.appending(pluginToolName)) } } + } diff --git a/Tests/FunctionalTests/PluginTests.swift b/Tests/FunctionalTests/PluginTests.swift index 94261adaa42..2646e3d3b16 100644 --- a/Tests/FunctionalTests/PluginTests.swift +++ b/Tests/FunctionalTests/PluginTests.swift @@ -1347,25 +1347,17 @@ final class PluginTests { .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - func testSnippetSupport() async throws { - try await fixture(name: "Miscellaneous/Plugins") { path in - let (stdout, stderr) = try await executeSwiftPackage( - path.appending("PluginsAndSnippets"), - configuration: .debug, - extraArgs: ["do-something"], - buildSystem: .native, - ) - #expect(stdout.contains("type of snippet target: snippet"), "output:\n\(stderr)\n\(stdout)") - } - - // Try again with the Swift Build build system + func testSnippetSupport( + buildData: BuildData, + ) async throws { try await fixture(name: "Miscellaneous/Plugins") { path in let (stdout, stderr) = try await executeSwiftPackage( path.appending("PluginsAndSnippets"), - configuration: .debug, - extraArgs: ["--build-system", "swiftbuild", "do-something"], - buildSystem: .native, + configuration: buildData.config, + extraArgs: ["do-something", "--skip-dump"], + buildSystem: buildData.buildSystem, ) #expect(stdout.contains("type of snippet target: snippet"), "output:\n\(stderr)\n\(stdout)") }