Bläddra i källkod

Gate derived test contexts on invariant readiness

Hypatia May 1 månad sedan
förälder
incheckning
573a062afd
2 ändrade filer med 72 tillägg och 7 borttagningar
  1. 50 1
      scripts/test-invariants.spec.ts
  2. 22 6
      scripts/test-invariants.ts

+ 50 - 1
scripts/test-invariants.spec.ts

@@ -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) => {

+ 22 - 6
scripts/test-invariants.ts

@@ -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.
-  // Adding the same root-owned dependency there would make child lifecycle
-  // depend on an unrelated isolation scope and can deadlock companion startup.
-  if (this.ctx !== root) return originalPlugin.call(this, plugin, config, getOuterStack)
+  // Causal descendants of a gated target have already crossed the barrier.
+  // Host service and companion descendants also bypass it so their own startup
+  // cannot depend on the readiness they are responsible for providing.
+  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) {