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
1 change: 1 addition & 0 deletions Documentation/EnvironmentVariables.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ names prefixed with `SWT_`.
| `HOME`[\*](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html) | `String` | Used to determine the user's home directory. |
| `SIMULATOR_RUNTIME_BUILD_VERSION`\* | `String` | Used when running in the iOS (etc.) Simulator to determine the simulator's version. |
| `SIMULATOR_RUNTIME_VERSION`\* | `String` | Used when running in the iOS (etc.) Simulator to determine the simulator's version. |
| `SWT_SERIALIZED_TRAIT_APPLIES_GLOBALLY` | `Bool` | Whether or not `.serialized` applies globally or just to its branch of the test graph. |
| `SWT_USE_LEGACY_TEST_DISCOVERY` | `Bool` | Used to explicitly enable or disable legacy test discovery. |
37 changes: 37 additions & 0 deletions Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,38 @@ extension Runner.Plan {
}
}

#if !hasFeature(Embedded)
/// Recursively deduplicate traits on the given test by calling
/// ``ReducibleTrait/reduce(_:)`` across all nodes in the graph.
///
/// - Parameters:
/// - testGraph: The graph of tests to modify.
private static func _recursivelyReduceTraits(in testGraph: inout Graph<String, Test?>) {
if var test = testGraph.value {
// O(n^2), but we expect n to be small, right?
test.traits = test.traits.reduce(into: []) { traits, trait in
for i in traits.indices {
let other = traits[i]
if let replacement = trait._reduce(into: other) {
traits[i] = replacement
return
}
}

// The trait wasn't reduced into any other traits, so preserve it.
traits.append(trait)
}
testGraph.value = test
}

testGraph.children = testGraph.children.mapValues { child in
var child = child
_recursivelyReduceTraits(in: &child)
return child
}
}
#endif

/// Recursively synthesize test instances representing suites for all missing
/// values in the specified test graph.
///
Expand Down Expand Up @@ -336,6 +368,11 @@ extension Runner.Plan {
// filtered out.
_recursivelyApplyTraits(to: &testGraph)

#if !hasFeature(Embedded)
// Recursively reduce traits in the graph.
_recursivelyReduceTraits(in: &testGraph)
#endif

// For each test value, determine the appropriate action for it.
testGraph = await testGraph.mapValues { keyPath, test in
// Skip any nil test, which implies this node is just a placeholder and
Expand Down
125 changes: 76 additions & 49 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,60 @@ extension Runner {
}
}

/// Post `testStarted` and `testEnded` (or `testSkipped`) events for the test
/// at the given plan step.
///
/// - Parameters:
/// - step: The plan step for which events should be posted.
/// - configuration: The configuration to use for running.
/// - body: A function to execute between the started/ended events.
///
/// - Throws: Whatever is thrown by `body` or while handling any issues
/// recorded in the process.
///
/// - Returns: Whatever is returned by `body`.
///
/// This function does _not_ post the `planStepStarted` and `planStepEnded`
/// events.
private static func _postingTestStartedAndEndedEvents<R>(for step: Plan.Step, configuration: Configuration, _ body: @Sendable () async throws -> R) async throws -> R {
// Whether to send a `.testEnded` event at the end of running this step.
// Some steps' actions may not require a final event to be sent — for
// example, a skip event only sends `.testSkipped`.
let shouldSendTestEnded: Bool

// Determine what kind of event to send for this step based on its action.
switch step.action {
case .run:
Event.post(.testStarted, for: (step.test, nil), configuration: configuration)
shouldSendTestEnded = true
case let .skip(skipInfo):
Event.post(.testSkipped(skipInfo), for: (step.test, nil), configuration: configuration)
shouldSendTestEnded = false
case let .recordIssue(issue):
// Scope posting the issue recorded event such that issue handling
// traits have the opportunity to handle it. This ensures that if a test
// has an issue handling trait _and_ some other trait which caused an
// issue to be recorded, the issue handling trait can process the issue
// even though it wasn't recorded by the test function.
try await Test.withCurrent(step.test) {
try await _applyIssueHandlingTraits(for: step.test) {
// Don't specify `configuration` when posting this issue so that
// traits can provide scope and potentially customize the
// configuration.
Event.post(.issueRecorded(issue), for: (step.test, nil))
}
}
shouldSendTestEnded = false
}
defer {
if shouldSendTestEnded {
Event.post(.testEnded, for: (step.test, nil), configuration: configuration)
}
}

return try await body()
}

/// Run this test.
///
/// - Parameters:
Expand All @@ -209,67 +263,40 @@ extension Runner {
///
/// - ``Runner/run()``
private static func _runStep(atRootOf stepGraph: Graph<String, Plan.Step?>, context: _Context) async throws {
// Whether to send a `.testEnded` event at the end of running this step.
// Some steps' actions may not require a final event to be sent — for
// example, a skip event only sends `.testSkipped`.
let shouldSendTestEnded: Bool
// Exit early if the task has already been cancelled.
try Task.checkCancellation()

let configuration = _configuration

// Determine what action to take for this step.
if let step = stepGraph.value {
let configuration = _configuration
Event.post(.planStepStarted(step), for: (step.test, nil), configuration: configuration)

// Determine what kind of event to send for this step based on its action.
switch step.action {
case .run:
Event.post(.testStarted, for: (step.test, nil), configuration: configuration)
shouldSendTestEnded = true
case let .skip(skipInfo):
Event.post(.testSkipped(skipInfo), for: (step.test, nil), configuration: configuration)
shouldSendTestEnded = false
case let .recordIssue(issue):
// Scope posting the issue recorded event such that issue handling
// traits have the opportunity to handle it. This ensures that if a test
// has an issue handling trait _and_ some other trait which caused an
// issue to be recorded, the issue handling trait can process the issue
// even though it wasn't recorded by the test function.
try await Test.withCurrent(step.test) {
try await _applyIssueHandlingTraits(for: step.test) {
// Don't specify `configuration` when posting this issue so that
// traits can provide scope and potentially customize the
// configuration.
Event.post(.issueRecorded(issue), for: (step.test, nil))
}
}
shouldSendTestEnded = false
}
} else {
shouldSendTestEnded = false
}
defer {
if let step = stepGraph.value {
if shouldSendTestEnded {
Event.post(.testEnded, for: (step.test, nil), configuration: configuration)
}
defer {
Event.post(.planStepEnded(step), for: (step.test, nil), configuration: configuration)
}
}

if let step = stepGraph.value, case .run = step.action {
await Test.withCurrent(step.test) {
_ = await Issue.withErrorRecording(at: step.test.sourceLocation, configuration: configuration) {
// Exit early if the task has already been cancelled.
try Task.checkCancellation()

try await _applyScopingTraits(for: step.test, testCase: nil) {
// Run the test function at this step (if one is present.)
if let testCases = step.test.testCases {
await _runTestCases(testCases, within: step, context: context)
switch step.action {
case .run:
try await _applyScopingTraits(for: step.test, testCase: nil) {
try await _postingTestStartedAndEndedEvents(for: step, configuration: configuration) {
// Run the test function at this step (if one is present.)
if let testCases = step.test.testCases {
await _runTestCases(testCases, within: step, context: context)
}

// Run the children of this test (i.e. the tests in this suite.)
try await _runChildren(of: stepGraph, context: context)
}
}
default:
// Skipping this step or otherwise not running it. Post appropriate
// started/ended events for the test and walk any child nodes.
try await _postingTestStartedAndEndedEvents(for: step, configuration: configuration) {
try await _runChildren(of: stepGraph, context: context)
}

// Run the children of this test (i.e. the tests in this suite.)
try await _runChildren(of: stepGraph, context: context)
}
}
}
Expand Down
35 changes: 27 additions & 8 deletions Sources/Testing/Support/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,41 @@ package enum Environment {
}()
#endif

/// The address of the environment block, if available.
///
/// The value of this property is always `nil` on Windows and on platforms
/// that do not support environment variables.
static var unsafeAddress: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? {
#if SWT_NO_ENVIRONMENT_VARIABLES
nil
#elseif SWT_TARGET_OS_APPLE
_NSGetEnviron()?.pointee
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
swt_environ()
#elseif os(WASI)
__wasilibc_get_environ()
#elseif os(Windows)
nil
#else
#warning("Platform-specific implementation missing: environment variables unavailable")
nil
#endif
}

/// Get all environment variables in the current process.
///
/// - Returns: A copy of the current process' environment dictionary.
package static func get() -> [String: String] {
#if SWT_NO_ENVIRONMENT_VARIABLES
simulatedEnvironment.rawValue
#elseif SWT_TARGET_OS_APPLE
#if !SWT_NO_DYNAMIC_LINKING
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI)
#if SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING
_environ_lock_np?()
defer {
_environ_unlock_np?()
}
#endif
return _get(fromEnviron: _NSGetEnviron()!.pointee!)
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
_get(fromEnviron: swt_environ())
#elseif os(WASI)
_get(fromEnviron: __wasilibc_get_environ())
return _get(fromEnviron: Self.unsafeAddress!)
#elseif os(Windows)
guard let environ = GetEnvironmentStringsW() else {
return [:]
Expand Down Expand Up @@ -153,7 +170,9 @@ package enum Environment {
defer {
_environ_unlock_np?()
}
let environ = _NSGetEnviron()!.pointee!
guard let environ = Self.unsafeAddress else {
return nil
}

return name.withCString { name in
for i in 0... {
Expand Down
4 changes: 3 additions & 1 deletion Sources/Testing/Support/Serializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ final actor Serializer {
/// - Returns: Whatever is returned from `workItem`.
///
/// - Throws: Whatever is thrown by `workItem`.
///
/// - Warning: Calling this function recursively on the same instance of
/// ``Serializer`` will cause a deadlock.
func run<R>(_ workItem: @isolated(any) @Sendable () async throws -> R) async rethrows -> R where R: Sendable {
#if !SWT_NO_UNSTRUCTURED_TASKS
_currentWidth += 1
Expand Down Expand Up @@ -96,4 +99,3 @@ final actor Serializer {
return try await workItem()
}
}

2 changes: 2 additions & 0 deletions Sources/Testing/Testing.docc/Parallelization.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ accomplished by the testing library using task groups, and tests generally all
run in the same process. The number of tests that run concurrently is controlled
by the Swift runtime.

<!-- TODO: discuss .serialized(for:) -->

## Disabling parallelization

Parallelization can be disabled on a per-function or per-suite basis using the
Expand Down
Loading