mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
Constrained tests tool implementation + skill (#5873)
* constrained tests tool implementation + skill * pr suggestions
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: constrained-tests
|
||||
description: Slow-CI simulation — run unit tests on saturated CPU to reproduce CI-style flakiness locally. Suggest when new/reworked suite lean heavy on awaits (deferFulfillment, waitForConfirmation, TestClock, timeouts) or test fail only on CI. Not for every change.
|
||||
---
|
||||
|
||||
# Constrained test runs
|
||||
|
||||
CI runners shared + slow. Background tasks starve, awaits stretch, scheduler races flake there, pass locally. `--constrained` saturate CPU with hog tasks — same conditions, local.
|
||||
|
||||
## When
|
||||
|
||||
- PR add/rework suite heavy on timing-sensitive async: `deferFulfillment`, `waitForConfirmation`, `TestClock`, `transitionValues`, timeouts.
|
||||
- Test fail on CI, pass locally.
|
||||
- Not routine changes, not sync suites — slow, no gain.
|
||||
|
||||
## How
|
||||
|
||||
Full unit test workflow (what CI run):
|
||||
|
||||
```bash
|
||||
swift run tools ci unit-tests --constrained
|
||||
```
|
||||
|
||||
One suite, iterate on new tests (prefer while developing):
|
||||
|
||||
```bash
|
||||
swift run tools ci run-tests --scheme UnitTests --test-name MyNewTests --constrained
|
||||
```
|
||||
|
||||
Hog count automatic (2× cores). Suite survive ~10 constrained iterations → good shape.
|
||||
|
||||
## Failures mean
|
||||
|
||||
Failure under constraint = real bug in test (sometimes code under test): assertion race async work, transient state observation missed, timeout too tight. Fix synchronisation, no blind timeout bump. Helpers in `UnitTests/Sources/TestUtilities/` (`deferFulfillment`, `deferFailure`, `waitForConfirmation`). Wait on durable state, not transient.
|
||||
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
|
||||
/// Saturates the CPU with busy processes for the duration of a test run, approximating
|
||||
/// a loaded CI runner where timing-sensitive tests are starved of processor time.
|
||||
final class CPUConstraint {
|
||||
private var processes = [Process]()
|
||||
|
||||
/// 2× the machine's cores is enough to keep the scheduler permanently oversubscribed.
|
||||
private let taskCount = ProcessInfo.processInfo.processorCount * 2
|
||||
|
||||
func start() {
|
||||
logger.info("\n🐌 Constraining the CPU with \(taskCount) tasks…\n")
|
||||
|
||||
for _ in 0..<taskCount {
|
||||
let process = Process()
|
||||
process.executableURL = URL(filePath: "/usr/bin/yes")
|
||||
process.standardOutput = FileHandle.nullDevice
|
||||
process.standardError = FileHandle.nullDevice
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
processes.append(process)
|
||||
} catch {
|
||||
logger.error("Failed to start a CPU constraining task: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
guard !processes.isEmpty else { return }
|
||||
|
||||
logger.info("\n🐌 Releasing the CPU…\n")
|
||||
processes.forEach { $0.terminate() }
|
||||
processes.removeAll()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,9 @@ struct RunTests: AsyncParsableCommand {
|
||||
@Option(help: "Only run a specific test (format: 'ClassName/testName').")
|
||||
var testName: String?
|
||||
|
||||
@Flag(help: "Run the tests on a saturated CPU to reproduce the flakiness of a busy CI runner.")
|
||||
var constrained = false
|
||||
|
||||
private var isCI: Bool {
|
||||
ProcessInfo.processInfo.environment["CI"] != nil
|
||||
}
|
||||
@@ -73,6 +76,12 @@ struct RunTests: AsyncParsableCommand {
|
||||
let outputDirectory = resultBundleURL.deletingLastPathComponent()
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let cpuConstraint = CPUConstraint()
|
||||
if constrained {
|
||||
cpuConstraint.start()
|
||||
}
|
||||
defer { cpuConstraint.stop() }
|
||||
|
||||
try await executeXcodeBuild()
|
||||
|
||||
try await shutdownSimulator()
|
||||
|
||||
@@ -8,6 +8,9 @@ struct UnitTests: AsyncParsableCommand {
|
||||
@Flag(help: "Skip preview tests")
|
||||
var skipPreviews = false
|
||||
|
||||
@Flag(help: "Run the unit tests on a saturated CPU to reproduce the flakiness of a busy CI runner.")
|
||||
var constrained = false
|
||||
|
||||
private static let osVersion = CI.defaultOSVersion
|
||||
private static let device = "iPhone 17"
|
||||
|
||||
@@ -19,12 +22,18 @@ struct UnitTests: AsyncParsableCommand {
|
||||
// Run unit tests
|
||||
do {
|
||||
logger.info("\n🧪 Running unit tests…\n")
|
||||
try await RunTests.parse([
|
||||
|
||||
var arguments = [
|
||||
"--scheme", "UnitTests",
|
||||
"--device", Self.device,
|
||||
"--os-version", Self.osVersion,
|
||||
"--retries", "3"
|
||||
]).run()
|
||||
]
|
||||
if constrained {
|
||||
arguments.append("--constrained")
|
||||
}
|
||||
|
||||
try await RunTests.parse(arguments).run()
|
||||
} catch {
|
||||
failures.append("UnitTests")
|
||||
logger.error("\n❌ Unit tests failed. \(error)\n")
|
||||
|
||||
Reference in New Issue
Block a user