Parallelize test-electron

This commit is contained in:
Fedor Indutny 2024-07-22 12:27:09 -07:00 committed by GitHub
parent c64762858e
commit 9006990e58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 70 deletions

View file

@ -124,7 +124,7 @@ jobs:
linux:
needs: lint
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
steps:

View file

@ -30,6 +30,7 @@ window.textsecure.storage.protocol = window.getSignalProtocolStore();
runner.on('pass', test => window.testUtilities.onTestEvent({
type: 'pass',
title: test.titlePath(),
duration: test.duration,
}));
runner.on('fail', (test, error) => window.testUtilities.onTestEvent({
type: 'fail',

View file

@ -4,15 +4,22 @@
import { spawn } from 'node:child_process';
import { join } from 'node:path';
import { pipeline } from 'node:stream/promises';
import { cpus, tmpdir } from 'node:os';
import { mkdtemp, rm } from 'node:fs/promises';
import z from 'zod';
import split2 from 'split2';
import logSymbols from 'log-symbols';
import { explodePromise } from '../util/explodePromise';
import { missingCaseError } from '../util/missingCaseError';
import { SECOND } from '../util/durations';
const ROOT_DIR = join(__dirname, '..', '..');
const WORKER_COUNT = process.env.WORKER_COUNT
? parseInt(process.env.WORKER_COUNT, 10)
: Math.min(8, cpus().length);
const ELECTRON = join(
ROOT_DIR,
'node_modules',
@ -23,45 +30,71 @@ const ELECTRON = join(
const MAX_RETRIES = 3;
const RETRIABLE_SIGNALS = ['SIGBUS'];
const failSchema = z.object({
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(failSchema)
.or(failureSchema)
.or(
z.object({
type: z.literal('end'),
})
);
async function launchElectron(attempt: number): Promise<void> {
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);
}
console.log(`Launching electron for tests, attempt #${attempt}...`);
if (attempt !== 1) {
console.log(
`Launching electron ${worker} for tests, attempt #${attempt}...`
);
}
const proc = spawn(ELECTRON, [ROOT_DIR, ...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',
},
// Since we run `.cmd` file on Windows - use shell
shell: process.platform === 'win32',
});
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>();
@ -76,38 +109,8 @@ async function launchElectron(attempt: number): Promise<void> {
});
let pass = 0;
const failures = new Array<z.infer<typeof failSchema>>();
const failures = new Array<Failure>();
let done = false;
let stack = new Array<string>();
function enter(path: ReadonlyArray<string>): void {
// Find the first different fragment
let i: number;
for (i = 0; i < path.length - 1; i += 1) {
if (stack[i] !== path[i]) {
break;
}
}
// Separate sections
if (i !== stack.length) {
console.log('');
// Remove different fragments
stack = stack.slice(0, i);
}
for (; i < path.length - 1; i += 1) {
const fragment = path[i];
console.log(indent(fragment));
stack.push(fragment);
}
}
function indent(value: string): string {
return `${' '.repeat(stack.length)}${value}`;
}
try {
await Promise.all([
@ -137,18 +140,20 @@ async function launchElectron(attempt: number): Promise<void> {
const event = eventSchema.parse(JSON.parse(match[1]));
if (event.type === 'pass') {
pass += 1;
enter(event.title);
console.log(
indent(`${logSymbols.success} ${event.title.at(-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);
enter(event.title);
console.error(
indent(`${logSymbols.error} ${event.title.at(-1)}`)
);
console.error('');
console.error(` ${logSymbols.error} ${event.title.join(' ')}`);
console.error('');
console.error(event.error);
} else if (event.type === 'end') {
@ -161,15 +166,38 @@ async function launchElectron(attempt: number): Promise<void> {
]);
} catch (error) {
if (exitSignal && RETRIABLE_SIGNALS.includes(exitSignal)) {
return launchElectron(attempt + 1);
return launchElectron(worker, attempt + 1);
}
throw error;
} finally {
try {
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:');
@ -181,6 +209,7 @@ async function launchElectron(attempt: number): Promise<void> {
}
}
console.log('');
console.log(
`Passed ${pass} | Failed ${failures.length} | ` +
`Total ${pass + failures.length}`
@ -191,10 +220,6 @@ async function launchElectron(attempt: number): Promise<void> {
}
}
async function main() {
await launchElectron(1);
}
main().catch(error => {
console.error(error);
process.exit(1);

View file

@ -3,8 +3,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import chai, { assert } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { assert } from 'chai';
import { clone } from 'lodash';
import {
Direction,
@ -38,8 +37,6 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
import { generateAci, generatePni } from '../types/ServiceId';
import type { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
chai.use(chaiAsPromised);
const {
RecordStructure,
SessionStructure,

View file

@ -12,6 +12,12 @@ import { MessageCache } from '../../services/MessageCache';
import { generateAci } from '../../types/ServiceId';
describe('MessageCache', () => {
beforeEach(async () => {
const ourAci = generateAci();
await window.textsecure.storage.put('uuid_id', `${ourAci}.1`);
await window.ConversationController.load();
});
describe('filterBySentAt', () => {
it('returns an empty iterable if no messages match', () => {
const mc = new MessageCache();

View file

@ -29,6 +29,7 @@ const {
describe('sql/sendLog', () => {
beforeEach(async () => {
await removeAllSentProtos();
await window.ConversationController.load();
});
it('roundtrips with insertSentProto/getAllSentProtos', async () => {

View file

@ -117,7 +117,9 @@ describe('both/state/ducks/conversations', () => {
let sinonSandbox: sinon.SinonSandbox;
let createGroupStub: sinon.SinonStub;
beforeEach(() => {
beforeEach(async () => {
await window.ConversationController.load();
sinonSandbox = sinon.createSandbox();
sinonSandbox.stub(window.Whisper.events, 'trigger');

View file

@ -8,7 +8,9 @@ import { ipcRenderer as ipc } from 'electron';
import { sync } from 'fast-glob';
// eslint-disable-next-line import/no-extraneous-dependencies
import { assert, config as chaiConfig } from 'chai';
import chai, { assert, config as chaiConfig } from 'chai';
// eslint-disable-next-line import/no-extraneous-dependencies
import chaiAsPromised from 'chai-as-promised';
// eslint-disable-next-line import/no-extraneous-dependencies
import { reporters, type MochaOptions } from 'mocha';
@ -19,6 +21,8 @@ import { initializeRedux } from '../../state/initializeRedux';
import * as Stickers from '../../types/Stickers';
import { ThemeType } from '../../types/Util';
chai.use(chaiAsPromised);
// Show actual objects instead of abbreviated errors
chaiConfig.truncateThreshold = 0;
@ -47,6 +51,8 @@ window.assert = assert;
// code in `test/test.js`.
const setup: MochaOptions = {};
let worker = 0;
let workerCount = 1;
{
const { values } = parseArgs({
@ -55,6 +61,12 @@ const setup: MochaOptions = {};
grep: {
type: 'string',
},
worker: {
type: 'string',
},
'worker-count': {
type: 'string',
},
},
strict: false,
});
@ -62,6 +74,12 @@ const setup: MochaOptions = {};
if (typeof values.grep === 'string') {
setup.grep = values.grep;
}
if (typeof values.worker === 'string') {
worker = parseInt(values.worker, 10);
}
if (typeof values['worker-count'] === 'string') {
workerCount = parseInt(values['worker-count'], 10);
}
}
window.testUtilities = {
@ -104,10 +122,17 @@ window.testUtilities = {
prepareTests() {
console.log('Preparing tests...');
sync('../../test-{both,electron}/**/*_test.js', {
const files = sync('../../test-{both,electron}/**/*_test.js', {
absolute: true,
cwd: __dirname,
}).forEach(require);
});
for (let i = 0; i < files.length; i += 1) {
if (i % workerCount === worker) {
// eslint-disable-next-line import/no-dynamic-require, global-require
require(files[i]);
}
}
},
};