mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-08 05:47:11 +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').")
|
@Option(help: "Only run a specific test (format: 'ClassName/testName').")
|
||||||
var testName: String?
|
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 {
|
private var isCI: Bool {
|
||||||
ProcessInfo.processInfo.environment["CI"] != nil
|
ProcessInfo.processInfo.environment["CI"] != nil
|
||||||
}
|
}
|
||||||
@@ -73,6 +76,12 @@ struct RunTests: AsyncParsableCommand {
|
|||||||
let outputDirectory = resultBundleURL.deletingLastPathComponent()
|
let outputDirectory = resultBundleURL.deletingLastPathComponent()
|
||||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let cpuConstraint = CPUConstraint()
|
||||||
|
if constrained {
|
||||||
|
cpuConstraint.start()
|
||||||
|
}
|
||||||
|
defer { cpuConstraint.stop() }
|
||||||
|
|
||||||
try await executeXcodeBuild()
|
try await executeXcodeBuild()
|
||||||
|
|
||||||
try await shutdownSimulator()
|
try await shutdownSimulator()
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ struct UnitTests: AsyncParsableCommand {
|
|||||||
@Flag(help: "Skip preview tests")
|
@Flag(help: "Skip preview tests")
|
||||||
var skipPreviews = false
|
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 osVersion = CI.defaultOSVersion
|
||||||
private static let device = "iPhone 17"
|
private static let device = "iPhone 17"
|
||||||
|
|
||||||
@@ -19,12 +22,18 @@ struct UnitTests: AsyncParsableCommand {
|
|||||||
// Run unit tests
|
// Run unit tests
|
||||||
do {
|
do {
|
||||||
logger.info("\n🧪 Running unit tests…\n")
|
logger.info("\n🧪 Running unit tests…\n")
|
||||||
try await RunTests.parse([
|
|
||||||
|
var arguments = [
|
||||||
"--scheme", "UnitTests",
|
"--scheme", "UnitTests",
|
||||||
"--device", Self.device,
|
"--device", Self.device,
|
||||||
"--os-version", Self.osVersion,
|
"--os-version", Self.osVersion,
|
||||||
"--retries", "3"
|
"--retries", "3"
|
||||||
]).run()
|
]
|
||||||
|
if constrained {
|
||||||
|
arguments.append("--constrained")
|
||||||
|
}
|
||||||
|
|
||||||
|
try await RunTests.parse(arguments).run()
|
||||||
} catch {
|
} catch {
|
||||||
failures.append("UnitTests")
|
failures.append("UnitTests")
|
||||||
logger.error("\n❌ Unit tests failed. \(error)\n")
|
logger.error("\n❌ Unit tests failed. \(error)\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user