From 8533c40b695644d174948af8632fa6b4072e53d6 Mon Sep 17 00:00:00 2001 From: Sam Khouri Date: Tue, 19 Aug 2025 09:02:15 -0400 Subject: [PATCH] Add Snippet support with SwiftBuild Snippets are treated as executable targets with the native build system. This change updates the PIF Builder to support snippet, giving Snippet support with the Swift Build build system. Also update the `testCommandPluginCancellation` in PluginTests as it was not properly handling the task cancellations. Depends on: https://github.com/swiftlang/swift-build/pull/775 Fixes: #9040 issue: rdar://158630024 issue: rdar://147705448 --- .../Plugins/PluginsAndSnippets/Package.swift | 15 +- .../Snippets/ContainsMain.swift | 9 + .../Snippets/ImportsProductTarget.swift | 3 + .../Snippets/MySnippet.swift | 1 + .../Snippets/SubDirectory/main.swift | 1 + .../Sources/MyLib/MyLib.swift | 4 + Package.swift | 1 + .../FileSystem/InMemoryFileSystem.swift | 16 +- .../SwiftModuleBuildDescription.swift | 58 +- .../PackageCommands/CompletionCommand.swift | 6 +- Sources/PackageModel/Module/Module.swift | 6 + Sources/PackageModel/Product.swift | 6 + .../SwiftBuildSupport/PackagePIFBuilder.swift | 4 +- .../PackagePIFProjectBuilder+Products.swift | 14 +- .../FileSystemHelpers.swift | 118 +++ .../SwiftTesting+Tags.swift | 1 + .../FileSystem/InMemoryFilesSystemTests.swift | 84 ++ Tests/BuildTests/BuildDescriptionTests.swift | 276 +++++ Tests/CommandsTests/PackageCommandTests.swift | 2 +- Tests/FunctionalTests/PluginTests.swift | 265 +++-- .../PackageBuilderTests.swift | 946 ++++++++++-------- Tests/PackageModelTests/SnippetTests.swift | 60 +- .../FileSystemHelpersTests.swift | 622 ++++++++++++ 23 files changed, 1947 insertions(+), 571 deletions(-) create mode 100644 Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift create mode 100644 Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift create mode 100644 Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift create mode 100644 Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift create mode 100644 Sources/_InternalTestSupport/FileSystemHelpers.swift create mode 100644 Tests/BuildTests/BuildDescriptionTests.swift create mode 100644 Tests/_InternalTestSupportTests/FileSystemHelpersTests.swift diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift index ac5587c1a32..c040212b25d 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift @@ -8,7 +8,13 @@ let package = Package( name: "PluginScriptProduct", targets: [ "PluginScriptTarget" - ] + ], + ), + .library( + name: "MyLib", + targets: [ + "MyLib", + ], ), ], targets: [ @@ -17,9 +23,10 @@ let package = Package( capability: .command( intent: .custom( verb: "do-something", - description: "Do something" - ) - ) + description: "Do something", + ), + ), ), + .target(name: "MyLib"), ] ) diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift new file mode 100644 index 00000000000..62a53afc43e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@main +struct foo { + static func main() { + print("hello, snippets. File: \(#file)") + } +} diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift new file mode 100644 index 00000000000..02b46f32b35 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift @@ -0,0 +1,3 @@ +import MyLib + +libraryCall() \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift index e69de29bb2d..601b98de34e 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift @@ -0,0 +1 @@ +print("hello, snippets. File: \(#file)") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift new file mode 100644 index 00000000000..ab9439d5444 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift @@ -0,0 +1 @@ +print("hello, snippets! File: \(#file)") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift new file mode 100644 index 00000000000..57aa1a92179 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift @@ -0,0 +1,4 @@ +public func libraryCall() { + print("From library") + print("hello, snippets. File: \(#file)") +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index f39bb5399ff..c8f536d5948 100644 --- a/Package.swift +++ b/Package.swift @@ -534,6 +534,7 @@ let package = Package( .target( name: "SwiftBuildSupport", dependencies: [ + "Build", "SPMBuildCore", "PackageGraph", ], diff --git a/Sources/Basics/FileSystem/InMemoryFileSystem.swift b/Sources/Basics/FileSystem/InMemoryFileSystem.swift index 56e6450aa55..0853f607b31 100644 --- a/Sources/Basics/FileSystem/InMemoryFileSystem.swift +++ b/Sources/Basics/FileSystem/InMemoryFileSystem.swift @@ -226,12 +226,24 @@ public final class InMemoryFileSystem: FileSystem { } /// Virtualized current working directory. + private var _currentWorkingDirectory: TSCBasic.AbsolutePath = try! .init(validating: "/") + public var currentWorkingDirectory: TSCBasic.AbsolutePath? { - return try? .init(validating: "/") + return _currentWorkingDirectory } public func changeCurrentWorkingDirectory(to path: TSCBasic.AbsolutePath) throws { - throw FileSystemError(.unsupported, path) + return try lock.withLock { + // Verify the path exists and is a directory + guard let node = try getNode(path) else { + throw FileSystemError(.noEntry, path) + } + + guard case .directory = node.contents else { + throw FileSystemError(.notDirectory, path) + } + _currentWorkingDirectory = path + } } public var homeDirectory: TSCBasic.AbsolutePath { diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index 19a7c3322f8..1d2bdb407a6 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -34,6 +34,34 @@ import struct TSCBasic.ByteString @available(*, deprecated, renamed: "SwiftModuleBuildDescription") public typealias SwiftTargetBuildDescription = SwiftModuleBuildDescription +// looking into the file content to see if it is using the @main annotation +// this is not bullet-proof since theoretically the file can contain the @main string for other reasons +// but it is the closest to accurate we can do at this point +package func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool { + let content: String = try fileSystem.readFileContents(path) + let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) } + + var multilineComment = false + for line in lines { + if line.hasPrefix("//") { + continue + } + if line.hasPrefix("/*") { + multilineComment = true + } + if line.hasSuffix("*/") { + multilineComment = false + } + if multilineComment { + continue + } + if line.hasPrefix("@main") { + return true + } + } + return false +} + /// Build description for a Swift module. public final class SwiftModuleBuildDescription { /// The package this target belongs to. @@ -216,40 +244,12 @@ public final class SwiftModuleBuildDescription { return false } // looking into the file content to see if it is using the @main annotation which requires parse-as-library - return (try? self.containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false + return (try? containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false default: return false } } - // looking into the file content to see if it is using the @main annotation - // this is not bullet-proof since theoretically the file can contain the @main string for other reasons - // but it is the closest to accurate we can do at this point - func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool { - let content: String = try self.fileSystem.readFileContents(path) - let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) } - - var multilineComment = false - for line in lines { - if line.hasPrefix("//") { - continue - } - if line.hasPrefix("/*") { - multilineComment = true - } - if line.hasSuffix("*/") { - multilineComment = false - } - if multilineComment { - continue - } - if line.hasPrefix("@main") { - return true - } - } - return false - } - /// The filesystem to operate on. let fileSystem: FileSystem diff --git a/Sources/Commands/PackageCommands/CompletionCommand.swift b/Sources/Commands/PackageCommands/CompletionCommand.swift index 49f68613943..14a61a08c2a 100644 --- a/Sources/Commands/PackageCommands/CompletionCommand.swift +++ b/Sources/Commands/PackageCommands/CompletionCommand.swift @@ -79,18 +79,18 @@ extension SwiftPackageCommand { case .listExecutables: let graph = try await swiftCommandState.loadPackageGraph() let package = graph.rootPackages[graph.rootPackages.startIndex].underlying - let executables = package.products.filter { $0.type == .executable } + let executables = package.products.filter { $0.type == .executable }.sorted() for executable in executables { print(executable.name) } case .listSnippets: let graph = try await swiftCommandState.loadPackageGraph() let package = graph.rootPackages[graph.rootPackages.startIndex].underlying - let executables = package.modules.filter { $0.type == .snippet } + let executables = package.modules.filter { $0.type == .snippet }.sorted() for executable in executables { print(executable.name) } } } } -} +} \ No newline at end of file diff --git a/Sources/PackageModel/Module/Module.swift b/Sources/PackageModel/Module/Module.swift index 34d6f72c8f8..7001c244212 100644 --- a/Sources/PackageModel/Module/Module.swift +++ b/Sources/PackageModel/Module/Module.swift @@ -292,6 +292,12 @@ extension Module: Hashable { } } +extension Module: Comparable { + public static func < (lhs: Module, rhs: Module) -> Bool { + return lhs.name < rhs.name + } +} + extension Module: CustomStringConvertible { public var description: String { return "<\(Swift.type(of: self)): \(name)>" diff --git a/Sources/PackageModel/Product.swift b/Sources/PackageModel/Product.swift index ac42bc458d6..4b5c79f696b 100644 --- a/Sources/PackageModel/Product.swift +++ b/Sources/PackageModel/Product.swift @@ -68,6 +68,12 @@ extension Product: Hashable { } } +extension Product: Comparable { + public static func < (lhs: Product, rhs: Product) -> Bool { + lhs.name < rhs.name + } +} + /// The type of product. public enum ProductType: Equatable, Hashable, Sendable { diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 8964cb87072..65b303f68b4 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -451,13 +451,13 @@ public final class PackagePIFBuilder { try projectBuilder.makeLibraryProduct(product, type: libraryType) } - case .executable, .test: + case .executable, .test, .snippet: try projectBuilder.makeMainModuleProduct(product) case .plugin: try projectBuilder.makePluginProduct(product) - case .snippet, .macro: + case .macro: break // TODO: Double-check what's going on here as we skip snippet modules too (rdar://147705448) } } diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift index baba5819900..69f1d6d8e35 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift @@ -17,6 +17,7 @@ import TSCUtility import struct Basics.AbsolutePath import class Basics.ObservabilitySystem import struct Basics.SourceControlURL +import func Build.containsAtMain import class PackageModel.BinaryModule import class PackageModel.Manifest @@ -55,7 +56,7 @@ extension PackagePIFProjectBuilder { let synthesizedResourceGeneratingPluginInvocationResults: [PackagePIFBuilder.BuildToolPluginInvocationResult] = [] - if product.type == .executable { + if [.executable, .snippet].contains(product.type) { if let customPIFProductType = pifBuilder.delegate.customProductType(forExecutable: product.underlying) { pifProductType = customPIFProductType moduleOrProductType = PackagePIFBuilder.ModuleOrProductType(from: customPIFProductType) @@ -138,6 +139,17 @@ extension PackagePIFProjectBuilder { settings[.INSTALL_PATH] = "/usr/local/bin" settings[.LD_RUNPATH_SEARCH_PATHS] = ["$(inherited)", "@executable_path/../lib"] } + } else if mainModule.type == .snippet { + let hasMainModule: Bool + if let mainModule = product.mainModule { + // Check if any source file in the main module contains @main + hasMainModule = mainModule.sources.paths.contains { (sourcePath: AbsolutePath) in + (try? containsAtMain(fileSystem: pifBuilder.fileSystem, path: sourcePath)) ?? false + } + } else { + hasMainModule = false + } + settings[.SWIFT_DISABLE_PARSE_AS_LIBRARY] = hasMainModule ? "NO" : "YES" } let mainTargetDeploymentTargets = mainModule.deploymentTargets(using: pifBuilder.delegate) diff --git a/Sources/_InternalTestSupport/FileSystemHelpers.swift b/Sources/_InternalTestSupport/FileSystemHelpers.swift new file mode 100644 index 00000000000..5f08a6ff35e --- /dev/null +++ b/Sources/_InternalTestSupport/FileSystemHelpers.swift @@ -0,0 +1,118 @@ +import Foundation +import Basics + +func getFiles(atPath path: String, matchingExtension fileExtension: String) -> [URL] { + let fileManager = FileManager.default + var matchingFiles: [URL] = [] + + guard + let enumerator = fileManager.enumerator( + at: URL(fileURLWithPath: path), + includingPropertiesForKeys: [.isRegularFileKey], + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) + else { + print("Error: Could not create enumerator for path: \(path)") + return [] + } + + for case let fileURL as URL in enumerator { + do { + let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) + if let isRegularFile = resourceValues.isRegularFile, isRegularFile { + if fileURL.pathExtension.lowercased() == fileExtension.lowercased() { + matchingFiles.append(fileURL) + } + } + } catch { + print("Error retrieving resource values for \(fileURL.lastPathComponent): \(error.localizedDescription)") + } + } + return matchingFiles +} + +/// Returns all files that match the given extension in the specified directory. +/// +/// - Parameters: +/// - directory: The directory to search in (AbsolutePath) +/// - extension: The file extension to match (without the leading dot) +/// - recursive: Whether to search subdirectories recursively (default: true) +/// - fileSystem: The file system to use for operations (defaults to localFileSystem) +/// - Returns: An array of AbsolutePath objects +/// - Throws: FileSystemError if the directory cannot be accessed or enumerated +public func getFiles( + in directory: AbsolutePath, + matchingExtension extension: String, + recursive: Bool = true, + fileSystem: FileSystem = localFileSystem +) throws -> [AbsolutePath] { + var matchingFiles: [AbsolutePath] = [] + let normalizedExtension = `extension`.lowercased() + + guard fileSystem.exists(directory) else { + throw StringError("Directory does not exist: \(directory)") + } + + guard fileSystem.isDirectory(directory) else { + throw StringError("Path is not a directory: \(directory)") + } + + if recursive { + try fileSystem.enumerate(directory: directory) { filePath in + if fileSystem.isFile(filePath) { + if let fileExtension = filePath.extension?.lowercased(), + fileExtension == normalizedExtension { + matchingFiles.append(filePath) + } + } + } + } else { + // Non-recursive: only check direct children + let contents = try fileSystem.getDirectoryContents(directory) + for item in contents { + let itemPath = directory.appending(component: item) + if fileSystem.isFile(itemPath) { + if let fileExtension = itemPath.extension?.lowercased(), + fileExtension == normalizedExtension { + matchingFiles.append(itemPath) + } + } + } + } + + return matchingFiles +} + +/// Returns all files that match the given extension in the specified directory. +/// +/// - Parameters: +/// - directory: The directory to search in (RelativePath) +/// - extension: The file extension to match (without the leading dot) +/// - recursive: Whether to search subdirectories recursively (default: true) +/// - fileSystem: The file system to use for operations (defaults to localFileSystem) +/// - Returns: An array of RelativePath objects +/// - Throws: FileSystemError if the directory cannot be accessed or enumerated +public func getFiles( + in directory: RelativePath, + matchingExtension extension: String, + recursive: Bool = true, + fileSystem: FileSystem = localFileSystem +) throws -> [RelativePath] { + // Convert RelativePath to AbsolutePath for enumeration + guard let currentWorkingDirectory = fileSystem.currentWorkingDirectory else { + throw StringError("Cannot determine current working directory") + } + + let absoluteDirectory = currentWorkingDirectory.appending(directory) + let absoluteResults = try getFiles( + in: absoluteDirectory, + matchingExtension: `extension`, + recursive: recursive, + fileSystem: fileSystem + ) + + // Convert results back to RelativePath + return absoluteResults.map { absolutePath in + absolutePath.relative(to: currentWorkingDirectory) + } +} diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index d49995cd172..f8af94b5f30 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -33,6 +33,7 @@ extension Tag.Feature { @Tag public static var NetRc: Tag @Tag public static var Resource: Tag @Tag public static var SpecialCharacters: Tag + @Tag public static var Snippets: Tag @Tag public static var Traits: Tag } diff --git a/Tests/BasicsTests/FileSystem/InMemoryFilesSystemTests.swift b/Tests/BasicsTests/FileSystem/InMemoryFilesSystemTests.swift index a0132d824dc..b2f50811f42 100644 --- a/Tests/BasicsTests/FileSystem/InMemoryFilesSystemTests.swift +++ b/Tests/BasicsTests/FileSystem/InMemoryFilesSystemTests.swift @@ -222,6 +222,90 @@ struct InMemoryFileSystemTests { #expect(actualContents == expectedContents, "Actual is not as expected") } + @Suite( + .tags( + .TestSize.small, + ), + ) + struct ChangeCurrentWorkingDirectoryTests { + func errorOccursWhenChangingDirectoryToAFile() async throws { + // GIVEN we have a file + let fileUnderTest = AbsolutePath.root.appending(components: "Foo", "Bar", "baz.txt") + + // AND filesytstem + let fs = InMemoryFileSystem( + emptyFiles: [ + fileUnderTest.pathString + ] + ) + + // WHEN We change directory to this file + // THEN An error occurs + #expect(throws: FileSystemError(.notDirectory, fileUnderTest)) { + try fs.changeCurrentWorkingDirectory(to: fileUnderTest) + } + } + + func errorOccursWhenChangingDirectoryDoesNotExists() async throws { + + // GIVEN we have a filesytstem + let fs = InMemoryFileSystem( + ) + let nonExistingDirectory = AbsolutePath.root.appending(components: "does-not-exists") + + // WHEN We change directory to this file + // THEN An error occurs + #expect(throws: FileSystemError(.noEntry, nonExistingDirectory)) { + try fs.changeCurrentWorkingDirectory(to: nonExistingDirectory) + } + } + + func changinDirectoryToTheParentOfAnExistingFileIsSuccessful() async throws { + // GIVEN we have a file + let fileUnderTest = AbsolutePath.root.appending(components: "Foo", "Bar", "baz.txt") + + // AND filesytstem + let fs = InMemoryFileSystem( + emptyFiles: [ + fileUnderTest.pathString + ] + ) + + // WHEN We change directory to this file + // THEN do not expect any errors + #expect(throws: Never.self) { + try fs.changeCurrentWorkingDirectory(to: fileUnderTest.parentDirectory) + } + } + } + + @Suite( + .tags( + .TestSize.small, + ), + ) + struct GetDirectoryContentsTests { + func returnsExpectedItemsWhenDirectoryHasASingleFile() async throws { + // GIVEN we have a file + let fileUnderTest = AbsolutePath.root.appending(components: "Foo", "Bar", "baz.txt") + + // AND filesytstem + let fs = InMemoryFileSystem( + emptyFiles: [ + fileUnderTest.pathString + ] + ) + + // WHEN We change directory to this file + // AND we retrieve the directory contents + try fs.changeCurrentWorkingDirectory(to: fileUnderTest.parentDirectory) + let contents = try fs.getDirectoryContents(fileUnderTest.parentDirectory) + + // THEN we expect the correct list of items + #expect(["baz.txt"] == contents) + } + } + @Test func readingADirectoryFailsWithAnError() async throws { // GIVEN we have a filesytstem diff --git a/Tests/BuildTests/BuildDescriptionTests.swift b/Tests/BuildTests/BuildDescriptionTests.swift new file mode 100644 index 00000000000..27f74d2c790 --- /dev/null +++ b/Tests/BuildTests/BuildDescriptionTests.swift @@ -0,0 +1,276 @@ +import Basics +import func Build.containsAtMain +import Testing + +struct ContainsAtMainReturnsExpectedValueTestData: CustomStringConvertible { + var description: String { + self.id + } + + let fileContent: String + let expected: Bool + let knownIssue: Bool + let id: String +} + +@Suite +struct BuildDescriptionTests { + @Test( + .tags( + .TestSize.small, + ), + arguments: [ + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + """, + expected: false, + knownIssue: false, + id: "Empty file", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "Simple @main case", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "@main with leading whitespace", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + // @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: false, + knownIssue: false, + id: "@main in single-line comment (should be ignored)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + struct MyApp { + // This is @main but not at start + static func main() { + print("Hello, World!") + } + } + """, + expected: false, + knownIssue: false, + id: "@main not at beginning of line (should not match)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + import Foundation + import SwiftUI + + @main + struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } + } + """, + expected: true, + knownIssue: false, + id: "@main with imports and other code", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + @main + struct FirstApp { + static func main() { + print("First") + } + } + + // @main (commented out) + struct SecondApp { + static func main() { + print("Second") + } + } + """, + expected: true, + knownIssue: false, + id: "Multiple @main occurrences (first one should be detected)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + @main + struct MyApp { + static func main() { + let text = "@main is cool" + print(text) + } + } + """, + expected: true, + knownIssue: false, + id: "@main in string literal (should still match as it's at line start)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: false, + knownIssue: false, + id: "No @main, just regular code", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + \t @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "@main with tabs and spaces", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + /* + @main + */ + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: false, + knownIssue: false, + id: "@main in multi-line comment (should be ignored)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + /* @main */ + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: false, + knownIssue: false, + id: "@main in multi-line comment on same line (should be ignored)", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + /* + Some comment + */ + @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "@main after multi-line comment ends", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + // This is a comment + /* Multi-line + comment */ + @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "@main with mixed comments", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + /* + This is a multi-line comment + that spans multiple lines + @main should be ignored here + */ + + @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: false, + id: "Complex multi-line comment scenario", + ), + ContainsAtMainReturnsExpectedValueTestData( + fileContent: """ + /* + This is a multi-line comment + /* @main + struct MyApp { + static func main() { + print("Hello, World!") + } + } + """, + expected: true, + knownIssue: true, + id: "Multi-line comment end on a line containing @main", + ) + ], + ) + func containsAtMainReturnsExpectedValue( + data: ContainsAtMainReturnsExpectedValueTestData, + ) async throws { + let fileContent = data.fileContent + let expected = data.expected + let knownIssue = data.knownIssue + + let fileUnderTest = AbsolutePath.root.appending("myfile.swift") + let fs = InMemoryFileSystem() + try fs.createDirectory(fileUnderTest.parentDirectory, recursive: true) + try fs.writeFileContents(fileUnderTest, string: fileContent) + + let actual = try containsAtMain(fileSystem: fs, path: fileUnderTest) + + withKnownIssue { + #expect(actual == expected) + } when: { + knownIssue + } + + } +} diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 3f2079de23f..a885ab93631 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -1134,7 +1134,7 @@ struct PackageCommandTests { configuration: data.config, buildSystem: data.buildSystem, ) - #expect(result.stdout == "MySnippet\n") + #expect(result.stdout == "ContainsMain\nImportsProductTarget\nMySnippet\nmain\n") } } diff --git a/Tests/FunctionalTests/PluginTests.swift b/Tests/FunctionalTests/PluginTests.swift index 2646e3d3b16..03f4af842b8 100644 --- a/Tests/FunctionalTests/PluginTests.swift +++ b/Tests/FunctionalTests/PluginTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2021-2024 Apple Inc. and the Swift project authors +// Copyright (c) 2021-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -23,7 +23,11 @@ import Workspace import Testing import Foundation -@Suite(.serialized) +@Suite( + .tags( + .Feature.Command.Package.Plugin, + ) +) final class PluginTests { @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), @@ -1092,74 +1096,79 @@ final class PluginTests { toolchain: try UserToolchain.default ) let delegate = PluginDelegate(delegateQueue: delegateQueue) - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - // TODO: have invoke natively support task cancellation instead - try await withTaskCancellationHandler { - _ = try await plugin.invoke( - action: .performCommand(package: package, arguments: []), - buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), - scriptRunner: scriptRunner, - workingDirectory: package.path, - outputDirectory: pluginDir.appending("output"), - toolSearchDirectories: [try UserToolchain.default.swiftCompilerPath.parentDirectory], - accessibleTools: [:], - writableDirectories: [pluginDir.appending("output")], - readOnlyDirectories: [package.path], - allowNetworkConnections: [], - pkgConfigDirectories: [], - sdkRootPath: try UserToolchain.default.sdkRootPath, - fileSystem: localFileSystem, - modulesGraph: packageGraph, - observabilityScope: observability.topScope, - callbackQueue: delegateQueue, - delegate: delegate - ) - } onCancel: { - do { - try scriptRunner.cancel(deadline: .now() + .seconds(5)) - } catch { - Issue.record("Cancelling script runner should not fail: \(error)") - } - } - } - group.addTask { + // Use a task with timeout to test cancellation + let pluginTask = Task { + try await withTaskCancellationHandler { + _ = try await plugin.invoke( + action: .performCommand(package: package, arguments: []), + buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), + scriptRunner: scriptRunner, + workingDirectory: package.path, + outputDirectory: pluginDir.appending("output"), + toolSearchDirectories: [try UserToolchain.default.swiftCompilerPath.parentDirectory], + accessibleTools: [:], + writableDirectories: [pluginDir.appending("output")], + readOnlyDirectories: [package.path], + allowNetworkConnections: [], + pkgConfigDirectories: [], + sdkRootPath: try UserToolchain.default.sdkRootPath, + fileSystem: localFileSystem, + modulesGraph: packageGraph, + observabilityScope: observability.topScope, + callbackQueue: delegateQueue, + delegate: delegate + ) + } onCancel: { do { - try await Task.sleep(nanoseconds: UInt64(DispatchTimeInterval.seconds(3).nanoseconds()!)) + try scriptRunner.cancel(deadline: .now() + .seconds(5)) } catch { - Issue.record("The plugin should not finish within 3 seconds") + Issue.record("Cancelling script runner should not fail: \(error)") } } + } - try await group.next() + // Wait for the plugin to start and get its PID + try await Task.sleep(nanoseconds: UInt64(DispatchTimeInterval.seconds(3).nanoseconds()!)) + // At this point we should have parsed out the process identifier. But it's possible we don't always — this is being investigated in rdar://88792829. + var pid: Int? = .none + delegateQueue.sync { + pid = delegate.parsedProcessIdentifier + } + guard let pid = pid else { + pluginTask.cancel() + print("skipping test because no pid was received from the plugin; being investigated as rdar://88792829\n\(delegate.diagnostics.description)") + return + } - // At this point we should have parsed out the process identifier. But it's possible we don't always — this is being investigated in rdar://88792829. - var pid: Int? = .none - delegateQueue.sync { - pid = delegate.parsedProcessIdentifier - } - guard let pid = pid else { - print("skipping test because no pid was received from the plugin; being investigated as rdar://88792829\n\(delegate.diagnostics.description)") - return - } + // Check that it's running (we do this by asking for its priority — this only works on some platforms). + #if os(macOS) + errno = 0 + getpriority(Int32(PRIO_PROCESS), UInt32(pid)) + #expect(errno == 0, "unexpectedly got errno \(errno) when trying to check process \(pid)") + #endif + + // Cancel the plugin task + pluginTask.cancel() + + // Wait a bit for cancellation to propagate + try await Task.sleep(nanoseconds: UInt64(DispatchTimeInterval.milliseconds(500).nanoseconds()!)) + + // Check that it's no longer running (we do this by asking for its priority — this only works on some platforms). + #if os(macOS) + errno = 0 + getpriority(Int32(PRIO_PROCESS), UInt32(pid)) + #expect(errno == ESRCH, "unexpectedly got errno \(errno) when trying to check process \(pid)") + #endif - // Check that it's running (we do this by asking for its priority — this only works on some platforms). - #if os(macOS) - errno = 0 - getpriority(Int32(PRIO_PROCESS), UInt32(pid)) - #expect(errno == 0, "unexpectedly got errno \(errno) when trying to check process \(pid)") - #endif - - // Ask the plugin running to cancel all plugins. - group.cancelAll() - - // Check that it's no longer running (we do this by asking for its priority — this only works on some platforms). - #if os(macOS) - errno = 0 - getpriority(Int32(PRIO_PROCESS), UInt32(pid)) - #expect(errno == ESRCH, "unexpectedly got errno \(errno) when trying to check process \(pid)") - #endif + // Ensure the task was actually cancelled + do { + _ = try await pluginTask.value + Issue.record("Plugin task should have been cancelled") + } catch is CancellationError { + // Expected - task was cancelled + } catch { + // Also acceptable - plugin may have been terminated } @@ -1343,26 +1352,124 @@ final class PluginTests { } } - @Test( - .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), + @Suite( + .issue("https://github.com/swiftlang/swift-package-manager/issues/9040", relationship: .verifies), + .tags( + .Feature.Snippets, + ) ) - func testSnippetSupport( - buildData: BuildData, - ) async throws { - try await fixture(name: "Miscellaneous/Plugins") { path in - let (stdout, stderr) = try await executeSwiftPackage( - path.appending("PluginsAndSnippets"), - configuration: buildData.config, - extraArgs: ["do-something", "--skip-dump"], - buildSystem: buildData.buildSystem, - ) - #expect(stdout.contains("type of snippet target: snippet"), "output:\n\(stderr)\n\(stdout)") + struct SnippetTests { + @Test( + .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( + buildData: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in + let (stdout, stderr) = try await executeSwiftPackage( + fixturePath, + configuration: buildData.config, + extraArgs: ["do-something"], + buildSystem: buildData.buildSystem, + ) + #expect(stdout.contains("type of snippet target: snippet"), "stderr:\n\(stderr)") + } } - } + @Test( + .disabled(), + .requiresSwiftConcurrencySupport, + .tags( + .Feature.Command.Package.CompletionTool, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func testBasicBuildSnippets( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in + await #expect(throws: Never.self) { + let _ = try await executeSwiftBuild( + fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, + ) + } + + let snippets = try await executeSwiftPackage( + fixturePath, + configuration: data.config, + extraArgs: ["completion-tool", "list-snippet"], + buildSystem: data.buildSystem, + ).stdout.split(whereSeparator: \.isNewline) + + for snippet in snippets { + try expectFileExists( + at: fixturePath.appending(components: data.buildSystem.binPath(for: data.config) + ["\(snippet)"]) + ) + } + } + } + + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/9040", relationship: .verifies), + .IssueWindowsCannotSaveAttachment, + .requiresSwiftConcurrencySupport, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), try getFiles(in: RelativePath(validating: "Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets"), matchingExtension: "swift",), + ) + func testBasicBuildIndividualSnippets( + data: BuildData, + targetPath: RelativePath, + ) async throws { + try await withKnownIssue(isIntermittent: true) { + try await fixture(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in + let targetName = targetPath.basenameWithoutExt + await #expect(throws: Never.self) { + let _ = try await executeSwiftBuild( + fixturePath, + configuration: data.config, + extraArgs: ["--product", targetName], + buildSystem: data.buildSystem, + ) + } + } + } when: { + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild + } + } + + @Test( + .issue("https://github.com/swiftlang/swift-package-manager/issues/9040", relationship: .verifies), + .IssueWindowsCannotSaveAttachment, + .requiresSwiftConcurrencySupport, + .IssueSwiftBuildLinuxRunnable, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), try getFiles(in: RelativePath(validating: "Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets"), matchingExtension: "swift",), + ) + func testBasicRunSnippets( + data: BuildData, + targetPath: RelativePath, + ) async throws { + let targetName = targetPath.basenameWithoutExt + try await withKnownIssue(isIntermittent: true) { + try await fixture(name: "Miscellaneous/Plugins/PluginsAndSnippets") { fixturePath in + let (stdout, stderr) = try await executeSwiftRun( + fixturePath, + targetName, + configuration: data.config, + buildSystem: data.buildSystem, + ) + + #expect(stdout.contains("hello, snippets"), "stderr: \(stderr)") + } + } when: { + [.windows, .linux].contains(ProcessInfo.hostOperatingSystem) && data.buildSystem == .swiftbuild + } + } + } + @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, diff --git a/Tests/PackageLoadingTests/PackageBuilderTests.swift b/Tests/PackageLoadingTests/PackageBuilderTests.swift index a00ad7eeeb2..c7c685ecab9 100644 --- a/Tests/PackageLoadingTests/PackageBuilderTests.swift +++ b/Tests/PackageLoadingTests/PackageBuilderTests.swift @@ -2,22 +2,28 @@ // // 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 PackageLoading import PackageModel import _InternalTestSupport -import XCTest +import Testing /// Tests for the handling of source layout conventions. -final class PackageBuilderTests: XCTestCase { +@Suite( + .tags( + .TestSize.medium + ) +) +struct PackageBuilderTests { + @Test func testDotFilesAreIgnored() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/.Bar.swift", @@ -30,14 +36,15 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "foo"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(c99name: "foo", type: .library) module.checkSources(root: "/Sources/foo", paths: "Foo.swift") } } } + @Test func testXCPrivacyIgnored() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/PrivacyInfo.xcprivacy", @@ -50,8 +57,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "foo"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(c99name: "foo", type: .library) module.checkSources(root: "/Sources/foo", paths: "Foo.swift") module.checkResources(resources: []) @@ -59,6 +66,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testMixedSources() throws { let foo: AbsolutePath = "/Sources/foo" @@ -74,11 +82,12 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "foo"), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "target at '\(foo)' contains mixed language source files; feature not supported", severity: .error) } } + @Test func testBrokenSymlink() throws { try testWithTemporaryDirectory { path in let fs = localFileSystem @@ -102,16 +111,17 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: path, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, path: path, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "ignoring broken symlink \(linkPath)", severity: .warning ) - package.checkModule("foo") + try package.checkModule("foo") } } } + @Test func testSymlinkedSourcesDirectory() throws { try testWithTemporaryDirectory { path in let fs = localFileSystem @@ -132,12 +142,13 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: path, in: fs) { package, _ in - package.checkModule("bar") + try PackageBuilderTester(manifest, path: path, in: fs) { package, _ in + try package.checkModule("bar") } } } + @Test func testCInTests() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/MyPackage/main.swift", @@ -150,13 +161,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "MyPackageTests", dependencies: ["MyPackage"], type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("MyPackage") { module in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("MyPackage") { module in module.check(type: .executable) module.checkSources(root: "/Sources/MyPackage", paths: "main.swift") } - package.checkModule("MyPackageTests") { module in + try package.checkModule("MyPackageTests") { module in module.check(type: .test) module.checkSources(root: "/Tests/MyPackageTests", paths: "abc.c") } @@ -174,6 +185,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testValidSources() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/pkg/main.swift", @@ -191,8 +203,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkg"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("pkg") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("pkg") { module in module.check(type: .executable) module.checkSources(root: "/Sources/pkg", paths: "main.swift", "Package.swift") } @@ -200,6 +212,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testVersionSpecificManifests() throws { let fs = InMemoryFileSystem(emptyFiles: "/Package.swift", @@ -214,14 +227,15 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: name), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule(name) { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule(name) { module in module.check(c99name: name, type: .library) module.checkSources(root: "/Sources/Foo", paths: "Package.swift", "Package@swift-1.swift") } } } + @Test func testModuleMapLayout() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/clib/include/module.modulemap", @@ -235,8 +249,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "clib"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("clib") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("clib") { module in module.check(c99name: "clib", type: .library) module.checkSources(root: "/Sources/clib", paths: "clib.c") module.check(moduleMapType: .custom("/Sources/clib/include/module.modulemap")) @@ -244,6 +258,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testPublicIncludeDirMixedWithSources() throws { let Sources: AbsolutePath = "/Sources" @@ -268,12 +283,12 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, in: fs) { package, diags in + try PackageBuilderTester(manifest, in: fs) { package, diags in diags.check( diagnostic: "found duplicate sources declaration in the package manifest: \(Sources.appending(components: "clib"))", severity: .warning ) - package.checkModule("clib") { module in + try package.checkModule("clib") { module in module.check(c99name: "clib", type: .library) module.checkSources(root: Sources.pathString, paths: RelativePath("clib").appending(components: "clib.c").pathString, RelativePath("clib").appending(components: "clib2.c").pathString, RelativePath("clib").appending(components: "nested", "nested.c").pathString) module.check(moduleMapType: .umbrellaHeader(Sources.appending(components: "clib", "clib.h"))) @@ -281,6 +296,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testDeclaredSourcesWithDot() throws { let swiftLib: RelativePath = RelativePath("swift.lib") @@ -314,22 +330,23 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, in: fs) { result, _ in - result.checkModule("swift.lib") { module in + try PackageBuilderTester(manifest, in: fs) { result, _ in + try result.checkModule("swift.lib") { module in module.checkSources(sources: ["foo.swift"]) } - result.checkModule("swiftlib1") { module in + try result.checkModule("swiftlib1") { module in module.checkSources(sources: [swiftLib.appending(components: "foo.swift").pathString]) } - result.checkModule("swiftlib2") { module in + try result.checkModule("swiftlib2") { module in module.checkSources(sources: ["foo.swift"]) } - result.checkModule("swiftlib3") { module in + try result.checkModule("swiftlib3") { module in module.checkSources(sources: [RelativePath("foo.bar").appending(components: "bar.swift").pathString, "foo.swift"]) } } } + @Test func testOverlappingDeclaredSources() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/clib/subfolder/foo.h", @@ -349,13 +366,14 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, in: fs) { result, _ in - result.checkModule("clib") { module in + try PackageBuilderTester(manifest, in: fs) { result, _ in + try result.checkModule("clib") { module in module.checkSources(sources: [RelativePath("clib").appending(components: "bar.c").pathString, RelativePath("clib").appending(components: "subfolder", "foo.c").pathString]) } } } + @Test func testDeclaredExecutableProducts() throws { // Check that declaring executable product doesn't collide with the // inferred products. @@ -374,9 +392,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { _ in } - package.checkModule("exec") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { _ in } + try package.checkModule("exec") { _ in } package.checkProduct("exec") { product in product.check(type: .executable, targets: ["exec", "foo"]) } @@ -390,9 +408,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { _ in } - package.checkModule("exec") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { _ in } + try package.checkModule("exec") { _ in } package.checkProduct("exec") { product in product.check(type: .executable, targets: ["exec"]) } @@ -410,15 +428,16 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { _ in } - package.checkModule("exec") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { _ in } + try package.checkModule("exec") { _ in } package.checkProduct("exec1") { product in product.check(type: .executable, targets: ["exec"]) } } } + @Test func testExecutableTargets() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/exec1/exec.swift", @@ -438,9 +457,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "lib"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { _ in } - package.checkModule("exec1") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { _ in } + try package.checkModule("exec1") { _ in } package.checkProduct("exec1") { product in product.check(type: .executable, targets: ["exec1", "lib"]) } @@ -456,9 +475,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "lib"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { _ in } - package.checkModule("exec1") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { _ in } + try package.checkModule("exec1") { _ in } package.checkProduct("exec1") { product in product.check(type: .executable, targets: ["exec1"]) } @@ -476,9 +495,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec1", type: .executable), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { _ in } - package.checkModule("exec1") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { _ in } + try package.checkModule("exec1") { _ in } package.checkProduct("exec1") { product in product.check(type: .executable, targets: ["exec1"]) } @@ -496,9 +515,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec1", type: .executable), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { _ in } - package.checkModule("exec1") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { _ in } + try package.checkModule("exec1") { _ in } package.checkProduct("exec1") { product in product.check(type: .executable, targets: ["exec1"]) } @@ -516,19 +535,20 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec2"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "'exec2' was identified as an executable target given the presence of a 'main' file. Starting with tools version 5.4.0 executable targets should be declared as 'executableTarget()'", severity: .warning ) - package.checkModule("lib") { _ in } - package.checkModule("exec2") { _ in } + try package.checkModule("lib") { _ in } + try package.checkModule("exec2") { _ in } package.checkProduct("exec2") { product in product.check(type: .executable, targets: ["exec2"]) } } } + @Test func testTestEntryPointFound() throws { try SwiftModule.testEntryPointNames.forEach { name in let fs = InMemoryFileSystem(emptyFiles: @@ -544,13 +564,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "tests", path: "swift/tests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("exe") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("exe") { module in module.check(c99name: "exe", type: .library) module.checkSources(root: "/swift/exe", paths: "foo.swift") } - package.checkModule("tests") { module in + try package.checkModule("tests") { module in module.check(c99name: "tests", type: .test) module.checkSources(root: "/swift/tests", paths: "footests.swift") } @@ -563,9 +583,13 @@ final class PackageBuilderTests: XCTestCase { } } + @Test( + .IssueWindowsLongPath, + .IssueWindowsPathLastConponent, + .IssueWindowsRelativePathAssert + ) func testTestManifestSearch() throws { - try XCTSkipOnWindows(because: "possibly related to https://github.com/swiftlang/swift-package-manager/issues/8511") - + try withKnownIssue(isIntermittent: true) { let fs = InMemoryFileSystem(emptyFiles: "/pkg/foo.swift", "/pkg/footests.swift" @@ -587,17 +611,21 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, path: "/pkg", in: fs) { package, _ in - package.checkModule("exe") { _ in } - package.checkModule("tests") { _ in } + try PackageBuilderTester(manifest, path: "/pkg", in: fs) { package, _ in + try package.checkModule("exe") { _ in } + try package.checkModule("tests") { _ in } package.checkProduct("pkgPackageTests") { product in product.check(type: .test, targets: ["tests"]) product.check(testEntryPointPath: nil) } } + } when: { + ProcessInfo.hostOperatingSystem == .windows + } } + @Test func testEmptyProductNameError() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/best/best.swift") @@ -611,11 +639,12 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check(diagnostic: "product names can not be empty", severity: .error) } } + @Test func testMultipleTestEntryPointsError() throws { let name = SwiftModule.defaultTestEntryPointName let swift: AbsolutePath = "/swift" @@ -636,11 +665,12 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check(diagnostic: "package '\(package.packageIdentity)' has multiple test entry point files: \(try! AbsolutePath(validating: "/\(name)")), \(swift.appending(components: name))", severity: .error) } } + @Test func testCustomTargetPaths() throws { let Sources: AbsolutePath = "/Sources" let swift: RelativePath = "swift" @@ -679,26 +709,26 @@ final class PackageBuilderTests: XCTestCase { sources: ["bar"]), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: Sources, testTarget: "/Tests") - package.checkModule("exe") { module in + try package.checkModule("exe") { module in module.check(c99name: "exe", type: .executable) module.checkSources(root: "/mah/target/exe", paths: swift.appending(components: "exe", "main.swift").pathString, swift.appending(components: "exe", "foo.swift").pathString, swift.appending(components: "bar.swift").pathString) } - package.checkModule("clib") { module in + try package.checkModule("clib") { module in module.check(c99name: "clib", type: .library) module.checkSources(root: "/mah/target/exe", paths: "foo.c") } - package.checkModule("foo") { module in + try package.checkModule("foo") { module in module.check(c99name: "foo", type: .library) module.checkSources(root: "/Sources/foo", paths: "foo.swift") } - package.checkModule("bar") { module in + try package.checkModule("bar") { module in module.check(c99name: "bar", type: .library) module.checkSources(root: bar.pathString, paths: RelativePath("bar").appending(components: "foo.swift").pathString) } @@ -707,6 +737,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testCustomTargetPathsOverlap() throws { let bar: AbsolutePath = "/target/bar" @@ -727,7 +758,7 @@ final class PackageBuilderTests: XCTestCase { type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check(diagnostic: "target 'barTests' has overlapping sources: \(bar.appending(components: "Tests", "barTests.swift"))", severity: .error) } @@ -744,15 +775,15 @@ final class PackageBuilderTests: XCTestCase { type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: "/Sources", testTarget: "/Tests") - package.checkModule("bar") { module in + try package.checkModule("bar") { module in module.check(c99name: "bar", type: .library) module.checkSources(root: "/target/bar", paths: "bar.swift") } - package.checkModule("barTests") { module in + try package.checkModule("barTests") { module in module.check(c99name: "barTests", type: .test) module.checkSources(root: bar.appending(components: "Tests").pathString, paths: "barTests.swift") } @@ -761,6 +792,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testPublicHeadersPath() throws { let Sources: AbsolutePath = "/Sources" let Tests: AbsolutePath = "/Tests" @@ -786,19 +818,19 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: Sources, testTarget: Tests) - package.checkModule("Foo") { module in - let clangTarget = module.target as? ClangModule - XCTAssertEqual(clangTarget?.headers.map{ $0.pathString }, [Sources.appending(components: "Foo", "Foo_private.h").pathString, Sources.appending(components: "Foo", "inc", "Foo.h").pathString]) + try package.checkModule("Foo") { module in + let clangTarget = try #require(module.target as? ClangModule) + #expect(clangTarget.headers.map{ $0.pathString } == [Sources.appending(components: "Foo", "Foo_private.h").pathString, Sources.appending(components: "Foo", "inc", "Foo.h").pathString]) module.check(c99name: "Foo", type: .library) module.checkSources(root: Sources.appending(components: "Foo").pathString, paths: "Foo.c") module.check(includeDir: Sources.appending(components: "Foo", "inc").pathString) module.check(moduleMapType: .custom(Sources.appending(components: "Foo", "inc", "module.modulemap"))) } - package.checkModule("Bar") { module in + try package.checkModule("Bar") { module in module.check(c99name: "Bar", type: .library) module.checkSources(root: Sources.appending(components: "Bar").pathString, paths: "Bar.c") module.check(includeDir: Sources.appending(components: "Bar", "include").pathString) @@ -807,6 +839,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testInvalidPublicHeadersPath() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/Foo/inc/module.modulemap", @@ -828,11 +861,12 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "invalid relative path \'/inc\'; relative path should not begin with \'\(AbsolutePath.root)\'", severity: .error) } } + @Test func testTestsLayoutsv4() throws { let Sources: AbsolutePath = "/Sources" @@ -852,27 +886,27 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "B", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: Sources, testTarget: "/Tests") - package.checkModule("A") { module in + try package.checkModule("A") { module in module.check(c99name: "A", type: .executable) module.checkSources(root: "/Sources/A", paths: "main.swift") } - package.checkModule("TheTestOfA") { module in + try package.checkModule("TheTestOfA") { module in module.check(c99name: "TheTestOfA", type: .test) module.checkSources(root: "/Tests/TheTestOfA", paths: "Foo.swift") module.check(targetDependencies: ["A"]) } - package.checkModule("B") { module in + try package.checkModule("B") { module in module.check(c99name: "B", type: .test) module.checkSources(root: "/Tests/B", paths: "Foo.swift") module.check(targetDependencies: []) } - package.checkModule("ATests") { module in + try package.checkModule("ATests") { module in module.check(c99name: "ATests", type: .test) module.checkSources(root: "/Tests/ATests", paths: "Foo.swift") module.check(targetDependencies: []) @@ -883,6 +917,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testMultipleTestProducts() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/foo.swift", @@ -899,10 +934,10 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, shouldCreateMultipleTestProducts: true, in: fs) { package, _ in - package.checkModule("foo") { _ in } - package.checkModule("fooTests") { _ in } - package.checkModule("barTests") { _ in } + try PackageBuilderTester(manifest, shouldCreateMultipleTestProducts: true, in: fs) { package, _ in + try package.checkModule("foo") { _ in } + try package.checkModule("fooTests") { _ in } + try package.checkModule("barTests") { _ in } package.checkProduct("fooTests") { product in product.check(type: .test, targets: ["fooTests"]) } @@ -911,16 +946,17 @@ final class PackageBuilderTests: XCTestCase { } } - PackageBuilderTester(manifest, shouldCreateMultipleTestProducts: false, in: fs) { package, _ in - package.checkModule("foo") { _ in } - package.checkModule("fooTests") { _ in } - package.checkModule("barTests") { _ in } + try PackageBuilderTester(manifest, shouldCreateMultipleTestProducts: false, in: fs) { package, _ in + try package.checkModule("foo") { _ in } + try package.checkModule("fooTests") { _ in } + try package.checkModule("barTests") { _ in } package.checkProduct("pkgPackageTests") { product in product.check(type: .test, targets: ["barTests", "fooTests"]) } } } + @Test func testCustomTargetDependencies() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/Foo/Foo.swift", @@ -936,15 +972,15 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Baz"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("Foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("Foo") { module in module.check(c99name: "Foo", type: .library) module.checkSources(root: "/Sources/Foo", paths: "Foo.swift") module.check(targetDependencies: ["Bar"]) } for target in ["Bar", "Baz"] { - package.checkModule(target) { module in + try package.checkModule(target) { module in module.check(c99name: target, type: .library) module.checkSources(root: "/Sources/\(target)", paths: "\(target).swift") } @@ -960,26 +996,27 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Baz"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("Foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("Foo") { module in module.check(c99name: "Foo", type: .library) module.checkSources(root: "/Sources/Foo", paths: "Foo.swift") module.check(targetDependencies: ["Bar"]) } - package.checkModule("Bar") { module in + try package.checkModule("Bar") { module in module.check(c99name: "Bar", type: .library) module.checkSources(root: "/Sources/Bar", paths: "Bar.swift") module.check(targetDependencies: ["Baz"]) } - package.checkModule("Baz") { module in + try package.checkModule("Baz") { module in module.check(c99name: "Baz", type: .library) module.checkSources(root: "/Sources/Baz", paths: "Baz.swift") } } } + @Test func testTargetDependencies() throws { let Sources: AbsolutePath = "/Sources" @@ -999,11 +1036,11 @@ final class PackageBuilderTests: XCTestCase { dependencies: ["Bar", "Baz", "Bam"]), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: Sources, testTarget: "/Tests") - package.checkModule("Foo") { module in + try package.checkModule("Foo") { module in module.check(c99name: "Foo", type: .library) module.checkSources(root: Sources.appending(components: "Foo").pathString, paths: "Foo.swift") module.check(targetDependencies: ["Bar", "Baz"]) @@ -1011,7 +1048,7 @@ final class PackageBuilderTests: XCTestCase { } for target in ["Bar", "Baz"] { - package.checkModule(target) { module in + try package.checkModule(target) { module in module.check(c99name: target, type: .library) module.checkSources(root: "/Sources/\(target)", paths: "\(target).swift") } @@ -1021,6 +1058,7 @@ final class PackageBuilderTests: XCTestCase { /// Starting with tools version 5.9, packages are permitted to place /// sources anywhere in ./Sources when a package has a single target. + @Test func testRelaxedSourceLocationSingleTargetRegular() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .regular) do { @@ -1036,9 +1074,9 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in - XCTAssertEqual("/\(predefinedSourceDir)", result.target.path) + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in + #expect("/\(predefinedSourceDir)" == result.target.path) } } } @@ -1057,7 +1095,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random"), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target Random should be located under '\(predefinedSourceDir)/Random', '\(predefinedSourceDir)', or a custom sources path can be set with the 'path' property in Package.swift", severity: .warning) } } @@ -1075,8 +1113,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") } } do { @@ -1094,8 +1132,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources(paths: "Random.swift") } } @@ -1114,12 +1152,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB"), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testRelaxedSourceLocationSingleTargetTest() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .test) do { @@ -1135,9 +1174,9 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("MyTests") { result in - XCTAssertEqual("/\(predefinedSourceDir)", result.target.path) + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("MyTests") { result in + #expect("/\(predefinedSourceDir)" == result.target.path) } package.checkProduct("pkgPackageTests") } @@ -1157,7 +1196,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "RandomTests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target RandomTests should be located under '\(predefinedSourceDir)/RandomTests', '\(predefinedSourceDir)', or a custom sources path can be set with the 'path' property in Package.swift", severity: .warning) } } @@ -1175,8 +1214,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "RandomTests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("RandomTests") + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("RandomTests") package.checkProduct("pkgPackageTests") } } @@ -1195,8 +1234,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "RandomTests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("RandomTests") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("RandomTests") { result in result.checkSources(paths: "Random.swift") } package.checkProduct("pkgPackageTests") @@ -1216,12 +1255,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testRelaxedSourceLocationSingleTargetPlugin() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .plugin) do { @@ -1237,8 +1277,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("MyPlugin") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("MyPlugin") { result in result.checkSources(root: result.target.path.appending(component: predefinedSourceDir).pathString, paths: "Foo.swift") } } @@ -1258,7 +1298,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .plugin, pluginCapability: .buildTool), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target Random should be located under '\(predefinedSourceDir)/Random', '\(predefinedSourceDir)', or a custom sources path can be set with the 'path' property in Package.swift", severity: .warning) } } @@ -1277,8 +1317,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .plugin, pluginCapability: .buildTool), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") } } do { @@ -1296,8 +1336,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .plugin, pluginCapability: .buildTool), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources(paths: "Random.swift") } } @@ -1316,12 +1356,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB", type: .plugin, pluginCapability: .buildTool), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testRelaxedSourceLocationSingleTargetExecutable() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .executable) do { @@ -1337,9 +1378,9 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("MyExe") { result in - XCTAssertEqual("/\(predefinedSourceDir)", result.target.path) + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("MyExe") { result in + #expect("/\(predefinedSourceDir)" == result.target.path) } package.checkProduct("MyExe") } @@ -1359,7 +1400,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .executable), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target Random should be located under '\(predefinedSourceDir)/Random', '\(predefinedSourceDir)', or a custom sources path can be set with the 'path' property in Package.swift", severity: .warning) } } @@ -1377,8 +1418,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .executable) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") package.checkProduct("Random") } } @@ -1397,8 +1438,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .executable) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources(paths: "Random.swift") } package.checkProduct("Random") @@ -1418,12 +1459,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB", type: .executable) ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testRelaxedSourceLocationSingleTargetSystem() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .system) do { @@ -1439,9 +1481,9 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Foo") { result in - XCTAssertEqual("/\(predefinedSourceDir)", result.target.path) + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Foo") { result in + #expect("/\(predefinedSourceDir)" == result.target.path) } } } @@ -1461,7 +1503,7 @@ final class PackageBuilderTests: XCTestCase { ] ) let map = "/\(predefinedSourceDir)/module.modulemap" - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in #if _runtime(_ObjC) diagnostics.check(diagnostic: "package has unsupported layout; missing system target module map at '\(map)'", severity: .error) #else @@ -1484,8 +1526,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .system) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") } } do { @@ -1503,8 +1545,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .system) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources() } } @@ -1523,12 +1565,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB", type: .system) ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testRelaxedSourceLocationSingleTargetMacro() throws { let predefinedSourceDir = PackageBuilder.suggestedPredefinedSourceDirectory(type: .macro) do { @@ -1545,9 +1588,9 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Foo") { result in - XCTAssertEqual("/\(predefinedSourceDir)", result.target.path) + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Foo") { result in + #expect("/\(predefinedSourceDir)" == result.target.path) } package.checkProduct("Foo") } @@ -1567,7 +1610,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .macro), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target Random should be located under '\(predefinedSourceDir)/Random', '\(predefinedSourceDir)', or a custom sources path can be set with the 'path' property in Package.swift", severity: .warning) } } @@ -1585,8 +1628,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .macro) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources(root: "/\(predefinedSourceDir)", paths: "Random.swift") } package.checkProduct("Random") @@ -1607,8 +1650,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random", type: .macro) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("Random") { result in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("Random") { result in result.checkSources(root: "/\(predefinedSourceDir)/Random", paths: "Random.swift") } package.checkProduct("Random") @@ -1628,12 +1671,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "TargetB", type: .macro) ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "Source files for target TargetA should be located under '\(predefinedSourceDir)/TargetA', or a custom sources path can be set with the 'path' property in Package.swift", severity: .error) } } } + @Test func testStrictSourceLocationPre5_9() throws { do { for fs in [ @@ -1651,13 +1695,14 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Random"), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: .contains("Source files for target Random should be located under 'Sources/Random'"), severity: .error) } } } } + @Test func testManifestTargetDeclErrors() throws { do { let fs = InMemoryFileSystem(emptyFiles: @@ -1669,7 +1714,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkg", dependencies: [.target(name: "Foo")]), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: .contains("Source files for target Foo should be located under 'Sources/Foo'"), severity: .error) } } @@ -1684,7 +1729,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkgTests", dependencies: [], type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: .contains("Source files for target pkgTests should be located under 'Tests/pkgTests'"), severity: .error) } } @@ -1700,7 +1745,7 @@ final class PackageBuilderTests: XCTestCase { ], traits: [] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "cyclic dependency declaration found: pkg -> pkg", severity: .error) } } @@ -1722,9 +1767,9 @@ final class PackageBuilderTests: XCTestCase { "foo": BinaryArtifact(kind: .xcframework, originURL: "https://foo.com/foo.zip", path: "/foo.xcframework"), "foo2": BinaryArtifact(kind: .xcframework, originURL: nil, path: "/foo2.xcframework") ] - PackageBuilderTester(manifest, binaryArtifacts: binaryArtifacts, in: fs) { package, _ in - package.checkModule("foo") - package.checkModule("foo2") + try PackageBuilderTester(manifest, binaryArtifacts: binaryArtifacts, in: fs) { package, _ in + try package.checkModule("foo") + try package.checkModule("foo2") } } @@ -1743,7 +1788,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkg3", dependencies: ["pkg1"]), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "cyclic dependency declaration found: pkg1 -> pkg2 -> pkg3 -> pkg1", severity: .error) } @@ -1755,7 +1800,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkg3", dependencies: ["pkg2"]), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "cyclic dependency declaration found: pkg1 -> pkg2 -> pkg3 -> pkg2", severity: .error) } } @@ -1776,12 +1821,12 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "pkg2"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: .contains("Source files for target pkg2 should be located under 'Sources/pkg2'"), severity: .warning ) - package.checkModule("pkg1") { module in + try package.checkModule("pkg1") { module in module.check(c99name: "pkg1", type: .library) module.checkSources(root: "/Sources/pkg1", paths: "Foo.swift") } @@ -1800,7 +1845,7 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "public headers (\"include\") directory path for 'Foo' is invalid or not contained in the target", severity: .error) } @@ -1810,7 +1855,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Bar", publicHeadersPath: "inc/../../../foo"), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "public headers (\"include\") directory path for 'Bar' is invalid or not contained in the target", severity: .error) } } @@ -1826,7 +1871,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Foo", path: "../foo"), ] ) - PackageBuilderTester(manifest, path: "/pkg", in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, path: "/pkg", in: fs) { package, diagnostics in diagnostics.check(diagnostic: "target 'Foo' in package '\(package.packageIdentity)' is outside the package root", severity: .error) } } @@ -1841,7 +1886,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Foo", path: "/foo"), ] ) - PackageBuilderTester(manifest, path: "/pkg", in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, path: "/pkg", in: fs) { _, diagnostics in diagnostics.check(diagnostic: "target path \'/foo\' is not supported; it should be relative to package root", severity: .error) } } @@ -1858,12 +1903,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Foo", path: "~/foo"), ] ) - PackageBuilderTester(manifest, path: "/pkg", in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, path: "/pkg", in: fs) { _, diagnostics in diagnostics.check(diagnostic: "target path \'~/foo\' is not supported; it should be relative to package root", severity: .error) } }*/ } + @Test func testExecutableAsADep() throws { // Executable as dependency. let fs = InMemoryFileSystem(emptyFiles: @@ -1877,13 +1923,13 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "exec"), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("exec") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("exec") { module in module.check(c99name: "exec", type: .executable) module.checkSources(root: "/Sources/exec", paths: "main.swift") } - package.checkModule("lib") { module in + try package.checkModule("lib") { module in module.check(c99name: "lib", type: .library) module.checkSources(root: "/Sources/lib", paths: "lib.swift") } @@ -1892,7 +1938,8 @@ final class PackageBuilderTests: XCTestCase { } } - func testInvalidManifestConfigForNonSystemModules() { + @Test + func testInvalidManifestConfigForNonSystemModules() throws { var fs = InMemoryFileSystem(emptyFiles: "/Sources/main.swift" ) @@ -1902,7 +1949,7 @@ final class PackageBuilderTests: XCTestCase { pkgConfig: "foo" ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "configuration of package '\(package.packageIdentity)' is invalid; the 'pkgConfig' property can only be used with a System Module Package", severity: .error) @@ -1916,26 +1963,28 @@ final class PackageBuilderTests: XCTestCase { providers: [.brew(["foo"])] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "configuration of package '\(package.packageIdentity)' is invalid; the 'providers' property can only be used with a System Module Package", severity: .error) } } + @Test func testResolvesSystemModulePackage() throws { let fs = InMemoryFileSystem(emptyFiles: "/module.modulemap") let manifest = Manifest.createRootManifest(displayName: "SystemModulePackage") - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("SystemModulePackage") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("SystemModulePackage") { module in module.check(c99name: "SystemModulePackage", type: .systemModule) module.checkSources(root: "/") } } } + @Test func testCompatibleSwiftVersions() throws { // Single swift executable target. let fs = InMemoryFileSystem(emptyFiles: @@ -1954,55 +2003,55 @@ final class PackageBuilderTests: XCTestCase { var manifest = try createManifest(swiftVersions: [.v3, .v4]) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(swiftVersion: "4") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: [.v3]) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(swiftVersion: "3") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: [.v4]) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(swiftVersion: "4") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: nil) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(swiftVersion: "4") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: [SwiftLanguageVersion(string: "5")!]) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo") { module in module.check(swiftVersion: "5") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: [SwiftLanguageVersion(string: "6")!]) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo") { module in module.check(swiftVersion: "6") } package.checkProduct("foo") { _ in } } manifest = try createManifest(swiftVersions: []) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "package '\(package.packageIdentity)' supported Swift language versions is empty", severity: .error @@ -2010,7 +2059,7 @@ final class PackageBuilderTests: XCTestCase { } manifest = try createManifest(swiftVersions: [SwiftLanguageVersion(string: "7")!]) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in diagnostics.check( diagnostic: "package '\(package.packageIdentity)' requires minimum Swift language version 7 which is not supported by the current tools version (\(ToolsVersion.current))", severity: .error @@ -2018,6 +2067,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testPredefinedTargetSearchError() throws { do { @@ -2034,7 +2084,7 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: .contains("Source files for target Bar should be located under 'Sources/Bar'"), severity: .error) } } @@ -2053,7 +2103,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "FooTests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: .contains("Source files for target BarTests should be located under 'Tests/BarTests'"), severity: .error) } @@ -2065,11 +2115,11 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "FooTests", type: .test), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("BarTests") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("BarTests") { module in module.check(c99name: "BarTests", type: .test) } - package.checkModule("FooTests") { module in + try package.checkModule("FooTests") { module in module.check(c99name: "FooTests", type: .test) } package.checkProduct("pkgPackageTests") { _ in } @@ -2077,6 +2127,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testSpecifiedCustomPathDoesNotExist() throws { let fs = InMemoryFileSystem(emptyFiles: "/Foo.swift") @@ -2087,11 +2138,12 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check(diagnostic: "invalid custom path './NotExist' for target 'Foo'", severity: .error) } } + @Test func testSpecialTargetDir() throws { let src: AbsolutePath = "/src" // Special directory should be src because both target and test target are under it. @@ -2108,13 +2160,13 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkPredefinedPaths(target: src, testTarget: src) - package.checkModule("A") { module in + try package.checkModule("A") { module in module.check(c99name: "A", type: .library) } - package.checkModule("ATests") { module in + try package.checkModule("ATests") { module in module.check(c99name: "ATests", type: .test) } @@ -2122,6 +2174,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testExcludes() throws { // The exclude should win if a file is in exclude as well as sources. let fs = InMemoryFileSystem(emptyFiles: @@ -2139,14 +2192,15 @@ final class PackageBuilderTests: XCTestCase { ), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("bar") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("bar") { module in module.check(c99name: "bar", type: .library) module.checkSources(root: "/Sources/bar", paths: "bar.swift") } } } + @Test func testDuplicateProducts() throws { // Check that declaring executable product doesn't collide with the // inferred products. @@ -2166,8 +2220,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "foo"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo") { _ in } package.checkProduct("foo") { product in product.check(type: .library(.automatic), targets: ["foo"]) } @@ -2185,6 +2239,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testSystemPackageDeclaresTargetsDiagnostic() throws { let fs = InMemoryFileSystem(emptyFiles: "/module.modulemap", @@ -2199,8 +2254,8 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "bar"), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("SystemModulePackage") { module in + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("SystemModulePackage") { module in module.check(c99name: "SystemModulePackage", type: .systemModule) module.checkSources(root: "/") } @@ -2211,6 +2266,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testSystemLibraryTarget() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/module.modulemap", @@ -2227,12 +2283,12 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "bar", dependencies: ["foo"]), ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { module in module.check(c99name: "foo", type: .systemModule) module.checkSources(root: "/Sources/foo") } - package.checkModule("bar") { module in + try package.checkModule("bar") { module in module.check(c99name: "bar", type: .library) module.checkSources(root: "/Sources/bar", paths: "bar.swift") module.check(targetDependencies: ["foo"]) @@ -2243,6 +2299,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testSystemLibraryTargetDiagnostics() throws { let Sources: AbsolutePath = "/Sources" @@ -2261,9 +2318,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "bar", dependencies: ["foo"]), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo") { _ in } - package.checkModule("bar") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo") { _ in } + try package.checkModule("bar") { _ in } diagnostics.check( diagnostic: "system library product foo shouldn't have a type and contain only one target", severity: .error @@ -2280,9 +2337,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "bar", dependencies: ["foo"]), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo") { _ in } - package.checkModule("bar") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo") { _ in } + try package.checkModule("bar") { _ in } diagnostics.check( diagnostic: "system library product foo shouldn't have a type and contain only one target", severity: .error @@ -2298,7 +2355,7 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "bar", type: .system) ] ) - PackageBuilderTester(manifest, in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, in: fs) { _, diagnostics in diagnostics.check( diagnostic: "package has unsupported layout; missing system target module map at '\(Sources.appending(components: "bar", "module.modulemap"))'", severity: .error @@ -2306,6 +2363,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testBadExecutableProductDecl() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo1/main.swift", @@ -2331,12 +2389,12 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Plugin1", type: .plugin, pluginCapability: .buildTool), ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("foo1") { _ in } - package.checkModule("foo2") { _ in } - package.checkModule("FooLib1") { _ in } - package.checkModule("FooLib2") { _ in } - package.checkModule("Plugin1") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("foo1") { _ in } + try package.checkModule("foo2") { _ in } + try package.checkModule("FooLib1") { _ in } + try package.checkModule("FooLib2") { _ in } + try package.checkModule("Plugin1") { _ in } diagnostics.check( diagnostic: """ executable product 'foo1' expects target 'FooLib1' to be executable; an executable target requires \ @@ -2362,6 +2420,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testLibraryProductDiagnostics() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/MyLibrary/library.swift", @@ -2378,9 +2437,9 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "MyPlugin", type: .plugin, pluginCapability: .buildTool) ] ) - PackageBuilderTester(manifest, in: fs) { package, diagnostics in - package.checkModule("MyLibrary") { _ in } - package.checkModule("MyPlugin") { _ in } + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("MyLibrary") { _ in } + try package.checkModule("MyPlugin") { _ in } diagnostics.check( diagnostic: """ library product 'MyLibrary' should not contain plugin targets (it has 'MyPlugin') @@ -2391,6 +2450,7 @@ final class PackageBuilderTests: XCTestCase { } + @Test func testBadREPLPackage() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/exe/main.swift" @@ -2403,8 +2463,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, createREPLProduct: true, in: fs) { package, diagnostics in - package.checkModule("exe") { _ in } + try PackageBuilderTester(manifest, createREPLProduct: true, in: fs) { package, diagnostics in + try package.checkModule("exe") { _ in } package.checkProduct("exe") { _ in } diagnostics.check( diagnostic: "unable to synthesize a REPL product as there are no library targets in the package", @@ -2413,6 +2473,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testAsmIsIgnoredInV4_2Manifest() throws { // .s is not considered a valid source in 4.2 manifest. let fs = InMemoryFileSystem(emptyFiles: @@ -2430,13 +2491,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { module in module.checkSources(root: "/Sources/lib", paths: "lib.c") } } } + @Test func testAsmInV5Manifest() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/lib/lib.s", @@ -2445,7 +2507,6 @@ final class PackageBuilderTests: XCTestCase { "/Sources/lib/include/lib.h" ) - //let observability = ObservabilitySystem.makeForTesting() let manifest = Manifest.createRootManifest( displayName: "Pkg", toolsVersion: .v5, @@ -2453,15 +2514,15 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "lib", dependencies: []), ] ) - //XCTAssertNoDiagnostics(observability.diagnostics) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { module in module.checkSources(root: "/Sources/lib", paths: "lib.c", "lib.s", "lib2.S") } } } + @Test func testUnknownSourceFilesUnderDeclaredSourcesIgnoredInV5_2Manifest() throws { let lib: AbsolutePath = "/Sources/lib" @@ -2480,8 +2541,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { module in module.checkSources(root: lib.pathString, paths: "lib.c") module.check(includeDir: lib.appending(components: "include").pathString) module.check(moduleMapType: .umbrellaHeader(lib.appending(components: "include", "lib.h"))) @@ -2489,6 +2550,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testUnknownSourceFilesUnderDeclaredSourcesCompiledInV5_3Manifest() throws { let lib: AbsolutePath = "/Sources/lib" @@ -2507,8 +2569,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("lib") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("lib") { module in module.checkSources(root: lib.pathString, paths: "movie.mkv", "lib.c") module.check(includeDir: lib.appending(components: "include").pathString) module.check(moduleMapType: .umbrellaHeader(lib.appending(components: "include", "lib.h"))) @@ -2516,6 +2578,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testBuildSettings() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/exe/main.swift", @@ -2564,63 +2627,63 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("cbar") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("cbar") { package in let scope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(scope.evaluate(.GCC_PREPROCESSOR_DEFINITIONS), ["CCC=2", "CXX"]) - XCTAssertEqual(scope.evaluate(.HEADER_SEARCH_PATHS), ["Sources/headers", "Sources/cppheaders"]) - XCTAssertEqual(scope.evaluate(.OTHER_CFLAGS), ["-Icfoo", "-L", "cbar"]) - XCTAssertEqual(scope.evaluate(.OTHER_CPLUSPLUSFLAGS), ["-Icxxfoo", "-L", "cxxbar"]) + #expect(scope.evaluate(.GCC_PREPROCESSOR_DEFINITIONS) == ["CCC=2", "CXX"]) + #expect(scope.evaluate(.HEADER_SEARCH_PATHS) == ["Sources/headers", "Sources/cppheaders"]) + #expect(scope.evaluate(.OTHER_CFLAGS) == ["-Icfoo", "-L", "cbar"]) + #expect(scope.evaluate(.OTHER_CPLUSPLUSFLAGS) == ["-Icxxfoo", "-L", "cxxbar"]) let releaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual(releaseScope.evaluate(.GCC_PREPROCESSOR_DEFINITIONS), ["CCC=2", "CXX", "RCXX"]) + #expect(releaseScope.evaluate(.GCC_PREPROCESSOR_DEFINITIONS) == ["CCC=2", "CXX", "RCXX"]) } - package.checkModule("bar") { package in + try package.checkModule("bar") { package in let scope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .debug) ) - XCTAssertEqual(scope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS), ["SOMETHING", "LINUX"]) - XCTAssertEqual(scope.evaluate(.OTHER_SWIFT_FLAGS), ["-Isfoo", "-L", "sbar"]) + #expect(scope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS) == ["SOMETHING", "LINUX"]) + #expect(scope.evaluate(.OTHER_SWIFT_FLAGS) == ["-Isfoo", "-L", "sbar"]) let rscope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .release) ) - XCTAssertEqual(rscope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS), ["SOMETHING", "LINUX", "RLINUX"]) + #expect(rscope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS) == ["SOMETHING", "LINUX", "RLINUX"]) let mscope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(mscope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS), ["SOMETHING", "DMACOS"]) + #expect(mscope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS) == ["SOMETHING", "DMACOS"]) } - package.checkModule("exe") { package in + try package.checkModule("exe") { package in let scope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .debug) ) - XCTAssertEqual(scope.evaluate(.LINK_LIBRARIES), ["sqlite3"]) - XCTAssertEqual(scope.evaluate(.OTHER_LDFLAGS), ["-Ilfoo", "-L", "lbar"]) - XCTAssertEqual(scope.evaluate(.LINK_FRAMEWORKS), []) - XCTAssertEqual(scope.evaluate(.OTHER_SWIFT_FLAGS), []) - XCTAssertEqual(scope.evaluate(.OTHER_CFLAGS), []) - XCTAssertEqual(scope.evaluate(.OTHER_CPLUSPLUSFLAGS), []) + #expect(scope.evaluate(.LINK_LIBRARIES) == ["sqlite3"]) + #expect(scope.evaluate(.OTHER_LDFLAGS) == ["-Ilfoo", "-L", "lbar"]) + #expect(scope.evaluate(.LINK_FRAMEWORKS) == []) + #expect(scope.evaluate(.OTHER_SWIFT_FLAGS) == []) + #expect(scope.evaluate(.OTHER_CFLAGS) == []) + #expect(scope.evaluate(.OTHER_CPLUSPLUSFLAGS) == []) let mscope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .iOS, configuration: .debug) ) - XCTAssertEqual(mscope.evaluate(.LINK_LIBRARIES), ["sqlite3"]) - XCTAssertEqual(mscope.evaluate(.LINK_FRAMEWORKS), ["CoreData"]) + #expect(mscope.evaluate(.LINK_LIBRARIES) == ["sqlite3"]) + #expect(mscope.evaluate(.LINK_FRAMEWORKS) == ["CoreData"]) } @@ -2628,6 +2691,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testEmptyUnsafeFlagsAreAllowed() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/foo.swift", @@ -2660,50 +2724,51 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(macosDebugScope.evaluate(.OTHER_CFLAGS), []) - XCTAssertEqual(macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS), []) - XCTAssertEqual(macosDebugScope.evaluate(.OTHER_LDFLAGS), []) + #expect(macosDebugScope.evaluate(.OTHER_CFLAGS) == []) + #expect(macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == []) + #expect(macosDebugScope.evaluate(.OTHER_LDFLAGS) == []) let macosReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual(macosReleaseScope.evaluate(.OTHER_CFLAGS), []) - XCTAssertEqual(macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS), []) - XCTAssertEqual(macosReleaseScope.evaluate(.OTHER_LDFLAGS), []) + #expect(macosReleaseScope.evaluate(.OTHER_CFLAGS) == []) + #expect(macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == []) + #expect(macosReleaseScope.evaluate(.OTHER_LDFLAGS) == []) } - package.checkModule("bar") { package in + try package.checkModule("bar") { package in let linuxDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .debug) ) - XCTAssertEqual(linuxDebugScope.evaluate(.OTHER_SWIFT_FLAGS), []) - XCTAssertEqual(linuxDebugScope.evaluate(.OTHER_LDFLAGS), []) + #expect(linuxDebugScope.evaluate(.OTHER_SWIFT_FLAGS) == []) + #expect(linuxDebugScope.evaluate(.OTHER_LDFLAGS) == []) let linuxReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .release) ) - XCTAssertEqual(linuxReleaseScope.evaluate(.OTHER_SWIFT_FLAGS), []) - XCTAssertEqual(linuxReleaseScope.evaluate(.OTHER_LDFLAGS), []) + #expect(linuxReleaseScope.evaluate(.OTHER_SWIFT_FLAGS) == []) + #expect(linuxReleaseScope.evaluate(.OTHER_LDFLAGS) == []) let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS), []) - XCTAssertEqual(macosDebugScope.evaluate(.OTHER_LDFLAGS), []) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS) == []) + #expect(macosDebugScope.evaluate(.OTHER_LDFLAGS) == []) } } } + @Test func testInvalidHeaderSearchPath() throws { let fs = InMemoryFileSystem(emptyFiles: "/pkg/Sources/exe/main.swift" @@ -2722,7 +2787,7 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest1, path: "/pkg", in: fs) { package, diagnostics in + try PackageBuilderTester(manifest1, path: "/pkg", in: fs) { package, diagnostics in diagnostics.check(diagnostic: "invalid relative path '/Sources/headers'; relative path should not begin with '\(AbsolutePath.root)'", severity: .error) } @@ -2739,11 +2804,12 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest2, path: "/pkg", in: fs) { _, diagnostics in + try PackageBuilderTester(manifest2, path: "/pkg", in: fs) { _, diagnostics in diagnostics.check(diagnostic: "invalid header search path '../../..'; header search path should not be outside the package root", severity: .error) } } + @Test func testDuplicateTargetDependencies() throws { let fs = InMemoryFileSystem(emptyFiles: "/Foo/Sources/Foo/foo.swift", @@ -2792,11 +2858,11 @@ final class PackageBuilderTests: XCTestCase { traits: [] ) - PackageBuilderTester(manifest, path: "/Foo", in: fs) { package, diagnostics in - package.checkModule("Foo") - package.checkModule("Foo2") - package.checkModule("Foo3") - package.checkModule("Qux") + try PackageBuilderTester(manifest, path: "/Foo", in: fs) { package, diagnostics in + try package.checkModule("Foo") + try package.checkModule("Foo2") + try package.checkModule("Foo3") + try package.checkModule("Qux") diagnostics.checkUnordered( diagnostic: "invalid duplicate target dependency declaration 'Bar' in target 'Foo' from package '\(package.packageIdentity)'", severity: .warning @@ -2816,6 +2882,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testConditionalDependencies() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/Foo/main.swift", @@ -2853,11 +2920,11 @@ final class PackageBuilderTests: XCTestCase { traits: [] ) - PackageBuilderTester(manifest, in: fs) { package, _ in + try PackageBuilderTester(manifest, in: fs) { package, _ in package.checkProduct("Foo") - package.checkModule("Bar") - package.checkModule("Baz") - package.checkModule("Foo") { target in + try package.checkModule("Bar") + try package.checkModule("Baz") + try package.checkModule("Foo") { target in target.check(dependencies: ["Bar", "Baz", "Biz"]) target.checkDependency("Bar") { result in @@ -2882,6 +2949,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testMissingDefaultLocalization() throws { let fs = InMemoryFileSystem(emptyFiles: "/Foo/Sources/Foo/foo.swift", @@ -2898,11 +2966,12 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: "/Foo", in: fs) { _, diagnostics in + try PackageBuilderTester(manifest, path: "/Foo", in: fs) { _, diagnostics in diagnostics.check(diagnostic: "manifest property 'defaultLocalization' not set; it is required in the presence of localized resources", severity: .error) } } + @Test func testXcodeResources5_4AndEarlier() throws { // In SwiftTools 5.4 and earlier, supported xcbuild file types are supported by default. // Of course, modern file types such as xcstrings won't be supported here because those require a newer Swift tools version in general. @@ -2926,8 +2995,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: root, in: fs) { result, diagnostics in - result.checkModule("Foo") { result in + try PackageBuilderTester(manifest, path: root, in: fs) { result, diagnostics in + try result.checkModule("Foo") { result in result.checkSources(sources: ["foo.swift"]) result.checkResources(resources: [ foo.appending(components: "Foo.xib").pathString, @@ -2939,6 +3008,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testXcodeResources5_5AndLater() throws { // In SwiftTools 5.5 and later, xcbuild file types are only supported when explicitly passed via additionalFileRules. @@ -2962,8 +3032,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: root, supportXCBuildTypes: true, in: fs) { result, diagnostics in - result.checkModule("Foo") { result in + try PackageBuilderTester(manifest, path: root, supportXCBuildTypes: true, in: fs) { result, diagnostics in + try result.checkModule("Foo") { result in result.checkSources(sources: ["foo.swift"]) result.checkResources(resources: [ foo.appending(components: "Foo.xib").pathString, @@ -2976,6 +3046,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testXcodeResources6_0AndLater() throws { // In SwiftTools 6.0 and later, xcprivacy file types are only supported when explicitly passed via additionalFileRules. @@ -3000,8 +3071,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: root, supportXCBuildTypes: true, in: fs) { result, diagnostics in - result.checkModule("Foo") { result in + try PackageBuilderTester(manifest, path: root, supportXCBuildTypes: true, in: fs) { result, diagnostics in + try result.checkModule("Foo") { result in result.checkSources(sources: ["foo.swift"]) result.checkResources(resources: [ foo.appending(components: "Foo.xib").pathString, @@ -3015,6 +3086,7 @@ final class PackageBuilderTests: XCTestCase { } } + @Test func testXCPrivacyNoDiagnostics() throws { // In SwiftTools 6.0 and later, xcprivacy file types should not produce diagnostics messages when included // as resources and built with `swift build`. @@ -3038,8 +3110,8 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, path: root, supportXCBuildTypes: false, in: fs) { result, diagnostics in - result.checkModule("Foo") { result in + try PackageBuilderTester(manifest, path: root, supportXCBuildTypes: false, in: fs) { result, diagnostics in + try result.checkModule("Foo") { result in result.checkSources(sources: ["foo.swift"]) result.checkResources(resources: [ foo.appending(components: "PrivacyInfo.xcprivacy").pathString, @@ -3050,6 +3122,11 @@ final class PackageBuilderTests: XCTestCase { } } + @Test( + .tags( + .Feature.Snippets, + ), + ) func testSnippetsLinkProductLibraries() throws { let root = AbsolutePath("/Foo") let internalSourcesDir = root.appending(components: "Sources", "Internal") @@ -3070,26 +3147,27 @@ final class PackageBuilderTests: XCTestCase { try TargetDescription(name: "Product"), ]) - PackageBuilderTester(manifest, path: root, in: fs) { result, diagnostics in + try PackageBuilderTester(manifest, path: root, in: fs) { result, diagnostics in result.checkProduct("Product") { product in product.check(type: .library(.automatic), targets: ["Product"]) } result.checkProduct("ASnippet") { aSnippet in aSnippet.check(type: .snippet, targets: ["ASnippet"]) } - result.checkModule("Internal") { foo in + try result.checkModule("Internal") { foo in foo.checkSources(sources: ["Internal.swift"]) } - result.checkModule("Product") { foo in + try result.checkModule("Product") { foo in foo.checkSources(sources: ["Product.swift"]) } - result.checkModule("ASnippet") { aSnippet in + try result.checkModule("ASnippet") { aSnippet in aSnippet.checkSources(sources: ["ASnippet.swift"]) aSnippet.check(targetDependencies: ["Product"]) } } } + @Test func testCustomPlatformInConditionals() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/Foo/Best.swift") @@ -3117,14 +3195,15 @@ final class PackageBuilderTests: XCTestCase { settings.add(assignment, for: .SWIFT_ACTIVE_COMPILATION_CONDITIONS) settings.add(versionAssignment, for: .SWIFT_VERSION) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("Foo") { module in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("Foo") { module in module.check(c99name: "Foo", type: .library) module.check(buildSettings: settings) } } } + @Test func testSwiftLanguageVersionPerTarget() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/foo.swift", @@ -3151,43 +3230,44 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(macosDebugScope.evaluate(.SWIFT_VERSION), ["5"]) + #expect(macosDebugScope.evaluate(.SWIFT_VERSION) == ["5"]) let macosReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual(macosReleaseScope.evaluate(.SWIFT_VERSION), ["5"]) + #expect(macosReleaseScope.evaluate(.SWIFT_VERSION) == ["5"]) } - package.checkModule("bar") { package in + try package.checkModule("bar") { package in let linuxDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .debug) ) - XCTAssertEqual(linuxDebugScope.evaluate(.SWIFT_VERSION), ["3"]) + #expect(linuxDebugScope.evaluate(.SWIFT_VERSION) == ["3"]) let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual(macosDebugScope.evaluate(.SWIFT_VERSION), ["4"]) + #expect(macosDebugScope.evaluate(.SWIFT_VERSION) == ["4"]) let macosReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual(macosReleaseScope.evaluate(.SWIFT_VERSION), ["5"]) + #expect(macosReleaseScope.evaluate(.SWIFT_VERSION) == ["5"]) } } } + @Test func testSwiftWarningControlFlags() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/foo/foo.swift" @@ -3209,14 +3289,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("foo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("foo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual( - macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS), + #expect( + macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS) == ["-no-warnings-as-errors", "-Wwarning", "DeprecatedDeclaration"] ) @@ -3224,14 +3304,15 @@ final class PackageBuilderTests: XCTestCase { package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual( - macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS), + #expect( + macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS) == ["-warnings-as-errors", "-Werror", "DeprecatedDeclaration"] ) } } } + @Test func testCWarningControlFlags() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/cfoo/foo.c", @@ -3254,14 +3335,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("cfoo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("cfoo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual( - macosDebugScope.evaluate(.OTHER_CFLAGS), + #expect( + macosDebugScope.evaluate(.OTHER_CFLAGS) == ["-Wno-error", "-Wno-error=deprecated-declarations"] ) @@ -3269,14 +3350,15 @@ final class PackageBuilderTests: XCTestCase { package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual( - macosReleaseScope.evaluate(.OTHER_CFLAGS), + #expect( + macosReleaseScope.evaluate(.OTHER_CFLAGS) == ["-Werror", "-Werror=deprecated-declarations"] ) } } } + @Test func testCXXWarningControlFlags() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/cxxfoo/foo.cpp", @@ -3299,14 +3381,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("cxxfoo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("cxxfoo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual( - macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS), + #expect( + macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == ["-Wno-error", "-Wno-error=deprecated-declarations"] ) @@ -3314,14 +3396,15 @@ final class PackageBuilderTests: XCTestCase { package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual( - macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS), + #expect( + macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == ["-Werror", "-Werror=deprecated-declarations"] ) } } } + @Test func testCWarningEnableDisable() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/cfoo/foo.c", @@ -3342,14 +3425,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("cfoo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("cfoo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual( - macosDebugScope.evaluate(.OTHER_CFLAGS), + #expect( + macosDebugScope.evaluate(.OTHER_CFLAGS) == ["-Wimplicit-fallthrough"] ) @@ -3357,14 +3440,15 @@ final class PackageBuilderTests: XCTestCase { package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual( - macosReleaseScope.evaluate(.OTHER_CFLAGS), + #expect( + macosReleaseScope.evaluate(.OTHER_CFLAGS) == ["-Wno-unused-parameter"] ) } } } + @Test func testCXXWarningEnableDisable() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/cxxfoo/foo.cpp", @@ -3385,14 +3469,14 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("cxxfoo") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("cxxfoo") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertEqual( - macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS), + #expect( + macosDebugScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == ["-Wimplicit-fallthrough"] ) @@ -3400,14 +3484,15 @@ final class PackageBuilderTests: XCTestCase { package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertEqual( - macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS), + #expect( + macosReleaseScope.evaluate(.OTHER_CPLUSPLUSFLAGS) == ["-Wno-unused-parameter"] ) } } } + @Test func testDefaultIsolationPerTarget() throws { let fs = InMemoryFileSystem(emptyFiles: "/Sources/A/a.swift", @@ -3434,45 +3519,45 @@ final class PackageBuilderTests: XCTestCase { ] ) - PackageBuilderTester(manifest, in: fs) { package, _ in - package.checkModule("A") { package in + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("A") { package in let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertMatch(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS), - [.anySequence, "-default-isolation", "MainActor", .anySequence]) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) let macosReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertMatch(macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS), - [.anySequence, "-default-isolation", "MainActor", .anySequence]) + #expect(macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) } - package.checkModule("B") { package in + try package.checkModule("B") { package in let linuxDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .linux, configuration: .debug) ) - XCTAssertMatch(linuxDebugScope.evaluate(.OTHER_SWIFT_FLAGS), - [.anySequence, "-default-isolation", "nonisolated", .anySequence]) + #expect(linuxDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(linuxDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("nonisolated")) let macosDebugScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .debug) ) - XCTAssertMatch(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS), - [.anySequence, "-default-isolation", "MainActor", .anySequence]) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) let macosReleaseScope = BuildSettings.Scope( package.target.buildSettings, environment: BuildEnvironment(platform: .macOS, configuration: .release) ) - XCTAssertNoMatch(macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS), - [.anySequence, "-default-isolation", "MainActor", .anySequence]) + #expect(!macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation") || + !macosReleaseScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) } } @@ -3507,10 +3592,9 @@ final class PackageBuilderTester { createREPLProduct: Bool = false, supportXCBuildTypes: Bool = false, in fs: FileSystem, - file: StaticString = #file, - line: UInt = #line, - _ body: (PackageBuilderTester, DiagnosticsTestResult) -> Void - ) { + sourceLocation: SourceLocation = #_sourceLocation, + _ body: (PackageBuilderTester, DiagnosticsTestResult) throws -> Void + ) throws { self.packageIdentity = PackageIdentity(urlString: manifest.packageLocation) let observability = ObservabilitySystem.makeForTesting() do { @@ -3540,49 +3624,54 @@ final class PackageBuilderTester { observability.topScope.emit(error) } - testDiagnostics(observability.diagnostics, file: file, line: line) { diagnostics in - body(self, diagnostics) + try expectDiagnostics(observability.diagnostics, sourceLocation: sourceLocation) { diagnostics in + try body(self, diagnostics) } - validateCheckedModules(file: file, line: line) + validateCheckedModules(sourceLocation: sourceLocation) } - private func validateCheckedModules(file: StaticString, line: UInt) { + private func validateCheckedModules(sourceLocation: SourceLocation = #_sourceLocation) { if !uncheckedModules.isEmpty { - XCTFail("Unchecked targets: \(uncheckedModules)", file: file, line: line) + Issue.record("Unchecked targets: \(uncheckedModules)", sourceLocation: sourceLocation) } if !uncheckedProducts.isEmpty { - XCTFail("Unchecked products: \(uncheckedProducts)", file: file, line: line) + Issue.record("Unchecked products: \(uncheckedProducts)", sourceLocation: sourceLocation) } } - func checkPredefinedPaths(target: AbsolutePath, testTarget: AbsolutePath, file: StaticString = #file, line: UInt = #line) { + func checkPredefinedPaths(target: AbsolutePath, testTarget: AbsolutePath, sourceLocation: SourceLocation = #_sourceLocation) { guard case .package(let package) = result else { - return XCTFail("Expected package did not load \(self)", file: file, line: line) + Issue.record("Expected package did not load \(self)", sourceLocation: sourceLocation) + return } - XCTAssertEqual(target, package.targetSearchPath, file: file, line: line) - XCTAssertEqual(testTarget, package.testTargetSearchPath, file: file, line: line) + #expect(target == package.targetSearchPath, sourceLocation: sourceLocation) + #expect(testTarget == package.testTargetSearchPath, sourceLocation: sourceLocation) } - func checkModule(_ name: String, file: StaticString = #file, line: UInt = #line, _ body: ((ModuleResult) -> Void)? = nil) { + func checkModule(_ name: String, sourceLocation: SourceLocation = #_sourceLocation, _ body: ((ModuleResult) throws -> Void)? = nil) throws { guard case .package(let package) = result else { - return XCTFail("Expected package did not load \(self)", file: file, line: line) + Issue.record("Expected package did not load \(self)", sourceLocation: sourceLocation) + return } guard let target = package.modules.first(where: {$0.name == name}) else { - return XCTFail("Module: \(name) not found", file: file, line: line) + Issue.record("Module: \(name) not found", sourceLocation: sourceLocation) + return } uncheckedModules.remove(target) - body?(ModuleResult(target)) + try body?(ModuleResult(target)) } - func checkProduct(_ name: String, file: StaticString = #file, line: UInt = #line, _ body: ((ProductResult) -> Void)? = nil) { + func checkProduct(_ name: String, sourceLocation: SourceLocation = #_sourceLocation, _ body: ((ProductResult) -> Void)? = nil) { guard case .package(let package) = result else { - return XCTFail("Expected package did not load \(self)", file: file, line: line) + Issue.record("Expected package did not load \(self)", sourceLocation: sourceLocation) + return } let foundProducts = package.products.filter{$0.name == name} guard foundProducts.count == 1 else { - return XCTFail("Couldn't get the product: \(name). Found products \(foundProducts)", file: file, line: line) + Issue.record("Couldn't get the product: \(name). Found products \(foundProducts)", sourceLocation: sourceLocation) + return } uncheckedProducts.remove(foundProducts[0]) body?(ProductResult(foundProducts[0])) @@ -3595,13 +3684,14 @@ final class PackageBuilderTester { self.product = product } - func check(type: PackageModel.ProductType, targets: [String], file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(product.type, type, file: file, line: line) - XCTAssertEqual(product.modules.map{$0.name}.sorted(), targets.sorted(), file: file, line: line) + func check(type: PackageModel.ProductType, targets: [String], sourceLocation: SourceLocation = #_sourceLocation) { + #expect(product.type == type, sourceLocation: sourceLocation) + #expect(product.modules.map{$0.name}.sorted() == targets.sorted(), sourceLocation: sourceLocation) } - func check(testEntryPointPath: String?, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(product.testEntryPointPath, testEntryPointPath.map({ try! AbsolutePath(validating: $0) }), file: file, line: line) + func check(testEntryPointPath: String?, sourceLocation: SourceLocation = #_sourceLocation) { + let expectedPath = testEntryPointPath.map({ try! AbsolutePath(validating: $0) }) + #expect(product.testEntryPointPath == expectedPath, sourceLocation: sourceLocation) } } @@ -3612,106 +3702,113 @@ final class PackageBuilderTester { self.target = target } - func check(includeDir: String, file: StaticString = #file, line: UInt = #line) { + func check(includeDir: String, sourceLocation: SourceLocation = #_sourceLocation) { guard case let target as ClangModule = target else { - return XCTFail("Include directory is being checked on a non clang target", file: file, line: line) + Issue.record("Include directory is being checked on a non clang target", sourceLocation: sourceLocation) + return } - XCTAssertEqual(target.includeDir.pathString, includeDir, file: file, line: line) + #expect(target.includeDir.pathString == includeDir, sourceLocation: sourceLocation) } - func check(moduleMapType: ModuleMapType, file: StaticString = #file, line: UInt = #line) { + func check(moduleMapType: ModuleMapType, sourceLocation: SourceLocation = #_sourceLocation) { guard case let target as ClangModule = target else { - return XCTFail("Module map type is being checked on a non-Clang target", file: file, line: line) + Issue.record("Module map type is being checked on a non-Clang target", sourceLocation: sourceLocation) + return } - XCTAssertEqual(target.moduleMapType, moduleMapType, file: file, line: line) + #expect(target.moduleMapType == moduleMapType, sourceLocation: sourceLocation) } - func check(c99name: String? = nil, type: PackageModel.Module.Kind? = nil, file: StaticString = #file, line: UInt = #line) { + func check(c99name: String? = nil, type: PackageModel.Module.Kind? = nil, sourceLocation: SourceLocation = #_sourceLocation) { if let c99name { - XCTAssertEqual(target.c99name, c99name, file: file, line: line) + #expect(target.c99name == c99name, sourceLocation: sourceLocation) } if let type { - XCTAssertEqual(target.type, type, file: file, line: line) + #expect(target.type == type, sourceLocation: sourceLocation) } } - func checkSources(root: String? = nil, sources paths: [String], file: StaticString = #file, line: UInt = #line) { + func checkSources(root: String? = nil, sources paths: [String], sourceLocation: SourceLocation = #_sourceLocation) { if let root { - XCTAssertEqual(target.sources.root, try! AbsolutePath(validating: root), file: file, line: line) + let expectedRoot = try! AbsolutePath(validating: root) + #expect(target.sources.root == expectedRoot, sourceLocation: sourceLocation) } let sources = Set(self.target.sources.relativePaths.map({ $0.pathString })) - XCTAssertEqual(sources, Set(paths), "unexpected source files in \(target.name)", file: file, line: line) + #expect(sources == Set(paths), "unexpected source files in \(target.name)", sourceLocation: sourceLocation) } - func checkSources(root: String? = nil, paths: String..., file: StaticString = #file, line: UInt = #line) { - checkSources(root: root, sources: paths, file: file, line: line) + func checkSources(root: String? = nil, paths: String..., sourceLocation: SourceLocation = #_sourceLocation) { + checkSources(root: root, sources: paths, sourceLocation: sourceLocation) } - func checkResources(resources: [String], file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(Set(resources), Set(self.target.resources.map{ $0.path.pathString }), "unexpected resource files in \(target.name)", file: file, line: line) + func checkResources(resources: [String], sourceLocation: SourceLocation = #_sourceLocation) { + #expect(Set(resources) == Set(self.target.resources.map{ $0.path.pathString }), "unexpected resource files in \(target.name)", sourceLocation: sourceLocation) } - func check(targetDependencies depsToCheck: [String], file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(Set(depsToCheck), Set(target.dependencies.compactMap { $0.module?.name }), "unexpected dependencies in \(target.name)", file: file, line: line) + func check(targetDependencies depsToCheck: [String], sourceLocation: SourceLocation = #_sourceLocation) { + #expect(Set(depsToCheck) == Set(target.dependencies.compactMap { $0.module?.name }), "unexpected dependencies in \(target.name)", sourceLocation: sourceLocation) } func check( productDependencies depsToCheck: [Module.ProductReference], - file: StaticString = #file, - line: UInt = #line + sourceLocation: SourceLocation = #_sourceLocation ) { let productDependencies = target.dependencies.compactMap { $0.product } guard depsToCheck.count == productDependencies.count else { - return XCTFail("Incorrect product dependencies", file: file, line: line) + Issue.record("Incorrect product dependencies", sourceLocation: sourceLocation) + return } for (idx, element) in depsToCheck.enumerated() { let rhs = productDependencies[idx] guard element.name == rhs.name && element.package == rhs.package else { - return XCTFail("Incorrect product dependencies", file: file, line: line) + Issue.record("Incorrect product dependencies", sourceLocation: sourceLocation) + return } } } - func check(dependencies: [String], file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual( - Set(dependencies), - Set(target.dependencies.map({ $0.name })), + func check(dependencies: [String], sourceLocation: SourceLocation = #_sourceLocation) { + #expect( + Set(dependencies) == Set(target.dependencies.map({ $0.name })), "unexpected dependencies in \(target.name)", - file: file, - line: line + sourceLocation: sourceLocation ) } func checkDependency( _ name: String, - file: StaticString = #file, - line: UInt = #line, + sourceLocation: SourceLocation = #_sourceLocation, _ body: (ModuleDependencyResult) -> Void ) { guard let dependency = target.dependencies.first(where: { $0.name == name }) else { - return XCTFail("Module: \(name) not found", file: file, line: line) + Issue.record("Module: \(name) not found", sourceLocation: sourceLocation) + return } body(ModuleDependencyResult(dependency)) } - func check(swiftVersion: String, file: StaticString = #file, line: UInt = #line) { + func check(swiftVersion: String, sourceLocation: SourceLocation = #_sourceLocation) { guard case let swiftTarget as SwiftModule = target else { - return XCTFail("\(target) is not a swift target", file: file, line: line) + Issue.record("\(target) is not a swift target", sourceLocation: sourceLocation) + return } - let versionAssignments = swiftTarget.buildSettings.assignments[.SWIFT_VERSION]? - .filter { $0.conditions.isEmpty }.flatMap(\.values) - XCTAssertNotNil(versionAssignments?.contains(swiftVersion), file: file, line: line) + guard let versionAssignments = swiftTarget.buildSettings.assignments[.SWIFT_VERSION]? + .filter { $0.conditions.isEmpty }.flatMap(\.values) else { + Issue.record("\(target) has no version assignments", sourceLocation: sourceLocation) + return + } + #expect(versionAssignments.contains(swiftVersion) != nil, sourceLocation: sourceLocation) } - func check(pluginCapability: PluginCapability, file: StaticString = #file, line: UInt = #line) { + func check(pluginCapability: PluginCapability, sourceLocation: SourceLocation = #_sourceLocation) { guard case let target as PluginModule = target else { - return XCTFail("Plugin capability is being checked on a target", file: file, line: line) + Issue.record("Plugin capability is being checked on a target", sourceLocation: sourceLocation) + return } - XCTAssertEqual(target.capability, pluginCapability, file: file, line: line) + #expect(target.capability == pluginCapability, sourceLocation: sourceLocation) } - func check(buildSettings: PackageModel.BuildSettings.AssignmentTable, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(target.buildSettings.assignments, buildSettings.assignments, file: file, line: line) + func check(buildSettings: PackageModel.BuildSettings.AssignmentTable, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(target.buildSettings.assignments == buildSettings.assignments, sourceLocation: sourceLocation) } } @@ -3722,16 +3819,15 @@ final class PackageBuilderTester { self.dependency = dependency } - func checkConditions(satisfy environment: BuildEnvironment, file: StaticString = #file, line: UInt = #line) { - XCTAssert(dependency.conditions.allSatisfy { $0.satisfies(environment) }, file: file, line: line) + func checkConditions(satisfy environment: BuildEnvironment, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(dependency.conditions.allSatisfy { $0.satisfies(environment) }, sourceLocation: sourceLocation) } func checkConditions( dontSatisfy environment: BuildEnvironment, - file: StaticString = #file, - line: UInt = #line + sourceLocation: SourceLocation = #_sourceLocation ) { - XCTAssert(!dependency.conditions.allSatisfy { $0.satisfies(environment) }, file: file, line: line) + #expect(!dependency.conditions.allSatisfy { $0.satisfies(environment) }, sourceLocation: sourceLocation) } } } diff --git a/Tests/PackageModelTests/SnippetTests.swift b/Tests/PackageModelTests/SnippetTests.swift index 634fe2e22c4..fa9af97da89 100644 --- a/Tests/PackageModelTests/SnippetTests.swift +++ b/Tests/PackageModelTests/SnippetTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2021 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 @@ -12,26 +12,34 @@ import Basics @testable import PackageModel -import XCTest - -class SnippetTests: XCTestCase { +import Testing + +@Suite( + .tags( + .TestSize.small, + .Feature.Snippets, + ), +) +struct SnippetTests { let fakeSourceFilePath = AbsolutePath("/fake/path/to/test.swift") /// Test the contents of the ``Snippet`` model when parsing an empty file. /// Currently, no errors are emitted and most things are either nil or empty. - func testEmptySourceFile() { + @Test + func testEmptySourceFile() async throws { let source = "" let snippet = Snippet(parsing: source, path: fakeSourceFilePath) - XCTAssertEqual(snippet.path, fakeSourceFilePath) - XCTAssertTrue(snippet.explanation.isEmpty) - XCTAssertTrue(snippet.presentationCode.isEmpty) - XCTAssertNil(snippet.groupName) - XCTAssertEqual("test", snippet.name) + #expect(snippet.path == fakeSourceFilePath) + #expect(snippet.explanation.isEmpty) + #expect(snippet.presentationCode.isEmpty) + #expect(snippet.groupName == nil) + #expect("test" == snippet.name) } /// Test the contents of the ``Snippet`` model when parsing a typical /// source file. - func testBasic() { + @Test + func testBasic() async throws { let explanation = "This snippet does a foo. Try it when XYZ." let presentationCode = """ import Module @@ -52,17 +60,18 @@ class SnippetTests: XCTestCase { let snippet = Snippet(parsing: source, path: fakeSourceFilePath) - XCTAssertEqual(snippet.path, fakeSourceFilePath) - XCTAssertEqual(explanation, snippet.explanation) - XCTAssertEqual(presentationCode, snippet.presentationCode) - XCTAssertNil(snippet.groupName) - XCTAssertEqual("test", snippet.name) + #expect(snippet.path == fakeSourceFilePath) + #expect(explanation == snippet.explanation) + #expect(presentationCode == snippet.presentationCode) + #expect(snippet.groupName == nil) + #expect("test" == snippet.name) } /// Test that multiple consecutive newlines in a snippet's /// presentation code is coalesced into no more than two newlines, /// and test that newlines at the beginning and end of are stripped. - func testMultiNewlineCoalescing() { + @Test + func testMultiNewlineCoalescing() async throws { let explanation = "This snippet does a foo. Try it when XYZ." let presentationCode = """ @@ -97,13 +106,13 @@ class SnippetTests: XCTestCase { """ let snippet = Snippet(parsing: source, path: fakeSourceFilePath) - XCTAssertEqual(explanation, snippet.explanation) - XCTAssertEqual(expectedPresentationCode, snippet.presentationCode) + #expect(explanation == snippet.explanation) + #expect(expectedPresentationCode == snippet.presentationCode) } /// Test that toggling back and forth with `mark: hide` and `mark: show` /// works as intended. - func testMarkHideShowToggle() { + func testMarkHideShowToggle() async throws { let source = """ shown1 @@ -129,14 +138,15 @@ class SnippetTests: XCTestCase { """ let snippet = Snippet(parsing: source, path: fakeSourceFilePath) - XCTAssertFalse(snippet.presentationCode.contains("hidden")) - XCTAssertFalse(snippet.explanation.contains("hidden")) - XCTAssertEqual(expectedPresentationCode, snippet.presentationCode) + #expect(!snippet.presentationCode.contains("hidden")) + #expect(!snippet.explanation.contains("hidden")) + #expect(expectedPresentationCode == snippet.presentationCode) } /// Tests that extra indentation is removed when extracting some inner /// part of nested code. - func testRemoveExtraIndentation() { + @Test + func testRemoveExtraIndentation() async throws { let source = """ // mark: hide struct Outer { @@ -154,6 +164,6 @@ class SnippetTests: XCTestCase { } """ let snippet = Snippet(parsing: source, path: fakeSourceFilePath) - XCTAssertEqual(expectedPresentationCode, snippet.presentationCode) + #expect(expectedPresentationCode == snippet.presentationCode) } } diff --git a/Tests/_InternalTestSupportTests/FileSystemHelpersTests.swift b/Tests/_InternalTestSupportTests/FileSystemHelpersTests.swift new file mode 100644 index 00000000000..754e0c9f2e0 --- /dev/null +++ b/Tests/_InternalTestSupportTests/FileSystemHelpersTests.swift @@ -0,0 +1,622 @@ +//===----------------------------------------------------------------------===// +// +// 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 Basics +import _InternalTestSupport +import Testing + +@Suite( + .tags( + .TestSize.small, + ), +) +struct FileSystemHelpersTests { + @Test + func testGetFilesAbsolutePathRecursive() throws { + // Create an in-memory file system for testing + let fileSystem = InMemoryFileSystem() + + // Create a test directory structure + let testDir = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(testDir, recursive: true) + + // Create some test files + let swiftFile1 = testDir.appending("file1.swift") + let swiftFile2 = testDir.appending("subdir").appending("file2.swift") + let txtFile = testDir.appending("readme.txt") + let swiftFile3 = testDir.appending("subdir").appending("nested").appending("file3.swift") + + try fileSystem.createDirectory(swiftFile2.parentDirectory, recursive: true) + try fileSystem.createDirectory(swiftFile3.parentDirectory, recursive: true) + + try fileSystem.writeFileContents(swiftFile1, string: "// Swift file 1") + try fileSystem.writeFileContents(swiftFile2, string: "// Swift file 2") + try fileSystem.writeFileContents(txtFile, string: "This is a text file") + try fileSystem.writeFileContents(swiftFile3, string: "// Swift file 3") + + // Test recursive search (default) + let swiftFiles = try getFiles( + in: testDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + + // Verify results + #expect(swiftFiles.count == 3) + #expect(swiftFiles.contains(swiftFile1)) + #expect(swiftFiles.contains(swiftFile2)) + #expect(swiftFiles.contains(swiftFile3)) + #expect(!swiftFiles.contains(txtFile)) + + // Test with different extension + let txtFiles = try getFiles( + in: testDir, + matchingExtension: "txt", + fileSystem: fileSystem + ) + + #expect(txtFiles.count == 1) + #expect(txtFiles.contains(txtFile)) + } + + @Test + func testGetFilesAbsolutePathNonRecursive() throws { + let fileSystem = InMemoryFileSystem() + + let testDir = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(testDir, recursive: true) + + // Create files at different levels + let swiftFile1 = testDir.appending("file1.swift") + let swiftFile2 = testDir.appending("subdir").appending("file2.swift") + + try fileSystem.createDirectory(swiftFile2.parentDirectory, recursive: true) + try fileSystem.writeFileContents(swiftFile1, string: "// Swift file 1") + try fileSystem.writeFileContents(swiftFile2, string: "// Swift file 2") + + // Test non-recursive search + let swiftFiles = try getFiles( + in: testDir, + matchingExtension: "swift", + recursive: false, + fileSystem: fileSystem + ) + + // Should only find the top-level file + #expect(swiftFiles.count == 1) + #expect(swiftFiles.contains(swiftFile1)) + #expect(!swiftFiles.contains(swiftFile2)) + } + + @Test + func testGetFilesWithCaseInsensitiveExtensionReturnsexpectedValue() throws { + let fileSystem = InMemoryFileSystem() + + let testDir = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(testDir, recursive: true) + + // Create files with different case extensions + let swiftFile = testDir.appending("file1.swift") + let SwiftFile = testDir.appending("file2.Swift") + let SWIFTFile = testDir.appending("file3.SWIFT") + + try fileSystem.writeFileContents(swiftFile, string: "// file1") + try fileSystem.writeFileContents(SwiftFile, string: "// file2") + try fileSystem.writeFileContents(SWIFTFile, string: "// file3") + + // Test with lowercase extension + let results = try getFiles( + in: testDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + + #expect(results.count == 3) + #expect(results.contains(swiftFile)) + #expect(results.contains(SwiftFile)) + #expect(results.contains(SWIFTFile)) + } + + @Test + func testGetFilesNonExistentDirectoryRasiesAnError() throws { + let fileSystem = InMemoryFileSystem() + let nonExistentDir = try AbsolutePath(validating: "/nonexistent") + + #expect(throws: StringError.self) { + try getFiles( + in: nonExistentDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test + func testGetFilesWithFileAsInputRaisesAnError() throws { + let fileSystem = InMemoryFileSystem() + + let testFile = try AbsolutePath(validating: "/test.swift") + try fileSystem.writeFileContents(testFile, string: "// test file") + + #expect(throws: StringError.self) { + try getFiles( + in: testFile, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test + func testGetFilesEmptyDirectoryReturnsEmptyList() throws { + let fileSystem = InMemoryFileSystem() + + let testDir = try AbsolutePath(validating: "/empty") + try fileSystem.createDirectory(testDir, recursive: true) + + let results = try getFiles( + in: testDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + + #expect(results.isEmpty) + } + + @Test + func testGetFilesRelativePathNonRecursive() throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/current") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Create test directory structure + let testDir = try RelativePath(validating: "test") + let absoluteTestDir = cwd.appending(testDir) + try fileSystem.createDirectory(absoluteTestDir, recursive: true) + + // Create files at different levels + let swiftFile1 = absoluteTestDir.appending("file1.swift") + let swiftFile2 = absoluteTestDir.appending("subdir").appending("file2.swift") + + try fileSystem.createDirectory(swiftFile2.parentDirectory, recursive: true) + try fileSystem.writeFileContents(swiftFile1, string: "// Swift file 1") + try fileSystem.writeFileContents(swiftFile2, string: "// Swift file 2") + + // Test non-recursive search + let swiftFiles = try getFiles( + in: testDir, + matchingExtension: "swift", + recursive: false, + fileSystem: fileSystem + ) + + // Should only find the top-level file + #expect(swiftFiles.count == 1) + + let expectedFile1 = swiftFile1.relative(to: cwd) + #expect(swiftFiles.contains(expectedFile1)) + + // Should not find the nested file + let expectedFile2 = swiftFile2.relative(to: cwd) + #expect(!swiftFiles.contains(expectedFile2)) + } + + @Test + func testGetFilesRelativePathInvalid() throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/current") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Try to access non-existent relative directory + let nonExistentDir = try RelativePath(validating: "nonexistent") + + #expect(throws: StringError.self) { + try getFiles( + in: nonExistentDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test + func testGetFilesRelativePathWithFileAsInputRaisesAnError() throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/current") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Create a file instead of directory + let testFile = cwd.appending("test.swift") + try fileSystem.writeFileContents(testFile, string: "// test file") + + let relativeFile = try RelativePath(validating: "test.swift") + + #expect(throws: StringError.self) { + try getFiles( + in: relativeFile, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test + func testGetFilesRelativePathNoCwdRaisesAnError() throws { + let fileSystem = InMemoryFileSystem() + // Don't set a current working directory + + let testDir = try RelativePath(validating: "test") + + #expect(throws: StringError.self) { + try getFiles( + in: testDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test + func testGetFilesRelativePathComplexStructureReturnsExpectedList() throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/project") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Create complex directory structure + let sourcesDir = try RelativePath(validating: "Sources") + let absoluteSourcesDir = cwd.appending(sourcesDir) + try fileSystem.createDirectory(absoluteSourcesDir, recursive: true) + + // Create files in various subdirectories + let files = [ + "Sources/App/main.swift", + "Sources/App/Models/User.swift", + "Sources/App/Controllers/UserController.swift", + "Sources/Shared/Utils.swift", + "Sources/Shared/Extensions/String+Extensions.swift", + "Sources/Tests/AppTests.swift", + "Sources/README.md", // Non-Swift file + ] + + for filePath in files { + let absolutePath = try AbsolutePath(validating: filePath, relativeTo: cwd) + try fileSystem.createDirectory(absolutePath.parentDirectory, recursive: true) + try fileSystem.writeFileContents(absolutePath, string: "// \(absolutePath.basename)") + } + + // Test recursive search + let allSwiftFiles = try getFiles( + in: sourcesDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + + #expect(allSwiftFiles.count == 6) // All .swift files, excluding README.md + + // Verify all expected files are found + let expectedSwiftFiles = files.filter { $0.hasSuffix(".swift") } + for expectedFile in expectedSwiftFiles { + let relativePath = try RelativePath(validating: expectedFile) + #expect(allSwiftFiles.contains(relativePath)) + } + + // Test non-recursive search (should find no files at Sources root level) + let topLevelSwiftFiles = try getFiles( + in: sourcesDir, + matchingExtension: "swift", + recursive: false, + fileSystem: fileSystem + ) + + #expect(topLevelSwiftFiles.isEmpty) + } + + @Test + func testGetFilesRelativePathCaseSensitivity() throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Create test directory + let testDir = try RelativePath(validating: "files") + let absoluteTestDir = cwd.appending(testDir) + try fileSystem.createDirectory(absoluteTestDir, recursive: true) + + // Create files with different case extensions + let files = [ + "file1.swift", + "file2.Swift", + "file3.SWIFT", + "file4.swiFT", + "file5.txt", // Different extension + ] + + for fileName in files { + let filePath = absoluteTestDir.appending(fileName) + try fileSystem.writeFileContents(filePath, string: "// \(fileName)") + } + + // Test case-insensitive matching + let swiftFiles = try getFiles( + in: testDir, + matchingExtension: "swift", + fileSystem: fileSystem + ) + + #expect(swiftFiles.count == 4) // All .swift variants, excluding .txt + + // Test with uppercase extension + let swiftFilesUpper = try getFiles( + in: testDir, + matchingExtension: "SWIFT", + fileSystem: fileSystem + ) + + #expect(swiftFilesUpper.count == 4) // Should match the same files + #expect(Set(swiftFiles) == Set(swiftFilesUpper)) + } + + // MARK: - Parameterized Tests + + @Test( + arguments: [ + ( + extension: "swift", + expectedFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT"], + allFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT", "file5.txt"], + ), + ( + extension: "SWIFT", + expectedFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT"], + allFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT", "file5.txt"], + + ), + ( + extension: "Swift", + expectedFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT"], + allFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT", "file5.txt"], + ), + ( + extension: "txt", + expectedFiles: ["file5.txt"], + allFiles: ["file1.swift", "file2.Swift", "file3.SWIFT", "file4.swiFT", "file5.txt"], + ), + ], + ) + func testCaseInsensitiveExtensionsParameterized( + extension: String, + expectedFiles: [String], + allFiles: [String], + ) throws { + let fileSystem = InMemoryFileSystem() + let testDir = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(testDir, recursive: true) + + // Create files with different case extensions + for fileName in allFiles { + let filePath = testDir.appending(fileName) + try fileSystem.writeFileContents(filePath, string: "// \(fileName)") + } + + let results = try getFiles( + in: testDir, + matchingExtension: `extension`, + fileSystem: fileSystem + ) + + #expect(results.count == expectedFiles.count, "Expected \(expectedFiles.count) files for extension '\(`extension`)'") + + for expectedFile in expectedFiles { + let expectedPath = testDir.appending(expectedFile) + #expect(results.contains(expectedPath), "Should contain \(expectedFile)") + } + } + + @Test( + arguments: [ + ("non-existent directory", "/nonexistent", false), + ("file instead of directory", "/test.swift", true), + ] + ) + func testErrorHandling( + description: String, + path: String, + createFile: Bool, + ) throws { + let fileSystem = InMemoryFileSystem() + + if createFile { + // Create a file for the "file instead of directory" test + let testFile = try AbsolutePath(validating: path) + try fileSystem.writeFileContents(testFile, string: "// test file") + } + + let testPath = try AbsolutePath(validating: path) + + #expect(throws: StringError.self) { + try getFiles( + in: testPath, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test( + arguments: [ + ("invalid path", true, false), + ("file instead of directory", true, true), + ("no current working directory", false, false), + ] + ) + func testRelativePathErrorConditions( + description: String, + setCwd: Bool, + createFile: Bool, + ) throws { + let fileSystem = InMemoryFileSystem() + + if setCwd { + let cwd = try AbsolutePath(validating: "/current") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + if createFile { + // Create a file for the "file instead of directory" test + let testFile = cwd.appending("test.swift") + try fileSystem.writeFileContents(testFile, string: "// test file") + } + } + + let testPath = try RelativePath(validating: createFile ? "test.swift" : "nonexistent") + + #expect(throws: StringError.self) { + try getFiles( + in: testPath, + matchingExtension: "swift", + fileSystem: fileSystem + ) + } + } + + @Test( + arguments: [ + ( + recursive: true, + expectedCount: 3, + description: "recursive search should find all files" + ), + ( + recursive: false, + expectedCount: 1, + description: "non-recursive search should find only top-level files" + ), + ] + ) + func testgetFilesWithVariousRecursionModes( + recursive: Bool, + expectedCount: Int, + description: String, + ) throws { + let fileSystem = InMemoryFileSystem() + let testDir = try AbsolutePath(validating: "/test") + try fileSystem.createDirectory(testDir, recursive: true) + + // Create files at different levels + let swiftFile1 = testDir.appending("file1.swift") + let swiftFile2 = testDir.appending("subdir").appending("file2.swift") + let swiftFile3 = testDir.appending("subdir").appending("nested").appending("file3.swift") + + try fileSystem.createDirectory(swiftFile2.parentDirectory, recursive: true) + try fileSystem.createDirectory(swiftFile3.parentDirectory, recursive: true) + + try fileSystem.writeFileContents(swiftFile1, string: "// Swift file 1") + try fileSystem.writeFileContents(swiftFile2, string: "// Swift file 2") + try fileSystem.writeFileContents(swiftFile3, string: "// Swift file 3") + + let results = try getFiles( + in: testDir, + matchingExtension: "swift", + recursive: recursive, + fileSystem: fileSystem + ) + + #expect(results.count == expectedCount, "\(description): expected \(expectedCount), got \(results.count)") + + // Always should contain the top-level file + #expect(results.contains(swiftFile1), "Should always contain top-level file") + + if recursive { + // Should contain nested files + #expect(results.contains(swiftFile2), "Recursive search should contain nested files") + #expect(results.contains(swiftFile3), "Recursive search should contain deeply nested files") + } else { + // Should not contain nested files + #expect(!results.contains(swiftFile2), "Non-recursive search should not contain nested files") + #expect(!results.contains(swiftFile3), "Non-recursive search should not contain deeply nested files") + } + } + + @Test( + arguments: [ + ( + recursive: true, + expectedCount: 2, + description: "recursive RelativePath search", + ), + ( + recursive: false, + expectedCount: 1, + description: "non-recursive RelativePath search", + ), + ] + ) + func testRelativePathRecursion( + recursive: Bool, + expectedCount: Int, + description: String, + ) throws { + let fileSystem = InMemoryFileSystem() + + // Set up current working directory + let cwd = try AbsolutePath(validating: "/current") + try fileSystem.createDirectory(cwd, recursive: true) + try fileSystem.changeCurrentWorkingDirectory(to: cwd) + + // Create test directory structure + let testDir = try RelativePath(validating: "test") + let absoluteTestDir = cwd.appending(testDir) + try fileSystem.createDirectory(absoluteTestDir, recursive: true) + + // Create files at different levels + let swiftFile1 = absoluteTestDir.appending("file1.swift") + let swiftFile2 = absoluteTestDir.appending("subdir").appending("file2.swift") + + try fileSystem.createDirectory(swiftFile2.parentDirectory, recursive: true) + try fileSystem.writeFileContents(swiftFile1, string: "// Swift file 1") + try fileSystem.writeFileContents(swiftFile2, string: "// Swift file 2") + + let results = try getFiles( + in: testDir, + matchingExtension: "swift", + recursive: recursive, + fileSystem: fileSystem + ) + + #expect(results.count == expectedCount, "\(description): expected \(expectedCount), got \(results.count)") + + let expectedFile1 = swiftFile1.relative(to: cwd) + #expect(results.contains(expectedFile1), "Should contain top-level file") + + let expectedFile2 = swiftFile2.relative(to: cwd) + if recursive { + #expect(results.contains(expectedFile2), "Recursive search should contain nested file") + } else { + #expect(!results.contains(expectedFile2), "Non-recursive search should not contain nested file") + } + } +}