Expose an async main-loop pump helper for swift-testing suites (@_spi(Portico)) #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
swift-testing does not run
@MainActortest bodies on the process's initial thread. Portico's ownObservationBridge(Sources/Portico/State/ObservationBridge.swift) hops off-main mutations back to the main actor via_Concurrency.Task { @MainActor in ObservationBridge.markDirty(id) }(see_porticoObservationDidChange), andGLibMainExecutordrains scheduled Swift jobs from a GLib idle source. Both paths only make progress when the GLib default main context is iterated from a suspension point - a synchronous busy-loop pump called before an assertion can never observe the async-scheduled update, because theTaskthat performs the hop has not had a chance to run yet.Portico's own test suite already works around this:
Tests/PorticoTests/ObservableTests.swiftdeclares a privateasyncPump(until:turns:)that awaitsTask.yield()-style turns while iterating the main context, backed by a hand-rolled:This is internal to Portico's test target, so it is invisible to downstream consumers. In
gtk-swift/luminate, the exact same trap showed up twice while writing suites for Observation-driven state (Preferences,ClientSessionBinder), and both call sites had to redeclare an identical async pump plus an identical@_silgen_name("g_main_context_iteration")shim, just under different names:Tests/LuminateUITests/PreferenceTests.swift(preference_g_main_context_iteration)Tests/LuminateTests/ClientSessionBinderTests.swift(clientSessionBinder_g_main_context_iteration)That is three copies of the same ~15-line workaround across two repositories, and every future downstream package that tests
@Observablestate driven through Portico's main loop will need a fourth.Ask
Expose Portico's own async pump as a small
@_spi(Portico)API so downstream test targets can@_spi(Portico) import Porticoand call it directly instead of hand-rolling@_silgen_name. Something in the shape of:A precedent for this shape of API already exists:
_porticoObservationRegistrationCount()inObservationBridge.swiftis already@_spi(Portico) @MainActor public.Where this belongs
Sources/Portico/MainLoop/GLibMainExecutor.swiftalready ownsporticoIsMainThread()and theg_main_context_iterationmain-loop plumbing for the executor; the new helper is a thin wrapper over the same primitive and seems like a natural fit there (or a small new file underSources/Portico/State/next toObservationBridge.swift, since its only real caller is code awaiting an Observation-driven update). Happy to send a PR if a location is confirmed.Repro / context
Seen while writing
gtk-swift/luminate'sLuminateUITestsandLuminateTestssuites. Full failure signature before the fix: a synchronous pump loop runsg_main_context_iterationN times immediately after mutating an@Observablemodel's watched preference, then asserts the dependent state updated - and the assertion fails because theTask { @MainActor in ... }hop had not yet been scheduled onto the context being pumped. Switching each suite to anasyncpump thatawaits each turn (so the scheduledTaskgets a chance to run between iterations) fixes it; that fix is exactly what this issue asks to no longer have to hand-roll.