|
| 1 | +import JavaScriptEventLoop |
| 2 | +import JavaScriptKit |
| 3 | +import XCTest |
| 4 | + |
| 5 | +final class JavaScriptEventLoopYieldIntervalTests: XCTestCase { |
| 6 | + final class Box<Value>: @unchecked Sendable { |
| 7 | + var value: Value |
| 8 | + init(_ value: Value) { self.value = value } |
| 9 | + } |
| 10 | + |
| 11 | + override func tearDown() { |
| 12 | + JavaScriptEventLoop.shared.yieldInterval = nil |
| 13 | + super.tearDown() |
| 14 | + } |
| 15 | + |
| 16 | + static func busyWait(milliseconds: Int) { |
| 17 | + let end = ContinuousClock.now + .milliseconds(milliseconds) |
| 18 | + while ContinuousClock.now < end {} |
| 19 | + } |
| 20 | + |
| 21 | + /// Returns, for each busy task, whether a JavaScript timer scheduled before them had fired. |
| 22 | + func timerRanAfterEachTask(count: Int, milliseconds: Int) async -> [Bool] { |
| 23 | + let timerRan = Box(false) |
| 24 | + _ = JSObject.global.setTimeout!( |
| 25 | + JSOneshotClosure { _ in |
| 26 | + timerRan.value = true |
| 27 | + return .undefined |
| 28 | + }, |
| 29 | + 0 |
| 30 | + ) |
| 31 | + let observed = Box([Bool]()) |
| 32 | + let tasks = (0..<count).map { _ in |
| 33 | + Task { |
| 34 | + Self.busyWait(milliseconds: milliseconds) |
| 35 | + observed.value.append(timerRan.value) |
| 36 | + } |
| 37 | + } |
| 38 | + for task in tasks { await task.value } |
| 39 | + return observed.value |
| 40 | + } |
| 41 | + |
| 42 | + func testYieldIntervalIsNilByDefault() { |
| 43 | + XCTAssertNil(JavaScriptEventLoop.shared.yieldInterval) |
| 44 | + } |
| 45 | + |
| 46 | + func testJobsRunInOneDrainWithoutYieldInterval() async { |
| 47 | + let observed = await timerRanAfterEachTask(count: 20, milliseconds: 2) |
| 48 | + XCTAssertEqual(observed, Array(repeating: false, count: 20)) |
| 49 | + } |
| 50 | + |
| 51 | + func testJavaScriptTasksRunBetweenJobsWithYieldInterval() async { |
| 52 | + JavaScriptEventLoop.shared.yieldInterval = .milliseconds(5) |
| 53 | + let observed = await timerRanAfterEachTask(count: 20, milliseconds: 2) |
| 54 | + XCTAssertEqual(observed.first, false) |
| 55 | + XCTAssertEqual(observed.last, true) |
| 56 | + } |
| 57 | + |
| 58 | + func testYieldIntervalAppliesToALaterDrain() async throws { |
| 59 | + JavaScriptEventLoop.shared.yieldInterval = .milliseconds(5) |
| 60 | + // Start a new drain. |
| 61 | + try await Task.sleep(nanoseconds: 1_000_000) |
| 62 | + let observed = await timerRanAfterEachTask(count: 20, milliseconds: 2) |
| 63 | + XCTAssertEqual(observed.first, false) |
| 64 | + XCTAssertEqual(observed.last, true) |
| 65 | + } |
| 66 | + |
| 67 | + func testJobsKeepTheirOrderAcrossYields() async { |
| 68 | + JavaScriptEventLoop.shared.yieldInterval = .milliseconds(1) |
| 69 | + let order = Box([Int]()) |
| 70 | + let tasks = (0..<20).map { index in |
| 71 | + Task { |
| 72 | + Self.busyWait(milliseconds: 1) |
| 73 | + order.value.append(index) |
| 74 | + } |
| 75 | + } |
| 76 | + for task in tasks { await task.value } |
| 77 | + XCTAssertEqual(order.value, Array(0..<20)) |
| 78 | + } |
| 79 | +} |
0 commit comments