diff --git a/.swiftpm/configuration/coverage.html.report.args.txt b/.swiftpm/configuration/coverage.html.report.args.txt new file mode 100644 index 00000000000..bee3082c7db --- /dev/null +++ b/.swiftpm/configuration/coverage.html.report.args.txt @@ -0,0 +1,12 @@ +--tab-size=10 +--coverage-watermark=80,20 +--enable-vtable-value-profiling +--show-branch-summary +--show-region-summary +--show-branches=percent +--show-mcdc-summary +--show-expansions +--show-instantiations +--show-regions +--show-directory-coverage +--show-line-counts \ No newline at end of file diff --git a/Fixtures/Coverage/Simple/.gitignore b/Fixtures/Coverage/Simple/.gitignore new file mode 100644 index 00000000000..0023a534063 --- /dev/null +++ b/Fixtures/Coverage/Simple/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Fixtures/Coverage/Simple/Package.swift b/Fixtures/Coverage/Simple/Package.swift new file mode 100644 index 00000000000..4449f7edfff --- /dev/null +++ b/Fixtures/Coverage/Simple/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Simple", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Simple", + targets: ["Simple"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "Simple" + ), + .testTarget( + name: "SimpleTests", + dependencies: ["Simple"] + ), + ] +) diff --git a/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift b/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift new file mode 100644 index 00000000000..5e1e981ad5d --- /dev/null +++ b/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift @@ -0,0 +1,10 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +public func greet(name: String = "world") -> String { + return "Hello, \(name)!" +} + +public func libA() -> String { + return "libA" +} diff --git a/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift b/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift new file mode 100644 index 00000000000..89e144011f1 --- /dev/null +++ b/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift @@ -0,0 +1,24 @@ +import Testing +import XCTest +@testable import Simple + +@Test( + arguments: [ + "Bob", + "Alice", + "", + ] +) + func testGreet( + name: String + ) async throws { + let actual = greet(name: name) + + #expect(actual == "Hello, \(name)!") +} + +final class SimpleTests: XCTestCase { + func testExample() throws { + XCTAssertEqual(libA(), "libA", "Actual is not as expected") + } +} diff --git a/Package.swift b/Package.swift index f39bb5399ff..1488ecfe3d1 100644 --- a/Package.swift +++ b/Package.swift @@ -1104,7 +1104,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { // The 'swift-argument-parser' version declared here must match that // used by 'swift-driver' and 'sourcekit-lsp'. Please coordinate // dependency version changes here with those projects. - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.5.1")), + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.6.1")), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "3.0.0")), .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/apple/swift-system.git", from: "1.1.1"), diff --git a/Sources/Basics/FileSystem/AbsolutePath.swift b/Sources/Basics/FileSystem/AbsolutePath.swift index c472e5c0608..23d5172fe02 100644 --- a/Sources/Basics/FileSystem/AbsolutePath.swift +++ b/Sources/Basics/FileSystem/AbsolutePath.swift @@ -53,14 +53,19 @@ public struct AbsolutePath: Hashable, Sendable { /// The input string will be normalized if needed, as described in the /// documentation for AbsolutePath. public init(validating pathString: String) throws { - self.underlying = try .init(validating: pathString) + self.underlying = try .init( + validating: pathString.trimmingCharacters(in: .whitespacesAndNewlines), + ) } /// Initializes an AbsolutePath from a string that may be either absolute /// or relative; if relative, `basePath` is used as the anchor; if absolute, /// it is used as is, and in this case `basePath` is ignored. public init(validating pathString: String, relativeTo basePath: AbsolutePath) throws { - self.underlying = try .init(validating: pathString, relativeTo: basePath.underlying) + self.underlying = try .init( + validating: pathString.trimmingCharacters(in: .whitespacesAndNewlines), + relativeTo: basePath.underlying, + ) } /// Initializes the AbsolutePath by concatenating a relative path to an diff --git a/Sources/Basics/FileSystem/RelativePath.swift b/Sources/Basics/FileSystem/RelativePath.swift index 38af45df6cd..5ec2b48b37a 100644 --- a/Sources/Basics/FileSystem/RelativePath.swift +++ b/Sources/Basics/FileSystem/RelativePath.swift @@ -9,7 +9,7 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - +import Foundation import struct TSCBasic.RelativePath // public for transition @@ -40,7 +40,9 @@ public struct RelativePath: Hashable, Sendable { /// Convenience initializer that verifies that the path is relative. public init(validating pathString: String) throws { - self.underlying = try .init(validating: pathString) + self.underlying = try .init( + validating: pathString.trimmingCharacters(in: .whitespacesAndNewlines), + ) } /// Directory component. For a relative path without any path separators, diff --git a/Sources/Commands/SwiftBuildCommand.swift b/Sources/Commands/SwiftBuildCommand.swift index 5c366090224..9d418baa94d 100644 --- a/Sources/Commands/SwiftBuildCommand.swift +++ b/Sources/Commands/SwiftBuildCommand.swift @@ -76,9 +76,15 @@ struct BuildCommandOptions: ParsableArguments { var buildTests: Bool = false /// Whether to enable code coverage. - @Flag(name: .customLong("code-coverage"), - inversion: .prefixedEnableDisable, - help: "Enable code coverage.") + @Flag( + name: [ + .customLong("codecov"), + .customLong("code-coverage"), + .customLong("coverage"), + ], + inversion: .prefixedEnableDisable, + help: "Enable code coverage.", + ) var enableCodeCoverage: Bool = false /// If the binary output path should be printed. diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift index 669f04dab3b..d85fa47a389 100644 --- a/Sources/Commands/SwiftTestCommand.swift +++ b/Sources/Commands/SwiftTestCommand.swift @@ -10,10 +10,12 @@ // //===----------------------------------------------------------------------===// +import RegexBuilder import ArgumentParser @_spi(SwiftPMInternal) import Basics +import struct Basics.Triple import _Concurrency @@ -166,11 +168,6 @@ struct TestCommandOptions: ParsableArguments { help: "Lists test methods in specifier format.") var _deprecated_shouldListTests: Bool = false - /// If the path of the exported code coverage JSON should be printed. - @Flag(name: [.customLong("show-codecov-path"), .customLong("show-code-coverage-path"), .customLong("show-coverage-path")], - help: "Print the path of the exported code coverage JSON file.") - var shouldPrintCodeCovPath: Bool = false - var testCaseSpecifier: TestCaseSpecifier { if !filter.isEmpty { return .regex(filter) @@ -210,11 +207,10 @@ struct TestCommandOptions: ParsableArguments { @Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Enabled by default.") var enableTestableImports: Bool = true - /// Whether to enable code coverage. - @Flag(name: .customLong("code-coverage"), - inversion: .prefixedEnableDisable, - help: "Enable code coverage.") - var enableCodeCoverage: Bool = false + @OptionGroup( + title: "Coverage Options", + ) + var coverageOptions: CoverageOptions /// Configure test output. @Option(help: ArgumentHelp("", visibility: .hidden)) @@ -225,6 +221,95 @@ struct TestCommandOptions: ParsableArguments { } } + +package enum CoverageFormat: String, ExpressibleByArgument, CaseIterable, Comparable { + case json + case html + + package var defaultValueDescription: String { + switch self { + case .json: "Produces a JSON coverage report." + case .html: "Produces an HTML report producd by llvm-cov. The HTML report can be configured using a reponse file stored in the project repository. See 'TODO' for more." + } + } + + package static func < (lhs: CoverageFormat, rhs: CoverageFormat) -> Bool { + return lhs.rawValue < rhs.rawValue + } +} + +extension CoverageFormat: Encodable {} + +enum CoveragePrintPathMode: String, ExpressibleByArgument, CaseIterable { + case json + case text + + package var defaultValueDescription: String { + switch self { + case .json: "Display the output in JSON format." + case .text: "Display the output as plain text." + } + } + +} + + + +public struct CoverageOptions: ParsableArguments { + public init() {} + + /// If the path of the exported code coverage JSON should be printed. + @Flag( + name: [ + .customLong("show-codecov-path"), + .customLong("show-code-coverage-path"), + .customLong("show-coverage-path"), + ], + help: "Print the path of the exported code coverage files.", + ) + var shouldPrintPath: Bool = false + + /// If the path of the exported code coverage JSON should be printed. + @Option( + name: [ + .customLong("print-codecov-path-mode"), + .customLong("print-code-coverage-path-mode"), + .customLong("print-coverage-path-mode"), + ], + help: ArgumentHelp( + "How to display the paths of the selected code coverage file formats.", + ) + ) + var printPathMode: CoveragePrintPathMode = .text + + /// Whether to enable code coverage. + @Flag( + name: [ + .customLong("codecov"), + .customLong("code-coverage"), + .customLong("coverage"), + ], + inversion: .prefixedEnableDisable, + help: "Enable code coverage.", + ) + var isEnabled: Bool = false + + @Option( + name: [ + .customLong("codecov-format"), + .customLong("code-coverage-format"), + .customLong("coverage-format"), + ], + help: ArgumentHelp( + "Format of the code coverage output. Can be specified multiple times.", + valueName: "format", + ) + ) + var formats: [CoverageFormat] = [.json] + +} + + /// Tests filtering specifier, which is used to filter tests to run. public enum TestCaseSpecifier { /// No filtering @@ -252,6 +337,148 @@ public enum TestOutput: String, ExpressibleByArgument { case experimentalParseable } +package func getOutputDir( + from file: AbsolutePath, + fileSystem: FileSystem = localFileSystem, +) throws-> AbsolutePath? { + guard fileSystem.exists(file) else { + return nil + } + return try getOutputDir(from: try fileSystem.readFileContents(file)) +} + +package func getOutputDir(from content: String) throws -> AbsolutePath? { + var returnValue : AbsolutePath? = nil + let commandArg = "--output-dir" + let lines = content.split(whereSeparator: \.isNewline) + + let outputDir = Reference(AbsolutePath.self) + let outputDirRegex = Regex { + Optionally { + ZeroOrMore(.any, .reluctant) + OneOrMore(.whitespace) + } + "--output-dir" + ChoiceOf { + "=" + OneOrMore(.whitespace) + } + Capture(as: outputDir) { + OneOrMore(.any) + } transform: { + try AbsolutePath(validating: "\($0)") + } + } + + // Loop on the contents. + for (index, line) in lines.enumerated() { + if !line.contains(commandArg) { + continue + } + + if line == commandArg || line.hasSuffix(" \(commandArg)") { + // The argument value is on the next line + let value = "\(lines[index + 1])" + // Need to check if `value` is an absolute or relative path + returnValue = try AbsolutePath(validating: value) + continue + } + + // Let's parse via regular expression + if let match = line.wholeMatch(of: outputDirRegex) { + let (_, outputDir) = match.output + returnValue = outputDir + } + } + + return returnValue +} +package struct CoverageFormatOutput: Encodable { + private var _underlying: [CoverageFormat : AbsolutePath] + + package init() { + self._underlying = [CoverageFormat : AbsolutePath]() + } + + package init(data: [CoverageFormat : AbsolutePath]) { + self._underlying = data + } + + // Custom encoding to ensure the dictionary is encoded as a JSON object, not an array + public func encode(to encoder: Encoder) throws { + // Use keyed container to encode each format and its path + // This will create proper JSON objects and proper plain text "key: value" format + var container = encoder.container(keyedBy: DynamicCodingKey.self) + + // Sort entries for consistent output + let sortedEntries = _underlying.sorted { $0.key.rawValue < $1.key.rawValue } + + for (format, path) in sortedEntries { + let key = DynamicCodingKey(stringValue: format.rawValue)! + try container.encode(path.pathString, forKey: key) + } + } + + // Dynamic coding keys for the formats + private struct DynamicCodingKey: CodingKey { + var stringValue: String + var intValue: Int? { nil } + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + return nil + } + } + + /// Adds a key/value pair to the underlying dictionary. + /// - Parameters: + /// - format: The coverage format key + /// - path: The absolute path value + /// - Throws: `StringError` if the key already exists + package mutating func addFormat(_ format: CoverageFormat, path: AbsolutePath) throws { + guard !_underlying.keys.contains(format) else { + throw StringError("Coverage format '\(format.rawValue)' already exists") + } + _underlying[format] = path + } + + /// Access paths by format. Returns nil if format doesn't exist. + package subscript(format: CoverageFormat) -> AbsolutePath? { + return _underlying[format] + } + + /// Gets the path for a format, throwing an error if it doesn't exist. + /// - Parameter format: The coverage format + /// - Returns: The absolute path for the format + /// - Throws: `StringError` if the format is not found + package func getPath(for format: CoverageFormat) throws -> AbsolutePath { + guard let path = _underlying[format] else { + throw StringError("Missing coverage format output path for '\(format.rawValue)'") + } + return path + } + + /// Returns all formats currently stored + package var formats: [CoverageFormat] { + return Array(_underlying.keys).sorted() + } + + /// Iterate over format/path pairs + package func forEach(_ body: (CoverageFormat, AbsolutePath) throws -> Void) rethrows { + try _underlying.forEach(body) + } + +} + +struct CodeCoverageConfiguration { + // let outputDirMap: CoverageFormatOutput + let outputDir: AbsolutePath + let htmlArgumentFile: AbsolutePath +} + /// swift-test tool namespace public struct SwiftTestCommand: AsyncSwiftCommand { public static var configuration = CommandConfiguration( @@ -314,7 +541,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { let testSuites = try TestingSupport.getTestSuites( in: testProducts, swiftCommandState: swiftCommandState, - enableCodeCoverage: options.enableCodeCoverage, + enableCodeCoverage: options.coverageOptions.isEnabled, shouldSkipBuilding: options.sharedOptions.shouldSkipBuilding, experimentalTestOutput: options.enableExperimentalTestOutput, sanitizers: globalOptions.build.sanitizers @@ -405,7 +632,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { let testSuites = try TestingSupport.getTestSuites( in: testProducts, swiftCommandState: swiftCommandState, - enableCodeCoverage: options.enableCodeCoverage, + enableCodeCoverage: options.coverageOptions.isEnabled, shouldSkipBuilding: options.sharedOptions.shouldSkipBuilding, experimentalTestOutput: options.enableExperimentalTestOutput, sanitizers: globalOptions.build.sanitizers @@ -440,6 +667,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { // MARK: - Common implementation public func run(_ swiftCommandState: SwiftCommandState) async throws { + let uniqueCoverageFormats = Array(Set(self.options.coverageOptions.formats)).sorted( by: <) do { // Validate commands arguments try self.validateArguments(swiftCommandState: swiftCommandState) @@ -448,8 +676,12 @@ public struct SwiftTestCommand: AsyncSwiftCommand { throw ExitCode.failure } - if self.options.shouldPrintCodeCovPath { - try await printCodeCovPath(swiftCommandState) + if self.options.coverageOptions.shouldPrintPath { + try await printCodeCovPath( + swiftCommandState, + formats: uniqueCoverageFormats, + printMode: options.coverageOptions.printPathMode, + ) } else if self.options._deprecated_shouldListTests { // backward compatibility 6/2022 for deprecation of flag into a subcommand let command = try List.parse() @@ -460,7 +692,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { // Clean out the code coverage directory that may contain stale // profraw files from a previous run of the code coverage tool. - if self.options.enableCodeCoverage { + if self.options.coverageOptions.isEnabled { try swiftCommandState.fileSystem.removeFileTree(productsBuildParameters.codeCovPath) } @@ -468,8 +700,12 @@ public struct SwiftTestCommand: AsyncSwiftCommand { // Process code coverage if requested. We do not process it if the test run failed. // See https://github.com/swiftlang/swift-package-manager/pull/6894 for more info. - if self.options.enableCodeCoverage, swiftCommandState.executionStatus != .failure { - try await processCodeCoverage(testProducts, swiftCommandState: swiftCommandState) + if self.options.coverageOptions.isEnabled, swiftCommandState.executionStatus != .failure { + try await processCodeCoverage( + testProducts, + swiftCommandState: swiftCommandState, + formats: uniqueCoverageFormats, + ) } } } @@ -578,8 +814,10 @@ public struct SwiftTestCommand: AsyncSwiftCommand { /// Processes the code coverage data and emits a json. private func processCodeCoverage( _ testProducts: [BuiltTestProduct], - swiftCommandState: SwiftCommandState + swiftCommandState: SwiftCommandState, + formats: [CoverageFormat], ) async throws { + swiftCommandState.observabilityScope.emit(info: "Processing code coverage data...") let workspace = try swiftCommandState.getActiveWorkspace() let root = try swiftCommandState.getWorkspaceRoot() let rootManifests = try await workspace.loadRootManifests( @@ -591,52 +829,141 @@ public struct SwiftTestCommand: AsyncSwiftCommand { } // Merge all the profraw files to produce a single profdata file. - try await mergeCodeCovRawDataFiles(swiftCommandState: swiftCommandState) + let profData = try await mergeCodeCovRawDataFiles(swiftCommandState: swiftCommandState) + var coverageReportData = [CoverageFormat : AbsolutePath]() + defer { + swiftCommandState.outputStream.send("Code coverage report:\n") + for (format, path) in coverageReportData { + swiftCommandState.outputStream.send(" - \(format.rawValue.uppercased()): \(path.pathString)\n") + } + swiftCommandState.outputStream.flush() + } + for format in formats { + switch format { + case .json: + // let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(options: self.options) + for product in testProducts { + // Export the codecov data as JSON. + let jsonPath = try await self.getCodeCoverageConfiguration(swiftCommandState, format: .json).outputDir + coverageReportData[format] = try await exportCodeCovAsJSON( + to: jsonPath, + testBinary: product.binaryPath, + swiftCommandState: swiftCommandState, + ) + } + case .html: + let toolchain = try swiftCommandState.getHostToolchain() + let llvmCov = try toolchain.getLLVMCov() + + // Get all production source files from test targets + let buildSystem = try await swiftCommandState.createBuildSystem() + let packageGraph = try await buildSystem.getPackageGraph() + + let sourceFiles = try await getProductionSourceFiles( + testProducts: testProducts, + packageGraph: packageGraph, + ) + let configuration = try await self.getCodeCoverageConfiguration(swiftCommandState, format: .html) + for product in testProducts { + let coveragaHtmlReportPath = try await generateCoverageReport( + llvmCovPath: llvmCov, + fromFile: profData, + desiredOutputPath: configuration.outputDir, + testBinary: product.binaryPath, + sourceFiles: sourceFiles, + withTitle: rootManifest.displayName, + llvmCovShowArgumentFile: configuration.htmlArgumentFile, + // .appending("coverage.html.report.args.txt"), + ) + coverageReportData[format] = coveragaHtmlReportPath.appending("index.html") + } + } + } + } - let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(options: self.options) - for product in testProducts { - // Export the codecov data as JSON. - let jsonPath = productsBuildParameters.codeCovAsJSONPath(packageName: rootManifest.displayName) - try await exportCodeCovAsJSON(to: jsonPath, testBinary: product.binaryPath, swiftCommandState: swiftCommandState) + /// Gets all production source files from test targets and their dependencies. + private func getProductionSourceFiles( + testProducts: [BuiltTestProduct], + packageGraph: ModulesGraph, + ) async throws -> [AbsolutePath] { + var sourceFiles = Set() + + // Get all modules from root packages that are not test modules + // These are the production modules that tests are covering + for package in packageGraph.rootPackages { + for module in package.modules { + // Include all non-test, non-plugin modules from root packages + if module.type != .test && module.type != .plugin { + sourceFiles.formUnion(module.sources.paths) + } + } + } + + // If no source files found from root packages, fall back to all reachable modules + if sourceFiles.isEmpty { + for module in packageGraph.reachableModules { + if module.type != .test && module.type != .plugin { + sourceFiles.formUnion(module.sources.paths) + } + } } + + return Array(sourceFiles) } /// Merges all profraw profiles in codecoverage directory into default.profdata file. - private func mergeCodeCovRawDataFiles(swiftCommandState: SwiftCommandState) async throws { + private func mergeCodeCovRawDataFiles( + swiftCommandState: SwiftCommandState, + ) async throws -> AbsolutePath { // Get the llvm-prof tool. let llvmProf = try swiftCommandState.getTargetToolchain().getLLVMProf() // Get the profraw files. let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(options: self.options) - let codeCovFiles = try swiftCommandState.fileSystem.getDirectoryContents(productsBuildParameters.codeCovPath) + let covPath = productsBuildParameters.codeCovPath + let codeCovFiles: [String] = if swiftCommandState.fileSystem.exists(covPath) { + try swiftCommandState.fileSystem.getDirectoryContents(covPath) + } else { + [] + } // Construct arguments for invoking the llvm-prof tool. var args = [llvmProf.pathString, "merge", "-sparse"] for file in codeCovFiles { - let filePath = productsBuildParameters.codeCovPath.appending(component: file) + let filePath = covPath.appending(component: file) if filePath.extension == "profraw" { args.append(filePath.pathString) } } args += ["-o", productsBuildParameters.codeCovDataFile.pathString] - try await AsyncProcess.checkNonZeroExit(arguments: args) + + return productsBuildParameters.codeCovDataFile } /// Exports profdata as a JSON file. - private func exportCodeCovAsJSON( + func exportCodeCovAsJSON( to path: AbsolutePath, testBinary: AbsolutePath, swiftCommandState: SwiftCommandState - ) async throws { + ) async throws -> AbsolutePath{ // Export using the llvm-cov tool. let llvmCov = try swiftCommandState.getTargetToolchain().getLLVMCov() - let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(options: self.options) + let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest( + options: self.options, + ) + let archArgs: [String] = if let arch = productsBuildParameters.triple.llvmCovArchArgument { + // let archArgs: [String] = if let arch = productsBuildParameters.triple.arch { + ["--arch", "\(arch)"] + } else { + [] + } let args = [ llvmCov.pathString, "export", "-instr-profile=\(productsBuildParameters.codeCovDataFile)", - testBinary.pathString + ] + archArgs + [ + testBinary.pathString, ] let result = try await AsyncProcess.popen(arguments: args) @@ -645,6 +972,58 @@ public struct SwiftTestCommand: AsyncSwiftCommand { throw StringError("Unable to export code coverage:\n \(output)") } try swiftCommandState.fileSystem.writeFileContents(path, bytes: ByteString(result.output.get())) + return path + } + + /// Generates a code coverage HTML report. + package func generateCoverageReport( + llvmCovPath: AbsolutePath, + fromFile profData: AbsolutePath, + desiredOutputPath outputPath: AbsolutePath, + testBinary: AbsolutePath, + sourceFiles: [AbsolutePath], + withTitle title: String, + llvmCovShowArgumentFile: AbsolutePath, + ) async throws -> AbsolutePath { + // Generate the HTML report. + if localFileSystem.exists(outputPath) { + try localFileSystem.removeFileTree(outputPath) + } else { + try localFileSystem.createDirectory(outputPath, recursive: true) + } + + let argumentFile: [String] = if localFileSystem.exists(llvmCovShowArgumentFile) { + ["@\(llvmCovShowArgumentFile)"] + } else { + [] + } + + var args = [ + llvmCovPath.pathString, + "show", + "--project-title=\(title) Coverage Report", + "--instr-profile=\(profData.pathString)", + "--output-dir=\(outputPath.pathString)", + ] + argumentFile + [ + // ensure we overdie the fomat to HTML as that's what the user specified via + // the Swift test command line argument + "--format=html", + testBinary.pathString, + ] + + // Add all the production source files of the test targets + args.append(contentsOf: sourceFiles.map { $0.pathString }) + + let result = try await AsyncProcess.popen(arguments: args) + + if result.exitStatus != .terminated(code: 0) { + let output = try result.utf8Output() + result.utf8stderrOutput() + throw StringError("Unable to generate HTML code coverage report:\n \(output)") + } + + // the output put can be updated via the command arg file + // return try! getOutputDir(from: llvmCovShowArgumentFile) ?? outputPath + return outputPath } /// Builds the "test" target if enabled in options. @@ -694,7 +1073,25 @@ public struct SwiftTestCommand: AsyncSwiftCommand { } extension SwiftTestCommand { - func printCodeCovPath(_ swiftCommandState: SwiftCommandState) async throws { + + func getCodeCoverageConfiguration( + _ swiftCommandState: SwiftCommandState, + format: CoverageFormat + ) async throws -> CodeCoverageConfiguration { + let htmlArgumentFile = try swiftCommandState.getActiveWorkspace().location.llvmCovShowArgumentFile + let outputDir = try await self.getCodeCovPath(swiftCommandState, format: format, argumentFile: htmlArgumentFile) + + return CodeCoverageConfiguration( + outputDir: outputDir, + htmlArgumentFile: htmlArgumentFile, + ) + } + + func getCodeCovPath( + _ swiftCommandState: SwiftCommandState, + format: CoverageFormat, + argumentFile: AbsolutePath, + ) async throws -> AbsolutePath { let workspace = try swiftCommandState.getActiveWorkspace() let root = try swiftCommandState.getWorkspaceRoot() let rootManifests = try await workspace.loadRootManifests( @@ -705,7 +1102,64 @@ extension SwiftTestCommand { throw StringError("invalid manifests at \(root.packages)") } let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(enableCodeCoverage: true) - print(productsBuildParameters.codeCovAsJSONPath(packageName: rootManifest.displayName)) + return switch format { + case .html: + try! getOutputDir(from: argumentFile) ?? productsBuildParameters.codeCovAsHTMLPath(packageName: rootManifest.displayName) + case .json: + productsBuildParameters.codeCovAsJSONPath(packageName: rootManifest.displayName) + } + } + + func printCodeCovPath( + _ swiftCommandState: SwiftCommandState, + formats: [CoverageFormat], + printMode: CoveragePrintPathMode, + ) async throws { + var coverageData = [CoverageFormat : AbsolutePath]() + for format in formats { + let config = try await self.getCodeCoverageConfiguration(swiftCommandState, format: format) + coverageData[format] = config.outputDir + } + + let data: Data + switch printMode { + case .json: + let coverageOutput = CoverageFormatOutput(data: coverageData) + let encoder = JSONEncoder.makeWithDefaults() + encoder.keyEncodingStrategy = .convertToSnakeCase + data = try encoder.encode(coverageOutput) + case .text: + // When there's only one format, don't show the key prefix + if formats.count == 1, let singlePath = coverageData.values.first { + swiftCommandState.observabilityScope.emit( + warning: """ + The contents of this output are subject to change in the future. Use `--print-coverage-path-mode json` if the output is required in a script. + """, + ) + data = Data("\(singlePath.pathString)".utf8) + } else { + let coverageOutput = CoverageFormatOutput(data: coverageData) + var encoder = PlainTextEncoder() + encoder.formattingOptions = [.prettyPrinted] + data = try encoder.encode(coverageOutput) + } + } + print(String(decoding: data, as: UTF8.self)) + } +} + +fileprivate extension Triple { + var llvmCovArchArgument: String? { + guard let arch = self.arch else { + return nil + } + switch arch { + case .aarch64: + // macOS uses arm64, Linux might use aarch64 + return ProcessInfo.hostOperatingSystem == .macOS ? "arm64" : "aarch64" + default: + return "\(arch)" + } } } @@ -1472,7 +1926,7 @@ extension SwiftCommandState { options: TestCommandOptions ) throws -> (productsBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters) { try self.buildParametersForTest( - enableCodeCoverage: options.enableCodeCoverage, + enableCodeCoverage: options.coverageOptions.isEnabled, enableTestability: options.enableTestableImports, shouldSkipBuilding: options.sharedOptions.shouldSkipBuilding, experimentalTestOutput: options.enableExperimentalTestOutput @@ -1521,6 +1975,10 @@ extension BuildParameters { fileprivate func codeCovAsJSONPath(packageName: String) -> AbsolutePath { return self.codeCovPath.appending(component: packageName + ".json") } + + fileprivate func codeCovAsHTMLPath(packageName: String) -> AbsolutePath { + return self.codeCovPath.appending(component: "\(packageName)-html") + } } private extension Basics.Diagnostic { diff --git a/Sources/Commands/Utilities/PlainTextEncoder.swift b/Sources/Commands/Utilities/PlainTextEncoder.swift index 00ddc760c9c..6f0e7739696 100644 --- a/Sources/Commands/Utilities/PlainTextEncoder.swift +++ b/Sources/Commands/Utilities/PlainTextEncoder.swift @@ -14,31 +14,34 @@ import struct Foundation.Data import class TSCBasic.BufferedOutputByteStream import protocol TSCBasic.OutputByteStream -struct PlainTextEncoder { +package struct PlainTextEncoder { /// The formatting of the output plain-text data. - struct FormattingOptions: OptionSet { - let rawValue: UInt + package struct FormattingOptions: OptionSet { + package let rawValue: UInt - init(rawValue: UInt) { + package init(rawValue: UInt) { self.rawValue = rawValue } /// Produce plain-text format with indented output. - static let prettyPrinted = FormattingOptions(rawValue: 1 << 0) + package static let prettyPrinted = FormattingOptions(rawValue: 1 << 0) } /// The output format to produce. Defaults to `[]`. - var formattingOptions: FormattingOptions = [] + package var formattingOptions: FormattingOptions = [] /// Contextual user-provided information for use during encoding. - var userInfo: [CodingUserInfoKey: Any] = [:] + package var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Initializes a new PlainTextEncoder. + package init() {} /// Encodes the given top-level value and returns its plain text representation. /// /// - parameter value: The value to encode. /// - returns: A new `Data` value containing the encoded plan-text data. /// - throws: An error if any value throws an error during encoding. - func encode(_ value: T) throws -> Data { + package func encode(_ value: T) throws -> Data { let outputStream = BufferedOutputByteStream() let encoder = _PlainTextEncoder( outputStream: outputStream, diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 46a8c508300..0cf816656d6 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -1077,8 +1077,8 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { private static func constructTestingSettingsOverrides(from parameters: BuildParameters.Testing) -> [String: String] { var settings: [String: String] = [:] - // TODO: enableCodeCoverage - // explicitlyEnabledTestability + + settings["CLANG_COVERAGE_MAPPING"] = parameters.enableCodeCoverage ? "YES" : "NO" switch parameters.explicitlyEnabledTestability { case true: diff --git a/Sources/Workspace/Workspace+Configuration.swift b/Sources/Workspace/Workspace+Configuration.swift index 9fecb0a0888..63ff5a62798 100644 --- a/Sources/Workspace/Workspace+Configuration.swift +++ b/Sources/Workspace/Workspace+Configuration.swift @@ -28,6 +28,7 @@ import class TSCUtility.SimplePersistence extension Workspace { /// Workspace location configuration public struct Location { + package static let coverageResponseFileName = "coverage.html.report.args.txt" /// Path to scratch space (working) directory for this workspace (aka .build). public var scratchDirectory: AbsolutePath @@ -105,6 +106,11 @@ extension Workspace { self.sharedConfigurationDirectory.map { DefaultLocations.mirrorsConfigurationFile(at: $0) } } + /// Path to the LLVM Cov Show argument file + public var llvmCovShowArgumentFile: AbsolutePath { + self.localConfigurationDirectory.appending(Location.coverageResponseFileName) + } + /// Path to the local registries configuration. public var localRegistriesConfigurationFile: AbsolutePath { DefaultLocations.registriesConfigurationFile(at: self.localConfigurationDirectory) diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index f8af94b5f30..b641155efb5 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -29,6 +29,7 @@ extension Tag.Feature { public enum TargetType {} @Tag public static var CodeCoverage: Tag + @Tag public static var Encoding: Tag @Tag public static var Mirror: Tag @Tag public static var NetRc: Tag @Tag public static var Resource: Tag diff --git a/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift b/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift index 99de658066a..3978f92b874 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+TraitsBug.swift @@ -54,6 +54,13 @@ extension Trait where Self == Testing.Bug { ) } + public static var IssueWindowsPathTestsFailures: Self { + .issue( + "https://github.com/swiftlang/swift-package-manager/issues/8511", + relationship: .defect, + ) + } + public static var IssueWindowsCannotSaveAttachment: Self { // error: unable to write file 'C:\Users\ContainerAdministrator\AppData\Local\Temp\CFamilyTargets_CDynamicLookup.hNxGHC\CFamilyTargets_CDynamicLookup\.build\x86_64-unknown-windows-msvc\Intermediates.noindex\CDynamicLookup.build\Release-windows\CDynamicLookup.build\Objects-normal\x86_64\CDynamicLookup.LinkFileList': No such file or directory (2) .issue( diff --git a/Tests/BasicsTests/FileSystem/PathTests.swift b/Tests/BasicsTests/FileSystem/PathTests.swift index 5cf741f1a34..0f6039a2524 100644 --- a/Tests/BasicsTests/FileSystem/PathTests.swift +++ b/Tests/BasicsTests/FileSystem/PathTests.swift @@ -43,6 +43,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/ab/cd/ef/", expected: (windows ? #"\ab\cd\ef"# : "/ab/cd/ef"), label: "Trailing path seperator"), (path: "/ab/cd/ef//", expected: (windows ? #"\ab\cd\ef"# : "/ab/cd/ef"), label: "Trailing path seperator"), @@ -110,6 +111,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/./a", expected: (windows ? #"\"# : "/")), (path: "/../..", expected: (windows ? #"\"# : "/")), @@ -143,6 +145,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/../..", expected: "/"), ] @@ -178,6 +181,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/../..", expected: "/"), ] @@ -204,6 +208,7 @@ struct PathTests { #expect(actual == expectedPath) } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/", numParentDirectoryCalls: 1, expected: "/"), (path: "/", numParentDirectoryCalls: 2, expected: "/"), @@ -215,6 +220,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/bar/../foo/..//", numParentDirectoryCalls: 2, expected: "/"), (path: "/bar/../foo/..//yabba/a/b", numParentDirectoryCalls: 2, expected: "/yabba") @@ -231,6 +237,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "/", expected: ["/"]), (path: "/.", expected: ["/"]), @@ -329,6 +336,87 @@ struct PathTests { } } + struct absolutePathValidationWithPathContainsLeadingAndTrailingWhitespacesReturnsExpectedValue { + func testImplementation( + data: (String, String), + whitespaces: String, + ) throws { + let path = data.0 + let expected = data.1 + do { + // Leading whitespaces + let actual = try AbsolutePath(validating: "\(whitespaces)\(path)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + + do { + // Training whitespaces + let actual = try AbsolutePath(validating: "\(path)\(whitespaces)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + + do { + // Leading and trailing whitespaces + let actual = try AbsolutePath(validating: "\(whitespaces)\(path)\(whitespaces)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + } + @Test( + arguments: [ + (path: "/", expected: (windows ? #"\"# : "/")), + ], [ + " ", + " ", + "\t", + "\t\t", + "\n", + "\n\n", + "\t ", + " \t", + " \n\t", + "\n \t", + ], + ) + func absolutePathValidationWithPathContainsLeadingAndTrailingWhitespaces( + data: (String, String), + whitespaces: String, + ) throws { + try testImplementation(data: data, whitespaces: whitespaces) + } + + @Test( + .IssueWindowsPathTestsFailures, + arguments: [ + (path: "/.", expected: (windows ? #"\"# : "/")), + (path: "/..", expected: (windows ? #"\"# : "/")), + (path: "/bar/", expected: (windows ? #"\bar"# : "/bar")), + ], [ + " ", + " ", + "\t", + "\t\t", + "\n", + "\n\n", + "\t ", + " \t", + " \n\t", + "\n \t", + ], + ) + func absolutePathValidationWithPathContainsLeadingAndTrailingWhitespacesFailsOnWindows( + data: (String, String), + whitespaces: String, + ) throws { + try withKnownIssue(": Path \(data.0) is not handled properly") { + try testImplementation(data: data, whitespaces: whitespaces) + } when: { + ProcessInfo.hostOperatingSystem == .windows + } + } + + } + + @Test func comparison() { #expect(AbsolutePath("/") <= AbsolutePath("/")); @@ -368,6 +456,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "ab//cd//ef", expected: (windows ? #"ab\cd\ef"# : "ab/cd/ef"), label: "repeated path seperators"), (path: "ab//cd///ef", expected: (windows ? #"ab\cd\ef"# : "ab/cd/ef"), label: "repeated path seperators"), @@ -440,7 +529,8 @@ struct PathTests { } @Test( - arguments: [ + .IssueWindowsPathTestsFailures, + arguments: [ (path: "../a/..", expected: "."), (path: "a/..", expected: "."), (path: "a/../////../////./////", expected: "."), @@ -481,6 +571,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "a/..", expected: "."), (path: "a/../////../////./////", expected: ".."), @@ -519,6 +610,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "../..", expected: ".."), (path: "../a/..", expected: ".."), @@ -560,7 +652,8 @@ struct PathTests { } @Test( - arguments:[ + .IssueWindowsPathTestsFailures, + arguments:[ "a.", ".a", "", @@ -601,6 +694,7 @@ struct PathTests { } @Test( + .IssueWindowsPathTestsFailures, arguments: [ (path: "foo/bar/..", expected: ["foo"]), (path: "bar/../foo", expected: ["foo"]), @@ -622,13 +716,15 @@ struct PathTests { } } - @Test + @Test( + .IssueWindowsPathTestsFailures, + ) func relativePathValidation() throws { #expect(throws: Never.self) { try RelativePath(validating: "a/b/c/d") } - withKnownIssue { + withKnownIssue("https://github.com/swiftlang/swift-package-manager/issues/8511: \\") { #expect {try RelativePath(validating: "/a/b/d")} throws: { error in ("\(error)" == "invalid relative path '/a/b/d'; relative path should not begin with '/'") } @@ -637,6 +733,87 @@ struct PathTests { } } + struct relativePathValidationWithPathContainsLeadingAndTrailingWhitespacesReturnsExpectedValue { + func testImplementation( + data: (String, String), + whitespaces: String, + ) async throws { + let path = data.0 + let expected = data.1 + do { + // Leading whitespaces + let actual = try RelativePath(validating: "\(whitespaces)\(path)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + + do { + // Training whitespaces + let actual = try RelativePath(validating: "\(path)\(whitespaces)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + + do { + // Leading and trailing whitespaces + let actual = try RelativePath(validating: "\(whitespaces)\(path)\(whitespaces)").pathString + #expect(actual == expected, "Actual is not as expected. Path is: '\(path)'") + } + } + + @Test( + arguments: [ + (path: ".", expected: "."), + (path: "bar/", expected: (windows ? #"bar\"# : "bar")), + (path: "bar/baz", expected: (windows ? #"bar\baz"# :"bar/baz")), + ], [ + " ", + " ", + "\t", + "\t\t", + "\n", + "\n\n", + "\t ", + " \t", + " \n\t", + "\n \t", + ], + ) + func relativePathValidationWithPathContainsLeadingAndTrailingWhitespaces( + data: (String, String), + whitespaces: String, + ) async throws { + try await testImplementation(data: data, whitespaces: whitespaces) + } + + + @Test( + .IssueWindowsPathTestsFailures, + arguments: [ + (path: "bar/", expected: "bar"), + ] as [(String, String)], [ + " ", + " ", + "\t", + "\t\t", + "\n", + "\n\n", + "\t ", + " \t", + " \n\t", + "\n \t", + ], + ) + func relativePathValidationWithPathContainsLeadingAndTrailingWhitespacesFailsOnWindows( + data: (String, String), + whitespaces: String, + ) async throws { + try await withKnownIssue("https://github.com/swiftlang/swift-package-manager/issues/8511: Path \(data.0) is not properly") { + try await testImplementation(data: data, whitespaces: whitespaces) + } when: { + ProcessInfo.hostOperatingSystem == .windows + } + } + } + } @Test diff --git a/Tests/BasicsTests/HTTPClientTests.swift b/Tests/BasicsTests/HTTPClientTests.swift index 6ea4442b3db..b4402c55919 100644 --- a/Tests/BasicsTests/HTTPClientTests.swift +++ b/Tests/BasicsTests/HTTPClientTests.swift @@ -259,11 +259,8 @@ struct HTTPClientTests { var request = HTTPClient.Request(method: .get, url: "http://test") request.options.validResponseCodes = [200] - do { - let response = try await httpClient.execute(request) - Issue.record("unexpected success \(response)") - } catch { - #expect(error as? HTTPClientError == .badResponseStatusCode(statusCode)) + await #expect(throws: HTTPClientError.badResponseStatusCode(statusCode)) { + try await httpClient.execute(request) } } @@ -407,11 +404,8 @@ struct HTTPClientTests { var request = HTTPClient.Request(url: "http://test") request.options.maximumResponseSizeInBytes = 10 - do { - let response = try await httpClient.execute(request) - Issue.record("unexpected success \(response)") - } catch { - #expect(error as? HTTPClientError == .responseTooLarge(maxSize * 2)) + await #expect(throws: HTTPClientError.responseTooLarge(maxSize * 2)) { + try await httpClient.execute(request) } } diff --git a/Tests/BuildTests/PluginsBuildPlanTests.swift b/Tests/BuildTests/PluginsBuildPlanTests.swift index 86158adfc16..60e04740d1e 100644 --- a/Tests/BuildTests/PluginsBuildPlanTests.swift +++ b/Tests/BuildTests/PluginsBuildPlanTests.swift @@ -28,7 +28,7 @@ struct PluginsBuildPlanTests { .tags( .Feature.Command.Build, ), - .issue("https://github.com/swiftlang/swift-package-manager/issues/8511", relationship: .defect), // Fails to build the project to due to incorrect Path handling + .IssueWindowsPathTestsFailures, // Fails to build the project to due to incorrect Path handling arguments: BuildConfiguration.allCases, ) func buildToolsDatabasePath( diff --git a/Tests/CommandsTests/BuildCommandTests.swift b/Tests/CommandsTests/BuildCommandTests.swift index aaa83258257..1ee9ed8c50d 100644 --- a/Tests/CommandsTests/BuildCommandTests.swift +++ b/Tests/CommandsTests/BuildCommandTests.swift @@ -32,141 +32,130 @@ struct BuildResult { let moduleContents: [String] } -@Suite( - .tags( - Tag.TestSize.small, - ), -) -struct SanitierTests { - @Test( - arguments: Sanitizer.allCases +@discardableResult +fileprivate func execute( + _ args: [String] = [], + environment: Environment? = nil, + packagePath: AbsolutePath? = nil, + configuration: BuildConfiguration, + buildSystem: BuildSystemProvider.Kind, + throwIfCommandFails: Bool = true, +) async throws -> (stdout: String, stderr: String) { + + return try await executeSwiftBuild( + packagePath, + configuration: configuration, + extraArgs: args, + env: environment, + buildSystem: buildSystem, + throwIfCommandFails: throwIfCommandFails, ) - func creatingSanitizers(sanitizer: Sanitizer) throws { - #expect(sanitizer == Sanitizer(argument: sanitizer.shortName)) - } - - @Test - func invalidSanitizer() throws { - #expect(Sanitizer(argument: "invalid") == nil) - } } -@Suite( - .tags( - Tag.TestSize.large, - Tag.Feature.Command.Build, - ), -) -struct BuildCommandTestCases { - - @discardableResult - func execute( - _ args: [String] = [], - environment: Environment? = nil, - packagePath: AbsolutePath? = nil, - configuration: BuildConfiguration = .debug, - buildSystem: BuildSystemProvider.Kind, - throwIfCommandFails: Bool = true, - ) async throws -> (stdout: String, stderr: String) { - - return try await executeSwiftBuild( - packagePath, +fileprivate func build( + _ args: [String], + packagePath: AbsolutePath? = nil, + configuration: BuildConfiguration, + cleanAfterward: Bool = true, + buildSystem: BuildSystemProvider.Kind, +) async throws -> BuildResult { + do { + let (stdout, stderr) = try await execute(args, packagePath: packagePath,configuration: configuration, buildSystem: buildSystem,) + defer { + } + let (binPathOutput, _) = try await execute( + ["--show-bin-path"], + packagePath: packagePath, configuration: configuration, - extraArgs: args, - env: environment, buildSystem: buildSystem, - throwIfCommandFails: throwIfCommandFails, ) - } - - func build( - _ args: [String], - packagePath: AbsolutePath? = nil, - configuration: BuildConfiguration = .debug, - cleanAfterward: Bool = true, - buildSystem: BuildSystemProvider.Kind, - ) async throws -> BuildResult { - do { - // let buildConfigurationArguments = isRelease ? ["-c", "release"] : [] - let (stdout, stderr) = try await execute(args, packagePath: packagePath,configuration: configuration, buildSystem: buildSystem,) - defer { + let binPath = try AbsolutePath(validating: binPathOutput.trimmingCharacters(in: .whitespacesAndNewlines)) + let binContents = try localFileSystem.getDirectoryContents(binPath).filter { + guard let contents = try? localFileSystem.getDirectoryContents(binPath.appending(component: $0)) else { + return true } - let (binPathOutput, _) = try await execute( - ["--show-bin-path"], - packagePath: packagePath, - configuration: configuration, - buildSystem: buildSystem, - ) - let binPath = try AbsolutePath(validating: binPathOutput.trimmingCharacters(in: .whitespacesAndNewlines)) - let binContents = try localFileSystem.getDirectoryContents(binPath).filter { - guard let contents = try? localFileSystem.getDirectoryContents(binPath.appending(component: $0)) else { - return true - } - // Filter directories which only contain an output file map since we didn't build anything for those which - // is what `binContents` is meant to represent. - return contents != ["output-file-map.json"] - } - var moduleContents: [String] = [] - if buildSystem == .native { - moduleContents = (try? localFileSystem.getDirectoryContents(binPath.appending(component: "Modules"))) ?? [] - } else { - let moduleDirs = (try? localFileSystem.getDirectoryContents(binPath).filter { - $0.hasSuffix(".swiftmodule") - }) ?? [] - for dir: String in moduleDirs { - moduleContents += - (try? localFileSystem.getDirectoryContents(binPath.appending(component: dir)).map { "\(dir)/\($0)" }) ?? [] - } + // Filter directories which only contain an output file map since we didn't build anything for those which + // is what `binContents` is meant to represent. + return contents != ["output-file-map.json"] + } + var moduleContents: [String] = [] + if buildSystem == .native { + moduleContents = (try? localFileSystem.getDirectoryContents(binPath.appending(component: "Modules"))) ?? [] + } else { + let moduleDirs = (try? localFileSystem.getDirectoryContents(binPath).filter { + $0.hasSuffix(".swiftmodule") + }) ?? [] + for dir: String in moduleDirs { + moduleContents += + (try? localFileSystem.getDirectoryContents(binPath.appending(component: dir)).map { "\(dir)/\($0)" }) ?? [] } + } - if cleanAfterward { - try await executeSwiftPackage( - packagePath, - extraArgs: ["clean"], - buildSystem: buildSystem - ) - } - return BuildResult( - binPath: binPath, - stdout: stdout, - stderr: stderr, - binContents: binContents, - moduleContents: moduleContents + if cleanAfterward { + try await executeSwiftPackage( + packagePath, + extraArgs: ["clean"], + buildSystem: buildSystem + ) + } + return BuildResult( + binPath: binPath, + stdout: stdout, + stderr: stderr, + binContents: binContents, + moduleContents: moduleContents + ) + } catch { + if cleanAfterward { + try await executeSwiftPackage( + packagePath, + extraArgs: ["clean"], + buildSystem: buildSystem ) - } catch { - if cleanAfterward { - try await executeSwiftPackage( - packagePath, - extraArgs: ["clean"], - buildSystem: buildSystem - ) - } - throw error } + throw error } +} + +@Suite( + .tags( + Tag.TestSize.large, + Tag.Feature.Command.Build, + ), +) +struct BuildCommandTestCases { + @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func usage(buildSystem: BuildSystemProvider.Kind) async throws { - let stdout = try await execute(["-help"], buildSystem: buildSystem).stdout + func usage( + data: BuildData, + ) async throws { + let stdout = try await execute(["-help"], configuration: data.config, buildSystem: data.buildSystem).stdout #expect(stdout.contains("USAGE: swift build")) } @Test( - arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func binSymlink( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration, + buildData: BuildData, ) async throws { + let buildSystem = buildData.buildSystem + let configuration = buildData.config // Test is not implemented for Xcode build system try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) let targetPath = try fullPath.appending(components: buildSystem.binPath(for: configuration)) - let path = try await self.execute(["--show-bin-path"], packagePath: fullPath, configuration: configuration, buildSystem: buildSystem).stdout.trimmingCharacters(in: .whitespacesAndNewlines) + let path = try await execute( + ["--show-bin-path"], + packagePath: fullPath, + configuration: configuration, + buildSystem: buildSystem, + ).stdout.trimmingCharacters(in: .whitespacesAndNewlines) #expect( AbsolutePath(path).pathString == targetPath.pathString ) @@ -174,44 +163,67 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func seeAlso(buildSystem: BuildSystemProvider.Kind) async throws { - let stdout = try await execute(["--help"], buildSystem: buildSystem).stdout + func seeAlso( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout #expect(stdout.contains("SEE ALSO: swift run, swift package, swift test")) } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func commandDoesNotEmitDuplicateSymbols(buildSystem: BuildSystemProvider.Kind) async throws { + func commandDoesNotEmitDuplicateSymbols( + data: BuildData, + ) async throws { let duplicateSymbolRegex = try #require(duplicateSymbolRegex) - let (stdout, stderr) = try await execute(["--help"], buildSystem: buildSystem) + let (stdout, stderr) = try await execute( + ["--help"], + configuration: data.config, + buildSystem: data.buildSystem, + ) #expect(!stdout.contains(duplicateSymbolRegex)) #expect(!stderr.contains(duplicateSymbolRegex)) } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func version(buildSystem: BuildSystemProvider.Kind) async throws { - let stdout = try await execute(["--version"], buildSystem: buildSystem).stdout + func version( + data: BuildData, + ) async throws { + let stdout = try await execute( + ["--version"], + configuration: data.config, + buildSystem: data.buildSystem, + ).stdout let expectedRegex = try Regex(#"Swift Package Manager -( \w+ )?\d+.\d+.\d+(-\w+)?"#) #expect(stdout.contains(expectedRegex)) } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func importOfMissedDepWarning(buildSystem: BuildSystemProvider.Kind) async throws { + func importOfMissedDepWarning( + buildData: BuildData, + ) async throws { + let buildSystem = buildData.buildSystem + let configuration = buildData.config try await withKnownIssue("SWBINTTODO: Test fails because the warning message regarding missing imports is expected to be more verbose and actionable at the SwiftPM level with mention of the involved targets. This needs to be investigated. See case targetDiagnostic(TargetDiagnosticInfo) as a message type that may help.") { try await fixture(name: "Miscellaneous/ImportOfMissingDependency") { path in let fullPath = try resolveSymlinks(path) let error = await #expect(throws: SwiftPMError.self ) { - try await self.build( + try await build( ["--explicit-target-dependency-import-check=warn"], packagePath: fullPath, + configuration: configuration, buildSystem: buildSystem, ) } @@ -231,16 +243,21 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func importOfMissedDepWarningVerifyingErrorFlow(buildSystem: BuildSystemProvider.Kind) async throws { + func importOfMissedDepWarningVerifyingErrorFlow( + data: BuildData + ) async throws { + let buildSystem = data.buildSystem + let config = data.config try await withKnownIssue("SWBINTTODO: Test fails because the warning message regarding missing imports is expected to be more verbose and actionable at the SwiftPM level with mention of the involved targets. This needs to be investigated. See case targetDiagnostic(TargetDiagnosticInfo) as a message type that may help.") { try await fixture(name: "Miscellaneous/ImportOfMissingDependency") { path in let fullPath = try resolveSymlinks(path) let error = await #expect(throws: SwiftPMError.self ) { - try await self.build( + try await build( ["--explicit-target-dependency-import-check=error"], packagePath: fullPath, + configuration: config, buildSystem: buildSystem, ) } @@ -260,13 +277,20 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) - func importOfMissedDepWarningVerifyingDefaultDoesNotRunTheCheck(buildSystem: BuildSystemProvider.Kind) async throws { + func importOfMissedDepWarningVerifyingDefaultDoesNotRunTheCheck( + data: BuildData, + ) async throws { try await fixture(name: "Miscellaneous/ImportOfMissingDependency") { path in let fullPath = try resolveSymlinks(path) let error = await #expect(throws: SwiftPMError.self ) { - try await self.build([], packagePath: fullPath, buildSystem: buildSystem) + try await build( + [], + packagePath: fullPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) } guard case SwiftPMError.executionFailure(_, _, let stderr) = try #require(error) else { Issue.record("Expected error did not occur") @@ -291,7 +315,7 @@ struct BuildCommandTestCases { try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) // Test symlink. - try await self.execute(packagePath: fullPath, configuration: configuration, buildSystem: buildSystem) + try await execute(packagePath: fullPath, configuration: configuration, buildSystem: buildSystem) let actualDebug = try resolveSymlinks(fullPath.appending(components: buildSystem.binPath(for: configuration))) let expectedDebug = try fullPath.appending(components: buildSystem.binPath(for: configuration)) #expect(actualDebug == expectedDebug) @@ -302,61 +326,88 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + .IssueWindowsLongPath, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildExistingExecutableProductIsSuccessfull( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { - try await withKnownIssue("Failures possibly due to long file paths") { + try await withKnownIssue("Failures possibly due to long file paths", isIntermittent: true) { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) - let result = try await build(["--product", "exec1"], packagePath: fullPath, buildSystem: buildSystem,) + let result = try await build( + ["--product", "exec1"], + packagePath: fullPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) #expect(result.binContents.contains(executableName("exec1"))) #expect(!result.binContents.contains("exec2.build")) } } when: { - ProcessInfo.hostOperatingSystem == .windows && buildSystem == .swiftbuild + ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild } } @Test( - .SWBINTTODO("Found multiple targets named 'lib1'"), - arguments: SupportedBuildSystemOnPlatform, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9137", relationship: .defect), + .IssueWindowsCannotSaveAttachment, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildExistingLibraryProductIsSuccessfull( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { - try await withKnownIssue("Found multiple targets named 'lib1'") { + let buildSystem = data.buildSystem + try await withKnownIssue(isIntermittent: true) { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) - let (_, stderr) = try await execute(["--product", "lib1"], packagePath: fullPath, buildSystem: buildSystem,) - if buildSystem != .xcode { - #expect( - stderr.contains( - "'--product' cannot be used with the automatic product 'lib1'; building the default target instead" - ) - ) + let (_, stderr) = try await execute( + ["--product", "lib1"], + packagePath: fullPath, + configuration: data.config, + buildSystem: buildSystem, + ) + switch buildSystem { + case .native, .swiftbuild: + withKnownIssue("Found multiple targets named 'lib1'") { + #expect( + stderr.contains( + "'--product' cannot be used with the automatic product 'lib1'; building the default target instead" + ) + ) + } when: { + .swiftbuild == buildSystem + } + case .xcode: + // Do nothing. + break } } } when: { - .swiftbuild == buildSystem + ProcessInfo.hostOperatingSystem == .windows && buildSystem == .swiftbuild } } @Test( - .SWBINTTODO("Could not find target named 'exec2'"), - arguments: SupportedBuildSystemOnPlatform, + .issue("https://github.com/swiftlang/swift-package-manager/issues/9138", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildExistingTargetIsSuccessfull( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem try await withKnownIssue("Could not find target named 'exec2'") { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) - let result = try await build(["--target", "exec2"], packagePath: fullPath, buildSystem: buildSystem,) + let result = try await build( + ["--target", "exec2"], + packagePath: fullPath, + configuration: data.config, + buildSystem: buildSystem, + ) #expect(result.binContents.contains("exec2.build")) #expect(!result.binContents.contains(executableName("exec1"))) } @@ -369,16 +420,18 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildProductAndTargetsFailsWithAMutuallyExclusiveMessage( - buildSystem: BuildSystemProvider.Kind, + buildData: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--product", "exec1", "--target", "exec2"], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--product", "exec1", "--target", "exec2"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, ) } // THEN I expect a failure @@ -391,16 +444,18 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildProductAndTestsFailsWithAMutuallyExclusiveMessage( - buildSystem: BuildSystemProvider.Kind, + buildData: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--product", "exec1", "--build-tests"], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--product", "exec1", "--build-tests"], + packagePath: fixturePath, + configuration: buildData.config, + buildSystem: buildData.buildSystem, ) } // THEN I expect a failure @@ -413,16 +468,18 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildTargetAndTestsFailsWithAMutuallyExclusiveMessage( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--build-tests", "--target", "exec2"], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--build-tests", "--target", "exec2"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, ) } // THEN I expect a failure @@ -435,16 +492,18 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildProductTargetAndTestsFailsWithAMutuallyExclusiveMessage( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--build-tests", "--target", "exec2", "--product", "exec1"], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--build-tests", "--target", "exec2", "--product", "exec1"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, ) } // THEN I expect a failure @@ -452,22 +511,32 @@ struct BuildCommandTestCases { Issue.record("Incorrect error was raised.") return } - #expect(stderr.contains("error: '--product', '--target', and '--build-tests' are mutually exclusive")) + withKnownIssue(isIntermittent: true) { + #expect(stderr.contains("error: '--product', '--target', and '--build-tests' are mutually exclusive"), "stout: \(stdout)") + } when: { + ( + ProcessInfo.hostOperatingSystem == .windows && ( + data.buildSystem == .native + || (data.buildSystem == .swiftbuild && data.config == .debug) + )) + } } } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildUnknownProductFailsWithAppropriateMessage( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in let productName = "UnknownProduct" let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--product", productName], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--product", productName], + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, ) } // THEN I expect a failure @@ -476,30 +545,34 @@ struct BuildCommandTestCases { return } - if .native == buildSystem { - #expect(stderr.contains("error: no product named '\(productName)'")) - } else { - let expectedErrorMessageRegex = try Regex("error: Could not find target named '\(productName).*'") - #expect( - stderr.contains(expectedErrorMessageRegex), - "expect log not emitted.\nstdout: '\(stdout)'\n\nstderr: '\(stderr)'", - ) + switch data.buildSystem { + case .native: + #expect(stderr.contains("error: no product named '\(productName)'")) + case .swiftbuild, .xcode: + let expectedErrorMessageRegex = try Regex("error: Could not find target named '\(productName).*'") + #expect( + stderr.contains(expectedErrorMessageRegex), + "expect log not emitted.\nstdout: '\(stdout)'\n\nstderr: '\(stderr)'", + ) } } } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildUnknownTargetFailsWithAppropriateMessage( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { try await fixture(name: "Miscellaneous/MultipleExecutables") { fixturePath in + let buildSystem = data.buildSystem let targetName = "UnknownTargetName" let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( - ["--target", targetName], - packagePath: fixturePath,buildSystem: buildSystem, + try await execute( + ["--target", targetName], + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, ) } // THEN I expect a failure @@ -508,10 +581,11 @@ struct BuildCommandTestCases { return } let expectedErrorMessage: String - if .native == buildSystem { - expectedErrorMessage = "error: no target named '\(targetName)'" - } else { - expectedErrorMessage = "error: Could not find target named '\(targetName)'" + switch buildSystem { + case .native: + expectedErrorMessage = "error: no target named '\(targetName)'" + case .swiftbuild, .xcode: + expectedErrorMessage = "error: Could not find target named '\(targetName)'" } #expect( stderr.contains(expectedErrorMessage), @@ -521,43 +595,43 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ["ClangExecSingleFile", "SwiftExecSingleFile", "SwiftExecMultiFile"], ) - func atMainSupport(buildSystem: BuildSystemProvider.Kind) async throws { - try await withKnownIssue("SWBINTTODO: File not found or missing libclang errors on non-macOS platforms. This needs to be investigated") { + func atMainSupport( + data: BuildData, + executable: String, + ) async throws { + let buildSystem = data.buildSystem + let config = data.config + try await withKnownIssue( + "SWBINTTODO: File not found or missing libclang errors on non-macOS platforms. This needs to be investigated", + isIntermittent: true, + ) { try await fixture(name: "Miscellaneous/AtMainSupport") { fixturePath in let fullPath = try resolveSymlinks(fixturePath) - - do { - let result = try await build(["--product", "ClangExecSingleFile"], packagePath: fullPath, buildSystem: buildSystem) - #expect(result.binContents.contains(executableName("ClangExecSingleFile"))) - } - - do { - let result = try await build(["--product", "SwiftExecSingleFile"], packagePath: fullPath, buildSystem: buildSystem) - #expect(result.binContents.contains(executableName("SwiftExecSingleFile"))) - } - - do { - let result = try await build(["--product", "SwiftExecMultiFile"], packagePath: fullPath, buildSystem: buildSystem) - #expect(result.binContents.contains(executableName("SwiftExecMultiFile"))) - } + let result = try await build(["--product", executable], packagePath: fullPath, configuration: config, buildSystem: buildSystem) + #expect(result.binContents.contains(executableName(executable))) } } when: { - ![.macOS, .linux].contains(ProcessInfo.hostOperatingSystem) && buildSystem == .swiftbuild + ProcessInfo.hostOperatingSystem == .windows && buildSystem == .swiftbuild } } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func nonReachableProductsAndTargetsFunctional( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { try await fixture(name: "Miscellaneous/UnreachableTargets") { fixturePath in let aPath = fixturePath.appending("A") - let result = try await build([], packagePath: aPath, buildSystem: buildSystem) + let result = try await build( + [], + packagePath: aPath, + configuration: data.config, + buildSystem: data.buildSystem, + ) #expect(!result.binContents.contains("bexec")) #expect(!result.binContents.contains("BTarget2.build")) #expect(!result.binContents.contains("cexec")) @@ -566,20 +640,24 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) func nonReachableProductsAndTargetsFunctionalWhereDependencyContainsADependentProducts( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { - // skipped on Xcode - // known failures in SwiftBuild + let buildSystem = data.buildSystem try await withKnownIssue("SWBINTTODO: Test failed. This needs to be investigated") { try await fixture(name: "Miscellaneous/UnreachableTargets") { fixturePath in let aPath = fixturePath.appending("A") // Dependency contains a dependent product - let result = try await build(["--product", "bexec"], packagePath: aPath, buildSystem: buildSystem) + let result = try await build( + ["--product", "bexec"], + packagePath: aPath, + configuration: data.config, + buildSystem: buildSystem, + ) #expect(result.binContents.contains("BTarget2.build")) #expect(result.binContents.contains(executableName("bexec"))) #expect(!result.binContents.contains(executableName("aexec"))) @@ -603,28 +681,35 @@ struct BuildCommandTestCases { #expect(!result.moduleContents.contains("CTarget.swiftinterface")) } } when: { - buildSystem != .native + buildSystem == .swiftbuild } } @Test( - arguments: SupportedBuildSystemOnPlatform, + .issue("https://github.com/swiftlang/swift-package-manager/pull/9130", relationship: .fixedBy), + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func parseableInterfaces( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem try await fixture(name: "Miscellaneous/ParseableInterfaces") { fixturePath in - try await withKnownIssue { - let result = try await build(["--enable-parseable-module-interfaces"], packagePath: fixturePath, buildSystem: buildSystem) + try await withKnownIssue(isIntermittent: ProcessInfo.hostOperatingSystem == .windows) { + let result = try await build( + ["--enable-parseable-module-interfaces"], + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, + ) switch buildSystem { case .native: #expect(result.moduleContents.contains("A.swiftinterface")) #expect(result.moduleContents.contains("B.swiftinterface")) - default: - let moduleARegex = try Regex(#"A[.]swiftmodule[/].*[.]swiftinterface"#) - let moduleBRegex = try Regex(#"B[.]swiftmodule[/].*[.]swiftmodule"#) - #expect(result.moduleContents.contains { $0.contains(moduleARegex) }) - #expect(result.moduleContents.contains { $0.contains(moduleBRegex) }) + case .swiftbuild, .xcode: + let moduleARegex = try Regex(#"A[.]swiftmodule[/].*[.]swiftinterface"#) + let moduleBRegex = try Regex(#"B[.]swiftmodule[/].*[.]swiftmodule"#) + #expect(result.moduleContents.contains { $0.contains(moduleARegex) }) + #expect(result.moduleContents.contains { $0.contains(moduleBRegex) }) } } when: { // errors with SwiftBuild on Windows possibly due to long path on windows only for swift build @@ -634,19 +719,25 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func automaticParseableInterfacesWithLibraryEvolution( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem try await withKnownIssue { try await fixture(name: "Miscellaneous/LibraryEvolution") { fixturePath in - let result = try await build([], packagePath: fixturePath, buildSystem: buildSystem) + let result = try await build( + [], + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, + ) switch buildSystem { case .native: #expect(result.moduleContents.contains("A.swiftinterface")) #expect(result.moduleContents.contains("B.swiftinterface")) - default: + case .swiftbuild, .xcode: let moduleARegex = try Regex(#"A[.]swiftmodule[/].*[.]swiftinterface"#) let moduleBRegex = try Regex(#"B[.]swiftmodule[/].*[.]swiftmodule"#) withKnownIssue("SWBINTTODO: Test failed because of missing 'A.swiftmodule/*.swiftinterface' files") { @@ -663,16 +754,21 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildCompleteMessage( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem try await withKnownIssue { try await fixture(name: "DependencyResolution/Internal/Simple") { fixturePath in let buildCompleteRegex = try Regex(#"Build complete!\s?(\([0-9]*\.[0-9]*\s*s(econds)?\))?"#) do { - let result = try await execute(packagePath: fixturePath, buildSystem: buildSystem) + let result = try await execute( + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, + ) // This test fails to match the 'Compiling' regex; rdar://101815761 // XCTAssertMatch(result.stdout, .regex("\\[[1-9][0-9]*\\/[1-9][0-9]*\\] Compiling")) let lines = result.stdout.split(whereSeparator: { $0.isNewline }) @@ -682,12 +778,20 @@ struct BuildCommandTestCases { do { // test second time, to stabilize the cache - try await self.execute(packagePath: fixturePath, buildSystem: buildSystem) + try await execute( + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, + ) } do { // test third time, to make sure message is presented even when nothing to build (cached) - let result = try await execute(packagePath: fixturePath, buildSystem: buildSystem) + let result = try await execute( + packagePath: fixturePath, + configuration: data.config, + buildSystem: buildSystem, + ) // This test fails to match the 'Compiling' regex; rdar://101815761 // XCTAssertNoMatch(result.stdout, .regex("\\[[1-9][0-9]*\\/[1-9][0-9]*\\] Compiling")) let lines = result.stdout.split(whereSeparator: { $0.isNewline }) @@ -696,17 +800,18 @@ struct BuildCommandTestCases { } } } when: { - buildSystem == .swiftbuild && ((ProcessInfo.hostOperatingSystem == .windows)) + buildSystem == .swiftbuild && (ProcessInfo.hostOperatingSystem == .windows) } } @Test( - arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildStartMessage( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem + let configuration = data.config try await fixture(name: "DependencyResolution/Internal/Simple") { fixturePath in let result = try await execute([], packagePath: fixturePath, configuration: configuration, buildSystem: buildSystem, throwIfCommandFails: false) let expectedString: String @@ -717,22 +822,27 @@ struct BuildCommandTestCases { expectedString = "production" } - withKnownIssue("Xcode build system does not emit the build started message.") { - #expect( - result.stdout.contains("Building for \(expectedString)"), - "expect log not emitted. got stdout: '\(result.stdout)'\n\nstderr '\(result.stderr)'") - } when: { - buildSystem == .xcode + switch buildSystem { + case .native, .swiftbuild: + #expect( + result.stdout.contains("Building for \(expectedString)"), + "expect log not emitted. got stdout: '\(result.stdout)'\n\nstderr '\(result.stderr)'", + ) + case .xcode: + // Xcode build system does not emit the build started message. + break } } } @Test( - arguments: SupportedBuildSystemOnPlatform, + .IssueWindowsLongPath, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func buildSystemDefaultSettings( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem try await withKnownIssue("Sometimes failed to build due to a possible path issue", isIntermittent: true) { try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in // try await building using XCBuild with default parameters. This should succeed. We build verbosely so we get @@ -740,7 +850,7 @@ struct BuildCommandTestCases { let output: (stdout: String, stderr: String) = try await execute( ["-v"], packagePath: fixturePath, - configuration: .debug, + configuration: data.config, buildSystem: buildSystem, ) @@ -756,18 +866,20 @@ struct BuildCommandTestCases { #expect(output.stdout.contains("Build complete!")) } } when: { - buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows + (buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows) + || (buildSystem == .native && data.config == .release && ProcessInfo.hostOperatingSystem == .windows) } } @Test( .disabled("Disabled for now because it is hitting 'IR generation failure: Cannot read legacy layout file' in CI (rdar://88828632)"), - arguments: [BuildSystemProvider.Kind.swiftbuild, .xcode], BuildConfiguration.allCases + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild, .xcode]), ) func xcodeBuildSystemWithAdditionalBuildFlags( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration + data: BuildData ) async throws { + let configuration = data.config + let buildSystem = data.buildSystem try await fixture(name: "ValidLayouts/SingleModule/ExecutableMixed") { fixturePath in // try await building using XCBuild with additional flags. This should succeed. We build verbosely so we get // full command lines. @@ -794,20 +906,22 @@ struct BuildCommandTestCases { @Test( .requireHostOS(.macOS), - arguments: [BuildSystemProvider.Kind.xcode], + arguments: getBuildData(for: [BuildSystemProvider.Kind.swiftbuild, .xcode]), ) func buildSystemOverrides( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { + let buildSystem = data.buildSystem + let config = data.config try await withKnownIssue { try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in let swiftCompilerPath = try UserToolchain.default.swiftCompilerPath // try await building without specifying overrides. This should succeed, and should use the default // compiler path. - let defaultOutput = try await self.execute( + let defaultOutput = try await execute( ["--vv"], packagePath: fixturePath, - configuration: .debug, + configuration: config, buildSystem: buildSystem, ).stdout #expect(defaultOutput.contains(swiftCompilerPath.pathString)) @@ -816,14 +930,14 @@ struct BuildCommandTestCases { // we need to set the executable to use for the manifest itself to the default one, since it defaults to // SWIFT_EXEC if not provided. let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( + try await execute( ["--vv"], environment: [ "SWIFT_EXEC": "/usr/bin/false", "SWIFT_EXEC_MANIFEST": swiftCompilerPath.pathString, ], packagePath: fixturePath, - configuration: .debug, + configuration: config, buildSystem: buildSystem, ) } @@ -840,12 +954,13 @@ struct BuildCommandTestCases { } @Test( - arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func printLLBuildManifestJobGraph( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration + data: BuildData, ) async throws { + let buildSystem = data.buildSystem + let configuration = data.config try await fixture(name: "DependencyResolution/Internal/Simple") { fixturePath in let output = try await execute( ["--print-manifest-job-graph"], @@ -895,12 +1010,13 @@ struct BuildCommandTestCases { @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8659", "SWIFT_EXEC override is not working"), .SWBINTTODO("Test fails because the dummy-swiftc used in the test isn't accepted by swift-build. This needs to be investigated"), - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func swiftGetVersion( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { - + let buildSystem = data.buildSystem + let config = data.config try await fixture(name: "Miscellaneous/Simple") { fixturePath in func findSwiftGetVersionFile() throws -> AbsolutePath { let buildArenaPath = fixturePath.appending(components: ".build", "debug") @@ -918,13 +1034,20 @@ struct BuildCommandTestCases { "CUSTOM_SWIFT_VERSION": "1.0", ] - try await withKnownIssue("https://github.com/swiftlang/swift-package-manager/issues/8659, SWIFT_EXEC override is not working"){ - - + try await withKnownIssue( + "https://github.com/swiftlang/swift-package-manager/issues/8659, SWIFT_EXEC override is not working", + isIntermittent: (buildSystem == .native && config == .release) + ){ // Build with a swiftc that returns version 1.0, we expect a successful build which compiles our one source // file. do { - let result = try await execute(["--verbose"], environment: environment, packagePath: fixturePath, buildSystem: buildSystem) + let result = try await execute( + ["--verbose"], + environment: environment, + packagePath: fixturePath, + configuration: config, + buildSystem: buildSystem, + ) #expect( result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task missing from build result: \(result.stdout)", @@ -941,7 +1064,13 @@ struct BuildCommandTestCases { // Build again with that same version, we do not expect any compilation tasks. do { - let result = try await execute(["--verbose"], environment: environment, packagePath: fixturePath, buildSystem: buildSystem) + let result = try await execute( + ["--verbose"], + environment: environment, + packagePath: fixturePath, + configuration: config, + buildSystem: buildSystem, + ) #expect( !result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task present in build result: \(result.stdout)", @@ -959,7 +1088,13 @@ struct BuildCommandTestCases { // Build again with a swiftc that returns version 2.0, we expect compilation happening once more. do { environment["CUSTOM_SWIFT_VERSION"] = "2.0" - let result = try await execute(["--verbose"], environment: environment, packagePath: fixturePath, buildSystem: buildSystem) + let result = try await execute( + ["--verbose"], + environment: environment, + packagePath: fixturePath, + configuration: config, + buildSystem: buildSystem, + ) #expect( result.stdout.contains("\(dummySwiftcPath.pathString) -module-name"), "compilation task missing from build result: \(result.stdout)", @@ -974,7 +1109,9 @@ struct BuildCommandTestCases { #expect(actualVersion == "2.0") } } when: { - (ProcessInfo.hostOperatingSystem == .windows) || ([.xcode, .swiftbuild].contains(buildSystem)) + (ProcessInfo.hostOperatingSystem == .windows) + || ([.xcode, .swiftbuild].contains(buildSystem)) + || (buildSystem == .native && config == .release) } } } @@ -991,18 +1128,18 @@ struct BuildCommandTestCases { #if os(macOS) // try await building with default parameters. This should succeed. We build verbosely so we get full command // lines. - var buildResult = try await build(["-v"], packagePath: fixturePath, buildSystem: buildSystem,) + var buildResult = try await build(["-v"], packagePath: fixturePath, configuration: .debug, buildSystem: buildSystem,) // TODO verification of the ad-hoc code signing can be done by `swift run` of the executable in these cases once swiftbuild build system is working with that #expect(buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build(["-v"], packagePath: fixturePath, configuration:.debug, buildSystem: buildSystem,) + buildResult = try await build(["-v"], packagePath: fixturePath, configuration:.debug, buildSystem: buildSystem,) #expect(buildResult.stdout.contains("codesign --force --sign - --entitlements")) // Build with different combinations of the entitlement flag and debug/release build configurations. - buildResult = try await self.build( + buildResult = try await build( ["--enable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .release, @@ -1011,7 +1148,7 @@ struct BuildCommandTestCases { #expect(buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build( + buildResult = try await build( ["--enable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .debug, @@ -1020,7 +1157,7 @@ struct BuildCommandTestCases { #expect(buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build( + buildResult = try await build( ["--disable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .debug, @@ -1029,7 +1166,7 @@ struct BuildCommandTestCases { #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build( + buildResult = try await build( ["--disable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .release, @@ -1038,15 +1175,15 @@ struct BuildCommandTestCases { #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) #else - var buildResult = try await self.build(["-v"], packagePath: fixturePath, buildSystem: buildSystem,) + var buildResult = try await build(["-v"], packagePath: fixturePath, configuration: .debug, buildSystem: buildSystem,) #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build(["-v"], packagePath: fixturePath, configuration: .release,buildSystem: buildSystem,) + buildResult = try await build(["-v"], packagePath: fixturePath, configuration: .release,buildSystem: buildSystem,) #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) - buildResult = try await self.build( + buildResult = try await build( ["--disable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .release, @@ -1056,7 +1193,7 @@ struct BuildCommandTestCases { #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) #expect(buildResult.stderr.contains(SwiftCommandState.entitlementsMacOSWarning)) - buildResult = try await self.build( + buildResult = try await build( ["--enable-get-task-allow-entitlement", "-v"], packagePath: fixturePath, configuration: .release, @@ -1067,7 +1204,7 @@ struct BuildCommandTestCases { #expect(buildResult.stderr.contains(SwiftCommandState.entitlementsMacOSWarning)) #endif - buildResult = try await self.build(["-v"], packagePath: fixturePath, configuration: .release, buildSystem: buildSystem) + buildResult = try await build(["-v"], packagePath: fixturePath, configuration: .release, buildSystem: buildSystem) #expect(!buildResult.stdout.contains("codesign --force --sign - --entitlements")) } @@ -1079,229 +1216,134 @@ struct BuildCommandTestCases { @Test( .requireHostOS(.linux), .SWBINTTODO("Swift build doesn't currently ignore Linux main when linking on Linux. This needs further investigation."), - arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func ignoresLinuxMain( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration, + data: BuildData, ) async throws { - try await withKnownIssue { - try await fixture(name: "Miscellaneous/TestDiscovery/IgnoresLinuxMain") { fixturePath in - let buildResult = try await self.build( - ["-v", "--build-tests", "--enable-test-discovery"], - packagePath: fixturePath, - configuration: configuration, - cleanAfterward: false, - buildSystem: buildSystem, - ) - let testBinaryPath = buildResult.binPath.appending("IgnoresLinuxMainPackageTests.xctest") + let buildSystem = data.buildSystem + let configuration = data.config + try await fixture(name: "Miscellaneous/TestDiscovery/IgnoresLinuxMain") { fixturePath in + let buildResult = try await build( + ["-v", "--build-tests", "--enable-test-discovery"], + packagePath: fixturePath, + configuration: configuration, + cleanAfterward: false, + buildSystem: buildSystem, + ) + let testBinaryPath = buildResult.binPath.appending("IgnoresLinuxMainPackageTests.xctest") - _ = try await AsyncProcess.checkNonZeroExit(arguments: [testBinaryPath.pathString]) + switch buildSystem { + case .native: + expectFileExists(at: testBinaryPath) + _ = try await AsyncProcess.checkNonZeroExit(arguments: [testBinaryPath.pathString]) + case .swiftbuild: + // there are no additional check + break + case .xcode: + Issue.record("Test expectations have not been implemented.") } - } when: { - buildSystem == .swiftbuild } } - private static func buildSystemAndOutputLocation() throws -> [(BuildSystemProvider.Kind, Basics.RelativePath)] { - return try SupportedBuildSystemOnPlatform.map { buildSystem in + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnPlatform),[ + ["--verbose"], + ["-Xswiftc", "-diagnostic-style=llvm"], + ] + ) + func doesNotRebuildWithFlags( + data: BuildData, + flags: [String], + ) async throws { + func buildSystemAndOutputLocation( + buildSystem: BuildSystemProvider.Kind, + configuration: BuildConfiguration, + ) throws -> Basics.RelativePath { let triple = try UserToolchain.default.targetTriple.withoutVersion() let base = try RelativePath(validating: ".build") - let path = try base.appending(components: buildSystem.binPath(for: .debug, scratchPath: [])) + let path = try base.appending(components: buildSystem.binPath(for: configuration, scratchPath: [])) switch buildSystem { case .xcode: - return ( - buildSystem, - triple.platformName() == "macosx" ? path.appending("ExecutableNew") : path + return triple.platformName() == "macosx" ? path.appending("ExecutableNew") : path .appending("ExecutableNew.swiftmodule") .appending("Project") .appending("\(triple).swiftsourceinfo") - ) case .swiftbuild: - return ( - buildSystem, - triple.platformName() == "macosx" ? path.appending("ExecutableNew") : path + return triple.platformName() == "macosx" ? path.appending("ExecutableNew") : path .appending("ExecutableNew.swiftmodule") .appending("Project") .appending("\(triple).swiftsourceinfo") - ) case .native: - return ( - buildSystem, - path.appending("ExecutableNew.build") + return path.appending("ExecutableNew.build") .appending("main.swift.o") - ) - } - } - } - - @Test(arguments: try buildSystemAndOutputLocation()) - func doesNotRebuildWithVerboseFlag( - buildSystem: BuildSystemProvider.Kind, - outputFile: Basics.RelativePath - ) async throws { - try await withKnownIssue("Sometimes failed to build due to a possible path issue", isIntermittent: true) { - try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in - _ = try await self.build( - [], - packagePath: fixturePath, - cleanAfterward: false, - buildSystem: buildSystem, - ) - - let mainOFile = fixturePath.appending(outputFile) - let initialMainOMtime = try FileManager.default.attributesOfItem(atPath: mainOFile.pathString)[.modificationDate] as? Date - - _ = try await self.build( - ["--verbose"], - packagePath: fixturePath, - cleanAfterward: false, - buildSystem: buildSystem, - ) - - let subsequentMainOMtime = try FileManager.default.attributesOfItem(atPath: mainOFile.pathString)[.modificationDate] as? Date - #expect(initialMainOMtime == subsequentMainOMtime, "Expected no rebuild to occur when using the verbose flag, but the file was modified.") } - } when: { - buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows } - } - @Test(arguments: try buildSystemAndOutputLocation()) - func doesNotRebuildWithSwiftcArgsThatDontAffectIncrementalBuilds( - buildSystem: BuildSystemProvider.Kind, - outputFile: Basics.RelativePath - ) async throws { try await withKnownIssue("Sometimes failed to build due to a possible path issue", isIntermittent: true) { try await fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { fixturePath in - _ = try await self.build( + _ = try await build( [], packagePath: fixturePath, + configuration: data.config, cleanAfterward: false, - buildSystem: buildSystem, + buildSystem: data.buildSystem, ) - - let mainOFile = fixturePath.appending(outputFile) + let mainOFile = try fixturePath.appending(buildSystemAndOutputLocation(buildSystem: data.buildSystem, configuration: data.config)) let initialMainOMtime = try FileManager.default.attributesOfItem(atPath: mainOFile.pathString)[.modificationDate] as? Date - _ = try await self.build( - ["-Xswiftc", "-diagnostic-style=llvm"], + _ = try await build( + flags, packagePath: fixturePath, + configuration: data.config, cleanAfterward: false, - buildSystem: buildSystem, + buildSystem: data.buildSystem, ) let subsequentMainOMtime = try FileManager.default.attributesOfItem(atPath: mainOFile.pathString)[.modificationDate] as? Date - #expect(initialMainOMtime == subsequentMainOMtime, "Expected no rebuild to occur when supplying -diagnostic-style, but the file was modified.") - } - } when: { - buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows - } - } - - @Test( - .SWBINTTODO("Test failed because of missing plugin support in the PIF builder. This can be reinvestigated after the support is there."), - .tags( - Tag.Feature.CodeCoverage, - Tag.Feature.Command.Test, - ), - arguments: SupportedBuildSystemOnPlatform, - ) - func executingTestsWithCoverageWithoutCodeBuiltWithCoverageGeneratesAFailure( - buildSystem: BuildSystemProvider.Kind, - ) async throws { - try await withKnownIssue(isIntermittent: (ProcessInfo.hostOperatingSystem == .linux && buildSystem == .swiftbuild)) { - try await fixture(name: "Miscellaneous/TestDiscovery/Simple") { path in - _ = try await self.build( - ["--build-tests"], - packagePath: path, - cleanAfterward: false, - buildSystem: buildSystem, - ) - await #expect(throws: (any Error).self ) { - try await executeSwiftTest( - path, - extraArgs: [ - "--skip-build", - "--enable-code-coverage", - ], - throwIfCommandFails: true, - buildSystem: buildSystem, - ) - } + #expect(initialMainOMtime == subsequentMainOMtime, "Expected no rebuild to occur when using flags \(flags), but the file was modified.") } } when: { - buildSystem == .xcode || (buildSystem == .swiftbuild && [.linux, .windows].contains(ProcessInfo.hostOperatingSystem)) + data.buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows } } @Test( - .SWBINTTODO("Test failed because of missing plugin support in the PIF builder. This can be reinvestigated after the support is there."), - .tags( - Tag.Feature.CodeCoverage, - Tag.Feature.Command.Test, - ), - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) - func executingTestsWithCoverageWithCodeBuiltWithCoverageGeneratesCodecove( - buildSystem: BuildSystemProvider.Kind, + func parseAsLibraryCriteria( + buildData: BuildData, ) async throws { - // Test that enabling code coverage during building produces the expected folder. try await withKnownIssue { - try await fixture(name: "Miscellaneous/TestDiscovery/Simple") { path in - let buildResult = try await self.build( - ["--build-tests", "--enable-code-coverage"], - packagePath: path, - cleanAfterward: false, - buildSystem: buildSystem, - ) - try await executeSwiftTest( - path, - extraArgs: [ - "--skip-build", - "--enable-code-coverage", - ], - throwIfCommandFails: true, - buildSystem: buildSystem, - ) - let codeCovPath = buildResult.binPath.appending("codecov") - let codeCovFiles = try localFileSystem.getDirectoryContents(codeCovPath) - #expect(codeCovFiles.count > 0) - } - } when: { - [.swiftbuild, .xcode].contains(buildSystem) - } - } - - @Test(arguments: [BuildSystemProvider.Kind.native, .swiftbuild]) - func parseAsLibraryCriteria(buildSystem: BuildSystemProvider.Kind) async throws { - try await withKnownIssue { - try await fixture(name: "Miscellaneous/ParseAsLibrary") { fixturePath in + try await fixture(name: "Miscellaneous/ParseAsLibrary") { fixturePath in _ = try await executeSwiftBuild( fixturePath, - buildSystem: buildSystem, + configuration: buildData.config, + buildSystem: buildData.buildSystem, throwIfCommandFails: true ) } - } when: { - ProcessInfo.hostOperatingSystem == .windows && - buildSystem == .swiftbuild - } + } when: { + ProcessInfo.hostOperatingSystem == .windows && + buildData.buildSystem == .swiftbuild + } } @Test( - arguments: SupportedBuildSystemOnPlatform, + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func fatalErrorDisplayedCorrectNumberOfTimesWhenSingleXCTestHasFatalErrorInBuildCompilation( - buildSystem: BuildSystemProvider.Kind, + data: BuildData, ) async throws { let expected = 0 try await fixture(name: "Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary") { fixturePath in // WHEN swift-build --build-tests is executed" let error = await #expect(throws: SwiftPMError.self ) { - try await self.execute( + try await execute( ["--build-tests"], - packagePath: fixturePath,buildSystem: buildSystem, + packagePath: fixturePath, + configuration: data.config, + buildSystem: data.buildSystem, ) } // THEN I expect a failure @@ -1321,13 +1363,14 @@ struct BuildCommandTestCases { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8844"), - arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases + .issue("https://github.com/swiftlang/swift-package-manager/issues/8844", relationship: .defect), + arguments: getBuildData(for: SupportedBuildSystemOnPlatform), ) func swiftBuildQuietLogLevel( - buildSystem: BuildSystemProvider.Kind, - configuration: BuildConfiguration + data: BuildData, ) async throws { + let buildSystem = data.buildSystem + let configuration = data.config try await withKnownIssue { // GIVEN we have a simple test package try await fixture(name: "Miscellaneous/SwiftBuild") { fixturePath in @@ -1350,7 +1393,7 @@ struct BuildCommandTestCases { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8844"), + .issue("https://github.com/swiftlang/swift-package-manager/issues/8844", relationship: .defect), arguments: SupportedBuildSystemOnPlatform, BuildConfiguration.allCases ) func swiftBuildQuietLogLevelWithError( @@ -1383,17 +1426,18 @@ struct BuildCommandTestCases { return } - if buildSystem == .swiftbuild { - // THEN we should see output in stderr - #expect(stderr.isEmpty == false) - // AND no content in stdout - #expect(stdout.isEmpty) - } else { - // THEN we should see content in stdout - #expect(stdout.isEmpty == false) - // AND no output in stderr - #expect(stderr.isEmpty) - } + switch buildSystem { + case .swiftbuild: + // THEN we should see output in stderr + #expect(stderr.isEmpty == false) + // AND no content in stdout + #expect(stdout.isEmpty) + case .native, .xcode: + // THEN we should see content in stdout + #expect(stdout.isEmpty == false) + // AND no output in stderr + #expect(stderr.isEmpty) + } } } } diff --git a/Tests/CommandsTests/CoverageTests.swift b/Tests/CommandsTests/CoverageTests.swift new file mode 100644 index 00000000000..7a101165805 --- /dev/null +++ b/Tests/CommandsTests/CoverageTests.swift @@ -0,0 +1,233 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import Commands +import _InternalTestSupport +import var Basics.localFileSystem +import struct Basics.AbsolutePath +import enum PackageModel.BuildConfiguration +import struct SPMBuildCore.BuildSystemProvider +import Testing + +@Suite( + .tags( + .TestSize.large, + .Feature.CodeCoverage, + ) +) +struct CoverageTests { + @Test( + .SWBINTTODO("Test failed because of missing plugin support in the PIF builder. This can be reinvestigated after the support is there."), + .tags( + Tag.Feature.CodeCoverage, + Tag.Feature.Command.Test, + ), + arguments: SupportedBuildSystemOnAllPlatforms, + ) + func executingTestsWithCoverageWithoutCodeBuiltWithCoverageGeneratesAFailure( + buildSystem: BuildSystemProvider.Kind, + ) async throws { + let config = BuildConfiguration.debug + try await withKnownIssue(isIntermittent: (ProcessInfo.hostOperatingSystem == .linux && buildSystem == .swiftbuild)) { + try await fixture(name: "Miscellaneous/TestDiscovery/Simple") { path in + _ = try await executeSwiftBuild( + path, + configuration: config, + extraArgs: ["--build-tests"], + buildSystem: buildSystem, + ) + await #expect(throws: (any Error).self ) { + try await executeSwiftTest( + path, + configuration: config, + extraArgs: [ + "--skip-build", + "--enable-code-coverage", + ], + throwIfCommandFails: true, + buildSystem: buildSystem, + ) + } + } + } when: { + buildSystem == .swiftbuild && [.linux, .windows].contains(ProcessInfo.hostOperatingSystem) + } + } + + @Test( + .SWBINTTODO("Test failed because of missing plugin support in the PIF builder. This can be reinvestigated after the support is there."), + .IssueWindowsCannotSaveAttachment, + .tags( + Tag.Feature.CodeCoverage, + Tag.Feature.Command.Test, + ), + arguments: SupportedBuildSystemOnAllPlatforms, + ) + func executingTestsWithCoverageWithCodeBuiltWithCoverageGeneratesCodeCoverage( + buildSystem: BuildSystemProvider.Kind, + ) async throws { + let config = BuildConfiguration.debug + // Test that enabling code coverage during building produces the expected folder. + try await withKnownIssue(isIntermittent: true) { + try await fixture(name: "Miscellaneous/TestDiscovery/Simple") { path in + let codeCovPathString = try await executeSwiftTest( + path, + configuration: config, + extraArgs: [ + "--show-coverage-path", + ], + throwIfCommandFails: true, + buildSystem: buildSystem, + ).stdout.trimmingCharacters(in: .whitespacesAndNewlines) + + let codeCovPath = try AbsolutePath(validating: codeCovPathString) + + // WHEN we build with coverage enabled + try await withKnownIssue { + try await executeSwiftBuild( + path, + configuration: config, + extraArgs: ["--build-tests", "--enable-code-coverage"], + buildSystem: buildSystem, + ) + + // AND we test with coverag enabled and skip the build + try await executeSwiftTest( + path, + configuration: config, + extraArgs: [ + "--skip-build", + "--enable-code-coverage", + ], + buildSystem: buildSystem, + ) + + // THEN we expect the file to exists + expectFileExists(at: codeCovPath) + + // AND the parent directory is non empty + let codeCovFiles = try localFileSystem.getDirectoryContents(codeCovPath.parentDirectory) + #expect(codeCovFiles.count > 0) + } when: { + ProcessInfo.hostOperatingSystem == .linux && buildSystem == .swiftbuild + } + } + } when: { + ProcessInfo.hostOperatingSystem == .windows && buildSystem == .swiftbuild + } + } + + struct GenerateCoverageReportTestData { + // let buildData: BuildData + let fixtureName: String + let coverageFormat: CoverageFormat + } + + @Test( + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), [ + "Coverage/Simple", + "Miscellaneous/TestDiscovery/Simple", + ].flatMap { fixturePath in + // getBuildData(for: SupportedBuildSystemOnAllPlatforms).flatMap { buildData in + CoverageFormat.allCases.map { format in + GenerateCoverageReportTestData( + // buildData: buildData, + fixtureName: fixturePath, + coverageFormat: format, + ) + } + // } + }, + ) + func generateSingleCoverageReport( + buildData: BuildData, + testData: GenerateCoverageReportTestData, + ) async throws { + let fixtureName = testData.fixtureName + let coverageFormat = testData.coverageFormat + // let buildData = testData.buildData + try await fixture(name: fixtureName) { path in + let commonCoverageArgs = [ + "--coverage-format", + "\(coverageFormat)", + ] + let coveragePathString = try await executeSwiftTest( + path, + configuration: buildData.config, + extraArgs: [ + "--show-coverage-path", + ] + commonCoverageArgs, + throwIfCommandFails: true, + buildSystem: buildData.buildSystem, + ).stdout + let coveragePath = try AbsolutePath(validating: coveragePathString) + try #require(!localFileSystem.exists(coveragePath)) + + // WHEN we test with coverage enabled + try await withKnownIssue { + try await executeSwiftTest( + path, + configuration: buildData.config, + extraArgs: [ + "--enable-code-coverage", + ] + commonCoverageArgs, + throwIfCommandFails: true, + buildSystem: buildData.buildSystem, + ) + + // THEN we expect the file to exists + #expect(localFileSystem.exists(coveragePath)) + } when: { + (buildData.buildSystem == .swiftbuild && [.windows, .linux].contains(ProcessInfo.hostOperatingSystem)) + } + } + } + + @Test func generateMultipleCoverageReports() async throws { + Issue.record("Test needs to be implemented") + } + + @Test + func htmlReportOutputDirectory() async throws { + // Verify the output directory argument specified in the response file override the default location. + Issue.record(""" + Test needs to be implemented. + - Provide output directory that is a relative path + - provide output directory that is an absolute path + """) + } + + @Test + func htmlReportResponseFile() async throws { + // Verify the arguments specified in the response file are used. + Issue.record(""" + Test needs to be implemented. + - verify command line argument + """) + } + + @Suite + struct showCoveragePathTests { + @Test + func printPathModeTests() async throws { + Issue.record(""" + Test needs to be implemented. Need to implnente cross matix of: + - single/multiple formats + - print in text/json format + - specifying the same format type multiple times + - response file [overrides | does not override] output directory + """) + } + } + +} diff --git a/Tests/CommandsTests/RunCommandTests.swift b/Tests/CommandsTests/RunCommandTests.swift index 58d24bc7a97..1281596d867 100644 --- a/Tests/CommandsTests/RunCommandTests.swift +++ b/Tests/CommandsTests/RunCommandTests.swift @@ -91,8 +91,8 @@ struct RunCommandTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .SWBINTTODO("Test package fails to build on Windows"), arguments: SupportedBuildSystemOnPlatform, ) @@ -135,8 +135,8 @@ struct RunCommandTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, arguments: SupportedBuildSystemOnPlatform, ) func productArgumentPassing( @@ -238,8 +238,8 @@ struct RunCommandTests { @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, arguments: SupportedBuildSystemOnPlatform, ) func unreachableExecutable( diff --git a/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift b/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift new file mode 100644 index 00000000000..c7008f269b1 --- /dev/null +++ b/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 Testing +import enum PackageModel.Sanitizer + +@Suite( + .tags( + Tag.TestSize.small, + ), +) +struct SanitizerExtensionTests { + @Test( + arguments: Sanitizer.allCases + ) + func creatingSanitizers(sanitizer: Sanitizer) throws { + #expect(sanitizer == Sanitizer(argument: sanitizer.shortName)) + } + + @Test + func invalidSanitizer() throws { + #expect(Sanitizer(argument: "invalid") == nil) + } +} diff --git a/Tests/CommandsTests/TestCommandTests+Helpers.swift b/Tests/CommandsTests/TestCommandTests+Helpers.swift new file mode 100644 index 00000000000..81d397ec8ea --- /dev/null +++ b/Tests/CommandsTests/TestCommandTests+Helpers.swift @@ -0,0 +1,581 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// 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 Testing + +import struct Basics.AbsolutePath +import func Commands.getOutputDir +import enum Commands.CoverageFormat +import struct Commands.CoverageFormatOutput +import typealias Basics.StringError +import struct Commands.PlainTextEncoder + +@Suite( + .tags( + .TestSize.small, + ) +) +struct TestCommmandHelpersTests { + + @Suite + struct getOutputDirTests { + @Test( + arguments: [ + "", + """ + line1 + """, + """ + line1 + line2 + """, + """ + line1 + line2 + line3 + """, + ] + ) + func outputDirArgumentNotPresentReturnsNil( + content: String + ) async throws { + let actual = try getOutputDir(from: content) + + #expect(actual == nil) + } + + struct GetOutputDirTestData: Identifiable { + let content: String + let expected: AbsolutePath? + let id: String + } + + @Test( + arguments: [ + "=", + "\n", + " ", + " ", + " ", + ].map { sep in + return [ + GetOutputDirTestData( + content: """ + --output-dir\(sep)/Bar/baz + """, + expected: AbsolutePath("/Bar/baz"), + id: "Single argument with seperator '\(sep)'", + ), + GetOutputDirTestData( + content: """ + --output-dir\(sep)/Bar/baz + --output-dir\(sep)/this/should/win + """, + expected: AbsolutePath("/this/should/win"), + id: "Two output dir arguments with seperator '\(sep)' returns the last occurrence", + ), + GetOutputDirTestData( + content: """ + --output-dir\(sep)/Bar/baz + --output-dir\(sep)/what + --output-dir\(sep)/this/should/win + """, + expected: AbsolutePath("/this/should/win"), + id: "three output dir arguments with seperator '\(sep)' returns the last occurrence", + ), + GetOutputDirTestData( + content: """ + prefix + --output-dir\(sep)/Bar/baz + """, + expected: AbsolutePath("/Bar/baz"), + id: "seperator '\(sep)': with content prefix", + ), + GetOutputDirTestData( + content: """ + --output-dir\(sep)/Bar/baz + suffix + """, + expected: AbsolutePath("/Bar/baz"), + id: "seperator '\(sep)': with content suffix", + ), + GetOutputDirTestData( + content: """ + line_prefix + --output-dir\(sep)/Bar/baz + suffix + """, + expected: AbsolutePath("/Bar/baz"), + id: "seperator '\(sep)': with line content and suffix", + ), + GetOutputDirTestData( + content: """ + prefix--output-dir\(sep)/Bar/baz + """, + expected: nil, + id: "seperator '\(sep)': with line prefix (no space)", + ), + GetOutputDirTestData( + content: """ + prefix --output-dir\(sep)/Bar/baz + """, + expected: AbsolutePath("/Bar/baz"), + id: "seperator '\(sep)': with line prefix (which contains a space)", + ), + ] + }.flatMap { $0 }, + ) + func contentContainsOutputDirectoryReturnsCorrectPath( + data: GetOutputDirTestData, + ) async throws { + let actual = try getOutputDir(from: data.content) + + #expect(actual == data.expected) + } + + @Test func sample() async throws { + let logMessage = "ERROR: User 'john.doe' failed login attempt from IP 192.168.1.100." + + // Create a Regex with named captue groups for user and ipAddress + let regex = try! Regex("User '(?[a-zA-Z0-9.]+)' failed login attempt from IP (?\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})") + + // Find the first match in the log message + if let match = logMessage.firstMatch(of: regex) { + // Access the captured values using their named properties + // let username = match.user + // let ipAddress = match.ipAddress + + #expect(Bool(true)) + } else { + #expect(Bool(false)) + } + + } + } + + @Suite + struct CoverageFormatOutputTests { + + var validData: [CoverageFormat: AbsolutePath] { + [ + CoverageFormat.json: AbsolutePath("/some/path/json"), + CoverageFormat.html: AbsolutePath("/some/path/html"), + ] + } + + // MARK: - Initialization Tests + + @Test + func initEmpty() async throws { + let output = CoverageFormatOutput() + #expect(output.formats.isEmpty) + } + + @Test + func initWithData() async throws { + let output = CoverageFormatOutput(data: validData) + #expect(output.formats.count == 2) + #expect(output.formats.contains(CoverageFormat.json)) + #expect(output.formats.contains(CoverageFormat.html)) + } + + // MARK: - addFormat Tests + + @Test + func addFormatSuccess() async throws { + var output = CoverageFormatOutput() + let jsonPath = AbsolutePath("/path/to/json") + + try output.addFormat(CoverageFormat.json, path: jsonPath) + + #expect(output.formats.count == 1) + #expect(output.formats.contains(CoverageFormat.json)) + #expect(output[CoverageFormat.json] == jsonPath) + } + + @Test + func addFormatMultiple() async throws { + var output = CoverageFormatOutput() + let jsonPath = AbsolutePath("/path/to/json") + let htmlPath = AbsolutePath("/path/to/html") + + try output.addFormat(CoverageFormat.json, path: jsonPath) + try output.addFormat(CoverageFormat.html, path: htmlPath) + + #expect(output.formats.count == 2) + #expect(output.formats.contains(CoverageFormat.json)) + #expect(output.formats.contains(CoverageFormat.html)) + #expect(output[CoverageFormat.json] == jsonPath) + #expect(output[CoverageFormat.html] == htmlPath) + } + + @Test + func addFormatDuplicateThrowsError() async throws { + var output = CoverageFormatOutput() + let jsonPath1 = AbsolutePath("/path/to/json1") + let jsonPath2 = AbsolutePath("/path/to/json2") + + try output.addFormat(CoverageFormat.json, path: jsonPath1) + + #expect(throws: StringError("Coverage format 'json' already exists")) { + try output.addFormat(CoverageFormat.json, path: jsonPath2) + } + + // Verify original path is unchanged + #expect(output[CoverageFormat.json] == jsonPath1) + #expect(output.formats.count == 1) + } + + // MARK: - Subscript Tests + + @Test + func subscriptExistingFormat() async throws { + let output = CoverageFormatOutput(data: validData) + + #expect(output[CoverageFormat.json] == AbsolutePath("/some/path/json")) + #expect(output[CoverageFormat.html] == AbsolutePath("/some/path/html")) + } + + @Test + func subscriptNonExistentFormat() async throws { + let output = CoverageFormatOutput() + + #expect(output[CoverageFormat.json] == nil) + #expect(output[CoverageFormat.html] == nil) + } + + // MARK: - getPath Tests + + @Test + func getPathExistingFormat() async throws { + let output = CoverageFormatOutput(data: validData) + + let jsonPath = try output.getPath(for: CoverageFormat.json) + let htmlPath = try output.getPath(for: CoverageFormat.html) + + #expect(jsonPath == AbsolutePath("/some/path/json")) + #expect(htmlPath == AbsolutePath("/some/path/html")) + } + + @Test + func getPathNonExistentFormatThrowsError() async throws { + let output = CoverageFormatOutput() + + #expect(throws: StringError("Missing coverage format output path for 'json'")) { + try output.getPath(for: CoverageFormat.json) + } + + #expect(throws: StringError("Missing coverage format output path for 'html'")) { + try output.getPath(for: CoverageFormat.html) + } + } + + // MARK: - formats Property Tests + + @Test + func formatsEmptyWhenNoData() async throws { + let output = CoverageFormatOutput() + #expect(output.formats.isEmpty) + } + + @Test + func formatsReturnsSortedFormats() async throws { + let output = CoverageFormatOutput(data: validData) + let formats = output.formats + + #expect(formats.count == 2) + // Formats should be sorted alphabetically by raw value + #expect(formats == [CoverageFormat.html, CoverageFormat.json]) // html comes before json alphabetically + } + + @Test + func formatsAfterAddingFormats() async throws { + var output = CoverageFormatOutput() + + try output.addFormat(CoverageFormat.json, path: AbsolutePath("/json/path")) + #expect(output.formats == [CoverageFormat.json]) + + try output.addFormat(CoverageFormat.html, path: AbsolutePath("/html/path")) + #expect(output.formats == [CoverageFormat.html, CoverageFormat.json]) // sorted + } + + // MARK: - forEach Tests + + @Test + func forEachEmptyOutput() async throws { + let output = CoverageFormatOutput() + var iterationCount = 0 + + output.forEach { format, path in + iterationCount += 1 + } + + #expect(iterationCount == 0) + } + + @Test + func forEachWithData() async throws { + let output = CoverageFormatOutput(data: validData) + var results: [CoverageFormat: AbsolutePath] = [:] + + output.forEach { format, path in + results[format] = path + } + + #expect(results.count == 2) + #expect(results[CoverageFormat.json] == AbsolutePath("/some/path/json")) + #expect(results[CoverageFormat.html] == AbsolutePath("/some/path/html")) + } + + @Test + func forEachCanThrow() async throws { + let output = CoverageFormatOutput(data: validData) + + struct TestError: Error, Equatable { + let message: String + } + + #expect(throws: TestError(message: "test error")) { + try output.forEach { format, path in + if format == CoverageFormat.json { + throw TestError(message: "test error") + } + } + } + } + + // MARK: - Integration Tests + + @Test + func completeWorkflow() async throws { + // Start with empty output + var output = CoverageFormatOutput() + #expect(output.formats.isEmpty) + + // Add first format + let jsonPath = AbsolutePath("/coverage/reports/coverage.json") + try output.addFormat(CoverageFormat.json, path: jsonPath) + #expect(output.formats == [CoverageFormat.json]) + #expect(output[CoverageFormat.json] == jsonPath) + let actualJsonPath = try output.getPath(for: CoverageFormat.json) + #expect(actualJsonPath == jsonPath) + + // Add second format + let htmlPath = AbsolutePath("/coverage/reports/html") + try output.addFormat(CoverageFormat.html, path: htmlPath) + #expect(output.formats == [CoverageFormat.html, CoverageFormat.json]) // sorted + #expect(output[CoverageFormat.html] == htmlPath) + let actualHmtlPath = try output.getPath(for: CoverageFormat.html) + #expect(actualHmtlPath == htmlPath) + + // Verify forEach works + var collectedPaths: [CoverageFormat: AbsolutePath] = [:] + output.forEach { format, path in + collectedPaths[format] = path + } + #expect(collectedPaths.count == 2) + #expect(collectedPaths[CoverageFormat.json] == jsonPath) + #expect(collectedPaths[CoverageFormat.html] == htmlPath) + + // Verify duplicate add fails + #expect(throws: StringError("Coverage format 'json' already exists")) { + try output.addFormat(CoverageFormat.json, path: AbsolutePath("/different/path")) + } + + // Verify original data is preserved + #expect(output[CoverageFormat.json] == jsonPath) + #expect(output.formats.count == 2) + } + + // MARK: - Encoding Tests + + @Test("Encode as JSON with single format") + func encodeAsJSONSingle() throws { + let path = try AbsolutePath(validating: "/path/to/coverage.json") + var output = CoverageFormatOutput() + try output.addFormat(.json, path: path) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let jsonData = try encoder.encode(output) + let jsonString = String(decoding: jsonData, as: UTF8.self) + let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String] + + #expect(decoded["json"] == "/path/to/coverage.json") + #expect(decoded.count == 1) + } + + @Suite( + .tags( + .TestSize.small, + .Feature.Encoding, + ), + ) + struct EncodingTests { + @Suite + struct JsonEncodingTests { + @Test("Encode as JSON with multiple formats") + func encodeAsJSONMultiple() throws { + let jsonPath = try AbsolutePath(validating: "/path/to/coverage.json") + let htmlPath = try AbsolutePath(validating: "/path/to/coverage-html") + + var output = CoverageFormatOutput() + try output.addFormat(.json, path: jsonPath) + try output.addFormat(.html, path: htmlPath) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = [.prettyPrinted] + let jsonData = try encoder.encode(output) + let jsonString = String(decoding: jsonData, as: UTF8.self) + let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String] + + #expect(decoded["json"] == "/path/to/coverage.json") + #expect(decoded["html"] == "/path/to/coverage-html") + #expect(decoded.count == 2) + + // Verify it's properly formatted JSON + #expect(jsonString.contains("{\n")) + #expect(jsonString.contains("\n}")) + } + + @Test("Encode as JSON with empty data") + func encodeAsJSONEmpty() throws { + let output = CoverageFormatOutput() + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = [.prettyPrinted] + let jsonData = try encoder.encode(output) + let jsonString = String(decoding: jsonData, as: UTF8.self) + let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String] + + #expect(decoded.isEmpty) + #expect(jsonString.contains("{\n\n}") || jsonString.contains("{}")) + } + } + + @Suite + struct TextEncodingTests { + @Test( + "Encode as text with single format", + arguments: CoverageFormat.allCases + ) + func encodeAsTextSingle( + format: CoverageFormat, + ) throws { + let path = try AbsolutePath(validating: "/path/to/coverage.json") + var output = CoverageFormatOutput() + try output.addFormat(format, path: path) + + var encoder = PlainTextEncoder() + encoder.formattingOptions = [.prettyPrinted] + let textData = try encoder.encode(output) + let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + + // PlainTextEncoder capitalizes first letter of keys + let expectedFormat = format.rawValue.prefix(1).uppercased() + format.rawValue.dropFirst() + #expect(textString == "\(expectedFormat): /path/to/coverage.json") + } + + @Test("Encode as text with multiple formats") + func encodeAsTextMultiple() throws { + let jsonPath = try AbsolutePath(validating: "/path/to/coverage.json") + let htmlPath = try AbsolutePath(validating: "/path/to/coverage-html") + + var output = CoverageFormatOutput() + try output.addFormat(.json, path: jsonPath) + try output.addFormat(.html, path: htmlPath) + + var encoder = PlainTextEncoder() + encoder.formattingOptions = [.prettyPrinted] + let textData = try encoder.encode(output) + let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + + // Should be sorted by format name (html comes before json alphabetically) + // PlainTextEncoder capitalizes first letter of keys + #expect(textString == "Html: /path/to/coverage-html\nJson: /path/to/coverage.json") + } + + @Test("Encode as text with empty data") + func encodeAsTextEmpty() throws { + let output = CoverageFormatOutput() + + var encoder = PlainTextEncoder() + encoder.formattingOptions = [.prettyPrinted] + let textData = try encoder.encode(output) + let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + + #expect(textString.isEmpty) + } + } + + @Test("Encoding consistency - formats maintain sorting") + func encodingConsistency() throws { + // Add formats in reverse alphabetical order to test sorting + let jsonPath = try AbsolutePath(validating: "/json/path") + let htmlPath: AbsolutePath = try AbsolutePath(validating: "/html/path") + + var output = CoverageFormatOutput() + try output.addFormat(.json, path: jsonPath) // Add json first + try output.addFormat(.html, path: htmlPath) // Add html second + + // Text encoding should show html first (alphabetically) + var textEncoder = PlainTextEncoder() + textEncoder.formattingOptions = [.prettyPrinted] + let textData = try textEncoder.encode(output) + let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + #expect(textString.hasPrefix("Html:")) + #expect(textString.hasSuffix("Json: /json/path")) + + // JSON encoding should also maintain consistent ordering + let jsonEncoder = JSONEncoder() + jsonEncoder.keyEncodingStrategy = .convertToSnakeCase + let jsonData = try jsonEncoder.encode(output) + let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String] + + #expect(decoded["html"] == "/html/path") + #expect(decoded["json"] == "/json/path") + } + + @Test("Text encoding handles special characters in paths") + func textEncodingSpecialCharacters() throws { + let specialPath = try AbsolutePath(validating: "/path with/spaces & symbols/coverage.json") + var output = CoverageFormatOutput() + try output.addFormat(.json, path: specialPath) + + var encoder = PlainTextEncoder() + encoder.formattingOptions = [.prettyPrinted] + let textData = try encoder.encode(output) + let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + + #expect(textString == "Json: /path with/spaces & symbols/coverage.json") + } + + @Test("JSON encoding handles special characters in paths") + func jsonEncodingSpecialCharacters() throws { + let specialPath = try AbsolutePath(validating: "/path with/spaces & symbols/coverage.json") + var output = CoverageFormatOutput() + try output.addFormat(.json, path: specialPath) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let jsonData = try encoder.encode(output) + let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String] + + #expect(decoded["json"] == "/path with/spaces & symbols/coverage.json") + } + } + + } +} diff --git a/Tests/CommandsTests/TestCommandTests.swift b/Tests/CommandsTests/TestCommandTests.swift index 22bc4bdd845..4f63d38187a 100644 --- a/Tests/CommandsTests/TestCommandTests.swift +++ b/Tests/CommandsTests/TestCommandTests.swift @@ -1067,7 +1067,7 @@ struct TestCommandTests { } @Test( - .issue("https://github.com/swiftlang/swift-package-manager/issues/8511", relationship: .defect), + .IssueWindowsPathTestsFailures, .issue("https://github.com/swiftlang/swift-package-manager/issues/8602", relationship: .defect), arguments: SupportedBuildSystemOnAllPlatforms, BuildConfiguration.allCases, ) diff --git a/Tests/FunctionalTests/PluginTests.swift b/Tests/FunctionalTests/PluginTests.swift index 03f4af842b8..2a6f72bf7c4 100644 --- a/Tests/FunctionalTests/PluginTests.swift +++ b/Tests/FunctionalTests/PluginTests.swift @@ -30,7 +30,7 @@ import Foundation ) final class PluginTests { @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8791"), .requiresSwiftConcurrencySupport, ) @@ -67,7 +67,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8786"), .requiresSwiftConcurrencySupport, ) @@ -98,7 +98,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, ) @@ -137,7 +137,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, ) @@ -176,7 +176,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, ) @@ -247,7 +247,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, ) @@ -284,7 +284,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .bug("https://github.com/swiftlang/swift-package-manager/issues/8791"), .requiresSwiftConcurrencySupport, @@ -320,7 +320,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), .requiresSwiftConcurrencySupport, ) @@ -428,7 +428,7 @@ final class PluginTests { @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8794"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .requiresSwiftConcurrencySupport, arguments: SupportedBuildSystemOnAllPlatforms, ) @@ -866,7 +866,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .requiresSwiftConcurrencySupport, arguments: [BuildSystemProvider.Kind.native, .swiftbuild] ) @@ -1361,7 +1361,7 @@ final class PluginTests { struct SnippetTests { @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .requiresSwiftConcurrencySupport, arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), ) @@ -1583,7 +1583,7 @@ final class PluginTests { @Test( .bug("https://github.com/swiftlang/swift-package-manager/issues/8774"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .requiresSwiftConcurrencySupport, arguments: SupportedBuildSystemOnAllPlatforms, ) @@ -1630,7 +1630,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8791"), .requiresSwiftConcurrencySupport, ) @@ -1666,7 +1666,7 @@ final class PluginTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsRelativePathAssert, .bug("https://github.com/swiftlang/swift-package-manager/issues/8791"), .requiresSwiftConcurrencySupport, ) diff --git a/Tests/FunctionalTests/TraitTests.swift b/Tests/FunctionalTests/TraitTests.swift index c178a89a1de..4b6d0558de9 100644 --- a/Tests/FunctionalTests/TraitTests.swift +++ b/Tests/FunctionalTests/TraitTests.swift @@ -27,8 +27,8 @@ import _InternalTestSupport ) struct TraitTests { @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -71,8 +71,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -125,8 +125,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -176,8 +176,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -231,8 +231,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -273,8 +273,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -320,8 +320,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -378,8 +378,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .IssueSwiftBuildLinuxRunnable, .IssueProductTypeForObjectLibraries, .tags( @@ -464,8 +464,8 @@ struct TraitTests { } @Test( - .bug("https://github.com/swiftlang/swift-package-manager/issues/8511"), - .bug("https://github.com/swiftlang/swift-package-manager/issues/8602"), + .IssueWindowsPathTestsFailures, + .IssueWindowsRelativePathAssert, .tags( Tag.Feature.Command.Test, ), diff --git a/Tests/IntegrationTests/SwiftPMTests.swift b/Tests/IntegrationTests/SwiftPMTests.swift index 5f93405658a..8212fb1ad86 100644 --- a/Tests/IntegrationTests/SwiftPMTests.swift +++ b/Tests/IntegrationTests/SwiftPMTests.swift @@ -13,7 +13,9 @@ import _IntegrationTestSupport import _InternalTestSupport import Testing import Basics +import enum PackageModel.BuildConfiguration import struct SPMBuildCore.BuildSystemProvider + @Suite( .tags(Tag.TestSize.large) ) @@ -234,20 +236,28 @@ private struct SwiftPMTests { } } - @Test(.requireSwift6_2) - func testCodeCoverageMergedAcrossSubprocesses() async throws { - try await withTemporaryDirectory { tmpDir in + @Test( + .requireSwift6_2, + arguments: SupportedBuildSystemOnAllPlatforms + ) + func testCodeCoverageMergedAcrossSubprocesses( + buildSystem: BuildSystemProvider.Kind, + ) async throws { + let config = BuildConfiguration.debug + try await withTemporaryDirectory(removeTreeOnDeinit: false) { tmpDir in let packagePath = tmpDir.appending(component: "test-package-coverage") try localFileSystem.createDirectory(packagePath) try await executeSwiftPackage( packagePath, + configuration: config, extraArgs: ["init", "--type", "empty"], - buildSystem: .native, + buildSystem: buildSystem, ) try await executeSwiftPackage( packagePath, + configuration: config, extraArgs: ["add-target", "--type", "test", "ReproTests"], - buildSystem: .native, + buildSystem: buildSystem, ) try localFileSystem.writeFileContents( AbsolutePath(validating: "Tests/ReproTests/Subject.swift", relativeTo: packagePath), @@ -275,66 +285,73 @@ private struct SwiftPMTests { ) let expectedCoveragePath = try await executeSwiftTest( packagePath, + configuration: config, extraArgs: ["--show-coverage-path"], - buildSystem: .native, + buildSystem: buildSystem, ).stdout.trimmingCharacters(in: .whitespacesAndNewlines) try await executeSwiftTest( packagePath, + configuration: config, extraArgs: ["--enable-code-coverage", "--disable-xctest"], - buildSystem: .native, + buildSystem: buildSystem, ) let coveragePath = try AbsolutePath(validating: expectedCoveragePath) // Check the coverage path exists. - #expect(localFileSystem.exists(coveragePath)) + try withKnownIssue { + // the CoveragePath file does not exists in Linux platform build + expectFileExists(at: coveragePath) - // This resulting coverage file should be merged JSON, with a schema that valiades against this subset. - struct Coverage: Codable { - var data: [Entry] - struct Entry: Codable { - var files: [File] - struct File: Codable { - var filename: String - var summary: Summary - struct Summary: Codable { - var functions: Functions - struct Functions: Codable { - var count, covered: Int - var percent: Double + // This resulting coverage file should be merged JSON, with a schema that valiades against this subset. + struct Coverage: Codable { + var data: [Entry] + struct Entry: Codable { + var files: [File] + struct File: Codable { + var filename: String + var summary: Summary + struct Summary: Codable { + var functions: Functions + struct Functions: Codable { + var count, covered: Int + var percent: Double + } } } } } - } - let coverageJSON = try localFileSystem.readFileContents(coveragePath) - let coverage = try JSONDecoder().decode(Coverage.self, from: Data(coverageJSON.contents)) + let coverageJSON = try localFileSystem.readFileContents(coveragePath) + let coverage = try JSONDecoder().decode(Coverage.self, from: Data(coverageJSON.contents)) - // Check for 100% coverage for Subject.swift, which should happen because the per-PID files got merged. - let subjectCoverage = coverage.data.first?.files.first(where: { $0.filename.hasSuffix("Subject.swift") }) - #expect(subjectCoverage?.summary.functions.count == 2) - #expect(subjectCoverage?.summary.functions.covered == 2) - #expect(subjectCoverage?.summary.functions.percent == 100) + // Check for 100% coverage for Subject.swift, which should happen because the per-PID files got merged. + let subjectCoverage = try #require(coverage.data.first?.files.first(where: { $0.filename.hasSuffix("Subject.swift") })) + #expect(subjectCoverage.summary.functions.count == 2) + #expect(subjectCoverage.summary.functions.covered == 2) + #expect(subjectCoverage.summary.functions.percent == 100) - // Check the directory with the coverage path contains the profraw files. - let coverageDirectory = coveragePath.parentDirectory - let coverageDirectoryContents = try localFileSystem.getDirectoryContents(coverageDirectory) + // Check the directory with the coverage path contains the profraw files. + let coverageDirectory = coveragePath.parentDirectory + let coverageDirectoryContents = try localFileSystem.getDirectoryContents(coverageDirectory) - // SwiftPM uses an LLVM_PROFILE_FILE that ends with ".%p.profraw", which we validated in the test above. - // Let's first check all the files have the expected extension. - let profrawFiles = coverageDirectoryContents.filter { $0.hasSuffix(".profraw") } + // SwiftPM uses an LLVM_PROFILE_FILE that ends with ".%p.profraw", which we validated in the test above. + // Let's first check all the files have the expected extension. + let profrawFiles = coverageDirectoryContents.filter { $0.hasSuffix(".profraw") } - // Then check that %p expanded as we expected: to something that plausibly looks like a PID. - for profrawFile in profrawFiles { - let shouldBePID = try #require(profrawFile.split(separator: ".").dropLast().last) - #expect(Int(shouldBePID) != nil) - } + // Then check that %p expanded as we expected: to something that plausibly looks like a PID. + for profrawFile in profrawFiles { + let shouldBePID = try #require(profrawFile.split(separator: ".").dropLast().last) + #expect(Int(shouldBePID) != nil) + } - // Group the files by binary identifier (have a different prefix, before the per-PID suffix). - let groups = Dictionary(grouping: profrawFiles) { path in path.split(separator: ".").dropLast(2) }.values + // Group the files by binary identifier (have a different prefix, before the per-PID suffix). + let groups = Dictionary(grouping: profrawFiles) { path in path.split(separator: ".").dropLast(2) }.values - // Check each group has 3 files: one per PID (the above suite has 2 exit tests => 2 forks => 3 PIDs total). - for binarySpecificProfrawFiles in groups { - #expect(binarySpecificProfrawFiles.count == 3) + // Check each group has 3 files: one per PID (the above suite has 2 exit tests => 2 forks => 3 PIDs total). + for binarySpecificProfrawFiles in groups { + #expect(binarySpecificProfrawFiles.count == 3) + } + } when: { + ProcessInfo.hostOperatingSystem == .linux && buildSystem == .swiftbuild } } } diff --git a/Tests/PackageGraphTests/ModulesGraphTests.swift b/Tests/PackageGraphTests/ModulesGraphTests.swift index 55699e5c739..7145a61ea3d 100644 --- a/Tests/PackageGraphTests/ModulesGraphTests.swift +++ b/Tests/PackageGraphTests/ModulesGraphTests.swift @@ -113,7 +113,7 @@ struct ModulesGraphTests { } @Test( - .issue("https://github.com/swiftlang/swift-package-manager/issues/8511", relationship: .defect), + .IssueWindowsPathTestsFailures, ) func basic() throws { try withKnownIssue {