From e5321fb2b61890a27646d192939208f484f99275 Mon Sep 17 00:00:00 2001 From: Bassam Khouri Date: Mon, 13 Jul 2026 15:36:52 -0400 Subject: [PATCH] Ensure attachments path directory is created When running `swift test --attachments-path /does/not/exist`, the Swift Testing library will fail with an error like: ``` Invalid value "/path/does/not/exists" for argument ---attachments-path ``` Ensure the path specified by `--attachments-path` is created, and update the error message to remote the additional `-` in the argument name. Closes #1665 --- .../Testing/ABI/EntryPoints/EntryPoint.swift | 5 +- Sources/Testing/Testing.docc/Attachments.md | 4 +- Tests/TestingTests/SwiftPMTests.swift | 72 +++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 102776238..d7e505cd5 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -8,6 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // +private import Foundation private import _TestingInternals #if canImport(Synchronization) @@ -622,9 +623,7 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr // Attachment output. if let attachmentsPath = args.attachmentsPath { - guard fileExists(atPath: attachmentsPath) else { - throw _EntryPointError.invalidArgument("---attachments-path", value: attachmentsPath) - } + try FileManager().createDirectory(atPath: attachmentsPath, withIntermediateDirectories: true) configuration.attachmentsPath = attachmentsPath } diff --git a/Sources/Testing/Testing.docc/Attachments.md b/Sources/Testing/Testing.docc/Attachments.md index 018576d95..9e28633e4 100644 --- a/Sources/Testing/Testing.docc/Attachments.md +++ b/Sources/Testing/Testing.docc/Attachments.md @@ -244,8 +244,8 @@ after your tests finish running: `.build/attachments` by default. Visual Studio Code reports the paths to individual attachments in its Tests Results panel. - When using Swift Package Manager's `swift test` command, you can pass the - `--attachments-path` option. The testing library saves attachments to the - specified directory. + `--attachments-path` option. The testing library creates a directory at the + specified path and saves attachments to it. If you do not pass the `--attachments-path` option, the testing library does not save any attachments you record. diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 6c7e27ab3..9e4b63bbf 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -250,6 +250,78 @@ struct SwiftPMTests { #expect(fileContents.contains(UInt8(ascii: ">"))) } + @Test( + "--attachments-path argument (creates missing directory)", + arguments: ["--attachments-path", "--experimental-attachments-path"] + ) + func attachmentsPathCreatesMissingDirectory(argumentName: String) throws { + let tempDirPath = try temporaryDirectory() + let attachmentsPath = appendPathComponent("swt_attachments_\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) + defer { + _ = remove(attachmentsPath) + } + #expect(!fileExists(atPath: attachmentsPath)) + let configuration = try configurationForEntryPoint(withArguments: ["PATH", argumentName, attachmentsPath]) + #expect(fileExists(atPath: attachmentsPath)) + let actualPath = try #require(configuration.attachmentsPath, "Attachments path is not expected to be nil") + #expect(canonicalizePath(actualPath) == canonicalizePath(attachmentsPath)) + } + + @Test("--attachments-path argument (creates missing intermediate directories)") + func attachmentsPathCreatesMissingIntermediateDirectories() throws { + let tempDirPath = try temporaryDirectory() + let root = appendPathComponent("swt_attachments_\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) + let intermediate = appendPathComponent("foo", to: root) + let attachmentsPath = appendPathComponent("bar", to: intermediate) + defer { + _ = remove(attachmentsPath) + _ = remove(intermediate) + _ = remove(root) + } + #expect(!fileExists(atPath: root)) + let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--attachments-path", attachmentsPath]) + #expect(fileExists(atPath: root)) + #expect(fileExists(atPath: intermediate)) + #expect(fileExists(atPath: attachmentsPath)) + let actualPath = try #require(configuration.attachmentsPath, "Attachments path is not expected to be nil") + #expect(canonicalizePath(actualPath) == canonicalizePath(attachmentsPath)) + } + + @Test("--attachments-path argument (accepts existing directory)") + func attachmentsPathAcceptsExistingDirectory() throws { + let tempDirPath = try temporaryDirectory() + #expect(fileExists(atPath: tempDirPath)) + let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--attachments-path", tempDirPath]) + let actualPath = try #require(configuration.attachmentsPath, "Attachments path is not expected to be nil") + #expect( + canonicalizePath(actualPath) == canonicalizePath(tempDirPath), + "Canonicalized actual path (\(actualPath)) is not equal to canonicalized expected path (\(tempDirPath))", + ) + } + + @Test("--attachments-path argument (bad path)") + func attachmentsPathWithBadPath() throws { + let attachmentPathParent: String + let attachmentPath: String + #if !os(Windows) + attachmentPathParent = "/usr/bin" + attachmentPath = "\(attachmentPathParent)/cannot-save-attachments-here" + #else + attachmentPathParent = #"C:\Windows\System32"# + attachmentPath = "\(attachmentPathParent)\\cannot-save-attachments-here" + #endif + + #expect(throws: (any Error).self) { + _ = try configurationForEntryPoint(withArguments: ["PATH", "--attachments-path", attachmentPath]) + } + } + + @Test("--attachments-path argument (missing path)") + func attachmentsPathWithMissingPath() throws { + let args = try parseCommandLineArguments(from: ["PATH", "--attachments-path"]) + #expect(args.attachmentsPath == nil) + } + @Test("--configuration-path argument", arguments: [ "--configuration-path", "--experimental-configuration-path", ])