139 lines
4.5 KiB
Swift
139 lines
4.5 KiB
Swift
import Testing
|
|
@_spi(Portico) import Portico
|
|
|
|
@_silgen_name("g_main_context_iteration")
|
|
private nonisolated func test_g_main_context_iteration(
|
|
_ context: UnsafeMutableRawPointer?, _ mayBlock: Int32
|
|
) -> Int32
|
|
|
|
@_silgen_name("g_main_context_find_source_by_id")
|
|
private nonisolated func test_g_main_context_find_source_by_id(
|
|
_ context: UnsafeMutableRawPointer?, _ sourceId: UInt32
|
|
) -> UnsafeMutableRawPointer?
|
|
|
|
@_silgen_name("g_source_destroy")
|
|
private nonisolated func test_g_source_destroy(_ source: UnsafeMutableRawPointer)
|
|
|
|
@MainActor @Suite(.serialized) struct MainLoopSourceTests {
|
|
|
|
/// Pumps the default main context until `condition` is true OR `timeout`
|
|
/// elapses. Returns `true` if the watchdog fired (timeout elapsed without
|
|
/// `condition` becoming true).
|
|
private func pump(
|
|
until condition: () -> Bool, timeout: Duration = .seconds(2)
|
|
) -> Bool {
|
|
var expired = false
|
|
let watchdog = Timeout(interval: timeout, repeats: false) { expired = true }
|
|
defer { watchdog.remove() }
|
|
while !condition() && !expired {
|
|
_ = test_g_main_context_iteration(nil, 1)
|
|
}
|
|
return expired
|
|
}
|
|
|
|
// MARK: - Duration conversion
|
|
|
|
@Test func clampedMilliseconds() {
|
|
#expect(_clampedMilliseconds(.seconds(1)) == 1000)
|
|
#expect(_clampedMilliseconds(.milliseconds(1500)) == 1500)
|
|
#expect(_clampedMilliseconds(.microseconds(500)) == 1)
|
|
#expect(_clampedMilliseconds(.zero) == 0)
|
|
#expect(_clampedMilliseconds(.seconds(-5)) == 0)
|
|
#expect(_clampedMilliseconds(.seconds(Int64(UInt32.max) / 1000 + 1)) == UInt32.max)
|
|
}
|
|
|
|
// MARK: - Idle
|
|
|
|
@Test func idleRunsOnceThenSelfRemoves() {
|
|
var fired = 0
|
|
let id = Idle { fired += 1 }
|
|
#expect(id.rawValue > 0)
|
|
let timedOut = pump(until: { fired > 0 })
|
|
#expect(!timedOut, "main loop pump timed out")
|
|
#expect(fired == 1)
|
|
#expect(id.isRemoved)
|
|
for _ in 0..<20 {
|
|
_ = test_g_main_context_iteration(nil, 0)
|
|
}
|
|
#expect(fired == 1)
|
|
}
|
|
|
|
@Test func removeBeforeDispatchCancels() {
|
|
var fired = false
|
|
let id = Idle { fired = true }
|
|
id.remove()
|
|
#expect(id.isRemoved)
|
|
for _ in 0..<50 {
|
|
_ = test_g_main_context_iteration(nil, 0)
|
|
}
|
|
#expect(!fired)
|
|
}
|
|
|
|
@Test func removeIsIdempotent() {
|
|
let id = Idle {}
|
|
id.remove()
|
|
id.remove()
|
|
#expect(id.isRemoved)
|
|
}
|
|
|
|
// MARK: - Timeout
|
|
|
|
@Test func timeoutRepeatsUntilSelfRemoved() {
|
|
var ticks = 0
|
|
Timeout(interval: .milliseconds(1)) { source in
|
|
ticks += 1
|
|
if ticks == 3 { source.remove() }
|
|
}
|
|
let timedOut = pump(until: { ticks >= 3 })
|
|
#expect(!timedOut, "main loop pump timed out")
|
|
#expect(ticks == 3)
|
|
for _ in 0..<50 {
|
|
_ = test_g_main_context_iteration(nil, 0)
|
|
}
|
|
#expect(ticks == 3)
|
|
}
|
|
|
|
@Test func nonRepeatingTimeoutFiresOnce() {
|
|
var ticks = 0
|
|
let id = Timeout(interval: .milliseconds(1), repeats: false) { ticks += 1 }
|
|
let timedOut = pump(until: { ticks > 0 })
|
|
#expect(!timedOut, "main loop pump timed out")
|
|
#expect(ticks == 1)
|
|
#expect(id.isRemoved)
|
|
for _ in 0..<50 {
|
|
_ = test_g_main_context_iteration(nil, 0)
|
|
}
|
|
#expect(ticks == 1)
|
|
}
|
|
|
|
// MARK: - Remove from inside callback (regression)
|
|
|
|
@Test func repeatingTimeoutRemoveFromCallback() {
|
|
var ticks = 0
|
|
Timeout(interval: .milliseconds(1)) { source in
|
|
ticks += 1
|
|
source.remove()
|
|
}
|
|
let timedOut = pump(until: { ticks > 0 })
|
|
#expect(!timedOut, "main loop pump timed out")
|
|
#expect(ticks == 1)
|
|
}
|
|
/// Destroy-notify may run on a worker thread and must release the callback
|
|
/// box without asserting main-actor isolation.
|
|
@Test func destroyNotifyOffMainDoesNotTrap() async {
|
|
var fired = false
|
|
let id = Idle(repeats: true) { fired = true }
|
|
guard let source = test_g_main_context_find_source_by_id(nil, id.rawValue) else {
|
|
#expect(Bool(false), "attached source not found by id")
|
|
return
|
|
}
|
|
let address = UInt(bitPattern: source)
|
|
await Task.detached {
|
|
test_g_source_destroy(UnsafeMutableRawPointer(bitPattern: address)!)
|
|
}.value
|
|
|
|
#expect(id.isRemoved)
|
|
for _ in 0..<20 { _ = test_g_main_context_iteration(nil, 0) }
|
|
#expect(!fired)
|
|
}
|
|
}
|