Skip to content
Draft
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if(NOT APPLE)
find_package(Foundation CONFIG)
if(BUILD_TESTING)
find_package(XCTest CONFIG)
find_package(Testing CONFIG)
endif()
endif()

Expand Down
7 changes: 6 additions & 1 deletion Sources/ArgumentParserTestHelpers/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_library(ArgumentParserTestHelpers
TestHelpers.swift)
TestHelpers.swift
TestHelpers+SwiftTesting.swift)
set_target_properties(ArgumentParserTestHelpers PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_libraries(ArgumentParserTestHelpers PUBLIC
Expand All @@ -13,3 +14,7 @@ if(XCTest_Found)
target_link_libraries(ArgumentParserTestHelpers PUBLIC
XCTest)
endif()
if(Testing_FOUND)
target_link_libraries(ArgumentParserTestHelpers PUBLIC
Testing)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// 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 ArgumentParser
import Testing

public func expectResultFailure<T, U: Error>(
_ expression: @autoclosure () -> Result<T, U>,
_ message: @autoclosure () -> String = "",
sourceLocation: SourceLocation = #_sourceLocation
) {
AssertResultFailure(expression(), message(), sourceLocation: sourceLocation)
}

public func expectErrorMessage<A: ParsableArguments>(
_ type: A.Type, _ arguments: [String], _ errorMessage: String,
sourceLocation: SourceLocation = #_sourceLocation
) {
AssertErrorMessage(
type, arguments, errorMessage, sourceLocation: sourceLocation)
}

public func expectFullErrorMessage<A: ParsableArguments>(
_ type: A.Type, _ arguments: [String], _ errorMessage: String,
sourceLocation: SourceLocation = #_sourceLocation
) {
AssertFullErrorMessage(
type, arguments, errorMessage, sourceLocation: sourceLocation)
}

public func expectParse<A: ParsableArguments>(
_ type: A.Type, _ arguments: [String],
sourceLocation: SourceLocation = #_sourceLocation,
closure: (A) throws -> Void
) {
AssertParse(type, arguments, sourceLocation: sourceLocation) {
try closure($0)
}
}

public func expectParseCommand<A: ParsableCommand>(
_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String],
sourceLocation: SourceLocation = #_sourceLocation,
closure: (A) throws -> Void
) {
AssertParseCommand(
rootCommand, type, arguments, sourceLocation: sourceLocation
) {
try closure($0)
}
}

public func expectEqualStrings(
actual: String,
expected: String,
sourceLocation: SourceLocation = #_sourceLocation
) {
AssertEqualStrings(
actual: actual, expected: expected, sourceLocation: sourceLocation)
}
115 changes: 94 additions & 21 deletions Sources/ArgumentParserTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserToolInfo
import Testing
import XCTest

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
Expand Down Expand Up @@ -80,12 +81,20 @@ public func AssertResultFailure<T, U: Error>(
_ expression: @autoclosure () -> Result<T, U>,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation
) {
switch expression() {
case .success:
let msg = message()
XCTFail(msg.isEmpty ? "Incorrectly succeeded" : msg, file: file, line: line)
if Test.current != nil {
Issue.record(
msg.isEmpty ? "Incorrectly succeeded" : "\(msg)",
sourceLocation: sourceLocation)
} else {
XCTFail(
msg.isEmpty ? "Incorrectly succeeded" : msg, file: file, line: line)
}
case .failure:
break
}
Expand All @@ -94,71 +103,127 @@ public func AssertResultFailure<T, U: Error>(
// swift-format-ignore: AlwaysUseLowerCamelCase
public func AssertErrorMessage<A>(
_ type: A.Type, _ arguments: [String], _ errorMessage: String,
file: StaticString = #filePath, line: UInt = #line
file: StaticString = #filePath,
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation
) where A: ParsableArguments {

do {
_ = try A.parse(arguments)
XCTFail("Parsing should have failed.", file: file, line: line)
if Test.current != nil {
Issue.record(
"Parsing should have failed.", sourceLocation: sourceLocation)
} else {
XCTFail("Parsing should have failed.", file: file, line: line)
}
} catch {
// We expect to hit this path, i.e. getting an error:
XCTAssertEqual(A.message(for: error), errorMessage, file: file, line: line)
if Test.current != nil {
#expect(
A.message(for: error) == errorMessage,
sourceLocation: sourceLocation
)
} else {
XCTAssertEqual(
A.message(for: error), errorMessage, file: file, line: line)
}
}
}

// swift-format-ignore: AlwaysUseLowerCamelCase
public func AssertFullErrorMessage<A>(
_ type: A.Type, _ arguments: [String], _ errorMessage: String,
file: StaticString = #filePath, line: UInt = #line
file: StaticString = #filePath,
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation
) where A: ParsableArguments {
do {
_ = try A.parse(arguments)
XCTFail("Parsing should have failed.", file: (file), line: line)
if Test.current != nil {
Issue.record(
"Parsing should have failed.", sourceLocation: sourceLocation)
} else {
XCTFail("Parsing should have failed.", file: (file), line: line)
}
} catch {
// We expect to hit this path, i.e. getting an error:
XCTAssertEqual(
A.fullMessage(for: error), errorMessage, file: (file), line: line)
if Test.current != nil {
#expect(
A.fullMessage(for: error) == errorMessage,
sourceLocation: sourceLocation
)
} else {
XCTAssertEqual(
A.fullMessage(for: error), errorMessage, file: (file), line: line)
}
}
}

// swift-format-ignore: AlwaysUseLowerCamelCase
public func AssertParse<A>(
_ type: A.Type, _ arguments: [String], file: StaticString = #filePath,
line: UInt = #line, closure: (A) throws -> Void
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation,
closure: (A) throws -> Void
) where A: ParsableArguments {
do {
let parsed = try type.parse(arguments)
try closure(parsed)
} catch {
let message = type.message(for: error)
XCTFail("\"\(message)\" — \(error)", file: (file), line: line)
if Test.current != nil {
Issue.record(
"\"\(message)\" — \(error)",
sourceLocation: sourceLocation
)
} else {
XCTFail("\"\(message)\" — \(error)", file: (file), line: line)
}
}
}

// swift-format-ignore: AlwaysUseLowerCamelCase
public func AssertParseCommand<A: ParsableCommand>(
_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String],
file: StaticString = #filePath, line: UInt = #line,
file: StaticString = #filePath,
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation,
closure: (A) throws -> Void
) {
do {
let command = try rootCommand.parseAsRoot(arguments)
guard let aCommand = command as? A else {
XCTFail(
"Command is of unexpected type: \(command)", file: (file), line: line)
if Test.current != nil {
Issue.record(
"Command is of unexpected type: \(command)",
sourceLocation: sourceLocation
)
} else {
XCTFail(
"Command is of unexpected type: \(command)", file: (file), line: line)
}
return
}
try closure(aCommand)
} catch {
let message = rootCommand.message(for: error)
XCTFail("\"\(message)\" — \(error)", file: file, line: line)
if Test.current != nil {
Issue.record(
"\"\(message)\" — \(error)",
sourceLocation: sourceLocation
)
} else {
XCTFail("\"\(message)\" — \(error)", file: file, line: line)
}
}
}

// swift-format-ignore: AlwaysUseLowerCamelCase
public func AssertParseCommandErrorMessage<A: ParsableCommand>(
_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String],
_ errorMessage: String,
file: StaticString = #filePath, line: UInt = #line
file: StaticString = #filePath,
line: UInt = #line
) {
do {
let command = try rootCommand.parseAsRoot(arguments)
Expand All @@ -179,7 +244,8 @@ public func AssertEqualStrings(
actual: String,
expected: String,
file: StaticString = #filePath,
line: UInt = #line
line: UInt = #line,
sourceLocation: SourceLocation = #_sourceLocation
) {
// Normalize line endings to '\n'.
let actual =
Expand Down Expand Up @@ -248,10 +314,17 @@ public func AssertEqualStrings(
"""
}

XCTFail(
"Actual output does not match the expected output:\n\(stringComparison)",
file: file,
line: line)
if Test.current != nil {
Issue.record(
"Actual output does not match the expected output:\n\(stringComparison)",
sourceLocation: sourceLocation
)
} else {
XCTFail(
"Actual output does not match the expected output:\n\(stringComparison)",
file: file,
line: line)
}
}

// swift-format-ignore: AlwaysUseLowerCamelCase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class ParsingEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class DefaultAsFlagEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParserTestHelpers
import ArgumentParserToolInfo
import Testing
import XCTest

@testable import ArgumentParser
Expand Down Expand Up @@ -132,7 +133,7 @@ extension DefaultSubcommandEndToEndTests {
}
}

func testNotMyParent() throws {
func testNotMyParent() {
AssertParseCommandErrorMessage(
MyCommand.self, BadParent.self, ["--verbose", "bad-parent"],
"Command 'Other' is not a parent of the current command.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class DefaultsEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class EnumEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class EqualsEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class FlagsEndToEndTests: XCTestCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class JoinedEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class LongNameWithSingleDashEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class NestedCommandEndToEndTests: XCTestCase {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import ArgumentParser
import ArgumentParserTestHelpers
import Testing
import XCTest

final class OptionGroupEndToEndTests: XCTestCase {}
Expand Down
Loading