Skip to content

Commit e5d0148

Browse files
test(enhanced): harden treeshake fixture setup for CI flake (#4441)
1 parent 2a2042d commit e5d0148

File tree

4 files changed

+137
-23
lines changed

4 files changed

+137
-23
lines changed

packages/enhanced/rstest.treeshake.serial.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export default defineConfig({
1919
maxWorkers: 1,
2020
minWorkers: 1,
2121
},
22+
// Also disable in-file test concurrency (default is 5), otherwise
23+
// describe blocks can still race on fixture generation and compilation.
24+
maxConcurrency: 1,
2225
globals: true,
2326
include: [
2427
path.resolve(__dirname, 'test/ConfigTestCases.treeshake.rstest.ts'),

packages/enhanced/test/ConfigTestCases.rstest.ts

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,51 @@ const ensureTreeShakingFixtures = (testDirectory: string) => {
160160
}
161161
};
162162

163+
const collectTreeShakingMissingModuleStubs = (outputDirectory: string) => {
164+
const independentPackagesDir = path.join(
165+
outputDirectory,
166+
'independent-packages',
167+
);
168+
if (!fs.existsSync(independentPackagesDir)) {
169+
return [] as string[];
170+
}
171+
const missing = new Set<string>();
172+
const pending = [independentPackagesDir];
173+
const pattern =
174+
/Cannot find module ['"]([^'"]*tree-shaking-share[^'"]*node_modules[^'"]*)['"]/g;
175+
while (pending.length) {
176+
const currentDir = pending.pop() as string;
177+
let entries: fs.Dirent[] = [];
178+
try {
179+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
180+
} catch {
181+
continue;
182+
}
183+
for (const entry of entries) {
184+
const fullPath = path.join(currentDir, entry.name);
185+
if (entry.isDirectory()) {
186+
pending.push(fullPath);
187+
continue;
188+
}
189+
if (!entry.isFile() || entry.name !== 'share-entry.js') {
190+
continue;
191+
}
192+
let content = '';
193+
try {
194+
content = fs.readFileSync(fullPath, 'utf-8');
195+
} catch {
196+
continue;
197+
}
198+
pattern.lastIndex = 0;
199+
let match: RegExpExecArray | null = null;
200+
while ((match = pattern.exec(content))) {
201+
missing.add(match[1]);
202+
}
203+
}
204+
}
205+
return Array.from(missing).sort();
206+
};
207+
163208
const dedupeByMessage = (items: any[]) => {
164209
if (!Array.isArray(items) || items.length === 0) {
165210
return [] as any[];
@@ -589,25 +634,28 @@ export const describeCases = (config: any) => {
589634
`${testName} should compile`,
590635
async () => {
591636
ensureTreeShakingFixturesIfNeeded();
592-
try {
593-
// Robust cleanup to avoid ENOTEMPTY and race conditions
594-
(fs as any).rmSync?.(outputDirectory, {
595-
recursive: true,
596-
force: true,
597-
});
598-
} catch {
637+
const isTreeShakingFixtureCase = testDirectory.includes(
638+
`${path.sep}tree-shaking-share${path.sep}`,
639+
);
640+
const cleanOutputDirectory = () => {
599641
try {
600-
rimrafSync(outputDirectory);
642+
// Robust cleanup to avoid ENOTEMPTY and race conditions
643+
(fs as any).rmSync?.(outputDirectory, {
644+
recursive: true,
645+
force: true,
646+
});
601647
} catch {
602-
/* ignore */
648+
try {
649+
rimrafSync(outputDirectory);
650+
} catch {
651+
/* ignore */
652+
}
603653
}
604-
}
605-
fs.mkdirSync(outputDirectory, { recursive: true });
606-
infraStructureLog.length = 0;
607-
608-
// 运行 webpack
609-
const { stats } = await new Promise<{ stats: any }>(
610-
(resolve, reject) => {
654+
};
655+
const runWebpackCompile = async () => {
656+
infraStructureLog.length = 0;
657+
stderr.reset();
658+
return new Promise<{ stats: any }>((resolve, reject) => {
611659
const onCompiled = (err: any, stats: any) => {
612660
if (err) return reject(err);
613661
resolve({ stats });
@@ -631,12 +679,58 @@ export const describeCases = (config: any) => {
631679
} catch (e: any) {
632680
reject(e);
633681
}
634-
},
635-
).catch((e) => {
682+
});
683+
};
684+
685+
cleanOutputDirectory();
686+
fs.mkdirSync(outputDirectory, { recursive: true });
687+
let { stats } = await runWebpackCompile().catch((e) => {
636688
handleFatalError(e);
637689
throw e; // rethrow for rstest to mark failure otherwise
638690
});
639691

692+
// Under heavy IO/CPU contention, tree-shaking fixture packages can
693+
// transiently resolve as missing. Rebuild with bounded retries
694+
// after re-ensuring fixtures when share-entry stubs contain
695+
// webpackMissingModule.
696+
if (isTreeShakingFixtureCase) {
697+
const maxTreeShakingAttempts = 4;
698+
for (
699+
let attempt = 1;
700+
attempt < maxTreeShakingAttempts;
701+
attempt++
702+
) {
703+
const missingModules =
704+
collectTreeShakingMissingModuleStubs(outputDirectory);
705+
if (!missingModules.length) {
706+
break;
707+
}
708+
ensureTreeShakingFixturesIfNeeded();
709+
// Give the FS a tiny settle window under extreme IO pressure.
710+
await new Promise<void>((resolve) =>
711+
setTimeout(resolve, 25 * attempt),
712+
);
713+
cleanOutputDirectory();
714+
fs.mkdirSync(outputDirectory, { recursive: true });
715+
({ stats } = await runWebpackCompile().catch((e) => {
716+
handleFatalError(e);
717+
throw e;
718+
}));
719+
if (attempt === maxTreeShakingAttempts - 1) {
720+
const remainingMissingModules =
721+
collectTreeShakingMissingModuleStubs(outputDirectory);
722+
if (remainingMissingModules.length) {
723+
throw new Error(
724+
[
725+
`Tree-shaking fixture modules remained unresolved after ${maxTreeShakingAttempts} compilation attempts:`,
726+
...remainingMissingModules,
727+
].join('\n'),
728+
);
729+
}
730+
}
731+
}
732+
}
733+
640734
// 写入 stats
641735
const statOptions = { preset: 'verbose', colors: false };
642736
fs.mkdirSync(outputDirectory, { recursive: true });

packages/enhanced/test/scripts/ensure-reshake-fixtures.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,8 @@ const uiLibSideEffectEntry = [
132132
].join('\n');
133133

134134
for (const baseDir of fixtureRoots) {
135-
try {
136-
fs.rmSync(baseDir, { recursive: true, force: true });
137-
} catch {
138-
// ignore cleanup errors and ensure the directory exists
139-
}
135+
// Keep fixture generation non-destructive so concurrent test workers/processes
136+
// never observe temporarily-missing files during resolution.
140137
fs.mkdirSync(baseDir, { recursive: true });
141138
const isReshake = baseDir.includes(`${path.sep}reshake-share${path.sep}`);
142139
if (isReshake) {

packages/enhanced/test/setupTestFramework.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,24 @@ const reshakeServerUiLibSideEffect = path.join(
157157
'ui-lib-side-effect',
158158
'index.js',
159159
);
160+
const reshakeServerUiLibDynamicSpecific = path.join(
161+
__dirname,
162+
'configCases',
163+
'tree-shaking-share',
164+
'server-strategy',
165+
'node_modules',
166+
'ui-lib-dynamic-specific-export',
167+
'index.js',
168+
);
169+
const reshakeServerUiLibDynamicDefault = path.join(
170+
__dirname,
171+
'configCases',
172+
'tree-shaking-share',
173+
'server-strategy',
174+
'node_modules',
175+
'ui-lib-dynamic-default-export',
176+
'index.js',
177+
);
160178
const inferStrategyUiLib = path.join(
161179
__dirname,
162180
'configCases',
@@ -179,6 +197,8 @@ const reshakeDep = path.join(
179197
if (
180198
!fs.existsSync(reshakeServerUiLib) ||
181199
!fs.existsSync(reshakeServerUiLibSideEffect) ||
200+
!fs.existsSync(reshakeServerUiLibDynamicSpecific) ||
201+
!fs.existsSync(reshakeServerUiLibDynamicDefault) ||
182202
!fs.existsSync(inferStrategyUiLib) ||
183203
!fs.existsSync(reshakeDep)
184204
) {

0 commit comments

Comments
 (0)