fix(test runner): keep track of remaining tests on the runner side (#7486)
This fixes two issues: - Sudden worker process exit is properly accounted for. - We can stop() workers willy-nilly, e.g. after reaching maxFailures. Details: - DonePayload does not send `reamining` anymore, and worker does not track it. - Instead, `Dispatcher._runJob` track remaining tests and acts accordingly. - Upon worker exit, we emulate a fatal error for all remaining tests. Drive-by: - Do not report onTestBegin after reaching maxFailures to avoid confusion. Before, we did report onTestBegin, but not onTestEnd. - List reporter aligned between "running" and "finished" state - it was one character misplaced. - Added a SIGINT test.
This commit is contained in:
parent
98bcf26656
commit
2073193c36
|
|
@ -149,34 +149,58 @@ export class Dispatcher {
|
||||||
worker.run(entry.runPayload);
|
worker.run(entry.runPayload);
|
||||||
let doneCallback = () => {};
|
let doneCallback = () => {};
|
||||||
const result = new Promise<void>(f => doneCallback = f);
|
const result = new Promise<void>(f => doneCallback = f);
|
||||||
worker.once('done', (params: DonePayload) => {
|
const doneWithJob = () => {
|
||||||
|
worker.removeListener('testBegin', onTestBegin);
|
||||||
|
worker.removeListener('testEnd', onTestEnd);
|
||||||
|
worker.removeListener('done', onDone);
|
||||||
|
worker.removeListener('exit', onExit);
|
||||||
|
doneCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const remainingByTestId = new Map(entry.runPayload.entries.map(e => [ e.testId, e ]));
|
||||||
|
let lastStartedTestId: string | undefined;
|
||||||
|
|
||||||
|
const onTestBegin = (params: TestBeginPayload) => {
|
||||||
|
lastStartedTestId = params.testId;
|
||||||
|
};
|
||||||
|
worker.addListener('testBegin', onTestBegin);
|
||||||
|
|
||||||
|
const onTestEnd = (params: TestEndPayload) => {
|
||||||
|
remainingByTestId.delete(params.testId);
|
||||||
|
};
|
||||||
|
worker.addListener('testEnd', onTestEnd);
|
||||||
|
|
||||||
|
const onDone = (params: DonePayload) => {
|
||||||
|
let remaining = [...remainingByTestId.values()];
|
||||||
|
|
||||||
// We won't file remaining if:
|
// We won't file remaining if:
|
||||||
// - there are no remaining
|
// - there are no remaining
|
||||||
// - we are here not because something failed
|
// - we are here not because something failed
|
||||||
// - no unrecoverable worker error
|
// - no unrecoverable worker error
|
||||||
if (!params.remaining.length && !params.failedTestId && !params.fatalError) {
|
if (!remaining.length && !params.failedTestId && !params.fatalError) {
|
||||||
this._freeWorkers.push(worker);
|
this._freeWorkers.push(worker);
|
||||||
this._notifyWorkerClaimer();
|
this._notifyWorkerClaimer();
|
||||||
doneCallback();
|
doneWithJob();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When worker encounters error, we will stop it and create a new one.
|
// When worker encounters error, we will stop it and create a new one.
|
||||||
worker.stop();
|
worker.stop();
|
||||||
|
|
||||||
let remaining = params.remaining;
|
|
||||||
const failedTestIds = new Set<string>();
|
const failedTestIds = new Set<string>();
|
||||||
|
|
||||||
// In case of fatal error, report all remaining tests as failing with this error.
|
// In case of fatal error, report all remaining tests as failing with this error.
|
||||||
if (params.fatalError) {
|
if (params.fatalError) {
|
||||||
for (const { testId } of remaining) {
|
for (const { testId } of remaining) {
|
||||||
const { test, result } = this._testById.get(testId)!;
|
const { test, result } = this._testById.get(testId)!;
|
||||||
this._reporter.onTestBegin(test);
|
// There might be a single test that has started but has not finished yet.
|
||||||
|
if (testId !== lastStartedTestId)
|
||||||
|
this._reportTestBegin(test);
|
||||||
result.error = params.fatalError;
|
result.error = params.fatalError;
|
||||||
this._reportTestEnd(test, result, 'failed');
|
this._reportTestEnd(test, result, 'failed');
|
||||||
failedTestIds.add(testId);
|
failedTestIds.add(testId);
|
||||||
}
|
}
|
||||||
// Since we pretent that all remaining tests failed, there is nothing else to run,
|
// Since we pretend that all remaining tests failed, there is nothing else to run,
|
||||||
// except for possible retries.
|
// except for possible retries.
|
||||||
remaining = [];
|
remaining = [];
|
||||||
}
|
}
|
||||||
|
|
@ -199,8 +223,15 @@ export class Dispatcher {
|
||||||
this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } });
|
this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } });
|
||||||
|
|
||||||
// This job is over, we just scheduled another one.
|
// This job is over, we just scheduled another one.
|
||||||
doneCallback();
|
doneWithJob();
|
||||||
});
|
};
|
||||||
|
worker.on('done', onDone);
|
||||||
|
|
||||||
|
const onExit = () => {
|
||||||
|
onDone({ fatalError: { value: 'Worker process exited unexpectedly' } });
|
||||||
|
};
|
||||||
|
worker.on('exit', onExit);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +269,7 @@ export class Dispatcher {
|
||||||
worker.on('testBegin', (params: TestBeginPayload) => {
|
worker.on('testBegin', (params: TestBeginPayload) => {
|
||||||
const { test, result: testRun } = this._testById.get(params.testId)!;
|
const { test, result: testRun } = this._testById.get(params.testId)!;
|
||||||
testRun.workerIndex = params.workerIndex;
|
testRun.workerIndex = params.workerIndex;
|
||||||
this._reporter.onTestBegin(test);
|
this._reportTestBegin(test);
|
||||||
});
|
});
|
||||||
worker.on('testEnd', (params: TestEndPayload) => {
|
worker.on('testEnd', (params: TestEndPayload) => {
|
||||||
const { test, result } = this._testById.get(params.testId)!;
|
const { test, result } = this._testById.get(params.testId)!;
|
||||||
|
|
@ -289,6 +320,14 @@ export class Dispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _reportTestBegin(test: Test) {
|
||||||
|
if (this._isStopped)
|
||||||
|
return;
|
||||||
|
const maxFailures = this._loader.fullConfig().maxFailures;
|
||||||
|
if (!maxFailures || this._failureCount < maxFailures)
|
||||||
|
this._reporter.onTestBegin(test);
|
||||||
|
}
|
||||||
|
|
||||||
private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) {
|
private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) {
|
||||||
if (this._isStopped)
|
if (this._isStopped)
|
||||||
return;
|
return;
|
||||||
|
|
@ -299,7 +338,7 @@ export class Dispatcher {
|
||||||
if (!maxFailures || this._failureCount <= maxFailures)
|
if (!maxFailures || this._failureCount <= maxFailures)
|
||||||
this._reporter.onTestEnd(test, result);
|
this._reporter.onTestEnd(test, result);
|
||||||
if (maxFailures && this._failureCount === maxFailures)
|
if (maxFailures && this._failureCount === maxFailures)
|
||||||
this._isStopped = true;
|
this.stop().catch(e => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasWorkerErrors(): boolean {
|
hasWorkerErrors(): boolean {
|
||||||
|
|
@ -314,6 +353,7 @@ class Worker extends EventEmitter {
|
||||||
runner: Dispatcher;
|
runner: Dispatcher;
|
||||||
hash = '';
|
hash = '';
|
||||||
index: number;
|
index: number;
|
||||||
|
private didSendStop = false;
|
||||||
|
|
||||||
constructor(runner: Dispatcher) {
|
constructor(runner: Dispatcher) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -356,7 +396,9 @@ class Worker extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.process.send({ method: 'stop' });
|
if (!this.didSendStop)
|
||||||
|
this.process.send({ method: 'stop' });
|
||||||
|
this.didSendStop = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,7 @@ export type RunPayload = {
|
||||||
|
|
||||||
export type DonePayload = {
|
export type DonePayload = {
|
||||||
failedTestId?: string;
|
failedTestId?: string;
|
||||||
fatalError?: any;
|
fatalError?: TestError;
|
||||||
remaining: TestEntry[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestOutputPayload = {
|
export type TestOutputPayload = {
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ function indent(lines: string, tab: string) {
|
||||||
return lines.replace(/^(?=.+$)/gm, tab);
|
return lines.replace(/^(?=.+$)/gm, tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionInFile(stack: string, file: string): { column: number; line: number; } {
|
function positionInFile(stack: string, file: string): { column: number; line: number; } | undefined {
|
||||||
// Stack will have /private/var/folders instead of /var/folders on Mac.
|
// Stack will have /private/var/folders instead of /var/folders on Mac.
|
||||||
file = fs.realpathSync(file);
|
file = fs.realpathSync(file);
|
||||||
for (const line of stack.split('\n')) {
|
for (const line of stack.split('\n')) {
|
||||||
|
|
@ -236,7 +236,6 @@ function positionInFile(stack: string, file: string): { column: number; line: nu
|
||||||
if (path.resolve(process.cwd(), parsed.file) === file)
|
if (path.resolve(process.cwd(), parsed.file) === file)
|
||||||
return {column: parsed.column || 0, line: parsed.line || 0};
|
return {column: parsed.column || 0, line: parsed.line || 0};
|
||||||
}
|
}
|
||||||
return { column: 0, line: 0 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function monotonicTime(): number {
|
function monotonicTime(): number {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ListReporter extends BaseReporter {
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
this._lastRow++;
|
this._lastRow++;
|
||||||
}
|
}
|
||||||
process.stdout.write(' ' + colors.gray(formatTestTitle(this.config, test) + ': ') + '\n');
|
process.stdout.write(' ' + colors.gray(formatTestTitle(this.config, test) + ': ') + '\n');
|
||||||
}
|
}
|
||||||
this._testRows.set(test, this._lastRow++);
|
this._testRows.set(test, this._lastRow++);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { TestBeginPayload, TestEndPayload, RunPayload, TestEntry, DonePayload, W
|
||||||
import { setCurrentTestInfo } from './globals';
|
import { setCurrentTestInfo } from './globals';
|
||||||
import { Loader } from './loader';
|
import { Loader } from './loader';
|
||||||
import { Modifier, Spec, Suite, Test } from './test';
|
import { Modifier, Spec, Suite, Test } from './test';
|
||||||
import { Annotations, TestInfo, WorkerInfo } from './types';
|
import { Annotations, TestError, TestInfo, WorkerInfo } from './types';
|
||||||
import { ProjectImpl } from './project';
|
import { ProjectImpl } from './project';
|
||||||
import { FixtureRunner } from './fixtures';
|
import { FixtureRunner } from './fixtures';
|
||||||
|
|
||||||
|
|
@ -40,9 +40,8 @@ export class WorkerRunner extends EventEmitter {
|
||||||
private _fixtureRunner: FixtureRunner;
|
private _fixtureRunner: FixtureRunner;
|
||||||
|
|
||||||
private _failedTestId: string | undefined;
|
private _failedTestId: string | undefined;
|
||||||
private _fatalError: any | undefined;
|
private _fatalError: TestError | undefined;
|
||||||
private _entries = new Map<string, TestEntry>();
|
private _entries = new Map<string, TestEntry>();
|
||||||
private _remaining = new Map<string, TestEntry>();
|
|
||||||
private _isStopped: any;
|
private _isStopped: any;
|
||||||
_currentTest: { testId: string, testInfo: TestInfo } | null = null;
|
_currentTest: { testId: string, testInfo: TestInfo } | null = null;
|
||||||
|
|
||||||
|
|
@ -58,6 +57,8 @@ export class WorkerRunner extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
|
// We have to load the project to get the right deadline below.
|
||||||
|
this._loadIfNeeded();
|
||||||
// TODO: separate timeout for teardown?
|
// TODO: separate timeout for teardown?
|
||||||
const result = await raceAgainstDeadline((async () => {
|
const result = await raceAgainstDeadline((async () => {
|
||||||
await this._fixtureRunner.teardownScope('test');
|
await this._fixtureRunner.teardownScope('test');
|
||||||
|
|
@ -111,7 +112,6 @@ export class WorkerRunner extends EventEmitter {
|
||||||
|
|
||||||
async run(runPayload: RunPayload) {
|
async run(runPayload: RunPayload) {
|
||||||
this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ]));
|
this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ]));
|
||||||
this._remaining = new Map(runPayload.entries.map(e => [ e.testId, e ]));
|
|
||||||
|
|
||||||
this._loadIfNeeded();
|
this._loadIfNeeded();
|
||||||
const fileSuite = await this._loader.loadTestFile(runPayload.file);
|
const fileSuite = await this._loader.loadTestFile(runPayload.file);
|
||||||
|
|
@ -194,7 +194,6 @@ export class WorkerRunner extends EventEmitter {
|
||||||
const entry = this._entries.get(test._id);
|
const entry = this._entries.get(test._id);
|
||||||
if (!entry)
|
if (!entry)
|
||||||
return;
|
return;
|
||||||
this._remaining.delete(test._id);
|
|
||||||
|
|
||||||
const startTime = monotonicTime();
|
const startTime = monotonicTime();
|
||||||
let deadlineRunner: DeadlineRunner<any> | undefined;
|
let deadlineRunner: DeadlineRunner<any> | undefined;
|
||||||
|
|
@ -448,7 +447,6 @@ export class WorkerRunner extends EventEmitter {
|
||||||
const donePayload: DonePayload = {
|
const donePayload: DonePayload = {
|
||||||
failedTestId: this._failedTestId,
|
failedTestId: this._failedTestId,
|
||||||
fatalError: this._fatalError,
|
fatalError: this._fatalError,
|
||||||
remaining: [...this._remaining.values()],
|
|
||||||
};
|
};
|
||||||
this.emit('done', donePayload);
|
this.emit('done', donePayload);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,36 @@ test('max-failures should work with retries', async ({ runInlineTest }) => {
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.output.split('\n').filter(l => l.includes('Received:')).length).toBe(2);
|
expect(result.output.split('\n').filter(l => l.includes('Received:')).length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('max-failures should stop workers', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('passed', async () => {
|
||||||
|
await new Promise(f => setTimeout(f, 500));
|
||||||
|
});
|
||||||
|
test('failed', async () => {
|
||||||
|
test.expect(1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'b.spec.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('passed short', async () => {
|
||||||
|
await new Promise(f => setTimeout(f, 1));
|
||||||
|
});
|
||||||
|
test('interrupted counts as skipped', async () => {
|
||||||
|
console.log('\\n%%interrupted');
|
||||||
|
await new Promise(f => setTimeout(f, 2000));
|
||||||
|
});
|
||||||
|
test('skipped', async () => {
|
||||||
|
console.log('\\n%%skipped');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { 'max-failures': 1, 'workers': 2 });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.skipped).toBe(2);
|
||||||
|
expect(result.output).toContain('%%interrupted');
|
||||||
|
expect(result.output).not.toContain('%%skipped');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ async function runTSC(baseDir: string): Promise<TSCResult> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPlaywrightTest(baseDir: string, params: any, env: Env): Promise<RunResult> {
|
async function runPlaywrightTest(baseDir: string, params: any, env: Env, options: RunOptions): Promise<RunResult> {
|
||||||
const paramList = [];
|
const paramList = [];
|
||||||
let additionalArgs = '';
|
let additionalArgs = '';
|
||||||
for (const key of Object.keys(params)) {
|
for (const key of Object.keys(params)) {
|
||||||
|
|
@ -151,6 +151,7 @@ async function runPlaywrightTest(baseDir: string, params: any, env: Env): Promis
|
||||||
cwd: baseDir
|
cwd: baseDir
|
||||||
});
|
});
|
||||||
let output = '';
|
let output = '';
|
||||||
|
let didSendSigint = false;
|
||||||
testProcess.stderr.on('data', chunk => {
|
testProcess.stderr.on('data', chunk => {
|
||||||
output += String(chunk);
|
output += String(chunk);
|
||||||
if (process.env.PW_RUNNER_DEBUG)
|
if (process.env.PW_RUNNER_DEBUG)
|
||||||
|
|
@ -158,6 +159,10 @@ async function runPlaywrightTest(baseDir: string, params: any, env: Env): Promis
|
||||||
});
|
});
|
||||||
testProcess.stdout.on('data', chunk => {
|
testProcess.stdout.on('data', chunk => {
|
||||||
output += String(chunk);
|
output += String(chunk);
|
||||||
|
if (options.sendSIGINTAfter && !didSendSigint && countTimes(output, '%%SEND-SIGINT%%') >= options.sendSIGINTAfter) {
|
||||||
|
didSendSigint = true;
|
||||||
|
process.kill(testProcess.pid, 'SIGINT');
|
||||||
|
}
|
||||||
if (process.env.PW_RUNNER_DEBUG)
|
if (process.env.PW_RUNNER_DEBUG)
|
||||||
process.stdout.write(String(chunk));
|
process.stdout.write(String(chunk));
|
||||||
});
|
});
|
||||||
|
|
@ -212,9 +217,12 @@ async function runPlaywrightTest(baseDir: string, params: any, env: Env): Promis
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RunOptions = {
|
||||||
|
sendSIGINTAfter?: number;
|
||||||
|
};
|
||||||
type Fixtures = {
|
type Fixtures = {
|
||||||
writeFiles: (files: Files) => Promise<string>;
|
writeFiles: (files: Files) => Promise<string>;
|
||||||
runInlineTest: (files: Files, params?: Params, env?: Env) => Promise<RunResult>;
|
runInlineTest: (files: Files, params?: Params, env?: Env, options?: RunOptions) => Promise<RunResult>;
|
||||||
runTSC: (files: Files) => Promise<TSCResult>;
|
runTSC: (files: Files) => Promise<TSCResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -225,9 +233,9 @@ export const test = base.extend<Fixtures>({
|
||||||
|
|
||||||
runInlineTest: async ({}, use, testInfo: TestInfo) => {
|
runInlineTest: async ({}, use, testInfo: TestInfo) => {
|
||||||
let runResult: RunResult | undefined;
|
let runResult: RunResult | undefined;
|
||||||
await use(async (files: Files, params: Params = {}, env: Env = {}) => {
|
await use(async (files: Files, params: Params = {}, env: Env = {}, options: RunOptions = {}) => {
|
||||||
const baseDir = await writeFiles(testInfo, files);
|
const baseDir = await writeFiles(testInfo, files);
|
||||||
runResult = await runPlaywrightTest(baseDir, params, env);
|
runResult = await runPlaywrightTest(baseDir, params, env, options);
|
||||||
return runResult;
|
return runResult;
|
||||||
});
|
});
|
||||||
if (testInfo.status !== testInfo.expectedStatus && runResult && !process.env.PW_RUNNER_DEBUG)
|
if (testInfo.status !== testInfo.expectedStatus && runResult && !process.env.PW_RUNNER_DEBUG)
|
||||||
|
|
@ -268,3 +276,15 @@ const asciiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*
|
||||||
export function stripAscii(str: string): string {
|
export function stripAscii(str: string): string {
|
||||||
return str.replace(asciiRegex, '');
|
return str.replace(asciiRegex, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countTimes(s: string, sub: string): number {
|
||||||
|
let result = 0;
|
||||||
|
for (let index = 0; index !== -1;) {
|
||||||
|
index = s.indexOf(sub, index);
|
||||||
|
if (index !== -1) {
|
||||||
|
result++;
|
||||||
|
index += sub.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,54 @@ test('it should not allow a focused test when forbid-only is used', async ({ run
|
||||||
expect(result.output).toContain('--forbid-only found a focused test.');
|
expect(result.output).toContain('--forbid-only found a focused test.');
|
||||||
expect(result.output).toContain(`- tests${path.sep}focused-test.spec.js:6 > i-am-focused`);
|
expect(result.output).toContain(`- tests${path.sep}focused-test.spec.js:6 > i-am-focused`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should not hang and report results when worker process suddenly exits', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('passed1', () => {});
|
||||||
|
test('passed2', () => {});
|
||||||
|
test('failed1', () => { process.exit(0); });
|
||||||
|
test('failed2', () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(result.failed).toBe(2);
|
||||||
|
expect(result.output).toContain('Worker process exited unexpectedly');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sigint should stop workers', async ({ runInlineTest }) => {
|
||||||
|
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||||
|
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('interrupted1', async () => {
|
||||||
|
console.log('\\n%%SEND-SIGINT%%1');
|
||||||
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
|
});
|
||||||
|
test('skipped1', async () => {
|
||||||
|
console.log('\\n%%skipped1');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'b.spec.js': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('interrupted2', async () => {
|
||||||
|
console.log('\\n%%SEND-SIGINT%%2');
|
||||||
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
|
});
|
||||||
|
test('skipped2', async () => {
|
||||||
|
console.log('\\n%%skipped2');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { 'workers': 2 }, {}, { sendSIGINTAfter: 2 });
|
||||||
|
expect(result.exitCode).toBe(130);
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
expect(result.skipped).toBe(4);
|
||||||
|
expect(result.output).toContain('%%SEND-SIGINT%%1');
|
||||||
|
expect(result.output).toContain('%%SEND-SIGINT%%2');
|
||||||
|
expect(result.output).not.toContain('%%skipped1');
|
||||||
|
expect(result.output).not.toContain('%%skipped2');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue