mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-28 12:23:31 +01:00
Rename files
This commit is contained in:
266
ts/scripts/test-electron.node.ts
Normal file
266
ts/scripts/test-electron.node.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import path, { join } from 'node:path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { cpus, tmpdir } from 'node:os';
|
||||
import { mkdir, mkdtemp, rename, rm } from 'node:fs/promises';
|
||||
import crypto from 'node:crypto';
|
||||
import z from 'zod';
|
||||
import split2 from 'split2';
|
||||
import logSymbols from 'log-symbols';
|
||||
|
||||
import { explodePromise } from '../util/explodePromise.std.js';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.js';
|
||||
import { SECOND } from '../util/durations/index.std.js';
|
||||
import { parseUnknown } from '../util/schemas.std.js';
|
||||
|
||||
const ROOT_DIR = join(__dirname, '..', '..');
|
||||
|
||||
function getWorkerCount(): number {
|
||||
if (process.env.WORKER_COUNT) {
|
||||
return parseInt(process.env.WORKER_COUNT, 10);
|
||||
}
|
||||
if (process.env.CI) {
|
||||
return Math.min(8, cpus().length);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const WORKER_COUNT = getWorkerCount();
|
||||
|
||||
const ELECTRON = join(
|
||||
ROOT_DIR,
|
||||
'node_modules',
|
||||
'.bin',
|
||||
process.platform === 'win32' ? 'electron.cmd' : 'electron'
|
||||
);
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRIABLE_SIGNALS = ['SIGBUS'];
|
||||
|
||||
const failureSchema = z.object({
|
||||
type: z.literal('fail'),
|
||||
title: z.string().array(),
|
||||
error: z.string(),
|
||||
});
|
||||
|
||||
type Failure = z.infer<typeof failureSchema>;
|
||||
|
||||
const eventSchema = z
|
||||
.object({
|
||||
type: z.literal('pass'),
|
||||
title: z.string().array(),
|
||||
duration: z.number(),
|
||||
})
|
||||
.or(failureSchema)
|
||||
.or(
|
||||
z.object({
|
||||
type: z.literal('end'),
|
||||
})
|
||||
);
|
||||
|
||||
async function launchElectron(
|
||||
worker: number,
|
||||
attempt: number
|
||||
): Promise<{ pass: number; failures: Array<Failure> }> {
|
||||
if (attempt > MAX_RETRIES) {
|
||||
console.error(`Failed after ${MAX_RETRIES} retries, exiting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (attempt !== 1) {
|
||||
console.log(
|
||||
`Launching electron ${worker} for tests, attempt #${attempt}...`
|
||||
);
|
||||
}
|
||||
|
||||
const storagePath = await mkdtemp(join(tmpdir(), 'signal-test-'));
|
||||
|
||||
const proc = spawn(
|
||||
ELECTRON,
|
||||
[
|
||||
'ci.js',
|
||||
'--worker',
|
||||
worker.toString(),
|
||||
'--worker-count',
|
||||
WORKER_COUNT.toString(),
|
||||
...process.argv.slice(2),
|
||||
],
|
||||
{
|
||||
cwd: ROOT_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
// Setting NODE_ENV to test triggers main.ts to load
|
||||
// 'test/index.html' instead of 'background.html', which loads the tests
|
||||
// via `test.js`
|
||||
NODE_ENV: 'test',
|
||||
TEST_QUIT_ON_COMPLETE: 'on',
|
||||
SIGNAL_CI_CONFIG: JSON.stringify({
|
||||
storagePath,
|
||||
}),
|
||||
},
|
||||
// Since we run `.cmd` file on Windows - use shell
|
||||
shell: process.platform === 'win32',
|
||||
}
|
||||
);
|
||||
|
||||
const { resolve, reject, promise: exitPromise } = explodePromise<void>();
|
||||
|
||||
let exitSignal: string | undefined;
|
||||
proc.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
exitSignal = signal || undefined;
|
||||
reject(new Error(`Exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
let pass = 0;
|
||||
const failures = new Array<Failure>();
|
||||
let done = false;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
exitPromise,
|
||||
pipeline(
|
||||
proc.stdout,
|
||||
split2()
|
||||
.resume()
|
||||
.on('data', line => {
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = line.match(/^ci:test-electron:event=(.*)/);
|
||||
if (!match) {
|
||||
const debugMatch = line.match(/ci:test-electron:debug=(.*)?/);
|
||||
if (debugMatch) {
|
||||
try {
|
||||
console.log('DEBUG:', JSON.parse(debugMatch[1]));
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const event = parseUnknown(
|
||||
eventSchema,
|
||||
JSON.parse(match[1]) as unknown
|
||||
);
|
||||
if (event.type === 'pass') {
|
||||
pass += 1;
|
||||
|
||||
process.stdout.write(logSymbols.success);
|
||||
if (event.duration > SECOND) {
|
||||
console.error('');
|
||||
console.error(
|
||||
` ${logSymbols.warning} ${event.title.join(' ')} ` +
|
||||
`took ${event.duration}ms`
|
||||
);
|
||||
}
|
||||
} else if (event.type === 'fail') {
|
||||
failures.push(event);
|
||||
|
||||
console.error('');
|
||||
console.error(` ${logSymbols.error} ${event.title.join(' ')}`);
|
||||
console.error('');
|
||||
console.error(event.error);
|
||||
} else if (event.type === 'end') {
|
||||
done = true;
|
||||
} else {
|
||||
throw missingCaseError(event);
|
||||
}
|
||||
})
|
||||
),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (exitSignal && RETRIABLE_SIGNALS.includes(exitSignal)) {
|
||||
return launchElectron(worker, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
if (failures.length) {
|
||||
const artifactsDir = await makeArtifactsDir();
|
||||
if (artifactsDir) {
|
||||
await rename(
|
||||
path.join(storagePath, 'logs'),
|
||||
path.join(artifactsDir, 'logs')
|
||||
);
|
||||
console.log('\n');
|
||||
console.log(`Saving logs to ${artifactsDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
await rm(storagePath, { recursive: true });
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
throw new Error('Tests terminated early!');
|
||||
}
|
||||
|
||||
return { pass, failures };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const promises = [];
|
||||
for (let i = 0; i < WORKER_COUNT; i += 1) {
|
||||
promises.push(launchElectron(i, 1));
|
||||
}
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
let pass = 0;
|
||||
let failures = new Array<Failure>();
|
||||
for (const result of results) {
|
||||
pass += result.pass;
|
||||
failures = failures.concat(result.failures);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error('');
|
||||
console.error('Failing tests:');
|
||||
console.error('');
|
||||
for (const { title, error } of failures) {
|
||||
console.log(` ${logSymbols.error} ${title.join(' ')}`);
|
||||
console.log(error);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
`Passed ${pass} | Failed ${failures.length} | ` +
|
||||
`Total ${pass + failures.length}`
|
||||
);
|
||||
|
||||
if (failures.length !== 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function makeArtifactsDir(): Promise<string | undefined> {
|
||||
const { ARTIFACTS_DIR } = process.env;
|
||||
if (!ARTIFACTS_DIR) {
|
||||
console.log('\nTo save artifacts, please set ARTIFACTS_DIR env variable\n');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedPath = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
const outDir = path.join(ARTIFACTS_DIR, normalizedPath);
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
return outDir;
|
||||
}
|
||||
Reference in New Issue
Block a user