diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index ea37326d5..89cad3806 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -275,9 +275,6 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--repeat-until` argument. public var repeatUntil: String? - - /// The value of the `--experimental-attachments-path` argument. - public var experimentalAttachmentsPath: String? } extension __CommandLineArguments_v0: Codable { @@ -298,7 +295,6 @@ extension __CommandLineArguments_v0: Codable { case skip case repetitions case repeatUntil - case experimentalAttachmentsPath } } @@ -359,11 +355,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum if let xunitOutputIndex = args.firstIndex(of: "--xunit-output"), !isLastArgument(at: xunitOutputIndex) { result.xunitOutput = args[args.index(after: xunitOutputIndex)] } - - // Attachment output - if let attachmentsPathIndex = args.firstIndex(of: "--experimental-attachments-path"), !isLastArgument(at: attachmentsPathIndex) { - result.experimentalAttachmentsPath = args[args.index(after: attachmentsPathIndex)] - } #endif if args.contains("--list-tests") { @@ -473,14 +464,6 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr } } - // Attachment output. - if let attachmentsPath = args.experimentalAttachmentsPath { - guard fileExists(atPath: attachmentsPath) else { - throw _EntryPointError.invalidArgument("--experimental-attachments-path", value: attachmentsPath) - } - configuration.attachmentsPath = attachmentsPath - } - #if canImport(Foundation) // Event stream output (experimental) if let eventStreamOutputPath = args.eventStreamOutputPath { diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift deleted file mode 100644 index 525f8718f..000000000 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedAttachment.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -extension ABIv0 { - /// A type implementing the JSON encoding of ``Test/Attachment`` for the ABI - /// entry point and event stream output. - /// - /// This type is not part of the public interface of the testing library. It - /// assists in converting values to JSON; clients that consume this JSON are - /// expected to write their own decoders. - /// - /// - Warning: Attachments are not yet part of the JSON schema. - struct EncodedAttachment: Sendable { - /// The path where the attachment was written. - var path: String? - - init(encoding attachment: borrowing Test.Attachment, in eventContext: borrowing Event.Context) { - path = attachment.fileSystemPath - } - } -} - -// MARK: - Codable - -extension ABIv0.EncodedAttachment: Codable {} diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift index fd9dc464a..65cd78234 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedEvent.swift @@ -27,7 +27,6 @@ extension ABIv0 { case testStarted case testCaseStarted case issueRecorded - case valueAttached = "_valueAttached" case testCaseEnded case testEnded case testSkipped @@ -46,14 +45,6 @@ extension ABIv0 { /// ``kind-swift.property`` property is ``Kind-swift.enum/issueRecorded``. var issue: EncodedIssue? - /// The value that was attached, if any. - /// - /// The value of this property is `nil` unless the value of the - /// ``kind-swift.property`` property is ``Kind-swift.enum/valueAttached``. - /// - /// - Warning: Attachments are not yet part of the JSON schema. - var _attachment: EncodedAttachment? - /// Human-readable messages associated with this event that can be presented /// to the user. var messages: [EncodedMessage] @@ -80,9 +71,6 @@ extension ABIv0 { case let .issueRecorded(recordedIssue): kind = .issueRecorded issue = EncodedIssue(encoding: recordedIssue, in: eventContext) - case let .valueAttached(attachment): - kind = .valueAttached - _attachment = EncodedAttachment(encoding: attachment, in: eventContext) case .testCaseEnded: if eventContext.test?.isParameterized == false { return nil diff --git a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift index 5cfbf647c..e67b15309 100644 --- a/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift +++ b/Sources/Testing/ABI/v0/Encoded/ABIv0.EncodedMessage.swift @@ -30,7 +30,6 @@ extension ABIv0 { case difference case warning case details - case attachment = "_attachment" init(encoding symbol: Event.Symbol) { self = switch symbol { @@ -52,8 +51,6 @@ extension ABIv0 { .warning case .details: .details - case .attachment: - .attachment } } } diff --git a/Sources/Testing/Attachments/Test.Attachable.swift b/Sources/Testing/Attachments/Test.Attachable.swift deleted file mode 100644 index 0053bec62..000000000 --- a/Sources/Testing/Attachments/Test.Attachable.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -@_spi(Experimental) -extension Test { - /// A protocol describing a type that can be attached to a test report or - /// written to disk when a test is run. - /// - /// To attach an attachable value to a test report or test run output, use it - /// to initialize a new instance of ``Test/Attachment``, then call - /// ``Test/Attachment/attach()``. An attachment can only be attached once. - /// - /// The testing library provides default conformances to this protocol for a - /// variety of standard library types. Most user-defined types do not need to - /// conform to this protocol. - /// - /// A type should conform to this protocol if it can be represented as a - /// sequence of bytes that would be diagnostically useful if a test fails. - public protocol Attachable: ~Copyable { - /// An estimate of the number of bytes of memory needed to store this value - /// as an attachment. - /// - /// The testing library uses this property to determine if an attachment - /// should be held in memory or should be immediately persisted to storage. - /// Larger attachments are more likely to be persisted, but the algorithm - /// the testing library uses is an implementation detail and is subject to - /// change. - /// - /// The value of this property is approximately equal to the number of bytes - /// that will actually be needed, or `nil` if the value cannot be computed - /// efficiently. The default implementation of this property returns `nil`. - /// - /// - Complexity: O(1) unless `Self` conforms to `Collection`, in which case - /// up to O(_n_) where _n_ is the length of the collection. - var estimatedAttachmentByteCount: Int? { get } - - /// Call a function and pass a buffer representing this instance to it. - /// - /// - Parameters: - /// - attachment: The attachment that is requesting a buffer (that is, the - /// attachment containing this instance.) - /// - body: A function to call. A temporary buffer containing a data - /// representation of this instance is passed to it. - /// - /// - Returns: Whatever is returned by `body`. - /// - /// - Throws: Whatever is thrown by `body`, or any error that prevented the - /// creation of the buffer. - /// - /// The testing library uses this function when writing an attachment to a - /// test report or to a file on disk. The format of the buffer is - /// implementation-defined, but should be "idiomatic" for this type: for - /// example, if this type represents an image, it would be appropriate for - /// the buffer to contain an image in PNG format, JPEG format, etc., but it - /// would not be idiomatic for the buffer to contain a textual description - /// of the image. - borrowing func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R - } -} - -// MARK: - Default implementations - -extension Test.Attachable where Self: ~Copyable { - public var estimatedAttachmentByteCount: Int? { - nil - } -} - -extension Test.Attachable where Self: Collection, Element == UInt8 { - public var estimatedAttachmentByteCount: Int? { - count - } - - // We do not provide an implementation of withUnsafeBufferPointer(for:_:) here - // because there is no way in the standard library to statically detect if a - // collection can provide contiguous storage (_HasContiguousBytes is not API.) - // If withContiguousBytesIfAvailable(_:) fails, we don't want to make a - // (potentially expensive!) copy of the collection. - // - // The planned Foundation cross-import overlay can provide a default - // implementation for collection types that conform to Foundation's - // ContiguousBytes protocol. -} - -extension Test.Attachable where Self: StringProtocol { - public var estimatedAttachmentByteCount: Int? { - // NOTE: utf8.count may be O(n) for foreign strings. - // SEE: https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringUTF8View.swift - utf8.count - } -} - -// MARK: - Default conformances - -// Implement the protocol requirements for byte arrays and buffers so that -// developers can attach raw data when needed. -@_spi(Experimental) -extension Array: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension ContiguousArray: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension ArraySlice: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withUnsafeBytes(body) - } -} - -@_spi(Experimental) -extension UnsafeBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try body(.init(self)) - } -} - -@_spi(Experimental) -extension UnsafeMutableBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try body(.init(self)) - } -} - -@_spi(Experimental) -extension UnsafeRawBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try body(self) - } -} - -@_spi(Experimental) -extension UnsafeMutableRawBufferPointer: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try body(.init(self)) - } -} - -@_spi(Experimental) -extension String: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var selfCopy = self - return try selfCopy.withUTF8 { utf8 in - try body(UnsafeRawBufferPointer(utf8)) - } - } -} - -@_spi(Experimental) -extension Substring: Test.Attachable { - public func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var selfCopy = self - return try selfCopy.withUTF8 { utf8 in - try body(UnsafeRawBufferPointer(utf8)) - } - } -} diff --git a/Sources/Testing/Attachments/Test.Attachment.swift b/Sources/Testing/Attachments/Test.Attachment.swift deleted file mode 100644 index 68fc44ffc..000000000 --- a/Sources/Testing/Attachments/Test.Attachment.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -private import _TestingInternals - -@_spi(Experimental) -extension Test { - /// A type describing values that can be attached to the output of a test run - /// and inspected later by the user. - /// - /// Attachments are included in test reports in Xcode or written to disk when - /// tests are run at the command line. To create an attachment, you need a - /// value of some type that conforms to ``Test/Attachable``. Initialize an - /// instance of ``Test/Attachment`` with that value and, optionally, a - /// preferred filename to use when writing to disk. - public struct Attachment: Sendable { - /// The value of this attachment. - /// - /// The type of this property's value may not match the type of the value - /// originally used to create this attachment. - public var attachableValue: any Attachable & Sendable /* & Copyable rdar://137614425 */ - - /// The source location where the attachment was initialized. - /// - /// The value of this property is used when recording issues associated with - /// the attachment. - public var sourceLocation: SourceLocation - - /// The default preferred name to use if the developer does not supply one. - package static var defaultPreferredName: String { - "untitled" - } - - /// The path to which the this attachment was written, if any. - /// - /// If a developer sets the ``Configuration/attachmentsPath`` property of - /// the current configuration before running tests, or if a developer passes - /// `--experimental-attachments-path` on the command line, then attachments - /// will be automatically written to disk when they are attached and the - /// value of this property will describe the path where they were written. - /// - /// If no destination path is set, or if an error occurred while writing - /// this attachment to disk, the value of this property is `nil`. - @_spi(ForToolsIntegrationOnly) - public var fileSystemPath: String? - - /// Initialize an instance of this type that encloses the given attachable - /// value. - /// - /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - public init( - _ attachableValue: some Attachable & Sendable & Copyable, - named preferredName: String? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - self.attachableValue = attachableValue - self.preferredName = preferredName ?? Self.defaultPreferredName - self.sourceLocation = sourceLocation - } - - /// A filename to use when writing this attachment to a test report or to a - /// file on disk. - /// - /// The value of this property is used as a hint to the testing library. The - /// testing library may substitute a different filename as needed. If the - /// value of this property has not been explicitly set, the testing library - /// will attempt to generate its own value. - public var preferredName: String - } -} - -// MARK: - - -extension Test.Attachment { - /// Attach this instance to the current test. - /// - /// An attachment can only be attached once. - public consuming func attach() { - Event.post(.valueAttached(self)) - } -} - -// MARK: - Non-sendable and move-only attachments - -/// A type that stands in for an attachable type that is not also sendable. -private struct _AttachableProxy: Test.Attachable, Sendable { - /// The result of `withUnsafeBufferPointer(for:_:)` from the original - /// attachable value. - var encodedValue = [UInt8]() - - var estimatedAttachmentByteCount: Int? - - func withUnsafeBufferPointer(for attachment: borrowing Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try encodedValue.withUnsafeBufferPointer(for: attachment, body) - } -} - -extension Test.Attachment { - /// Initialize an instance of this type that encloses the given attachable - /// value. - /// - /// - Parameters: - /// - attachableValue: The value that will be attached to the output of - /// the test run. - /// - preferredName: The preferred name of the attachment when writing it - /// to a test report or to disk. If `nil`, the testing library attempts - /// to derive a reasonable filename for the attached value. - /// - sourceLocation: The source location of the call to this initializer. - /// This value is used when recording issues associated with the - /// attachment. - /// - /// When attaching a value of a type that does not conform to both `Sendable` - /// and `Copyable`, the testing library encodes it as data immediately. If the - /// value cannot be encoded and an error is thrown, that error is recorded as - /// an issue in the current test and the resulting instance of - /// ``Test/Attachment`` is empty. - @_disfavoredOverload - public init( - _ attachableValue: borrowing some Test.Attachable & ~Copyable, - named preferredName: String? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - var proxyAttachable = _AttachableProxy() - proxyAttachable.estimatedAttachmentByteCount = attachableValue.estimatedAttachmentByteCount - - // BUG: the borrow checker thinks that withErrorRecording() is consuming - // attachableValue, so get around it with an additional do/catch clause. - do { - let proxyAttachment = Self(proxyAttachable, named: preferredName, sourceLocation: sourceLocation) - proxyAttachable.encodedValue = try attachableValue.withUnsafeBufferPointer(for: proxyAttachment) { buffer in - [UInt8](buffer) - } - proxyAttachable.estimatedAttachmentByteCount = proxyAttachable.encodedValue.count - } catch { - Issue.withErrorRecording(at: sourceLocation) { - // TODO: define new issue kind .valueAttachmentFailed(any Error) - // (but only use it if the caught error isn't ExpectationFailedError, - // SystemError, or APIMisuseError. We need a protocol for these things.) - throw error - } - } - - self.init(proxyAttachable, named: preferredName, sourceLocation: sourceLocation) - } -} - -#if !SWT_NO_FILE_IO -// MARK: - Writing - -extension Test.Attachment { - /// Write the attachment's contents to a file in the specified directory. - /// - /// - Parameters: - /// - directoryPath: The directory that should contain the attachment when - /// written. - /// - /// - Throws: Any error preventing writing the attachment. - /// - /// - Returns: The path to the file that was written. - /// - /// The attachment is written to a file _within_ `directoryPath`, whose name - /// is derived from the value of the ``Test/Attachment/preferredName`` - /// property. - /// - /// If you pass `--experimental-attachments-path` to `swift test`, the testing - /// library automatically uses this function to persist attachments to the - /// directory you specify. - /// - /// This function does not get or set the value of the attachment's - /// ``fileSystemPath`` property. The caller is responsible for setting the - /// value of this property if needed. - /// - /// This function is provided as a convenience to allow tools authors to write - /// attachments to persistent storage the same way that Swift Package Manager - /// does. You are not required to use this function. - @_spi(ForToolsIntegrationOnly) - public func write(toFileInDirectoryAtPath directoryPath: String) throws -> String { - try write( - toFileInDirectoryAtPath: directoryPath, - appending: String(UInt64.random(in: 0 ..< .max), radix: 36) - ) - } - - /// Write the attachment's contents to a file in the specified directory. - /// - /// - Parameters: - /// - directoryPath: The directory to which the attachment should be - /// written. - /// - usingPreferredName: Whether or not to use the attachment's preferred - /// name. If `false`, ``defaultPreferredName`` is used instead. - /// - suffix: A suffix to attach to the file name (instead of randomly - /// generating one.) This value may be evaluated multiple times. - /// - /// - Throws: Any error preventing writing the attachment. - /// - /// - Returns: The path to the file that was written. - /// - /// The attachment is written to a file _within_ `directoryPath`, whose name - /// is derived from the value of the ``Test/Attachment/preferredName`` - /// property and the value of `suffix`. - /// - /// If the argument `suffix` always produces the same string, the result of - /// this function is undefined. - func write(toFileInDirectoryAtPath directoryPath: String, usingPreferredName: Bool = true, appending suffix: @autoclosure () -> String) throws -> String { - let result: String - - let preferredName = usingPreferredName ? preferredName : Self.defaultPreferredName - - var file: FileHandle? - do { - // First, attempt to create the file with the exact preferred name. If a - // file exists at this path (note "x" in the mode string), an error will - // be thrown and we'll try again by adding a suffix. - let preferredPath = appendPathComponent(preferredName, to: directoryPath) - file = try FileHandle(atPath: preferredPath, mode: "wxb") - result = preferredPath - } catch { - // Split the extension(s) off the preferred name. The first component in - // the resulting array is our base name. - var preferredNameComponents = preferredName.split(separator: ".") - let firstPreferredNameComponent = preferredNameComponents[0] - - while true { - preferredNameComponents[0] = "\(firstPreferredNameComponent)-\(suffix())" - let preferredName = preferredNameComponents.joined(separator: ".") - let preferredPath = appendPathComponent(preferredName, to: directoryPath) - - // Propagate any error *except* EEXIST, which would indicate that the - // name was already in use (so we should try again with a new suffix.) - do { - file = try FileHandle(atPath: preferredPath, mode: "wxb") - result = preferredPath - break - } catch let error as CError where error.rawValue == EEXIST { - // Try again with a new suffix. - continue - } catch where usingPreferredName { - // Try again with the default name before giving up. - return try write(toFileInDirectoryAtPath: directoryPath, usingPreferredName: false, appending: suffix()) - } - } - } - - try attachableValue.withUnsafeBufferPointer(for: self) { buffer in - try file!.write(buffer) - } - - return result - } -} - -extension Configuration { - /// Handle the given "value attached" event. - /// - /// - Parameters: - /// - event: The event to handle. This event must be of kind - /// ``Event/Kind/valueAttached(_:)``. If the associated attachment's - /// ``Test/Attachment/fileSystemPath`` property is not `nil`, this - /// function does nothing. - /// - context: The context associated with the event. - /// - /// This function is called automatically by ``handleEvent(_:in:)``. You do - /// not need to call it elsewhere. It automatically persists the attachment - /// associated with `event` and modifies `event` to include the path where the - /// attachment was stored. - func handleValueAttachedEvent(_ event: inout Event, in eventContext: borrowing Event.Context) { - guard let attachmentsPath else { - // If there is no path to which attachments should be written, there's - // nothing to do. - return - } - - guard case let .valueAttached(attachment) = event.kind else { - preconditionFailure("Passed the wrong kind of event to \(#function) (expected valueAttached, got \(event.kind)). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new") - } - if attachment.fileSystemPath != nil { - // Somebody already persisted this attachment. This isn't necessarily a - // logic error in the testing library, but it probably means we shouldn't - // persist it again. - return - } - - // Write the attachment. If an error occurs, record it as an issue in the - // current test. - Issue.withErrorRecording(at: attachment.sourceLocation, configuration: self) { - var attachment = attachment - attachment.fileSystemPath = try attachment.write(toFileInDirectoryAtPath: attachmentsPath) - event.kind = .valueAttached(attachment) - } - } -} -#endif diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 12efce2b2..7faa0186e 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -13,7 +13,6 @@ add_library(Testing ABI/v0/ABIv0.Record.swift ABI/v0/ABIv0.Record+Streaming.swift ABI/v0/ABIv0.swift - ABI/v0/Encoded/ABIv0.EncodedAttachment.swift ABI/v0/Encoded/ABIv0.EncodedBacktrace.swift ABI/v0/Encoded/ABIv0.EncodedError.swift ABI/v0/Encoded/ABIv0.EncodedEvent.swift @@ -21,8 +20,6 @@ add_library(Testing ABI/v0/Encoded/ABIv0.EncodedIssue.swift ABI/v0/Encoded/ABIv0.EncodedMessage.swift ABI/v0/Encoded/ABIv0.EncodedTest.swift - Attachments/Test.Attachable.swift - Attachments/Test.Attachment.swift Events/Clock.swift Events/Event.swift Events/Recorder/Event.ConsoleOutputRecorder.swift diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index 54f4eea31..ffd55b5dd 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -98,13 +98,6 @@ public struct Event: Sendable { /// - issue: The issue which was recorded. indirect case issueRecorded(_ issue: Issue) - /// An attachment was created. - /// - /// - Parameters: - /// - attachment: The attachment that was created. - @_spi(Experimental) - indirect case valueAttached(_ attachment: Test.Attachment) - /// A test ended. /// /// The test that ended is contained in the ``Event/Context`` instance that @@ -423,9 +416,6 @@ extension Event.Kind { /// - issue: The issue which was recorded. indirect case issueRecorded(_ issue: Issue.Snapshot) - /// An attachment was created. - case valueAttached - /// A test ended. case testEnded @@ -485,8 +475,6 @@ extension Event.Kind { self = Snapshot.expectationChecked(expectationSnapshot) case let .issueRecorded(issue): self = .issueRecorded(Issue.Snapshot(snapshotting: issue)) - case .valueAttached: - self = .valueAttached case .testEnded: self = .testEnded case let .testSkipped(skipInfo): diff --git a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift index a0a6a8ee3..1f44de23a 100644 --- a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift @@ -166,8 +166,6 @@ extension Event.Symbol { return "\(_ansiEscapeCodePrefix)91m\(symbolCharacter)\(_resetANSIEscapeCode)" case .warning: return "\(_ansiEscapeCodePrefix)93m\(symbolCharacter)\(_resetANSIEscapeCode)" - case .attachment: - return "\(_ansiEscapeCodePrefix)94m\(symbolCharacter)\(_resetANSIEscapeCode)" case .details: return symbolCharacter } diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index ab1f56702..ad4b79b8e 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -459,23 +459,6 @@ extension Event.HumanReadableOutputRecorder { } return CollectionOfOne(primaryMessage) + additionalMessages - case let .valueAttached(attachment): - var result = [ - Message( - symbol: .attachment, - stringValue: "Attached '\(attachment.preferredName)' to \(testName)." - ) - ] - if let path = attachment.fileSystemPath { - result.append( - Message( - symbol: .details, - stringValue: "Written to '\(path)'." - ) - ) - } - return result - case .testCaseStarted: guard let testCase = eventContext.testCase, testCase.isParameterized else { break diff --git a/Sources/Testing/Events/Recorder/Event.Symbol.swift b/Sources/Testing/Events/Recorder/Event.Symbol.swift index 32acc8378..6bec7eb13 100644 --- a/Sources/Testing/Events/Recorder/Event.Symbol.swift +++ b/Sources/Testing/Events/Recorder/Event.Symbol.swift @@ -38,10 +38,6 @@ extension Event { /// The symbol to use when presenting details about an event to the user. case details - - /// The symbol to use when describing an instance of ``Test/Attachment``. - @_spi(Experimental) - case attachment } } @@ -70,8 +66,6 @@ extension Event.Symbol { ("\u{1001FF}", "exclamationmark.triangle.fill") case .details: ("\u{100135}", "arrow.turn.down.right") - case .attachment: - ("\u{100237}", "doc") } } @@ -134,10 +128,6 @@ extension Event.Symbol { case .details: // Unicode: DOWNWARDS ARROW WITH TIP RIGHTWARDS return "\u{21B3}" - case .attachment: - // TODO: decide on symbol - // Unicode: PRINT SCREEN SYMBOL - return "\u{2399}" } #elseif os(Windows) // The default Windows console font (Consolas) has limited Unicode support, @@ -169,10 +159,6 @@ extension Event.Symbol { case .details: // Unicode: RIGHTWARDS ARROW return "\u{2192}" - case .attachment: - // TODO: decide on symbol - // Unicode: PRINT SCREEN SYMBOL - return "\u{2399}" } #else #warning("Platform-specific implementation missing: Unicode characters unavailable") diff --git a/Sources/Testing/Running/Configuration+EventHandling.swift b/Sources/Testing/Running/Configuration+EventHandling.swift index 03931b790..95febe085 100644 --- a/Sources/Testing/Running/Configuration+EventHandling.swift +++ b/Sources/Testing/Running/Configuration+EventHandling.swift @@ -23,15 +23,6 @@ extension Configuration { var contextCopy = copy context contextCopy.configuration = self contextCopy.configuration?.eventHandler = { _, _ in } - -#if !SWT_NO_FILE_IO - if case .valueAttached = event.kind { - var eventCopy = copy event - handleValueAttachedEvent(&eventCopy, in: context) - return eventHandler(eventCopy, contextCopy) - } -#endif - - return eventHandler(event, contextCopy) + eventHandler(event, contextCopy) } } diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 786856e10..89cb93a07 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -204,33 +204,6 @@ public struct Configuration: Sendable { } #endif -#if !SWT_NO_FILE_IO - /// Storage for ``attachmentsPath``. - private var _attachmentsPath: String? - - /// The path to which attachments should be written. - /// - /// By default, attachments are not written to disk when they are created. If - /// the value of this property is not `nil`, then when an attachment is - /// created and attached to a test, it will automatically be written to a file - /// in this directory. - /// - /// The value of this property must refer to a directory on the local file - /// system that already exists and which the current user can write to. If it - /// is a relative path, it is resolved to an absolute path automatically. - @_spi(Experimental) - public var attachmentsPath: String? { - get { - _attachmentsPath - } - set { - _attachmentsPath = newValue.map { newValue in - canonicalizePath(newValue) ?? newValue - } - } - } -#endif - /// How verbose human-readable output should be. /// /// When the value of this property is greater than `0`, additional output diff --git a/Sources/Testing/Support/FileHandle.swift b/Sources/Testing/Support/FileHandle.swift index 5a58589db..e7b75e57a 100644 --- a/Sources/Testing/Support/FileHandle.swift +++ b/Sources/Testing/Support/FileHandle.swift @@ -580,85 +580,4 @@ func appendPathComponent(_ pathComponent: String, to path: String) -> String { "\(path)/\(pathComponent)" #endif } - -/// Check if a file exists at a given path. -/// -/// - Parameters: -/// - path: The path to check. -/// -/// - Returns: Whether or not the path `path` exists on disk. -func fileExists(atPath path: String) -> Bool { -#if os(Windows) - path.withCString(encodedAs: UTF16.self) { path in - PathFileExistsW(path) - } -#else - 0 == access(path, F_OK) -#endif -} - -/// Resolve a relative path or a path containing symbolic links to a canonical -/// absolute path. -/// -/// - Parameters: -/// - path: The path to resolve. -/// -/// - Returns: A fully resolved copy of `path`. If `path` is already fully -/// resolved, the resulting string may differ slightly but refers to the same -/// file system object. If the path could not be resolved, returns `nil`. -func canonicalizePath(_ path: String) -> String? { -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(Android) || os(WASI) - path.withCString { path in - if let resolvedCPath = realpath(path, nil) { - defer { - free(resolvedCPath) - } - return String(validatingCString: resolvedCPath) - } - return nil - } -#elseif os(Windows) - path.withCString(encodedAs: UTF16.self) { path in - if let resolvedCPath = _wfullpath(nil, path, 0) { - defer { - free(resolvedCPath) - } - return String.decodeCString(resolvedCPath, as: UTF16.self)?.result - } - return nil - } -#else -#warning("Platform-specific implementation missing: cannot resolve paths") - return nil -#endif -} - -/// Get the path to the user's or system's temporary directory. -/// -/// - Returns: The path to a directory suitable for storing temporary files. -/// -/// - Throws: If the user's or system's temporary directory could not be -/// determined. -func temporaryDirectoryPath() throws -> String { -#if SWT_TARGET_OS_APPLE - try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX)) { buffer in - if 0 != confstr(_CS_DARWIN_USER_TEMP_DIR, buffer.baseAddress, buffer.count) { - return String(cString: buffer.baseAddress!) - } - return try #require(Environment.variable(named: "TMPDIR")) - } -#elseif os(Linux) || os(FreeBSD) - "/tmp" -#elseif os(Android) - Environment.variable(named: "TMPDIR") ?? "/data/local/tmp" -#elseif os(Windows) - try withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(MAX_PATH + 1)) { buffer in - // NOTE: GetTempPath2W() was introduced in Windows 10 Build 20348. - if 0 == GetTempPathW(DWORD(buffer.count), buffer.baseAddress) { - throw Win32Error(rawValue: GetLastError()) - } - return try #require(String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result) - } -#endif -} #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift deleted file mode 100644 index a16c8c18f..000000000 --- a/Tests/TestingTests/AttachmentTests.swift +++ /dev/null @@ -1,319 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - -@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing -private import _TestingInternals - -@Suite("Attachment Tests") -struct AttachmentTests { - @Test func saveValue() { - let attachableValue = MyAttachable(string: "") - let attachment = Test.Attachment(attachableValue, named: "AttachmentTests.saveValue.html") - attachment.attach() - } - -#if !SWT_NO_FILE_IO - func compare(_ attachableValue: borrowing MySendableAttachable, toContentsOfFileAtPath filePath: String) throws { - let file = try FileHandle(forReadingAtPath: filePath) - let bytes = try file.readToEnd() - - let decodedValue = if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { - try #require(String(validating: bytes, as: UTF8.self)) - } else { - String(decoding: bytes, as: UTF8.self) - } - #expect(decodedValue == attachableValue.string) - } - - @Test func writeAttachment() throws { - let attachableValue = MySendableAttachable(string: "") - let attachment = Test.Attachment(attachableValue, named: "loremipsum.html") - - // Write the attachment to disk, then read it back. - let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath()) - defer { - remove(filePath) - } - try compare(attachableValue, toContentsOfFileAtPath: filePath) - } - - @Test func writeAttachmentWithNameConflict() throws { - // A sequence of suffixes that are guaranteed to cause conflict. - let randomBaseValue = UInt64.random(in: 0 ..< (.max - 10)) - var suffixes = (randomBaseValue ..< randomBaseValue + 10).lazy - .flatMap { [$0, $0, $0] } - .map { String($0, radix: 36) } - .makeIterator() - let baseFileName = "\(UInt64.random(in: 0 ..< .max))loremipsum.html" - var createdFilePaths = [String]() - defer { - for filePath in createdFilePaths { - remove(filePath) - } - } - - for i in 0 ..< 5 { - let attachableValue = MySendableAttachable(string: "\(i)") - let attachment = Test.Attachment(attachableValue, named: baseFileName) - - // Write the attachment to disk, then read it back. - let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath(), appending: suffixes.next()!) - createdFilePaths.append(filePath) - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) - if i == 0 { - #expect(fileName == baseFileName) - } else { - #expect(fileName != baseFileName) - } - try compare(attachableValue, toContentsOfFileAtPath: filePath) - } - } - - @Test func writeAttachmentWithMultiplePathExtensions() throws { - let attachableValue = MySendableAttachable(string: "") - let attachment = Test.Attachment(attachableValue, named: "loremipsum.tar.gz.gif.jpeg.html") - - // Write the attachment to disk once to ensure the original filename is not - // available and we add a suffix. - let originalFilePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath()) - defer { - remove(originalFilePath) - } - - // Write the attachment to disk, then read it back. - let suffix = String(UInt64.random(in: 0 ..< .max), radix: 36) - let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath(), appending: suffix) - defer { - remove(filePath) - } - let fileName = try #require(filePath.split { $0 == "/" || $0 == #"\"# }.last) - #expect(fileName == "loremipsum-\(suffix).tar.gz.gif.jpeg.html") - try compare(attachableValue, toContentsOfFileAtPath: filePath) - } - -#if os(Windows) - static let maximumNameCount = Int(_MAX_FNAME) - static let reservedNames = ["CON", "COM0", "LPT2"] -#else - static let maximumNameCount = Int(NAME_MAX) - static let reservedNames: [String] = [] -#endif - - @Test(arguments: [ - #"/\:"#, - String(repeating: "a", count: maximumNameCount), - String(repeating: "a", count: maximumNameCount + 1), - String(repeating: "a", count: maximumNameCount + 2), - ] + reservedNames) func writeAttachmentWithBadName(name: String) throws { - let attachableValue = MySendableAttachable(string: "") - let attachment = Test.Attachment(attachableValue, named: name) - - // Write the attachment to disk, then read it back. - let filePath = try attachment.write(toFileInDirectoryAtPath: temporaryDirectoryPath()) - defer { - remove(filePath) - } - try compare(attachableValue, toContentsOfFileAtPath: filePath) - } - - @Test func fileSystemPathIsSetAfterWritingViaEventHandler() async throws { - var configuration = Configuration() - configuration.attachmentsPath = try temporaryDirectoryPath() - - let attachableValue = MySendableAttachable(string: "") - - await confirmation("Attachment detected") { valueAttached in - await Test { - let attachment = Test.Attachment(attachableValue, named: "loremipsum.html") - attachment.attach() - }.run(configuration: configuration) { event, _ in - guard case let .valueAttached(attachment) = event.kind else { - return - } - valueAttached() - - // BUG: We could use #expect(throws: Never.self) here, but the Swift 6.1 - // compiler crashes trying to expand the macro (rdar://138997009) - do { - let filePath = try #require(attachment.fileSystemPath) - defer { - remove(filePath) - } - try compare(attachableValue, toContentsOfFileAtPath: filePath) - } catch { - Issue.record(error) - } - } - } - } -#endif - - @Test func attachValue() async { - await confirmation("Attachment detected") { valueAttached in - await Test { - let attachableValue = MyAttachable(string: "") - Test.Attachment(attachableValue, named: "loremipsum").attach() - }.run { event, _ in - guard case let .valueAttached(attachment) = event.kind else { - return - } - - #expect(attachment.preferredName == "loremipsum") - valueAttached() - } - } - } - - @Test func attachSendableValue() async { - await confirmation("Attachment detected") { valueAttached in - await Test { - let attachableValue = MySendableAttachable(string: "") - Test.Attachment(attachableValue, named: "loremipsum").attach() - }.run { event, _ in - guard case let .valueAttached(attachment) = event.kind else { - return - } - - #expect(attachment.preferredName == "loremipsum") - valueAttached() - } - } - } - - @Test func issueRecordedWhenAttachingNonSendableValueThatThrows() async { - await confirmation("Attachment detected") { valueAttached in - await confirmation("Issue recorded") { issueRecorded in - await Test { - var attachableValue = MyAttachable(string: "") - attachableValue.errorToThrow = MyError() - Test.Attachment(attachableValue, named: "loremipsum").attach() - }.run { event, _ in - if case .valueAttached = event.kind { - valueAttached() - } else if case let .issueRecorded(issue) = event.kind, - case let .errorCaught(error) = issue.kind, - error is MyError { - issueRecorded() - } - } - } - } - } -} - -extension AttachmentTests { - @Suite("Built-in conformances") - struct BuiltInConformances { - func test(_ value: borrowing some Test.Attachable & ~Copyable) throws { - #expect(value.estimatedAttachmentByteCount == 6) - let attachment = Test.Attachment(value) - try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in - #expect(buffer.elementsEqual("abc123".utf8)) - #expect(buffer.count == 6) - } - } - - @Test func uint8Array() throws { - let value: [UInt8] = Array("abc123".utf8) - try test(value) - } - - @Test func uint8ContiguousArray() throws { - let value: ContiguousArray = ContiguousArray("abc123".utf8) - try test(value) - } - - @Test func uint8ArraySlice() throws { - let value: ArraySlice = Array("abc123".utf8)[...] - try test(value) - } - - @Test func uint8UnsafeBufferPointer() throws { - let value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeBufferPointer { value in - try test(value) - } - } - - @Test func uint8UnsafeMutableBufferPointer() throws { - var value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeMutableBufferPointer { value in - try test(value) - } - } - - @Test func unsafeRawBufferPointer() throws { - let value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeBytes { value in - try test(value) - } - } - - @Test func unsafeMutableRawBufferPointer() throws { - var value: [UInt8] = Array("abc123".utf8) - try value.withUnsafeMutableBytes { value in - try test(value) - } - } - - @Test func string() throws { - let value = "abc123" - try test(value) - } - - @Test func substring() throws { - let value: Substring = "abc123"[...] - try test(value) - } - } -} - -// MARK: - Fixtures - -struct MyAttachable: Test.Attachable, ~Copyable { - var string: String - var errorToThrow: (any Error)? - - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - if let errorToThrow { - throw errorToThrow - } - - var string = string - return try string.withUTF8 { buffer in - try body(.init(buffer)) - } - } -} - -@available(*, unavailable) -extension MyAttachable: Sendable {} - -struct MySendableAttachable: Test.Attachable, Sendable { - var string: String - - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var string = string - return try string.withUTF8 { buffer in - try body(.init(buffer)) - } - } -} - -struct MySendableAttachableWithDefaultByteCount: Test.Attachable, Sendable { - var string: String - - func withUnsafeBufferPointer(for attachment: borrowing Testing.Test.Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - var string = string - return try string.withUTF8 { buffer in - try body(.init(buffer)) - } - } -} diff --git a/Tests/TestingTests/Support/FileHandleTests.swift b/Tests/TestingTests/Support/FileHandleTests.swift index 7d8b3e817..c7f347357 100644 --- a/Tests/TestingTests/Support/FileHandleTests.swift +++ b/Tests/TestingTests/Support/FileHandleTests.swift @@ -215,7 +215,7 @@ func withTemporaryPath(_ body: (_ path: String) throws -> R) throws -> R { return strnlen(buffer.baseAddress!, buffer.count) } #else - let path = appendPathComponent("file_named_\(UInt64.random(in: 0 ..< .max))", to: try temporaryDirectoryPath()) + let path = appendPathComponent("file_named_\(UInt64.random(in: 0 ..< .max))", to: try temporaryDirectory()) #endif defer { _ = remove(path) @@ -247,6 +247,29 @@ extension FileHandle { } #endif +func temporaryDirectory() throws -> String { +#if SWT_TARGET_OS_APPLE + try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX)) { buffer in + if 0 != confstr(_CS_DARWIN_USER_TEMP_DIR, buffer.baseAddress, buffer.count) { + return String(cString: buffer.baseAddress!) + } + return try #require(Environment.variable(named: "TMPDIR")) + } +#elseif os(Linux) || os(FreeBSD) + "/tmp" +#elseif os(Android) + Environment.variable(named: "TMPDIR") ?? "/data/local/tmp" +#elseif os(Windows) + try withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(MAX_PATH + 1)) { buffer in + // NOTE: GetTempPath2W() was introduced in Windows 10 Build 20348. + if 0 == GetTempPathW(DWORD(buffer.count), buffer.baseAddress) { + throw Win32Error(rawValue: GetLastError()) + } + return try #require(String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result) + } +#endif +} + #if SWT_TARGET_OS_APPLE func fileHandleForCloseMonitoring(with confirmation: Confirmation) throws -> FileHandle { let context = Unmanaged.passRetained(confirmation as AnyObject).toOpaque() diff --git a/Tests/TestingTests/SwiftPMTests.swift b/Tests/TestingTests/SwiftPMTests.swift index 3f9275e3b..6b114efe6 100644 --- a/Tests/TestingTests/SwiftPMTests.swift +++ b/Tests/TestingTests/SwiftPMTests.swift @@ -176,7 +176,7 @@ struct SwiftPMTests { func xunitOutputIsWrittenToFile() throws { // Test that a file is opened when requested. Testing of the actual output // occurs in ConsoleOutputRecorderTests. - let tempDirPath = try temporaryDirectoryPath() + let tempDirPath = try temporaryDirectory() let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) defer { _ = remove(temporaryFilePath) @@ -200,7 +200,7 @@ struct SwiftPMTests { "--configuration-path", "--experimental-configuration-path", ]) func configurationPath(argumentName: String) async throws { - let tempDirPath = try temporaryDirectoryPath() + let tempDirPath = try temporaryDirectory() let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) defer { _ = remove(temporaryFilePath) @@ -244,7 +244,7 @@ struct SwiftPMTests { func eventStreamOutput(outputArgumentName: String, versionArgumentName: String, version: String) async throws { // Test that JSON records are successfully streamed to a file and can be // read back into memory and decoded. - let tempDirPath = try temporaryDirectoryPath() + let tempDirPath = try temporaryDirectory() let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath) defer { _ = remove(temporaryFilePath) diff --git a/Tests/TestingTests/TestSupport/TestingAdditions.swift b/Tests/TestingTests/TestSupport/TestingAdditions.swift index a253d6b55..0f0d4641a 100644 --- a/Tests/TestingTests/TestSupport/TestingAdditions.swift +++ b/Tests/TestingTests/TestSupport/TestingAdditions.swift @@ -237,14 +237,7 @@ extension Test { /// runs it. It is provided as a convenience for use in the testing library's /// own test suite; when writing tests for other test suites, it should not be /// necessary to call this function. - func run(configuration: Configuration = .init(), eventHandler: Event.Handler? = nil) async { - var configuration = configuration - if let eventHandler { - configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, eventContext in - eventHandler(event, eventContext) - oldEventHandler(event, eventContext) - } - } + func run(configuration: Configuration = .init()) async { let runner = await Runner(testing: [self], configuration: configuration) await runner.run() } diff --git a/Tests/TestingTests/Traits/TagListTests.swift b/Tests/TestingTests/Traits/TagListTests.swift index f81564713..29b8e3909 100644 --- a/Tests/TestingTests/Traits/TagListTests.swift +++ b/Tests/TestingTests/Traits/TagListTests.swift @@ -121,7 +121,7 @@ struct TagListTests { #if !SWT_NO_FILE_IO @Test("Colors are read from disk") func tagColorsReadFromDisk() throws { - let tempDirPath = try temporaryDirectoryPath() + let tempDirPath = try temporaryDirectory() let jsonPath = appendPathComponent("tag-colors.json", to: tempDirPath) var jsonContent = """ {