Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

import Testing

/// A Swift Testing trait that disables a test on platforms where launching a
/// subprocess via `Foundation.Process` is not supported.
///
/// Apply with `@Test(.requiresProcessExecution)` to tests whose bodies
/// (transitively) call `requireExecuteCommand`, `expectDumpHelp(command:)`,
/// `expectGenerateManual`, or `expectGeneratedReference` *and* also compare
/// the output against a fixed expectation (e.g. via `expectSnapshot`). On
/// unsupported platforms these helpers return an empty string rather than
/// throwing, so a downstream snapshot comparison would fail spuriously; the
/// trait suppresses the test entirely instead.
extension Trait where Self == Testing.ConditionTrait {
public static var requiresProcessExecution: Self {
#if os(Windows) || (canImport(Darwin) && !os(macOS))
return .disabled(
"Process execution is not supported on this platform.")
#else
return .enabled(if: true)
#endif
}
}
134 changes: 134 additions & 0 deletions Sources/ArgumentParserTestHelpers/TestHelpers+SwiftTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@
//===----------------------------------------------------------------------===//

import ArgumentParser
import Foundation
import Testing

private final class _BundleMarker {}

private var _debugURL: URL {
let bundleURL = Bundle(for: _BundleMarker.self).bundleURL
return bundleURL.lastPathComponent.hasSuffix("xctest")
? bundleURL.deletingLastPathComponent()
: bundleURL
}

public func expectResultFailure<T, U: Error>(
_ expression: @autoclosure () -> Result<T, U>,
_ message: @autoclosure () -> String = "",
Expand Down Expand Up @@ -66,3 +76,127 @@ public func expectEqualStrings(
AssertEqualStrings(
actual: actual, expected: expected, sourceLocation: sourceLocation)
}

@discardableResult
public func requireExecuteCommand(
command: String,
expected: String? = nil,
exitCode: ExitCode = .success,
environment: [String: String] = [:],
sourceLocation: SourceLocation = #_sourceLocation
) throws -> String {
try requireExecuteCommand(
command: command.split(separator: " ").map(String.init),
expected: expected,
exitCode: exitCode,
environment: environment,
sourceLocation: sourceLocation)
}

@discardableResult
public func requireExecuteCommand(
command: [String],
expected: String? = nil,
exitCode: ExitCode = .success,
environment: [String: String] = [:],
sourceLocation: SourceLocation = #_sourceLocation
) throws -> String {
#if os(Windows)
return ""
#elseif !canImport(Darwin) || os(macOS)
let arguments = Array(command.dropFirst())
let commandName = String(command.first!)
let commandURL = _debugURL.appendingPathComponent(commandName)
_ = try #require(
try commandURL.checkResourceIsReachable(),
"No executable at '\(commandURL.standardizedFileURL.path)'.",
sourceLocation: sourceLocation
)

let process = Process()
process.executableURL = commandURL
process.arguments = arguments

let output = Pipe()
process.standardOutput = output
let error = Pipe()
process.standardError = error

if !environment.isEmpty {
if let existingEnvironment = process.environment {
process.environment =
existingEnvironment.merging(environment) { (_, new) in new }
} else {
process.environment = environment
}
}

try #require(
try? process.run(),
"Couldn't run command process.",
sourceLocation: sourceLocation
)
process.waitUntilExit()

let outputData = output.fileHandleForReading.readDataToEndOfFile()
let outputActual = String(data: outputData, encoding: .utf8)!

let errorData = error.fileHandleForReading.readDataToEndOfFile()
let errorActual = String(data: errorData, encoding: .utf8)!

if let expected = expected {
expectEqualStrings(
actual: errorActual + outputActual,
expected: expected,
sourceLocation: sourceLocation)
}

#expect(
process.terminationStatus == exitCode.rawValue,
sourceLocation: sourceLocation)
return outputActual
#else
return ""
#endif
}

@discardableResult
public func expectSnapshot(
actual: String,
extension: String,
record: Bool = false,
test: String = #function,
filePath: StaticString = #filePath,
sourceLocation: SourceLocation = #_sourceLocation
) throws -> String? {
let snapshotDirectoryURL = URL(fileURLWithPath: "\(filePath)")
.deletingLastPathComponent()
.appendingPathComponent("Snapshots")
let snapshotFileURL =
snapshotDirectoryURL
.appendingPathComponent("\(test).\(`extension`)")

let snapshotExists = FileManager.default.fileExists(
atPath: snapshotFileURL.path)
let recordEnvironment =
ProcessInfo.processInfo.environment["RECORD_SNAPSHOTS"] != nil

if record || recordEnvironment || !snapshotExists {
let recordedValue = actual
try FileManager.default.createDirectory(
at: snapshotDirectoryURL,
withIntermediateDirectories: true,
attributes: nil)
try recordedValue.write(
to: snapshotFileURL, atomically: true, encoding: .utf8)
Issue.record("Recorded new baseline", sourceLocation: sourceLocation)
return nil
} else {
let expected = try String(contentsOf: snapshotFileURL, encoding: .utf8)
expectEqualStrings(
actual: actual,
expected: expected,
sourceLocation: sourceLocation)
return expected
}
}
22 changes: 12 additions & 10 deletions Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Copyright (c) 2020-2026 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
Expand All @@ -11,26 +11,28 @@

#if os(macOS)

import XCTest
import ArgumentParserTestHelpers
import Foundation
import Testing

@testable import ArgumentParser

final class CountLinesExampleTests: XCTestCase {
override func setUp() {
@Suite struct CountLinesExampleTests {
init() {
Platform.Environment[.columns] = nil
}

func testCountLines() throws {
@Test func countLines() throws {
guard #available(macOS 12, *) else { return }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Out of curiosity, why do test test only run on macOS? Should the test, and related binary, be updated to run on all platforms os is the intents to verify Swift Argument Parser with the @available(...) API?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all these Qs, I think the gating factor was what I could get working at the time of setting up these executable tests. So the question is more about whether we can get these executable tests to run on Linux/Windows, there isn't a principled reason not to.

let testFile = try XCTUnwrap(
let testFile = try #require(
Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt"))
try AssertExecuteCommand(
try requireExecuteCommand(
command: "count-lines \(testFile.path)", expected: "20\n")
try AssertExecuteCommand(
try requireExecuteCommand(
command: "count-lines \(testFile.path) --prefix al", expected: "4\n")
}

func testCountLinesHelp() throws {
@Test func countLinesHelp() throws {
guard #available(macOS 12, *) else { return }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Out of curiosity, why do test test only run on macOS? Should the test, and related binary, be updated to run on all platforms os is the intents to verify Swift Argument Parser with the @available(...) API?

let helpText = """
USAGE: count-lines [<input-file>] [--prefix <prefix>] [--verbose]
Expand All @@ -46,7 +48,7 @@ final class CountLinesExampleTests: XCTestCase {


"""
try AssertExecuteCommand(command: "count-lines -h", expected: helpText)
try requireExecuteCommand(command: "count-lines -h", expected: helpText)
}
}

Expand Down
Loading