Parallelize test-electron
This commit is contained in:
parent
c64762858e
commit
9006990e58
8 changed files with 127 additions and 70 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -124,7 +124,7 @@ jobs:
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
needs: lint
|
needs: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest-8-cores
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -30,6 +30,7 @@ window.textsecure.storage.protocol = window.getSignalProtocolStore();
|
||||||
runner.on('pass', test => window.testUtilities.onTestEvent({
|
runner.on('pass', test => window.testUtilities.onTestEvent({
|
||||||
type: 'pass',
|
type: 'pass',
|
||||||
title: test.titlePath(),
|
title: test.titlePath(),
|
||||||
|
duration: test.duration,
|
||||||
}));
|
}));
|
||||||
runner.on('fail', (test, error) => window.testUtilities.onTestEvent({
|
runner.on('fail', (test, error) => window.testUtilities.onTestEvent({
|
||||||
type: 'fail',
|
type: 'fail',
|
||||||
|
|
|
@ -4,15 +4,22 @@
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import { cpus, tmpdir } from 'node:os';
|
||||||
|
import { mkdtemp, rm } from 'node:fs/promises';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import split2 from 'split2';
|
import split2 from 'split2';
|
||||||
import logSymbols from 'log-symbols';
|
import logSymbols from 'log-symbols';
|
||||||
|
|
||||||
import { explodePromise } from '../util/explodePromise';
|
import { explodePromise } from '../util/explodePromise';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import { SECOND } from '../util/durations';
|
||||||
|
|
||||||
const ROOT_DIR = join(__dirname, '..', '..');
|
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(
|
const ELECTRON = join(
|
||||||
ROOT_DIR,
|
ROOT_DIR,
|
||||||
'node_modules',
|
'node_modules',
|
||||||
|
@ -23,45 +30,71 @@ const ELECTRON = join(
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const RETRIABLE_SIGNALS = ['SIGBUS'];
|
const RETRIABLE_SIGNALS = ['SIGBUS'];
|
||||||
|
|
||||||
const failSchema = z.object({
|
const failureSchema = z.object({
|
||||||
type: z.literal('fail'),
|
type: z.literal('fail'),
|
||||||
title: z.string().array(),
|
title: z.string().array(),
|
||||||
error: z.string(),
|
error: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type Failure = z.infer<typeof failureSchema>;
|
||||||
|
|
||||||
const eventSchema = z
|
const eventSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal('pass'),
|
type: z.literal('pass'),
|
||||||
title: z.string().array(),
|
title: z.string().array(),
|
||||||
|
duration: z.number(),
|
||||||
})
|
})
|
||||||
.or(failSchema)
|
.or(failureSchema)
|
||||||
.or(
|
.or(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal('end'),
|
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) {
|
if (attempt > MAX_RETRIES) {
|
||||||
console.error(`Failed after ${MAX_RETRIES} retries, exiting.`);
|
console.error(`Failed after ${MAX_RETRIES} retries, exiting.`);
|
||||||
process.exit(1);
|
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)], {
|
const storagePath = await mkdtemp(join(tmpdir(), 'signal-test-'));
|
||||||
cwd: ROOT_DIR,
|
|
||||||
env: {
|
const proc = spawn(
|
||||||
...process.env,
|
ELECTRON,
|
||||||
// Setting NODE_ENV to test triggers main.ts to load
|
[
|
||||||
// 'test/index.html' instead of 'background.html', which loads the tests
|
'ci.js',
|
||||||
// via `test.js`
|
'--worker',
|
||||||
NODE_ENV: 'test',
|
worker.toString(),
|
||||||
TEST_QUIT_ON_COMPLETE: 'on',
|
'--worker-count',
|
||||||
},
|
WORKER_COUNT.toString(),
|
||||||
// Since we run `.cmd` file on Windows - use shell
|
...process.argv.slice(2),
|
||||||
shell: process.platform === 'win32',
|
],
|
||||||
});
|
{
|
||||||
|
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>();
|
const { resolve, reject, promise: exitPromise } = explodePromise<void>();
|
||||||
|
|
||||||
|
@ -76,38 +109,8 @@ async function launchElectron(attempt: number): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let pass = 0;
|
let pass = 0;
|
||||||
const failures = new Array<z.infer<typeof failSchema>>();
|
const failures = new Array<Failure>();
|
||||||
let done = false;
|
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 {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -137,18 +140,20 @@ async function launchElectron(attempt: number): Promise<void> {
|
||||||
const event = eventSchema.parse(JSON.parse(match[1]));
|
const event = eventSchema.parse(JSON.parse(match[1]));
|
||||||
if (event.type === 'pass') {
|
if (event.type === 'pass') {
|
||||||
pass += 1;
|
pass += 1;
|
||||||
enter(event.title);
|
|
||||||
|
|
||||||
console.log(
|
process.stdout.write(logSymbols.success);
|
||||||
indent(`${logSymbols.success} ${event.title.at(-1)}`)
|
if (event.duration > SECOND) {
|
||||||
);
|
console.error('');
|
||||||
|
console.error(
|
||||||
|
` ${logSymbols.warning} ${event.title.join(' ')} ` +
|
||||||
|
`took ${event.duration}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (event.type === 'fail') {
|
} else if (event.type === 'fail') {
|
||||||
failures.push(event);
|
failures.push(event);
|
||||||
enter(event.title);
|
|
||||||
|
|
||||||
console.error(
|
console.error('');
|
||||||
indent(`${logSymbols.error} ${event.title.at(-1)}`)
|
console.error(` ${logSymbols.error} ${event.title.join(' ')}`);
|
||||||
);
|
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(event.error);
|
console.error(event.error);
|
||||||
} else if (event.type === 'end') {
|
} else if (event.type === 'end') {
|
||||||
|
@ -161,15 +166,38 @@ async function launchElectron(attempt: number): Promise<void> {
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (exitSignal && RETRIABLE_SIGNALS.includes(exitSignal)) {
|
if (exitSignal && RETRIABLE_SIGNALS.includes(exitSignal)) {
|
||||||
return launchElectron(attempt + 1);
|
return launchElectron(worker, attempt + 1);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await rm(storagePath, { recursive: true });
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
throw new Error('Tests terminated early!');
|
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) {
|
if (failures.length) {
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Failing tests:');
|
console.error('Failing tests:');
|
||||||
|
@ -181,6 +209,7 @@ async function launchElectron(attempt: number): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
console.log(
|
console.log(
|
||||||
`Passed ${pass} | Failed ${failures.length} | ` +
|
`Passed ${pass} | Failed ${failures.length} | ` +
|
||||||
`Total ${pass + 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 => {
|
main().catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import chai, { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import chaiAsPromised from 'chai-as-promised';
|
|
||||||
import { clone } from 'lodash';
|
import { clone } from 'lodash';
|
||||||
import {
|
import {
|
||||||
Direction,
|
Direction,
|
||||||
|
@ -38,8 +37,6 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||||
import { generateAci, generatePni } from '../types/ServiceId';
|
import { generateAci, generatePni } from '../types/ServiceId';
|
||||||
import type { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
|
import type { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
RecordStructure,
|
RecordStructure,
|
||||||
SessionStructure,
|
SessionStructure,
|
||||||
|
|
|
@ -12,6 +12,12 @@ import { MessageCache } from '../../services/MessageCache';
|
||||||
import { generateAci } from '../../types/ServiceId';
|
import { generateAci } from '../../types/ServiceId';
|
||||||
|
|
||||||
describe('MessageCache', () => {
|
describe('MessageCache', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const ourAci = generateAci();
|
||||||
|
await window.textsecure.storage.put('uuid_id', `${ourAci}.1`);
|
||||||
|
await window.ConversationController.load();
|
||||||
|
});
|
||||||
|
|
||||||
describe('filterBySentAt', () => {
|
describe('filterBySentAt', () => {
|
||||||
it('returns an empty iterable if no messages match', () => {
|
it('returns an empty iterable if no messages match', () => {
|
||||||
const mc = new MessageCache();
|
const mc = new MessageCache();
|
||||||
|
|
|
@ -29,6 +29,7 @@ const {
|
||||||
describe('sql/sendLog', () => {
|
describe('sql/sendLog', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await removeAllSentProtos();
|
await removeAllSentProtos();
|
||||||
|
await window.ConversationController.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('roundtrips with insertSentProto/getAllSentProtos', async () => {
|
it('roundtrips with insertSentProto/getAllSentProtos', async () => {
|
||||||
|
|
|
@ -117,7 +117,9 @@ describe('both/state/ducks/conversations', () => {
|
||||||
let sinonSandbox: sinon.SinonSandbox;
|
let sinonSandbox: sinon.SinonSandbox;
|
||||||
let createGroupStub: sinon.SinonStub;
|
let createGroupStub: sinon.SinonStub;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
|
await window.ConversationController.load();
|
||||||
|
|
||||||
sinonSandbox = sinon.createSandbox();
|
sinonSandbox = sinon.createSandbox();
|
||||||
|
|
||||||
sinonSandbox.stub(window.Whisper.events, 'trigger');
|
sinonSandbox.stub(window.Whisper.events, 'trigger');
|
||||||
|
|
|
@ -8,7 +8,9 @@ import { ipcRenderer as ipc } from 'electron';
|
||||||
import { sync } from 'fast-glob';
|
import { sync } from 'fast-glob';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// 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
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { reporters, type MochaOptions } from 'mocha';
|
import { reporters, type MochaOptions } from 'mocha';
|
||||||
|
|
||||||
|
@ -19,6 +21,8 @@ import { initializeRedux } from '../../state/initializeRedux';
|
||||||
import * as Stickers from '../../types/Stickers';
|
import * as Stickers from '../../types/Stickers';
|
||||||
import { ThemeType } from '../../types/Util';
|
import { ThemeType } from '../../types/Util';
|
||||||
|
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
// Show actual objects instead of abbreviated errors
|
// Show actual objects instead of abbreviated errors
|
||||||
chaiConfig.truncateThreshold = 0;
|
chaiConfig.truncateThreshold = 0;
|
||||||
|
|
||||||
|
@ -47,6 +51,8 @@ window.assert = assert;
|
||||||
// code in `test/test.js`.
|
// code in `test/test.js`.
|
||||||
|
|
||||||
const setup: MochaOptions = {};
|
const setup: MochaOptions = {};
|
||||||
|
let worker = 0;
|
||||||
|
let workerCount = 1;
|
||||||
|
|
||||||
{
|
{
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
|
@ -55,6 +61,12 @@ const setup: MochaOptions = {};
|
||||||
grep: {
|
grep: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
worker: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
'worker-count': {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
strict: false,
|
strict: false,
|
||||||
});
|
});
|
||||||
|
@ -62,6 +74,12 @@ const setup: MochaOptions = {};
|
||||||
if (typeof values.grep === 'string') {
|
if (typeof values.grep === 'string') {
|
||||||
setup.grep = values.grep;
|
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 = {
|
window.testUtilities = {
|
||||||
|
@ -104,10 +122,17 @@ window.testUtilities = {
|
||||||
|
|
||||||
prepareTests() {
|
prepareTests() {
|
||||||
console.log('Preparing tests...');
|
console.log('Preparing tests...');
|
||||||
sync('../../test-{both,electron}/**/*_test.js', {
|
const files = sync('../../test-{both,electron}/**/*_test.js', {
|
||||||
absolute: true,
|
absolute: true,
|
||||||
cwd: __dirname,
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue