mirror of
https://github.com/apple/swift-argument-parser.git
synced 2026-06-06 20:18:23 +00:00
- Renames AssertEqualStringsIgnoringTrailingWhitespace to AssertEqualStrings and updates the implementation to require matching trailing whitespace. - Updates AssertEqualStrings to include much easier to read diff output when CollectionDifference is available. This should add developers when tests fail by providing more clear errors.
443 lines
14 KiB
Swift
443 lines
14 KiB
Swift
//===----------------------------------------------------------*- swift -*-===//
|
|
//
|
|
// This source file is part of the Swift Argument Parser open source project
|
|
//
|
|
// Copyright (c) 2020 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 ArgumentParserToolInfo
|
|
import XCTest
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension CollectionDifference.Change {
|
|
var offset: Int {
|
|
switch self {
|
|
case .insert(let offset, _, _):
|
|
return offset
|
|
case .remove(let offset, _, _):
|
|
return offset
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension CollectionDifference.Change: Comparable where ChangeElement: Equatable {
|
|
public static func < (lhs: Self, rhs: Self) -> Bool {
|
|
guard lhs.offset == rhs.offset else {
|
|
return lhs.offset < rhs.offset
|
|
}
|
|
switch (lhs, rhs) {
|
|
case (.remove, .insert):
|
|
return true
|
|
case (.insert, .remove):
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// extensions to the ParsableArguments protocol to facilitate XCTestExpectation support
|
|
public protocol TestableParsableArguments: ParsableArguments {
|
|
var didValidateExpectation: XCTestExpectation { get }
|
|
}
|
|
|
|
public extension TestableParsableArguments {
|
|
mutating func validate() throws {
|
|
didValidateExpectation.fulfill()
|
|
}
|
|
}
|
|
|
|
// extensions to the ParsableCommand protocol to facilitate XCTestExpectation support
|
|
public protocol TestableParsableCommand: ParsableCommand, TestableParsableArguments {
|
|
var didRunExpectation: XCTestExpectation { get }
|
|
}
|
|
|
|
public extension TestableParsableCommand {
|
|
mutating func run() throws {
|
|
didRunExpectation.fulfill()
|
|
}
|
|
}
|
|
|
|
extension XCTestExpectation {
|
|
public convenience init(singleExpectation description: String) {
|
|
self.init(description: description)
|
|
expectedFulfillmentCount = 1
|
|
assertForOverFulfill = true
|
|
}
|
|
}
|
|
|
|
public func AssertResultFailure<T, U: Error>(
|
|
_ expression: @autoclosure () -> Result<T, U>,
|
|
_ message: @autoclosure () -> String = "",
|
|
file: StaticString = #file,
|
|
line: UInt = #line)
|
|
{
|
|
switch expression() {
|
|
case .success:
|
|
let msg = message()
|
|
XCTFail(msg.isEmpty ? "Incorrectly succeeded" : msg, file: file, line: line)
|
|
case .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
public func AssertErrorMessage<A>(_ type: A.Type, _ arguments: [String], _ errorMessage: String, file: StaticString = #file, line: UInt = #line) where A: ParsableArguments {
|
|
do {
|
|
_ = try A.parse(arguments)
|
|
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)
|
|
}
|
|
}
|
|
|
|
public func AssertFullErrorMessage<A>(_ type: A.Type, _ arguments: [String], _ errorMessage: String, file: StaticString = #file, line: UInt = #line) where A: ParsableArguments {
|
|
do {
|
|
_ = try A.parse(arguments)
|
|
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)
|
|
}
|
|
}
|
|
|
|
public func AssertParse<A>(_ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, 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)
|
|
}
|
|
}
|
|
|
|
public func AssertParseCommand<A: ParsableCommand>(_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, 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)
|
|
return
|
|
}
|
|
try closure(aCommand)
|
|
} catch {
|
|
let message = rootCommand.message(for: error)
|
|
XCTFail("\"\(message)\" — \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
public func AssertEqualStrings(actual: String, expected: String, file: StaticString = #file, line: UInt = #line) {
|
|
// If the input strings are not equal, create a simple diff for debugging...
|
|
guard actual != expected else {
|
|
// Otherwise they are equal, early exit.
|
|
return
|
|
}
|
|
|
|
// Split in the inputs into lines.
|
|
let actualLines = actual.split(separator: "\n", omittingEmptySubsequences: false)
|
|
let expectedLines = expected.split(separator: "\n", omittingEmptySubsequences: false)
|
|
|
|
// If collectionDifference is available, use it to make a nicer error message.
|
|
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
|
|
// Compute the changes between the two strings.
|
|
let changes = actualLines.difference(from: expectedLines).sorted()
|
|
|
|
// Render the changes into a diff style string.
|
|
var diff = ""
|
|
var expectedLines = expectedLines[...]
|
|
for change in changes {
|
|
if expectedLines.startIndex < change.offset {
|
|
for line in expectedLines[..<change.offset] {
|
|
diff += " \(line)\n"
|
|
}
|
|
expectedLines = expectedLines[change.offset...].dropFirst()
|
|
}
|
|
|
|
switch change {
|
|
case .insert(_, let line, _):
|
|
diff += "- \(line)\n"
|
|
case .remove(_, let line, _):
|
|
diff += "+ \(line)\n"
|
|
}
|
|
}
|
|
for line in expectedLines {
|
|
diff += " \(line)\n"
|
|
}
|
|
XCTFail("Strings are not equal.\n\(diff)", file: file, line: line)
|
|
} else {
|
|
XCTAssertEqual(
|
|
actualLines.count,
|
|
expectedLines.count,
|
|
"Strings have different numbers of lines.",
|
|
file: file,
|
|
line: line)
|
|
for (actualLine, expectedLine) in zip(actualLines, expectedLines) {
|
|
XCTAssertEqual(actualLine, expectedLine, file: file, line: line)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func AssertHelp<T: ParsableArguments>(
|
|
_ visibility: ArgumentVisibility,
|
|
for _: T.Type,
|
|
equals expected: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line
|
|
) {
|
|
let flag: String
|
|
let includeHidden: Bool
|
|
|
|
switch visibility {
|
|
case .default:
|
|
flag = "--help"
|
|
includeHidden = false
|
|
case .hidden:
|
|
flag = "--help-hidden"
|
|
includeHidden = true
|
|
case .private:
|
|
XCTFail("Should not be called.", file: file, line: line)
|
|
return
|
|
default:
|
|
XCTFail("Unrecognized visibility.", file: file, line: line)
|
|
return
|
|
}
|
|
|
|
do {
|
|
_ = try T.parse([flag])
|
|
XCTFail(file: file, line: line)
|
|
} catch {
|
|
let helpString = T.fullMessage(for: error)
|
|
AssertEqualStrings(actual: helpString, expected: expected, file: file, line: line)
|
|
}
|
|
|
|
let helpString = T.helpMessage(includeHidden: includeHidden, columns: nil)
|
|
AssertEqualStrings(actual: helpString, expected: expected, file: file, line: line)
|
|
}
|
|
|
|
public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
|
|
_ visibility: ArgumentVisibility,
|
|
for _: T.Type,
|
|
root _: U.Type,
|
|
equals expected: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line
|
|
) {
|
|
let includeHidden: Bool
|
|
|
|
switch visibility {
|
|
case .default:
|
|
includeHidden = false
|
|
case .hidden:
|
|
includeHidden = true
|
|
case .private:
|
|
XCTFail("Should not be called.", file: file, line: line)
|
|
return
|
|
default:
|
|
XCTFail("Unrecognized visibility.", file: file, line: line)
|
|
return
|
|
}
|
|
|
|
let helpString = U.helpMessage(
|
|
for: T.self, includeHidden: includeHidden, columns: nil)
|
|
AssertEqualStrings(actual: helpString, expected: expected, file: file, line: line)
|
|
}
|
|
|
|
public func AssertDump<T: ParsableArguments>(
|
|
for _: T.Type, equals expected: String,
|
|
file: StaticString = #file, line: UInt = #line
|
|
) throws {
|
|
do {
|
|
_ = try T.parse(["--experimental-dump-help"])
|
|
XCTFail(file: file, line: line)
|
|
} catch {
|
|
let dumpString = T.fullMessage(for: error)
|
|
try AssertJSONEqualFromString(actual: dumpString, expected: expected, for: ToolInfoV0.self)
|
|
}
|
|
|
|
try AssertJSONEqualFromString(actual: T._dumpHelp(), expected: expected, for: ToolInfoV0.self)
|
|
}
|
|
|
|
public func AssertJSONEqualFromString<T: Codable & Equatable>(actual: String, expected: String, for type: T.Type) throws {
|
|
let actualJSONData = try XCTUnwrap(actual.data(using: .utf8))
|
|
let actualDumpJSON = try XCTUnwrap(JSONDecoder().decode(type, from: actualJSONData))
|
|
|
|
let expectedJSONData = try XCTUnwrap(expected.data(using: .utf8))
|
|
let expectedDumpJSON = try XCTUnwrap(JSONDecoder().decode(type, from: expectedJSONData))
|
|
XCTAssertEqual(actualDumpJSON, expectedDumpJSON)
|
|
}
|
|
|
|
extension XCTest {
|
|
public var debugURL: URL {
|
|
let bundleURL = Bundle(for: type(of: self)).bundleURL
|
|
return bundleURL.lastPathComponent.hasSuffix("xctest")
|
|
? bundleURL.deletingLastPathComponent()
|
|
: bundleURL
|
|
}
|
|
|
|
public func AssertExecuteCommand(
|
|
command: String,
|
|
expected: String? = nil,
|
|
exitCode: ExitCode = .success,
|
|
file: StaticString = #file, line: UInt = #line) throws
|
|
{
|
|
try AssertExecuteCommand(
|
|
command: command.split(separator: " ").map(String.init),
|
|
expected: expected,
|
|
exitCode: exitCode,
|
|
file: file,
|
|
line: line)
|
|
}
|
|
|
|
public func AssertExecuteCommand(
|
|
command: [String],
|
|
expected: String? = nil,
|
|
exitCode: ExitCode = .success,
|
|
file: StaticString = #file, line: UInt = #line) throws
|
|
{
|
|
#if os(Windows)
|
|
throw XCTSkip("Unsupported on this platform")
|
|
#endif
|
|
|
|
let arguments = Array(command.dropFirst())
|
|
let commandName = String(command.first!)
|
|
let commandURL = debugURL.appendingPathComponent(commandName)
|
|
guard (try? commandURL.checkResourceIsReachable()) ?? false else {
|
|
XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.",
|
|
file: file, line: line)
|
|
return
|
|
}
|
|
|
|
#if !canImport(Darwin) || os(macOS)
|
|
let process = Process()
|
|
if #available(macOS 10.13, *) {
|
|
process.executableURL = commandURL
|
|
} else {
|
|
process.launchPath = commandURL.path
|
|
}
|
|
process.arguments = arguments
|
|
|
|
let output = Pipe()
|
|
process.standardOutput = output
|
|
let error = Pipe()
|
|
process.standardError = error
|
|
|
|
if #available(macOS 10.13, *) {
|
|
guard (try? process.run()) != nil else {
|
|
XCTFail("Couldn't run command process.", file: file, line: line)
|
|
return
|
|
}
|
|
} else {
|
|
process.launch()
|
|
}
|
|
process.waitUntilExit()
|
|
|
|
let outputData = output.fileHandleForReading.readDataToEndOfFile()
|
|
let outputActual = String(data: outputData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
let errorData = error.fileHandleForReading.readDataToEndOfFile()
|
|
let errorActual = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
if let expected = expected {
|
|
AssertEqualStrings(
|
|
actual: errorActual + outputActual,
|
|
expected: expected,
|
|
file: file,
|
|
line: line)
|
|
}
|
|
|
|
XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line)
|
|
#else
|
|
throw XCTSkip("Not supported on this platform")
|
|
#endif
|
|
}
|
|
|
|
public func AssertJSONOutputEqual(
|
|
command: String,
|
|
expected: String,
|
|
file: StaticString = #file, line: UInt = #line
|
|
) throws {
|
|
#if os(Windows)
|
|
throw XCTSkip("Unsupported on this platform")
|
|
#endif
|
|
|
|
let splitCommand = command.split(separator: " ")
|
|
let arguments = splitCommand.dropFirst().map(String.init)
|
|
|
|
let commandName = String(splitCommand.first!)
|
|
let commandURL = debugURL.appendingPathComponent(commandName)
|
|
guard (try? commandURL.checkResourceIsReachable()) ?? false else {
|
|
XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.",
|
|
file: file, line: line)
|
|
return
|
|
}
|
|
|
|
#if !canImport(Darwin) || os(macOS)
|
|
let process = Process()
|
|
if #available(macOS 10.13, *) {
|
|
process.executableURL = commandURL
|
|
} else {
|
|
process.launchPath = commandURL.path
|
|
}
|
|
process.arguments = arguments
|
|
|
|
let output = Pipe()
|
|
process.standardOutput = output
|
|
let error = Pipe()
|
|
process.standardError = error
|
|
|
|
if #available(macOS 10.13, *) {
|
|
guard (try? process.run()) != nil else {
|
|
XCTFail("Couldn't run command process.", file: file, line: line)
|
|
return
|
|
}
|
|
} else {
|
|
process.launch()
|
|
}
|
|
process.waitUntilExit()
|
|
|
|
let outputString = try XCTUnwrap(String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8))
|
|
XCTAssertTrue(error.fileHandleForReading.readDataToEndOfFile().isEmpty, "Error occurred with `--experimental-dump-help`")
|
|
try AssertJSONEqualFromString(actual: outputString, expected: expected, for: ToolInfoV0.self)
|
|
#else
|
|
throw XCTSkip("Not supported on this platform")
|
|
#endif
|
|
}
|
|
|
|
public func AssertGenerateManual(
|
|
multiPage: Bool,
|
|
command: String,
|
|
expected: String,
|
|
file: StaticString = #file,
|
|
line: UInt = #line
|
|
) throws {
|
|
#if os(Windows)
|
|
throw XCTSkip("Unsupported on this platform")
|
|
#endif
|
|
|
|
let commandURL = debugURL.appendingPathComponent(command)
|
|
var command = [
|
|
"generate-manual", commandURL.path,
|
|
"--date", "1996-05-12",
|
|
"--section", "9",
|
|
"--authors", "Jane Appleseed",
|
|
"--authors", "<johnappleseed@apple.com>",
|
|
"--authors", "The Appleseeds<appleseeds@apple.com>",
|
|
"--output-directory", "-",
|
|
]
|
|
if multiPage {
|
|
command.append("--multi-page")
|
|
}
|
|
try AssertExecuteCommand(
|
|
command: command,
|
|
expected: expected,
|
|
exitCode: .success,
|
|
file: file,
|
|
line: line)
|
|
}
|
|
}
|