Skip to content
Open
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
53 changes: 45 additions & 8 deletions Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -624,17 +624,54 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr

// Filtering
var filters = [Configuration.TestFilter]()
func testFilter(forRegularExpressions regexes: [String]?, label: String, membership: Configuration.TestFilter.Membership) throws -> Configuration.TestFilter {
guard let regexes, !regexes.isEmpty else {
// Return early if empty, even though the `reduce` logic below can handle
// this case, in order to avoid the `#available` guard.
return .unfiltered
func testFilters(forOptionArguments optionArguments: [String]?, label: String, membership: Configuration.TestFilter.Membership) throws -> [Configuration.TestFilter] {

// Filters will come in two flavors: those with `tag:` as a prefix, and
// those without. We split them into two collections, taking care to handle
// an escaped colon, treating it as a pseudo-operator.
let tagPrefix = "tag:"
let escapedTagPrefix = #"tag\:"#
var tags = [Tag]()
var regexes = [String]()

// Loop through all the option arguments, separating tags from regex filters
for var optionArg in optionArguments ?? [] {
if optionArg.hasPrefix(tagPrefix) {
// Running into the `tag:` prefix means we should strip it and use the
// actual tag name the user has provided
let tagStringWithoutPrefix = String(optionArg.dropFirst(tagPrefix.count))
tags.append(Tag(userProvidedStringValue: tagStringWithoutPrefix))
} else {
// If we run into the escaped tag prefix, the user has indicated they
// want to us to treat it as a regex filter. We need to to unescape it
// before adding it as a regex filter
if optionArg.hasPrefix(escapedTagPrefix) {
optionArg.replaceSubrange(escapedTagPrefix.startIndex..<escapedTagPrefix.endIndex, with: tagPrefix)
}
regexes.append(optionArg)
}
}

// If we didn't find any tags, the tagFilter should be .unfiltered,
// otherwise we construct it with the provided tags
let tagFilter: Configuration.TestFilter = switch (membership, tags.isEmpty) {
case (_, true): .unfiltered
case (.including, false): Configuration.TestFilter(includingAnyOf: tags)
case (.excluding, false): Configuration.TestFilter(excludingAnyOf: tags)
}

guard !regexes.isEmpty else {
// Return early with just the tag filter, otherwise we try to match
// against an empty array of regular expressions which is _not_
// equivalent to `.unfiltered`.
return [tagFilter]
}

return try Configuration.TestFilter(membership: membership, matchingAnyOf: regexes)
return [try Configuration.TestFilter(membership: membership, matchingAnyOf: regexes), tagFilter]
}
filters.append(try testFilter(forRegularExpressions: args.filter, label: "--filter", membership: .including))
filters.append(try testFilter(forRegularExpressions: args.skip, label: "--skip", membership: .excluding))

filters += try testFilters(forOptionArguments: args.filter, label: "--filter", membership: .including)
filters += try testFilters(forOptionArguments: args.skip, label: "--skip", membership: .excluding)

configuration.testFilter = filters.reduce(.unfiltered) { $0.combining(with: $1) }
if args.includeHiddenTests == true {
Expand Down
27 changes: 27 additions & 0 deletions Tests/TestingTests/SwiftPMTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ private func configurationForEntryPoint(withArguments args: [String]) throws ->
return try configurationForEntryPoint(from: args)
}

private extension Tag {
@Tag static var testTag: Self
}

/// Reads event stream output from the provided file matching event stream
/// version `V`.
private func decodedEventStreamRecords<V: ABI.Version>(fromPath filePath: String) throws -> [ABI.Record<V>] {
Expand Down Expand Up @@ -116,6 +120,18 @@ struct SwiftPMTests {
#expect(!planTests.contains(test2))
}


@Test("--filter argument with tag: prefix")
func filterByTag() async throws {
let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--filter", "tag:testTag"])
let test1 = Test(.tags(.testTag), name: "hello") {}
let test2 = Test(name: "goodbye") {}
let plan = await Runner.Plan(tests: [test1, test2], configuration: configuration)
let planTests = plan.steps.map(\.test)
#expect(planTests.contains(test1))
#expect(!planTests.contains(test2))
}

@Test("Multiple --filter arguments")
func multipleFilter() async throws {
let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--filter", "hello", "--filter", "sorry"])
Expand Down Expand Up @@ -159,6 +175,17 @@ struct SwiftPMTests {
#expect(planTests.contains(test2))
}

@Test("--skip argument with tag: prefix")
func skipByTag() async throws {
let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--skip", "tag:testTag"])
let test1 = Test(.tags(.testTag), name: "hello") {}
let test2 = Test(name: "goodbye") {}
let plan = await Runner.Plan(tests: [test1, test2], configuration: configuration)
let planTests = plan.steps.map(\.test)
#expect(!planTests.contains(test1))
#expect(planTests.contains(test2))
}

@Test("--filter or --skip argument as last argument")
func filterOrSkipAsLast() async throws {
_ = try configurationForEntryPoint(withArguments: ["PATH", "--filter"])
Expand Down