From baa82f4aabb05822ca20762b502f8119b7ef0c1b Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Thu, 8 Jan 2026 12:33:59 -0800 Subject: [PATCH] Tests: make the explicit module build tests more resilient to spaces If `SDKROOT` contains a space, the command line is improperly tokenised on the scanner side. --- .../ExplicitModuleBuildTests.swift | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift index a70669e13..a39e052f8 100644 --- a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift +++ b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift @@ -16,6 +16,46 @@ import TSCBasic import XCTest import TestUtilities +private func argumentNeedsQuoting(_ argument: String) -> Bool { + if argument.isEmpty { return false } + let chars: Set = Set("\t \"&'()*<>\\`^|\n") + return argument.firstIndex(where: { chars.contains($0) }) != argument.endIndex +} + +private func quoteArgument(_ argument: String) -> String { +#if os(Windows) + var unquoted: Substring = argument[...] + var quoted: String = "\"" + while !unquoted.isEmpty { + guard let firstNonBS = unquoted.firstIndex(where: { $0 != "\\" }) else { + // The rest of the string is backslashes. Escape all of them and exit. + (0 ..< (2 * unquoted.count)).forEach { _ in quoted += "\\" } + break + } + + let bsCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBS) + if unquoted[firstNonBS] == "\"" { + // This is an embedded quote. Escape all preceding backslashes, then + // add one additional backslash to escape the quote. + (0 ..< (2 * bsCount + 1)).forEach { _ in quoted += "\\" } + quoted += "\"" + } else { + // This is just a normal character. Don't escape any of the preceding + // backslashes, just append them as they are and then append the + // character. + (0 ..< bsCount).forEach { _ in quoted += "\\" } + quoted += "\(unquoted[firstNonBS])" + } + + unquoted = unquoted.dropFirst(bsCount + 1) + } + return quoted + "\"" +#else + return "'" + argument + "'" +#endif +} + + private var testInputsPath: AbsolutePath { get throws { var root: AbsolutePath = try AbsolutePath(validating: #file) @@ -1659,7 +1699,7 @@ final class ExplicitModuleBuildTests: XCTestCase { let deps = try dependencyOracle.getImports(workingDirectory: path, moduleAliases: ["Car": "Bar"], - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 }}, diagnostics: &scanDiagnostics) XCTAssertTrue(deps.imports.contains("Bar")) @@ -1891,7 +1931,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let dependencyGraph = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) let fooDependencyInfo = try XCTUnwrap(dependencyGraph.modules[.swiftPrebuiltExternal("Foo")]) @@ -1942,7 +1982,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let imports = try dependencyOracle.getImports(workingDirectory: path, - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) let expectedImports = ["C", "E", "G", "Swift", "SwiftOnoneSupport"] // Dependnig on how recent the platform we are running on, the _Concurrency module may or may not be present. @@ -2049,7 +2089,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let _ = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) XCTAssertEqual(scanDiagnostics.count, 5) let diags = try XCTUnwrap(scanDiagnostics) @@ -2120,7 +2160,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let _ = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) XCTAssertEqual(scanDiagnostics.count, 2) let diags = try XCTUnwrap(scanDiagnostics) @@ -2299,7 +2339,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let dependencyGraph = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: iterationCommand, + commandLine: iterationCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) // The _Concurrency and _StringProcessing modules are automatically @@ -2452,7 +2492,7 @@ final class ExplicitModuleBuildTests: XCTestCase { do { let _ = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: scannerCommands[scanIndex], + commandLine: scannerCommands[scanIndex].map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics[scanIndex]) } catch { XCTFail("Unexpected error: \(error)") @@ -2627,7 +2667,7 @@ final class ExplicitModuleBuildTests: XCTestCase { var scanDiagnostics: [ScannerDiagnosticPayload] = [] let dependencyGraph = try dependencyOracle.getDependencies(workingDirectory: path, - commandLine: scannerCommand, + commandLine: scannerCommand.map { if argumentNeedsQuoting($0) { quoteArgument($0) } else { $0 } }, diagnostics: &scanDiagnostics) let serializer = DOTModuleDependencyGraphSerializer(dependencyGraph)