| 19 | ]) |
| 20 | |
| 21 | export function detectAsyncLeaks(testFile: string, projectName: string): () => Promise<AsyncLeak[]> { |
| 22 | const resources = new Map<number, PossibleLeak>() |
| 23 | |
| 24 | const hook = createHook({ |
| 25 | init(asyncId, type, triggerAsyncId, resource) { |
| 26 | if (IGNORED_TYPES.has(type)) { |
| 27 | return |
| 28 | } |
| 29 | |
| 30 | let stack = '' |
| 31 | const limit = Error.stackTraceLimit |
| 32 | |
| 33 | // VitestModuleEvaluator's async wrapper of node:vm causes out-of-bound stack traces, simply skip it. |
| 34 | // Crash fixed in https://github.com/vitejs/vite/pull/21585 |
| 35 | try { |
| 36 | Error.stackTraceLimit = 100 |
| 37 | stack = new Error('VITEST_DETECT_ASYNC_LEAKS').stack || '' |
| 38 | } |
| 39 | catch { |
| 40 | return |
| 41 | } |
| 42 | finally { |
| 43 | Error.stackTraceLimit = limit |
| 44 | } |
| 45 | |
| 46 | if (!stack.includes(testFile)) { |
| 47 | const trigger = resources.get(triggerAsyncId) |
| 48 | |
| 49 | if (!trigger) { |
| 50 | return |
| 51 | } |
| 52 | |
| 53 | stack = trigger.stack |
| 54 | } |
| 55 | |
| 56 | let isActive = isActiveDefault |
| 57 | |
| 58 | if ('hasRef' in resource) { |
| 59 | const ref = new WeakRef(resource as { hasRef: () => boolean }) |
| 60 | |
| 61 | isActive = () => ref.deref()?.hasRef() ?? false |
| 62 | } |
| 63 | |
| 64 | resources.set(asyncId, { type, stack, projectName, filename: testFile, isActive }) |
| 65 | }, |
| 66 | destroy(asyncId) { |
| 67 | if (resources.get(asyncId)?.type !== 'PROMISE') { |
| 68 | resources.delete(asyncId) |
| 69 | } |
| 70 | }, |
| 71 | promiseResolve(asyncId) { |
| 72 | resources.delete(asyncId) |
| 73 | }, |
| 74 | }) |
| 75 | |
| 76 | hook.enable() |
| 77 | |
| 78 | return async function collect() { |