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
16 changes: 13 additions & 3 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,17 +159,27 @@ extension Runner.Plan {
// them, in which case it will be .recordIssue().
var testGraph = Graph<String, Test?>()
var actionGraph = Graph<String, Action>(value: .run)
for test in tests where _isTestIncluded(test, using: configuration.testFilter) {
for test in tests {
let idComponents = test.id.keyPathRepresentation
testGraph.insertValue(test, at: idComponents)
actionGraph.insertValue(.run, at: idComponents, intermediateValue: .run)
Copy link
Contributor

Choose a reason for hiding this comment

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

Moving the where ... clause here will cause the actionGraph to always be updated, even if some of those tests are later filtered out. Does something else later on cause the corresponding nodes in that graph to be pruned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, immediately below we prune them out (after applying traits recursively) on line 173.

}

// Ensure the trait lists are complete for all nested tests. (Make sure to
// do this before we start calling prepare(for:) or we'll miss the
// recursively-added ones.)
// do this before we start calling configuration.testFilter or prepare(for:)
// or we'll miss the recursively-added traits.)
_recursivelyApplyTraits(to: &testGraph)

// Remove any tests that should be filtered out per the runner's
// configuration. The action graph is not modified here: actions that lose
// their corresponding tests are effectively filtered out by the call to
// zip() near the end of the function.
testGraph = testGraph.mapValues { test in
test.flatMap { test in
_isTestIncluded(test, using: configuration.testFilter) ? test : nil
}
}

// For each test value, determine the appropriate action for it.
await testGraph.forEach { keyPath, test in
// Skip any nil test, which implies this node is just a placeholder and
Expand Down
63 changes: 48 additions & 15 deletions Sources/Testing/Running/XCTestScaffold.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,44 @@ public enum XCTestScaffold: Sendable {
/// ### Filtering tests
///
/// This function does not support the `--filter` argument passed to
/// `swift test`. Instead, set the `SWT_SELECTED_TEST_IDS` environment
/// variable to the ``Test/ID`` of the test that should run (or, if multiple
/// tests should be run, their IDs separated by `";"`.)
/// `swift test`. Instead, use one of several environment variables to control
/// which tests run.
///
/// #### Filtering by ID
///
/// To run a specific test, set the `SWT_SELECTED_TEST_IDS` environment
/// variable to the ``Test/ID`` of that test (or, if multiple tests should be
/// run, their IDs separated by `";"`.)
///
/// A test ID is composed of its module name, containing type name, and (if
/// the test is a function rather than a suite), the name of the function
/// including parentheses and any parameter labels. For example, given the
/// following test functions in a module named `"MyTests"`:
/// following test functions in a module named `"FoodTruckTests"`:
///
/// ```swift
/// struct MySuite {
/// @Test func hello() { ... }
/// @Test(arguments: 0 ..< 10) func world(i: Int) { ... }
/// struct CashRegisterTests {
/// @Test func hasCash() { ... }
/// @Test(arguments: Card.allCases) func acceptsCard(card: Card) { ... }
/// }
/// ```
///
/// Their IDs are the strings `"MyTests/MySuite/hello()"` and
/// `"MyTests/MySuite/world(i:)"` respectively, and they can be passed as the
/// environment variable value
/// `"MyTests/MySuite/hello();MyTests/MySuite/world(i:)"`.
/// Their IDs are the strings `"FoodTruckTests/CashRegisterTests/hasCash()"`
/// and `"FoodTruckTests/CashRegisterTests/acceptsCard(card:)"` respectively,
/// and they can be passed as the environment variable value
/// `"FoodTruckTests/CashRegisterTests/hasCash();FoodTruckTests/CashRegisterTests/acceptsCard(card:)"`.
///
/// - Note: The module name of a test target in a Swift package is typically
/// the name of the test target.
///
/// #### Filtering by tag
///
/// To run only those tests with a given ``Tag``, set the `SWT_SELECTED_TAGS`
/// environment variable to the string value of that tag. Separate multiple
/// tags with `";"`; tests with _any_ of the specified tags will be run. For
/// example, to run all tests tagged `"critical"` _or_ ``Tag/red`` (or both),
/// set the value of the `SWT_SELECTED_TAGS` environment variable to
/// `"critical;red"`.
///
/// ### Configuring output
///
/// By default, this function uses
Expand Down Expand Up @@ -200,15 +214,34 @@ public enum XCTestScaffold: Sendable {
// the configuration's test filter to match it.
//
// This environment variable stands in for `swift test --filter`.
let testIDs: [Test.ID]? = Environment.variable(named: "SWT_SELECTED_TEST_IDS").map { testIDs in
testIDs.split(separator: ";", omittingEmptySubsequences: true).map { testID in
Test.ID(testID.split(separator: "/", omittingEmptySubsequences: true).map(String.init))
let testIDs: [Test.ID]? = Environment.variable(named: "SWT_SELECTED_TEST_IDS")
.map { testIDs in
testIDs.split(separator: ";", omittingEmptySubsequences: true).map { testID in
Test.ID(testID.split(separator: "/", omittingEmptySubsequences: true).map(String.init))
}
}
}
if let testIDs {
configuration.setTestFilter(toMatch: Set(testIDs))
}

// If the SWT_SELECTED_TAGS environment variable is set, split it by ";"
// (similar to test IDs above) and check if tests' tags overlap.
let tags: Set<Tag>? = Environment.variable(named: "SWT_SELECTED_TAGS")
.map { tags in
tags
.split(separator: ";", omittingEmptySubsequences: true)
.map(String.init)
.map(Tag.init(rawValue:))
}.map(Set.init)
if let tags {
// Check if the test's tags intersect the set of selected tags. If there
// was a previous filter function, it must also pass.
let oldTestFilter = configuration.testFilter ?? { _ in true }
configuration.testFilter = { test in
!tags.isDisjoint(with: test.tags) && oldTestFilter(test)
}
}

let runner = await Runner(configuration: configuration)
await runner.run()
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Testing/Traits/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ extension Tag: ExpressibleByStringLiteral, CustomStringConvertible {
// MARK: - Equatable, Hashable, Comparable

extension Tag: Equatable, Hashable, Comparable {
public static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.rawValue == rhs.rawValue
}

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

/// The index of this color, relative to other colors.
///
/// The value of this property can be used for sorting color tags distinctly
Expand Down