Gate derived test contexts on invariant readiness
This commit is contained in:
@@ -120,18 +120,20 @@ describe('global test invariant host', () => {
|
|||||||
const releaseDelayed = deferred()
|
const releaseDelayed = deferred()
|
||||||
const order: string[] = []
|
const order: string[] = []
|
||||||
let delayedCompanion: TestInvariantCompanion | undefined
|
let delayedCompanion: TestInvariantCompanion | undefined
|
||||||
|
const companionNestedApply = vi.fn(function companionNestedApply() {})
|
||||||
|
|
||||||
await withFakeCompanions(
|
await withFakeCompanions(
|
||||||
(path, index) => async () => {
|
(path, index) => async () => {
|
||||||
const companion: TestInvariantCompanion = {
|
const companion: TestInvariantCompanion = {
|
||||||
name: `test-invariant-${index}`,
|
name: `test-invariant-${index}`,
|
||||||
inject: ['invariants'],
|
inject: ['invariants'],
|
||||||
async apply() {
|
async apply(companionCtx) {
|
||||||
order.push(`companion-start:${path}`)
|
order.push(`companion-start:${path}`)
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
delayedStarted.resolve()
|
delayedStarted.resolve()
|
||||||
await releaseDelayed.promise
|
await releaseDelayed.promise
|
||||||
}
|
}
|
||||||
|
if (index === 1) await companionCtx.plugin(companionNestedApply)
|
||||||
order.push(`companion-active:${path}`)
|
order.push(`companion-active:${path}`)
|
||||||
return () => {}
|
return () => {}
|
||||||
},
|
},
|
||||||
@@ -173,6 +175,7 @@ describe('global test invariant host', () => {
|
|||||||
expect(targetFiber.state).toBe(FiberState.ACTIVE)
|
expect(targetFiber.state).toBe(FiberState.ACTIVE)
|
||||||
expect(targetApply).toHaveBeenCalledOnce()
|
expect(targetApply).toHaveBeenCalledOnce()
|
||||||
expect(nestedApply).toHaveBeenCalledOnce()
|
expect(nestedApply).toHaveBeenCalledOnce()
|
||||||
|
expect(companionNestedApply).toHaveBeenCalledOnce()
|
||||||
const targetIndex = order.indexOf('target')
|
const targetIndex = order.indexOf('target')
|
||||||
expect(targetIndex).toBeGreaterThan(-1)
|
expect(targetIndex).toBeGreaterThan(-1)
|
||||||
expect(order.slice(0, targetIndex)).toHaveLength(Object.keys(testInvariantCompanions).length * 2)
|
expect(order.slice(0, targetIndex)).toHaveLength(Object.keys(testInvariantCompanions).length * 2)
|
||||||
@@ -187,6 +190,52 @@ describe('global test invariant host', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('holds plugins registered on a root-derived context until companion readiness', async () => {
|
||||||
|
const delayedStarted = deferred()
|
||||||
|
const releaseDelayed = deferred()
|
||||||
|
|
||||||
|
await withFakeCompanions(
|
||||||
|
(_path, index) => async () => ({
|
||||||
|
name: `test-invariant-${index}`,
|
||||||
|
inject: ['invariants'],
|
||||||
|
async apply() {
|
||||||
|
if (index === 0) {
|
||||||
|
delayedStarted.resolve()
|
||||||
|
await releaseDelayed.promise
|
||||||
|
}
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async () => {
|
||||||
|
const ctx = new Context()
|
||||||
|
const rootApply = vi.fn(function rootApply() {})
|
||||||
|
const derivedApply = vi.fn(function derivedApply() {})
|
||||||
|
const derived = ctx.extend()
|
||||||
|
.isolate('testInvariantDerived')
|
||||||
|
.intercept('testInvariantDerived', {})
|
||||||
|
|
||||||
|
const rootFiber = ctx.plugin(rootApply)
|
||||||
|
const derivedFiber = derived.plugin(derivedApply)
|
||||||
|
|
||||||
|
await delayedStarted.promise
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(rootApply).not.toHaveBeenCalled()
|
||||||
|
expect(derivedApply).not.toHaveBeenCalled()
|
||||||
|
expect(derivedFiber.inject).toEqual({
|
||||||
|
[TEST_INVARIANT_READY_SERVICE]: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
releaseDelayed.resolve()
|
||||||
|
await Promise.all([rootFiber, derivedFiber])
|
||||||
|
expect(rootFiber.state).toBe(FiberState.ACTIVE)
|
||||||
|
expect(derivedFiber.state).toBe(FiberState.ACTIVE)
|
||||||
|
expect(rootApply).toHaveBeenCalledOnce()
|
||||||
|
expect(derivedApply).toHaveBeenCalledOnce()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it.each(['load', 'startup'] as const)(
|
it.each(['load', 'startup'] as const)(
|
||||||
'rejects a target when a lazy companion fails during %s without starting the target',
|
'rejects a target when a lazy companion fails during %s without starting the target',
|
||||||
async (phase) => {
|
async (phase) => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
|
|||||||
|
|
||||||
interface InvariantHost {
|
interface InvariantHost {
|
||||||
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
|
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
|
||||||
|
readonly barrierOwners: WeakSet<Context['fiber']>
|
||||||
readonly ready: Promise<void>
|
readonly ready: Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +66,15 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
|
|||||||
const callback = this.resolve(plugin)
|
const callback = this.resolve(plugin)
|
||||||
const existing = callback === undefined ? undefined : host.byCallback.get(callback)
|
const existing = callback === undefined ? undefined : host.byCallback.get(callback)
|
||||||
if (existing !== undefined) {
|
if (existing !== undefined) {
|
||||||
return this.ctx === root ? joinInvariantStartup(existing, host.ready) : existing
|
return hasBarrierOwner(host, this.ctx) ? existing : joinInvariantStartup(existing, host.ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested plugins run inside a target that already crossed the root barrier.
|
// Causal descendants of a gated target have already crossed the barrier.
|
||||||
// Adding the same root-owned dependency there would make child lifecycle
|
// Host service and companion descendants also bypass it so their own startup
|
||||||
// depend on an unrelated isolation scope and can deadlock companion startup.
|
// cannot depend on the readiness they are responsible for providing.
|
||||||
if (this.ctx !== root) return originalPlugin.call(this, plugin, config, getOuterStack)
|
if (hasBarrierOwner(host, this.ctx)) {
|
||||||
|
return originalPlugin.call(this, plugin, config, getOuterStack)
|
||||||
|
}
|
||||||
if (callback === undefined) return originalPlugin.call(this, plugin, config, getOuterStack)
|
if (callback === undefined) return originalPlugin.call(this, plugin, config, getOuterStack)
|
||||||
|
|
||||||
const fiber = originalPlugin.call(
|
const fiber = originalPlugin.call(
|
||||||
@@ -80,6 +83,7 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
|
|||||||
config,
|
config,
|
||||||
getOuterStack,
|
getOuterStack,
|
||||||
)
|
)
|
||||||
|
host.barrierOwners.add(fiber.ctx.fiber)
|
||||||
return joinInvariantStartup(fiber, host.ready)
|
return joinInvariantStartup(fiber, host.ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +124,13 @@ export function testInvariantCompanionPaths(testPath: string): string[] {
|
|||||||
|
|
||||||
function startInvariantHost(root: Context): InvariantHost {
|
function startInvariantHost(root: Context): InvariantHost {
|
||||||
const byCallback = new Map<unknown, PluginFiber>()
|
const byCallback = new Map<unknown, PluginFiber>()
|
||||||
|
const barrierOwners = new WeakSet<Context['fiber']>()
|
||||||
const mount = (plugin: Plugin, config?: unknown): PluginFiber => {
|
const mount = (plugin: Plugin, config?: unknown): PluginFiber => {
|
||||||
const fiber = originalPlugin.call(root.registry, plugin, config)
|
const fiber = originalPlugin.call(root.registry, plugin, config)
|
||||||
const callback = root.registry.resolve(plugin)
|
const callback = root.registry.resolve(plugin)
|
||||||
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
|
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
|
||||||
byCallback.set(callback, fiber)
|
byCallback.set(callback, fiber)
|
||||||
|
barrierOwners.add(fiber.ctx.fiber)
|
||||||
return fiber
|
return fiber
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,11 +163,21 @@ function startInvariantHost(root: Context): InvariantHost {
|
|||||||
await Promise.all(companionFibers.map(({ fiber, path }) => requireActive(fiber, path)))
|
await Promise.all(companionFibers.map(({ fiber, path }) => requireActive(fiber, path)))
|
||||||
root.provide(TEST_INVARIANT_READY_SERVICE, true)
|
root.provide(TEST_INVARIANT_READY_SERVICE, true)
|
||||||
})
|
})
|
||||||
const host = { byCallback, ready }
|
const host = { byCallback, barrierOwners, ready }
|
||||||
hosts.set(root, host)
|
hosts.set(root, host)
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBarrierOwner(host: InvariantHost, ctx: Context): boolean {
|
||||||
|
let fiber = ctx.fiber
|
||||||
|
while (true) {
|
||||||
|
if (host.barrierOwners.has(fiber)) return true
|
||||||
|
const parent = fiber.parent.fiber
|
||||||
|
if (parent === fiber) return false
|
||||||
|
fiber = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function requireActive(fiber: PluginFiber, label: string): Promise<void> {
|
async function requireActive(fiber: PluginFiber, label: string): Promise<void> {
|
||||||
await fiber.await()
|
await fiber.await()
|
||||||
if (fiber.state !== FiberState.ACTIVE) {
|
if (fiber.state !== FiberState.ACTIVE) {
|
||||||
|
|||||||
Reference in New Issue
Block a user