Improve desktop runtime execution tests
Test improvements: - Capture titles for all macOS windows, not just the frontmost. - Incorporate missing application log files. - Improve log clarity with enriched context. - Improve application termination on macOS by reducing grace period. - Ensure complete application termination on macOS. - Validate Vue application loading through an initial log. - Support ignoring environment-specific `stderr` errors. - Do not fail the test if working directory cannot be deleted. - Use retry pattern when installing dependencies due to network errors. Refactorings: - Migrate the test code to TypeScript. - Replace deprecated `rmdir` with `rm` for error-resistant directory removal. - Improve sanity checking by shifting from App.vue to Vue bootstrapper. - Centralize environment variable management with `EnvironmentVariables` construct. - Rename infrastructure/Environment to RuntimeEnvironment for clarity. - Isolate WindowVariables and SystemOperations from RuntimeEnvironment. - Inject logging via preloader. - Correct mislabeled RuntimeSanity tests. Configuration: - Introduce `npm run check:desktop` for simplified execution. - Omit `console.log` override due to `nodeIntegration` restrictions and reveal logging functionality using context-bridging.
This commit is contained in:
181
scripts/check-desktop-runtime-errors/app/check-for-errors.ts
Normal file
181
scripts/check-desktop-runtime-errors/app/check-for-errors.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { splitTextIntoLines, indentText } from '../utils/text';
|
||||
import { log, die } from '../utils/log';
|
||||
import { readAppLogFile } from './app-logs';
|
||||
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
||||
|
||||
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
||||
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
||||
const EXPECTED_LOG_MARKERS = [
|
||||
'[WINDOW_INIT]',
|
||||
'[PRELOAD_INIT]',
|
||||
'[APP_INIT]',
|
||||
];
|
||||
|
||||
type ProcessType = 'main' | 'renderer';
|
||||
|
||||
export async function checkForErrors(
|
||||
stderr: string,
|
||||
windowTitles: readonly string[],
|
||||
projectDir: string,
|
||||
) {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
||||
if (errors.length) {
|
||||
die(formatErrors(errors));
|
||||
}
|
||||
}
|
||||
|
||||
async function gatherErrors(
|
||||
stderr: string,
|
||||
windowTitles: readonly string[],
|
||||
projectDir: string,
|
||||
): Promise<ExecutionError[]> {
|
||||
if (!projectDir) { throw new Error('missing project directory'); }
|
||||
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir, 'main');
|
||||
const { logFileContent: rendererLogs, logFilePath: rendererLogFile } = await readAppLogFile(projectDir, 'renderer');
|
||||
const allLogs = [mainLogs, rendererLogs, stderr].filter(Boolean).join('\n');
|
||||
return [
|
||||
verifyStdErr(stderr),
|
||||
verifyApplicationLogsExist('main', mainLogs, mainLogFile),
|
||||
verifyApplicationLogsExist('renderer', rendererLogs, rendererLogFile),
|
||||
...EXPECTED_LOG_MARKERS.map(
|
||||
(marker) => verifyLogMarkerExistsInLogs(allLogs, marker),
|
||||
),
|
||||
verifyWindowTitle(windowTitles),
|
||||
verifyErrorsInLogs(allLogs),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
interface ExecutionError {
|
||||
readonly reason: string;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
function formatErrors(errors: readonly ExecutionError[]): string {
|
||||
if (!errors?.length) { throw new Error('missing errors'); }
|
||||
return [
|
||||
'Errors detected during execution:',
|
||||
...errors.map(
|
||||
(error) => formatError(error),
|
||||
),
|
||||
].join('\n---\n');
|
||||
}
|
||||
|
||||
function formatError(error: ExecutionError): string {
|
||||
if (!error) { throw new Error('missing error'); }
|
||||
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
||||
let message = `Reason: ${indentText(error.reason, 1)}`;
|
||||
if (error.description) {
|
||||
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function verifyApplicationLogsExist(
|
||||
processType: ProcessType,
|
||||
logContent: string | undefined,
|
||||
logFilePath: string,
|
||||
): ExecutionError | undefined {
|
||||
if (!logContent?.length) {
|
||||
return describeError(
|
||||
`Missing application (${processType}) logs`,
|
||||
'Application logs are empty not were not found.'
|
||||
+ `\nLog path: ${logFilePath}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyLogMarkerExistsInLogs(
|
||||
logContent: string | undefined,
|
||||
marker: string,
|
||||
) : ExecutionError | undefined {
|
||||
if (!marker) {
|
||||
throw new Error('missing marker');
|
||||
}
|
||||
if (!logContent?.includes(marker)) {
|
||||
return describeError(
|
||||
'Incomplete application logs',
|
||||
`Missing identifier "${marker}" in application logs.`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyWindowTitle(
|
||||
windowTitles: readonly string[],
|
||||
) : ExecutionError | undefined {
|
||||
const errorTitles = windowTitles.filter(
|
||||
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
||||
);
|
||||
if (errorTitles.length) {
|
||||
return describeError(
|
||||
'Unexpected window title',
|
||||
'One or more window titles suggest an error occurred in the application:'
|
||||
+ `\nError Titles: ${errorTitles.join(', ')}`
|
||||
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyStdErr(
|
||||
stderrOutput: string | undefined,
|
||||
) : ExecutionError | undefined {
|
||||
if (stderrOutput && stderrOutput.length > 0) {
|
||||
const ignoredErrorLines = new Set();
|
||||
const relevantErrors = getNonEmptyLines(stderrOutput)
|
||||
.filter((line) => {
|
||||
line = line.trim();
|
||||
if (STDERR_IGNORE_PATTERNS.some((pattern) => pattern.test(line))) {
|
||||
ignoredErrorLines.add(line);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (ignoredErrorLines.size > 0) {
|
||||
log(`Ignoring \`stderr\` lines:\n${indentText([...ignoredErrorLines].join('\n'), 1)}`);
|
||||
}
|
||||
if (relevantErrors.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return describeError(
|
||||
'Standard error stream (`stderr`) is not empty.',
|
||||
`Relevant errors (${relevantErrors.length}):\n${indentText(relevantErrors.map((error) => `- ${error}`).join('\n'), 1)}`
|
||||
+ `\nFull \`stderr\` output:\n${indentText(stderrOutput, 1)}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function verifyErrorsInLogs(
|
||||
logContent: string | undefined,
|
||||
) : ExecutionError | undefined {
|
||||
if (!logContent?.length) {
|
||||
return undefined;
|
||||
}
|
||||
const logLines = getNonEmptyLines(logContent)
|
||||
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
||||
if (!logLines.length) {
|
||||
return undefined;
|
||||
}
|
||||
return describeError(
|
||||
'Application log file',
|
||||
logLines.join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
function describeError(
|
||||
reason: string,
|
||||
description: string,
|
||||
) : ExecutionError | undefined {
|
||||
return {
|
||||
reason,
|
||||
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
||||
};
|
||||
}
|
||||
|
||||
function getNonEmptyLines(text: string) {
|
||||
return splitTextIntoLines(text)
|
||||
.filter((line) => line?.trim().length > 0);
|
||||
}
|
||||
Reference in New Issue
Block a user