Simplify the serial suite trait (#1497)

## Motivation and Context
The `SuiteGate` actor introduced for the serial test trait in
https://github.com/apple/container/pull/1489 could be simplified using
containerization's
[AsyncLock](https://github.com/apple/containerization/blob/f2c42402e744df992fecb84eb3a82935c6fa01d3/Sources/ContainerizationExtras/AsyncLock.swift#L23).

## Testing
- [ ] Tested locally

Note: working on A/B performance testing to validate this approach does
not significantly impact test run performance.

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
This commit is contained in:
Kathryn Baldauf
2026-05-01 11:38:07 -07:00
committed by GitHub
parent a141002de1
commit e57c755911
3 changed files with 6 additions and 128 deletions
@@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
import Testing
/// Integration test suites share mutable system state (apiserver, containers, networks, volumes).
@@ -22,20 +23,18 @@ import Testing
/// each suite still run in parallel.
struct SerialSuiteTrait: SuiteTrait, TestScoping {
var isRecursive: Bool { false }
private static let lock = AsyncLock()
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
await SuiteGate.shared.enter()
do {
try await function()
} catch {
await SuiteGate.shared.leave()
throw error
try await withoutActuallyEscaping(function) { escapingFunction in
try await Self.lock.withLock { _ in
try await escapingFunction()
}
}
await SuiteGate.shared.leave()
}
}
@@ -16,26 +16,6 @@
import Testing
/// Helper actor that tracks how many callers are active simultaneously.
actor ConcurrencyTracker {
private var current = 0
private var peak = 0
func enterAndGetCount() -> Int {
current += 1
if current > peak { peak = current }
return current
}
func leave() {
current -= 1
}
func peakConcurrency() -> Int {
peak
}
}
/// Async countdown latch. N callers suspend on arriveAndWait(); the Nth arrival
/// resumes them all simultaneously, proving they were running concurrently.
actor Barrier {
@@ -62,61 +42,6 @@ actor Barrier {
}
}
/// Helper actor that records the order in which tasks complete.
private actor CompletionRecorder {
private var order: [Int] = []
func record(_ id: Int) {
order.append(id)
}
func completedCount() -> Int {
order.count
}
}
@Suite struct SuiteGateTests {
@Test func mutualExclusion() async {
let gate = SuiteGate()
let tracker = ConcurrencyTracker()
let barrier = Barrier(count: 5)
await withTaskGroup(of: Void.self) { group in
for _ in 0..<5 {
group.addTask {
// Barrier ensures all 5 tasks are live before any enters the gate
await barrier.arriveAndWait()
await gate.enter()
let count = await tracker.enterAndGetCount()
#expect(count == 1, "Only 1 task should be inside the gate at a time")
await tracker.leave()
await gate.leave()
}
}
}
}
@Test func multipleWaiters() async {
let gate = SuiteGate()
let recorder = CompletionRecorder()
let barrier = Barrier(count: 3)
await withTaskGroup(of: Void.self) { group in
for i in 0..<3 {
group.addTask {
await barrier.arriveAndWait()
await gate.enter()
await recorder.record(i)
await gate.leave()
}
}
}
let count = await recorder.completedCount()
#expect(count == 3, "All 3 tasks should have completed")
}
}
/// Verifies that tests within a `.serialSuites`-annotated suite still run in parallel.
/// All 3 tests rendezvous at a shared barrier. If they run concurrently, the barrier
/// opens and tests pass instantly. If serialized, the first test deadlocks waiting for
-46
View File
@@ -1,46 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
/// Async semaphore with capacity 1. Used by SerialSuiteTrait to ensure only one
/// integration test suite executes at a time.
actor SuiteGate {
static let shared = SuiteGate()
private var isOccupied = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func enter() async {
if !isOccupied {
isOccupied = true
return
}
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
// Must remain synchronous (non-async body). This guarantees the call
// completes even if the calling task is cancelled, because actor hops
// for synchronous methods are not cancellation points.
func leave() {
if let next = waiters.first {
waiters.removeFirst()
next.resume()
} else {
isOccupied = false
}
}
}