Skip to content

Commit 75d5929

Browse files
authored
Support command-line arguments specified as --foo=bar. (#1357)
SwiftPM and `swift test` use Swift Argument Parser which allows developers to specify arguments of the form `--foo=bar`. Our bare-bones argument parser doesn't currently recognize that pattern, which means that the developer could write `--foo=bar` but get the wrong behavior. This PR adds support for that pattern by changing how we parse command-line arguments to allow for both `--foo bar` and `--foo=bar`. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 533538b commit 75d5929

File tree

2 files changed

+89
-34
lines changed

2 files changed

+89
-34
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,45 @@ extension __CommandLineArguments_v0: Codable {
352352
}
353353
}
354354

355+
extension RandomAccessCollection<String> {
356+
/// Get the value of the command line argument with the given name.
357+
///
358+
/// - Parameters:
359+
/// - label: The label or name of the argument, e.g. `"--attachments-path"`.
360+
/// - index: The index where `label` should be found, or `nil` to search the
361+
/// entire collection.
362+
///
363+
/// - Returns: The value of the argument named by `label` at `index`. If no
364+
/// value is available, or if `index` is not `nil` and the argument at
365+
/// `index` is not named `label`, returns `nil`.
366+
///
367+
/// This function handles arguments of the form `--label value` and
368+
/// `--label=value`. Other argument syntaxes are not supported.
369+
fileprivate func argumentValue(forLabel label: String, at index: Index? = nil) -> String? {
370+
guard let index else {
371+
return indices.lazy
372+
.compactMap { argumentValue(forLabel: label, at: $0) }
373+
.first
374+
}
375+
376+
let element = self[index]
377+
if element == label {
378+
let nextIndex = self.index(after: index)
379+
if nextIndex < endIndex {
380+
return self[nextIndex]
381+
}
382+
} else {
383+
// Find an element equal to something like "--foo=bar" and split it.
384+
let prefix = "\(label)="
385+
if element.hasPrefix(prefix), let equalsIndex = element.firstIndex(of: "=") {
386+
return String(element[equalsIndex...].dropFirst())
387+
}
388+
}
389+
390+
return nil
391+
}
392+
}
393+
355394
/// Initialize this instance given a sequence of command-line arguments passed
356395
/// from Swift Package Manager.
357396
///
@@ -366,10 +405,6 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
366405
// Do not consider the executable path AKA argv[0].
367406
let args = args.dropFirst()
368407

369-
func isLastArgument(at index: [String].Index) -> Bool {
370-
args.index(after: index) >= args.endIndex
371-
}
372-
373408
#if !SWT_NO_FILE_IO
374409
#if canImport(Foundation)
375410
// Configuration for the test run passed in as a JSON file (experimental)
@@ -379,9 +414,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
379414
// NOTE: While the output event stream is opened later, it is necessary to
380415
// open the configuration file early (here) in order to correctly construct
381416
// the resulting __CommandLineArguments_v0 instance.
382-
if let configurationIndex = args.firstIndex(of: "--configuration-path") ?? args.firstIndex(of: "--experimental-configuration-path"),
383-
!isLastArgument(at: configurationIndex) {
384-
let path = args[args.index(after: configurationIndex)]
417+
if let path = args.argumentValue(forLabel: "--configuration-path") ?? args.argumentValue(forLabel: "--experimental-configuration-path") {
385418
let file = try FileHandle(forReadingAtPath: path)
386419
let configurationJSON = try file.readToEnd()
387420
result = try configurationJSON.withUnsafeBufferPointer { configurationJSON in
@@ -394,24 +427,22 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
394427
}
395428

396429
// Event stream output
397-
if let eventOutputIndex = args.firstIndex(of: "--event-stream-output-path") ?? args.firstIndex(of: "--experimental-event-stream-output"),
398-
!isLastArgument(at: eventOutputIndex) {
399-
result.eventStreamOutputPath = args[args.index(after: eventOutputIndex)]
430+
if let path = args.argumentValue(forLabel: "--event-stream-output-path") ?? args.argumentValue(forLabel: "--experimental-event-stream-output") {
431+
result.eventStreamOutputPath = path
400432
}
433+
401434
// Event stream version
402435
do {
403-
var eventOutputVersionIndex: Array<String>.Index?
436+
var versionString: String?
404437
var allowExperimental = false
405-
eventOutputVersionIndex = args.firstIndex(of: "--event-stream-version")
406-
if eventOutputVersionIndex == nil {
407-
eventOutputVersionIndex = args.firstIndex(of: "--experimental-event-stream-version")
408-
if eventOutputVersionIndex != nil {
438+
versionString = args.argumentValue(forLabel: "--event-stream-version")
439+
if versionString == nil {
440+
versionString = args.argumentValue(forLabel: "--experimental-event-stream-version")
441+
if versionString != nil {
409442
allowExperimental = true
410443
}
411444
}
412-
if let eventOutputVersionIndex, !isLastArgument(at: eventOutputVersionIndex) {
413-
let versionString = args[args.index(after: eventOutputVersionIndex)]
414-
445+
if let versionString {
415446
// If the caller specified a version that could not be parsed, treat it as
416447
// an invalid argument.
417448
guard let eventStreamVersion = VersionNumber(versionString) else {
@@ -432,14 +463,13 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
432463
#endif
433464

434465
// XML output
435-
if let xunitOutputIndex = args.firstIndex(of: "--xunit-output"), !isLastArgument(at: xunitOutputIndex) {
436-
result.xunitOutput = args[args.index(after: xunitOutputIndex)]
466+
if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") {
467+
result.xunitOutput = xunitOutputPath
437468
}
438469

439470
// Attachment output
440-
if let attachmentsPathIndex = args.firstIndex(of: "--attachments-path") ?? args.firstIndex(of: "--experimental-attachments-path"),
441-
!isLastArgument(at: attachmentsPathIndex) {
442-
result.attachmentsPath = args[args.index(after: attachmentsPathIndex)]
471+
if let attachmentsPath = args.argumentValue(forLabel: "--attachments-path") ?? args.argumentValue(forLabel: "--experimental-attachments-path") {
472+
result.attachmentsPath = attachmentsPath
443473
}
444474
#endif
445475

@@ -457,13 +487,12 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
457487
}
458488

459489
// Whether or not to symbolicate backtraces in the event stream.
460-
if let symbolicateBacktracesIndex = args.firstIndex(of: "--symbolicate-backtraces"), !isLastArgument(at: symbolicateBacktracesIndex) {
461-
result.symbolicateBacktraces = args[args.index(after: symbolicateBacktracesIndex)]
490+
if let symbolicateBacktraces = args.argumentValue(forLabel: "--symbolicate-backtraces") {
491+
result.symbolicateBacktraces = symbolicateBacktraces
462492
}
463493

464494
// Verbosity
465-
if let verbosityIndex = args.firstIndex(of: "--verbosity"), !isLastArgument(at: verbosityIndex),
466-
let verbosity = Int(args[args.index(after: verbosityIndex)]) {
495+
if let verbosity = args.argumentValue(forLabel: "--verbosity").flatMap(Int.init) {
467496
result.verbosity = verbosity
468497
}
469498
if args.contains("--verbose") || args.contains("-v") {
@@ -478,9 +507,7 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
478507

479508
// Filtering
480509
func filterValues(forArgumentsWithLabel label: String) -> [String] {
481-
args.indices.lazy
482-
.filter { args[$0] == label && $0 < args.endIndex }
483-
.map { args[args.index(after: $0)] }
510+
args.indices.compactMap { args.argumentValue(forLabel: label, at: $0) }
484511
}
485512
let filter = filterValues(forArgumentsWithLabel: "--filter")
486513
if !filter.isEmpty {
@@ -492,11 +519,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
492519
}
493520

494521
// Set up the iteration policy for the test run.
495-
if let repetitionsIndex = args.firstIndex(of: "--repetitions"), !isLastArgument(at: repetitionsIndex) {
496-
result.repetitions = Int(args[args.index(after: repetitionsIndex)])
522+
if let repetitions = args.argumentValue(forLabel: "--repetitions").flatMap(Int.init) {
523+
result.repetitions = repetitions
497524
}
498-
if let repeatUntilIndex = args.firstIndex(of: "--repeat-until"), !isLastArgument(at: repeatUntilIndex) {
499-
result.repeatUntil = args[args.index(after: repeatUntilIndex)]
525+
if let repeatUntil = args.argumentValue(forLabel: "--repeat-until") {
526+
result.repeatUntil = repeatUntil
500527
}
501528

502529
return result

Tests/TestingTests/SwiftPMTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ struct SwiftPMTests {
145145
#expect(planTests.contains(test2))
146146
}
147147

148+
@Test("--filter or --skip argument as last argument")
149+
@available(_regexAPI, *)
150+
func filterOrSkipAsLast() async throws {
151+
_ = try configurationForEntryPoint(withArguments: ["PATH", "--filter"])
152+
_ = try configurationForEntryPoint(withArguments: ["PATH", "--skip"])
153+
}
154+
148155
@Test(".hidden trait", .tags(.traitRelated))
149156
func hidden() async throws {
150157
let configuration = try configurationForEntryPoint(withArguments: ["PATH"])
@@ -492,4 +499,25 @@ struct SwiftPMTests {
492499
let args = try parseCommandLineArguments(from: ["PATH", "--verbosity", "12345"])
493500
#expect(args.verbosity == 12345)
494501
}
502+
503+
@Test("--foo=bar form")
504+
func equalsSignForm() throws {
505+
// We can split the string and parse the result correctly.
506+
do {
507+
let args = try parseCommandLineArguments(from: ["PATH", "--verbosity=12345"])
508+
#expect(args.verbosity == 12345)
509+
}
510+
511+
// We don't overrun the string and correctly handle empty values.
512+
do {
513+
let args = try parseCommandLineArguments(from: ["PATH", "--xunit-output="])
514+
#expect(args.xunitOutput == "")
515+
}
516+
517+
// We split at the first equals-sign.
518+
do {
519+
let args = try parseCommandLineArguments(from: ["PATH", "--xunit-output=abc=123"])
520+
#expect(args.xunitOutput == "abc=123")
521+
}
522+
}
495523
}

0 commit comments

Comments
 (0)