Skip to content

Commit bae8feb

Browse files
committed
refactor(@angular/build): add debug logging to Vitest executor
This commit introduces a multi-level debug logging system within the Vitest test runner's executor. This logging is intended to aid in troubleshooting internal issues and provide deeper insights into the test execution process. Logging is controlled by the `NG_TEST_LOG` environment variable, which accepts numeric levels (0 for off, 1 for debug, 2 for verbose). Crucial lifecycle events and state changes in the `VitestExecutor` are now logged, such as constructor instantiation, build result processing (full and incremental), test re-run specifications, Vitest initialization details (including browser configuration, external configuration path, and included files), and executor disposal. These logs are guarded by the `logLevel` to prevent any performance penalties during normal execution. Furthermore, specific areas like incremental run file mappings and external dependency metadata are logged at a verbose level.
1 parent 7f36a36 commit bae8feb

File tree

1 file changed

+99
-36
lines changed
  • packages/angular/build/src/builders/unit-test/runners/vitest

1 file changed

+99
-36
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class VitestExecutor implements TestExecutor {
4040
explicitBrowser: [],
4141
explicitServer: [],
4242
};
43+
private readonly logLevel: number;
4344

4445
// This is a reverse map of the entry points created in `build-options.ts`.
4546
// It is used by the in-memory provider plugin to map the requested test file
@@ -54,27 +55,56 @@ export class VitestExecutor implements TestExecutor {
5455
testEntryPointMappings: Map<string, string> | undefined,
5556
logger: BuilderContext['logger'],
5657
) {
58+
const level = parseInt(process.env['NG_TEST_LOG'] ?? '0', 10);
59+
this.logLevel = isNaN(level) ? 0 : level;
60+
5761
this.projectName = projectName;
5862
this.options = options;
5963
this.logger = logger;
6064

65+
this.log(1, 'VitestExecutor instantiated.');
66+
this.log(2, 'NormalizedUnitTestBuilderOptions:', options);
67+
6168
if (testEntryPointMappings) {
6269
for (const [entryPoint, testFile] of testEntryPointMappings) {
6370
this.testFileToEntryPoint.set(testFile, entryPoint);
6471
this.entryPointToTestFile.set(entryPoint + '.js', testFile);
6572
}
73+
this.log(2, 'Test entry point mappings:', Object.fromEntries(testEntryPointMappings));
74+
}
75+
}
76+
77+
private log(level: number, message: string, data?: object) {
78+
if (this.logLevel < level) {
79+
return;
6680
}
81+
82+
const formattedMessage = `[VitestExecutor:L${level}] ${message}`;
83+
// Custom formatting for data object to ensure it's readable
84+
const logData = data ? JSON.stringify(data, null, 2) : '';
85+
this.logger.info(`${formattedMessage}${logData ? `\n${logData}` : ''}`);
6786
}
6887

6988
async *execute(buildResult: FullResult | IncrementalResult): AsyncIterable<BuilderOutput> {
89+
this.log(1, `Executing test run (kind: ${buildResult.kind}).`);
7090
this.normalizePath ??= (await import('vite')).normalizePath;
7191

7292
if (buildResult.kind === ResultKind.Full) {
7393
this.buildResultFiles.clear();
7494
for (const [path, file] of Object.entries(buildResult.files)) {
7595
this.buildResultFiles.set(this.normalizePath(path), file);
7696
}
97+
this.log(1, `Full build results received. Total files: ${this.buildResultFiles.size}.`);
7798
} else {
99+
this.log(
100+
1,
101+
`Incremental build results received.` +
102+
`Added: ${buildResult.added.length}, Modified: ${buildResult.modified.length}, Removed: ${buildResult.removed.length}.`,
103+
);
104+
this.log(2, 'Added files:', buildResult.added);
105+
this.log(2, 'Modified files:', buildResult.modified);
106+
this.log(2, 'Removed files:', buildResult.removed);
107+
78108
for (const file of buildResult.removed) {
79109
this.buildResultFiles.delete(this.normalizePath(file.path));
80110
}
@@ -84,6 +114,7 @@ export class VitestExecutor implements TestExecutor {
84114
}
85115

86116
updateExternalMetadata(buildResult, this.externalMetadata, undefined, true);
117+
this.log(2, 'Updated external metadata:', this.externalMetadata);
87118

88119
// Reset the exit code to allow for a clean state.
89120
// This is necessary because Vitest may set the exit code on failure, which can
@@ -103,7 +134,16 @@ export class VitestExecutor implements TestExecutor {
103134
// We need to find the original source file path to pass to Vitest.
104135
const source = this.entryPointToTestFile.get(modifiedFile);
105136
if (source) {
137+
this.log(
138+
2,
139+
`Mapped output file '${modifiedFile}' to source file '${source}' for re-run.`,
140+
);
106141
modifiedSourceFiles.add(source);
142+
} else {
143+
this.log(
144+
2,
145+
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
146+
);
107147
}
108148
vitest.invalidateFile(
109149
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
@@ -120,28 +160,38 @@ export class VitestExecutor implements TestExecutor {
120160
}
121161

122162
if (specsToRerun.length > 0) {
163+
this.log(1, `Re-running ${specsToRerun.length} test specifications.`);
164+
this.log(2, 'Specs to rerun:', specsToRerun);
123165
testResults = await vitest.rerunTestSpecifications(specsToRerun);
166+
} else {
167+
this.log(1, 'No test specifications to rerun.');
124168
}
125169
}
126170

127171
// Check if all the tests pass to calculate the result
128172
const testModules = testResults?.testModules ?? this.vitest.state.getTestModules();
129173

130174
let success = testModules.every((testModule) => testModule.ok());
175+
let finalResultReason = 'All tests passed.';
176+
131177
// Vitest does not return a failure result when coverage thresholds are not met.
132178
// Instead, it sets the process exit code to 1.
133179
// We check this exit code to determine if the test run should be considered a failure.
134180
if (success && process.exitCode === 1) {
135181
success = false;
182+
finalResultReason = 'Test run failed due to unmet coverage thresholds.';
136183
// Reset the exit code to prevent it from carrying over to subsequent runs/builds
137184
process.exitCode = 0;
138185
}
139186

187+
this.log(1, `Test run finished with success: ${success}. Reason: ${finalResultReason}`);
140188
yield { success };
141189
}
142190

143191
async [Symbol.asyncDispose](): Promise<void> {
192+
this.log(1, 'Disposing VitestExecutor: Closing Vitest instance.');
144193
await this.vitest?.close();
194+
this.log(1, 'Vitest instance closed.');
145195
}
146196

147197
private prepareSetupFiles(): string[] {
@@ -154,10 +204,13 @@ export class VitestExecutor implements TestExecutor {
154204
testSetupFiles.unshift('polyfills.js');
155205
}
156206

207+
this.log(1, 'Prepared setup files:', testSetupFiles);
208+
157209
return testSetupFiles;
158210
}
159211

160212
private async initializeVitest(): Promise<Vitest> {
213+
this.log(1, 'Initializing Vitest.');
161214
const {
162215
coverage,
163216
reporters,
@@ -180,6 +233,7 @@ export class VitestExecutor implements TestExecutor {
180233
vitestNodeModule = await import('vitest/node');
181234
} catch (error: unknown) {
182235
assertIsError(error);
236+
this.log(1, `Failed to import 'vitest/node'. Error code: ${error.code}`);
183237
if (error.code !== 'ERR_MODULE_NOT_FOUND') {
184238
throw error;
185239
}
@@ -199,6 +253,7 @@ export class VitestExecutor implements TestExecutor {
199253
browserViewport,
200254
);
201255
if (browserOptions.errors?.length) {
256+
this.log(1, 'Browser configuration errors found.', { errors: browserOptions.errors });
202257
throw new Error(browserOptions.errors.join('\n'));
203258
}
204259

@@ -207,7 +262,9 @@ export class VitestExecutor implements TestExecutor {
207262
this.logger.info(message);
208263
}
209264
}
265+
this.log(1, 'Browser configuration complete.', { config: browserOptions.browser });
210266

267+
this.log(1, `Verifying build results. File count: ${this.buildResultFiles.size}.`);
211268
assert(
212269
this.buildResultFiles.size > 0,
213270
'buildResult must be available before initializing vitest',
@@ -235,6 +292,8 @@ export class VitestExecutor implements TestExecutor {
235292
? await findVitestBaseConfig([projectRoot, workspaceRoot])
236293
: runnerConfig;
237294

295+
this.log(1, 'External Vitest configuration path:', { externalConfigPath });
296+
238297
let project = projectName;
239298
if (debug && browserOptions.browser?.instances) {
240299
if (browserOptions.browser.instances.length > 1) {
@@ -246,6 +305,7 @@ export class VitestExecutor implements TestExecutor {
246305
// When running browser tests, Vitest appends the browser name to the project identifier.
247306
// The project name must match this augmented name to ensure the correct project is targeted.
248307
project = `${projectName} (${browserOptions.browser.instances[0].browser})`;
308+
this.log(1, 'Adjusted project name for debugging with browser:', { project });
249309
}
250310

251311
// Filter internal entries and setup files from the include list
@@ -256,43 +316,46 @@ export class VitestExecutor implements TestExecutor {
256316
!internalEntries.some((internal) => entry.startsWith(internal)) && !setupFileSet.has(entry)
257317
);
258318
});
319+
this.log(2, 'Included test files (after filtering):', include);
259320

260-
return startVitest(
261-
'test',
262-
undefined,
263-
{
264-
config: externalConfigPath,
265-
root: workspaceRoot,
266-
project,
267-
outputFile,
268-
cache: cacheOptions.enabled ? undefined : false,
269-
testNamePattern: this.options.filter,
270-
watch,
271-
...(typeof ui === 'boolean' ? { ui } : {}),
272-
...debugOptions,
273-
},
274-
{
275-
// Note `.vitest` is auto appended to the path.
276-
cacheDir: cacheOptions.path,
277-
server: {
278-
// Disable the actual file watcher. The boolean watch option above should still
279-
// be enabled as it controls other internal behavior related to rerunning tests.
280-
watch: null,
281-
},
282-
plugins: [
283-
await createVitestConfigPlugin({
284-
browser: browserOptions.browser,
285-
coverage,
286-
projectName,
287-
projectSourceRoot,
288-
optimizeDepsInclude: this.externalMetadata.implicitBrowser,
289-
reporters,
290-
setupFiles: testSetupFiles,
291-
projectPlugins,
292-
include,
293-
}),
294-
],
321+
const vitestConfig = {
322+
config: externalConfigPath,
323+
root: workspaceRoot,
324+
project,
325+
outputFile,
326+
cache: cacheOptions.enabled ? undefined : (false as const),
327+
testNamePattern: this.options.filter,
328+
watch,
329+
...(typeof ui === 'boolean' ? { ui } : {}),
330+
...debugOptions,
331+
};
332+
const vitestServerConfig = {
333+
// Note `.vitest` is auto appended to the path.
334+
cacheDir: cacheOptions.path,
335+
server: {
336+
// Disable the actual file watcher. The boolean watch option above should still
337+
// be enabled as it controls other internal behavior related to rerunning tests.
338+
watch: null,
295339
},
296-
);
340+
plugins: [
341+
await createVitestConfigPlugin({
342+
browser: browserOptions.browser,
343+
coverage,
344+
projectName,
345+
projectSourceRoot,
346+
optimizeDepsInclude: this.externalMetadata.implicitBrowser,
347+
reporters,
348+
setupFiles: testSetupFiles,
349+
projectPlugins,
350+
include,
351+
}),
352+
],
353+
};
354+
355+
this.log(1, 'Calling startVitest with final configuration.');
356+
this.log(2, 'Vitest config:', vitestConfig);
357+
this.log(2, 'Vitest server config:', vitestServerConfig);
358+
359+
return startVitest('test', undefined, vitestConfig, vitestServerConfig);
297360
}
298361
}

0 commit comments

Comments
 (0)