Skip to content

Commit e57821a

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 70a81fa commit e57821a

File tree

1 file changed

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

1 file changed

+97
-36
lines changed

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

Lines changed: 97 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,55 @@ 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));
6674
}
6775
}
6876

77+
private log(level: number, message: string, data?: object) {
78+
if (this.logLevel < level) {
79+
return;
80+
}
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}` : ''}`);
86+
}
87+
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. Added: ${buildResult.added.length}, Modified: ${buildResult.modified.length}, Removed: ${buildResult.removed.length}.`,
102+
);
103+
this.log(2, 'Added files:', buildResult.added);
104+
this.log(2, 'Modified files:', buildResult.modified);
105+
this.log(2, 'Removed files:', buildResult.removed);
106+
78107
for (const file of buildResult.removed) {
79108
this.buildResultFiles.delete(this.normalizePath(file.path));
80109
}
@@ -84,6 +113,7 @@ export class VitestExecutor implements TestExecutor {
84113
}
85114

86115
updateExternalMetadata(buildResult, this.externalMetadata, undefined, true);
116+
this.log(2, 'Updated external metadata:', this.externalMetadata);
87117

88118
// Reset the exit code to allow for a clean state.
89119
// This is necessary because Vitest may set the exit code on failure, which can
@@ -103,7 +133,16 @@ export class VitestExecutor implements TestExecutor {
103133
// We need to find the original source file path to pass to Vitest.
104134
const source = this.entryPointToTestFile.get(modifiedFile);
105135
if (source) {
136+
this.log(
137+
2,
138+
`Mapped output file '${modifiedFile}' to source file '${source}' for re-run.`,
139+
);
106140
modifiedSourceFiles.add(source);
141+
} else {
142+
this.log(
143+
2,
144+
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
145+
);
107146
}
108147
vitest.invalidateFile(
109148
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
@@ -120,28 +159,38 @@ export class VitestExecutor implements TestExecutor {
120159
}
121160

122161
if (specsToRerun.length > 0) {
162+
this.log(1, `Re-running ${specsToRerun.length} test specifications.`);
163+
this.log(2, 'Specs to rerun:', specsToRerun);
123164
testResults = await vitest.rerunTestSpecifications(specsToRerun);
165+
} else {
166+
this.log(1, 'No test specifications to rerun.');
124167
}
125168
}
126169

127170
// Check if all the tests pass to calculate the result
128171
const testModules = testResults?.testModules ?? this.vitest.state.getTestModules();
129172

130173
let success = testModules.every((testModule) => testModule.ok());
174+
let finalResultReason = 'All tests passed.';
175+
131176
// Vitest does not return a failure result when coverage thresholds are not met.
132177
// Instead, it sets the process exit code to 1.
133178
// We check this exit code to determine if the test run should be considered a failure.
134179
if (success && process.exitCode === 1) {
135180
success = false;
181+
finalResultReason = 'Test run failed due to unmet coverage thresholds.';
136182
// Reset the exit code to prevent it from carrying over to subsequent runs/builds
137183
process.exitCode = 0;
138184
}
139185

186+
this.log(1, `Test run finished with success: ${success}. Reason: ${finalResultReason}`);
140187
yield { success };
141188
}
142189

143190
async [Symbol.asyncDispose](): Promise<void> {
191+
this.log(1, 'Disposing VitestExecutor: Closing Vitest instance.');
144192
await this.vitest?.close();
193+
this.log(1, 'Vitest instance closed.');
145194
}
146195

147196
private prepareSetupFiles(): string[] {
@@ -154,10 +203,12 @@ export class VitestExecutor implements TestExecutor {
154203
testSetupFiles.unshift('polyfills.js');
155204
}
156205

206+
this.log(1, 'Prepared setup files:', testSetupFiles);
157207
return testSetupFiles;
158208
}
159209

160210
private async initializeVitest(): Promise<Vitest> {
211+
this.log(1, 'Initializing Vitest.');
161212
const {
162213
coverage,
163214
reporters,
@@ -180,6 +231,7 @@ export class VitestExecutor implements TestExecutor {
180231
vitestNodeModule = await import('vitest/node');
181232
} catch (error: unknown) {
182233
assertIsError(error);
234+
this.log(1, `Failed to import 'vitest/node'. Error code: ${error.code}`);
183235
if (error.code !== 'ERR_MODULE_NOT_FOUND') {
184236
throw error;
185237
}
@@ -199,6 +251,7 @@ export class VitestExecutor implements TestExecutor {
199251
browserViewport,
200252
);
201253
if (browserOptions.errors?.length) {
254+
this.log(1, 'Browser configuration errors found.', { errors: browserOptions.errors });
202255
throw new Error(browserOptions.errors.join('\n'));
203256
}
204257

@@ -207,7 +260,9 @@ export class VitestExecutor implements TestExecutor {
207260
this.logger.info(message);
208261
}
209262
}
263+
this.log(1, 'Browser configuration complete.', { config: browserOptions.browser });
210264

265+
this.log(1, `Verifying build results. File count: ${this.buildResultFiles.size}.`);
211266
assert(
212267
this.buildResultFiles.size > 0,
213268
'buildResult must be available before initializing vitest',
@@ -235,6 +290,8 @@ export class VitestExecutor implements TestExecutor {
235290
? await findVitestBaseConfig([projectRoot, workspaceRoot])
236291
: runnerConfig;
237292

293+
this.log(1, 'External Vitest configuration path:', { externalConfigPath });
294+
238295
let project = projectName;
239296
if (debug && browserOptions.browser?.instances) {
240297
if (browserOptions.browser.instances.length > 1) {
@@ -246,6 +303,7 @@ export class VitestExecutor implements TestExecutor {
246303
// When running browser tests, Vitest appends the browser name to the project identifier.
247304
// The project name must match this augmented name to ensure the correct project is targeted.
248305
project = `${projectName} (${browserOptions.browser.instances[0].browser})`;
306+
this.log(1, 'Adjusted project name for debugging with browser:', { project });
249307
}
250308

251309
// Filter internal entries and setup files from the include list
@@ -256,43 +314,46 @@ export class VitestExecutor implements TestExecutor {
256314
!internalEntries.some((internal) => entry.startsWith(internal)) && !setupFileSet.has(entry)
257315
);
258316
});
317+
this.log(2, 'Included test files (after filtering):', include);
259318

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-
],
319+
const vitestConfig = {
320+
config: externalConfigPath,
321+
root: workspaceRoot,
322+
project,
323+
outputFile,
324+
cache: cacheOptions.enabled ? undefined : (false as const),
325+
testNamePattern: this.options.filter,
326+
watch,
327+
...(typeof ui === 'boolean' ? { ui } : {}),
328+
...debugOptions,
329+
};
330+
const vitestServerConfig = {
331+
// Note `.vitest` is auto appended to the path.
332+
cacheDir: cacheOptions.path,
333+
server: {
334+
// Disable the actual file watcher. The boolean watch option above should still
335+
// be enabled as it controls other internal behavior related to rerunning tests.
336+
watch: null,
295337
},
296-
);
338+
plugins: [
339+
await createVitestConfigPlugin({
340+
browser: browserOptions.browser,
341+
coverage,
342+
projectName,
343+
projectSourceRoot,
344+
optimizeDepsInclude: this.externalMetadata.implicitBrowser,
345+
reporters,
346+
setupFiles: testSetupFiles,
347+
projectPlugins,
348+
include,
349+
}),
350+
],
351+
};
352+
353+
this.log(1, 'Calling startVitest with final configuration.');
354+
this.log(2, 'Vitest config:', vitestConfig);
355+
this.log(2, 'Vitest server config:', vitestServerConfig);
356+
357+
return startVitest('test', undefined, vitestConfig, vitestServerConfig);
297358
}
298359
}

0 commit comments

Comments
 (0)