Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
13 changes: 13 additions & 0 deletions Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ public struct __CommandLineArguments_v0: Sendable {
/// The value of the `--parallel` or `--no-parallel` argument.
public var parallel: Bool?

/// The maximum number of test tasks to run in parallel.
public var experimentalMaximumParallelizationWidth: Int?

/// The value of the `--symbolicate-backtraces` argument.
public var symbolicateBacktraces: String?

Expand Down Expand Up @@ -336,6 +339,7 @@ extension __CommandLineArguments_v0: Codable {
enum CodingKeys: String, CodingKey {
case listTests
case parallel
case experimentalMaximumParallelizationWidth
case symbolicateBacktraces
case verbose
case veryVerbose
Expand Down Expand Up @@ -485,6 +489,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum
if args.contains("--no-parallel") {
result.parallel = false
}
if let maximumParallelizationWidth = args.argumentValue(forLabel: "--experimental-maximum-parallelization-width").flatMap(Int.init)
?? Environment.variable(named: "SWT_EXPERIMENTAL_MAXIMUM_PARALLELIZATION_WIDTH").flatMap(Int.init) {
// TODO: decide if we want to repurpose --num-workers for this use case?
result.experimentalMaximumParallelizationWidth = maximumParallelizationWidth
}

// Whether or not to symbolicate backtraces in the event stream.
if let symbolicateBacktraces = args.argumentValue(forLabel: "--symbolicate-backtraces") {
Expand Down Expand Up @@ -546,6 +555,10 @@ public func configurationForEntryPoint(from args: __CommandLineArguments_v0) thr

// Parallelization (on by default)
configuration.isParallelizationEnabled = args.parallel ?? true
if let maximumParallelizationWidth = args.experimentalMaximumParallelizationWidth {
try! FileHandle.stderr.write("MAX WIDTH: \(maximumParallelizationWidth)\n")
configuration.maximumParallelizationWidth = maximumParallelizationWidth
}

// Whether or not to symbolicate backtraces in the event stream.
if let symbolicateBacktraces = args.symbolicateBacktraces {
Expand Down
1 change: 1 addition & 0 deletions Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ add_library(Testing
Support/Graph.swift
Support/JSON.swift
Support/Locked.swift
Support/Serializer.swift
Support/VersionNumber.swift
Support/Versions.swift
Discovery+Macro.swift
Expand Down
45 changes: 45 additions & 0 deletions Sources/Testing/Running/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

private import _TestingInternals

/// A type containing settings for preparing and running tests.
@_spi(ForToolsIntegrationOnly)
public struct Configuration: Sendable {
Expand All @@ -20,6 +22,49 @@ public struct Configuration: Sendable {
/// Whether or not to parallelize the execution of tests and test cases.
public var isParallelizationEnabled: Bool = true

/// The number of CPU cores on the current system, or `nil` if that
/// information is not available.
private static var _cpuCoreCount: Int? {
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
return Int(sysconf(Int32(_SC_NPROCESSORS_CONF)))
#elseif os(Windows)
var siInfo = SYSTEM_INFO()
GetSystemInfo(&siInfo)
return Int(siInfo.dwNumberOfProcessors)
#elseif os(WASI)
return 1
#else
#warning("Platform-specific implementation missing: CPU core count unavailable")
return nil
#endif

}

/// The maximum width of parallelization.
///
/// The value of this property determines how many tests (or rather, test
/// cases) will run in parallel. The default value of this property is equal
/// to twice the number of CPU cores reported by the operating system, or
/// `Int.max` if that value is not available.
@_spi(Experimental)
public var maximumParallelizationWidth: Int {
get {
serializer.maximumWidth
}
set {
serializer = Serializer(maximumWidth: newValue)
}
}

/// The serializer that backs ``maximumParallelizationWidth``.
///
/// - Note: This serializer is ignored if ``isParallelizationEnabled`` is
/// `false`.
var serializer: Serializer = {
let cpuCoreCount = Self._cpuCoreCount.map { max(1, $0) * 2 } ?? .max
return Serializer(maximumWidth: cpuCoreCount)
}()

/// How to symbolicate backtraces captured during a test run.
///
/// If the value of this property is not `nil`, symbolication will be
Expand Down
10 changes: 8 additions & 2 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,10 @@ extension Runner {
/// If parallelization is supported and enabled, the generated test cases will
/// be run in parallel using a task group.
private static func _runTestCases(_ testCases: some Sequence<Test.Case>, within step: Plan.Step) async {
let configuration = _configuration

// Apply the configuration's test case filter.
let testCaseFilter = _configuration.testCaseFilter
let testCaseFilter = configuration.testCaseFilter
let testCases = testCases.lazy.filter { testCase in
testCaseFilter(testCase, step.test)
}
Expand All @@ -359,7 +361,11 @@ extension Runner {
}

await _forEach(in: testCases.enumerated(), namingTasksWith: taskNamer) { _, testCase in
await _runTestCase(testCase, within: step)
if configuration.isParallelizationEnabled {
await configuration.serializer.run { await _runTestCase(testCase, within: step) }
} else {
await _runTestCase(testCase, within: step)
}
}
}

Expand Down
65 changes: 65 additions & 0 deletions Sources/Testing/Support/Serializer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024–2025 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
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

/// A type whose instances can run a series of work items in strict order.
///
/// When a work item is scheduled on an instance of this type, it runs after any
/// previously-scheduled work items. If it suspends, subsequently-scheduled work
/// items do not start running; they must wait until the suspended work item
/// either returns or throws an error.
final actor Serializer {
/// The maximum number of work items that may run concurrently.
nonisolated let maximumWidth: Int

/// The number of scheduled work items, including any currently running.
private var _currentWidth = 0

/// Continuations for any scheduled work items that haven't started yet.
private var _continuations = [CheckedContinuation<Void, Never>]()

init(maximumWidth: Int = 1) {
self.maximumWidth = maximumWidth
}

/// Run a work item serially after any previously-scheduled work items.
///
/// - Parameters:
/// - workItem: A closure to run.
///
/// - Returns: Whatever is returned from `workItem`.
///
/// - Throws: Whatever is thrown by `workItem`.
func run<R>(_ workItem: @Sendable @isolated(any) () async throws -> R) async rethrows -> R where R: Sendable {
_currentWidth += 1
defer {
// Resume the next scheduled closure.
if !_continuations.isEmpty {
let continuation = _continuations.removeFirst()
continuation.resume()
}

_currentWidth -= 1
}

await withCheckedContinuation { continuation in
if _currentWidth <= maximumWidth {
// Nothing else was scheduled, so we can resume immediately.
continuation.resume()
} else {
// Something was scheduled, so add the continuation to the
// list. When it resumes, we can run.
_continuations.append(continuation)
}
}

return try await workItem()
}
}