|
|
@@ -11,23 +11,104 @@
|
|
|
* exemptions as stale. The gate's own note applies to itself: a gate whose
|
|
|
* verdict depends on how it was launched is not a gate.
|
|
|
*
|
|
|
+ * Four Node-loader processes divide the discovered files, and the union check
|
|
|
+ * proves that each bundle appears once. The two test-support bundles and the
|
|
|
+ * ACL/win32-process pair stay in one ordered shard because their pinned loader
|
|
|
+ * exemptions depend on the same preceding module state as the unsharded
|
|
|
+ * checker.
|
|
|
+ *
|
|
|
* The corpus is the build output, so this skips on a tree that has none.
|
|
|
*/
|
|
|
-import { spawnSync } from 'node:child_process'
|
|
|
+import { spawn } from 'node:child_process'
|
|
|
+import { globSync } from 'node:fs'
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
import { expect, test } from 'vitest'
|
|
|
|
|
|
const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.url))
|
|
|
+const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
|
|
|
+const corpusShards = 4
|
|
|
+const shardAffinity = new Set([
|
|
|
+ 'packages/test-support/acp-snapshot/lib/index.js',
|
|
|
+ 'packages/test-support/client-runtime/lib/index.js',
|
|
|
+ 'packages/sandbox/sandbox-windows-acl/lib/index.js',
|
|
|
+ 'packages/subprocess/win32-process/lib/index.js',
|
|
|
+])
|
|
|
+
|
|
|
+interface CorpusResult {
|
|
|
+ readonly output: string
|
|
|
+ readonly status: number | null
|
|
|
+ readonly error?: string
|
|
|
+}
|
|
|
+
|
|
|
+/** @returns Built bundle paths in the same stable order as the checker. */
|
|
|
+function discoverBuiltBundles(): string[] {
|
|
|
+ return [
|
|
|
+ ...globSync('packages/*/*/lib/index.js', { cwd: repositoryRoot }),
|
|
|
+ ...globSync('vendor/*/lib/index.js', { cwd: repositoryRoot }),
|
|
|
+ ].map(path => path.replaceAll('\\', '/')).sort()
|
|
|
+}
|
|
|
+
|
|
|
+/** @returns Every bundle assigned exactly once while preserving Koffi loader affinity. */
|
|
|
+function partitionBundles(files: readonly string[], count: number): string[][] {
|
|
|
+ const partitions = Array.from({ length: count }, () => [] as string[])
|
|
|
+ files.forEach((file, index) => {
|
|
|
+ const assigned = shardAffinity.has(file) ? 0 : index % count
|
|
|
+ partitions[assigned]?.push(file)
|
|
|
+ })
|
|
|
+ return partitions
|
|
|
+}
|
|
|
|
|
|
-test('every built bundle transforms to the export shape Node loads', (context) => {
|
|
|
- const finished = spawnSync(process.execPath, ['--import', 'tsx/esm', runner], { encoding: 'utf8' })
|
|
|
- const output = `${finished.stdout}${finished.stderr}`
|
|
|
- if (output.includes('no built bundles found')) {
|
|
|
+/** @returns One isolated Node-loader corpus shard. */
|
|
|
+function runCorpusShard(files: readonly string[]): Promise<CorpusResult> {
|
|
|
+ return new Promise((resolveResult) => {
|
|
|
+ let output = ''
|
|
|
+ let spawnError: string | undefined
|
|
|
+ const child = spawn(process.execPath, ['--import', 'tsx/esm', runner, ...files], {
|
|
|
+ cwd: repositoryRoot,
|
|
|
+ stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
+ })
|
|
|
+ child.stdout.setEncoding('utf8')
|
|
|
+ child.stderr.setEncoding('utf8')
|
|
|
+ child.stdout.on('data', (chunk: string) => { output += chunk })
|
|
|
+ child.stderr.on('data', (chunk: string) => { output += chunk })
|
|
|
+ child.once('error', (reason) => { spawnError = reason.message })
|
|
|
+ child.once('close', (status) => {
|
|
|
+ resolveResult({
|
|
|
+ output,
|
|
|
+ status,
|
|
|
+ ...spawnError === undefined ? {} : { error: spawnError },
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+test('partitions every bundle once while retaining loader-state affinity', () => {
|
|
|
+ const files = [
|
|
|
+ 'packages/example/first/lib/index.js',
|
|
|
+ ...shardAffinity,
|
|
|
+ 'packages/example/last/lib/index.js',
|
|
|
+ ]
|
|
|
+ const shards = partitionBundles(files, corpusShards)
|
|
|
+
|
|
|
+ expect(shards.flat().sort()).toEqual([...files].sort())
|
|
|
+ expect(shards[0]?.filter(file => shardAffinity.has(file))).toEqual(files.filter(file => shardAffinity.has(file)))
|
|
|
+})
|
|
|
+
|
|
|
+test('every built bundle transforms to the export shape Node loads', async (context) => {
|
|
|
+ const files = discoverBuiltBundles()
|
|
|
+ if (files.length === 0) {
|
|
|
context.skip('the workspace has no build output to sweep')
|
|
|
return
|
|
|
}
|
|
|
+ const shards = partitionBundles(files, Math.min(corpusShards, files.length))
|
|
|
+ expect(shards.flat().sort()).toEqual(files)
|
|
|
+ const finished = await Promise.all(shards.map(runCorpusShard))
|
|
|
+ const output = finished.map((result, index) => `shard ${String(index + 1)}/${String(shards.length)}:\n${result.output}`).join('\n')
|
|
|
// The runner prefixes every finding with '- ', so a failure reads as the
|
|
|
// findings themselves rather than as a diff of its whole report.
|
|
|
expect(output.split('\n').filter(line => line.startsWith('- ')).join('\n')).toBe('')
|
|
|
- expect(finished.status, output).toBe(0)
|
|
|
+ for (const result of finished) {
|
|
|
+ expect(result.error, output).toBeUndefined()
|
|
|
+ expect(result.status, output).toBe(0)
|
|
|
+ }
|
|
|
}, 900_000)
|