fix(test runner): avoid internal error for step end without begin (#8327)
fix(test runner): avoid internal error for step end without begin Consider the following scenario: - Test finishes and starts tearing down fixtures. - Fixture teardown starts a step S and then times out. - We declare the test finished (with timeout). - Dispatcher shuts down the worker and spins a new one for a retry. Additionally, it clears steps information for the test to be ready for the new retry. Step S information is lost. - Meanwhile, during worker teardown, the step S does actually finish (usually with an error), and we send stepEnd for S. - Dispatcher does not know what to do with step S end and prints an internal error. The fix is to ignore certain messages from the shutting down worker that failed.
This commit is contained in:
parent
cad7fc2ef7
commit
70eca0401d
|
|
@ -124,6 +124,7 @@ export class Dispatcher {
|
||||||
|
|
||||||
// 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();
|
||||||
|
worker.didFail = true;
|
||||||
|
|
||||||
const retryCandidates = new Set<string>();
|
const retryCandidates = new Set<string>();
|
||||||
|
|
||||||
|
|
@ -255,6 +256,11 @@ export class Dispatcher {
|
||||||
_createWorker(testGroup: TestGroup) {
|
_createWorker(testGroup: TestGroup) {
|
||||||
const worker = new Worker(this);
|
const worker = new Worker(this);
|
||||||
worker.on('testBegin', (params: TestBeginPayload) => {
|
worker.on('testBegin', (params: TestBeginPayload) => {
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Ignore test-related messages from failed workers, because timed out tests/fixtures
|
||||||
|
// may be triggering unexpected messages.
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._hasReachedMaxFailures())
|
if (this._hasReachedMaxFailures())
|
||||||
return;
|
return;
|
||||||
const { test, result: testRun } = this._testById.get(params.testId)!;
|
const { test, result: testRun } = this._testById.get(params.testId)!;
|
||||||
|
|
@ -263,6 +269,11 @@ export class Dispatcher {
|
||||||
this._reporter.onTestBegin?.(test, testRun);
|
this._reporter.onTestBegin?.(test, testRun);
|
||||||
});
|
});
|
||||||
worker.on('testEnd', (params: TestEndPayload) => {
|
worker.on('testEnd', (params: TestEndPayload) => {
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Ignore test-related messages from failed workers, because timed out tests/fixtures
|
||||||
|
// may be triggering unexpected messages.
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._hasReachedMaxFailures())
|
if (this._hasReachedMaxFailures())
|
||||||
return;
|
return;
|
||||||
const { test, result } = this._testById.get(params.testId)!;
|
const { test, result } = this._testById.get(params.testId)!;
|
||||||
|
|
@ -281,6 +292,11 @@ export class Dispatcher {
|
||||||
this._reportTestEnd(test, result);
|
this._reportTestEnd(test, result);
|
||||||
});
|
});
|
||||||
worker.on('stepBegin', (params: StepBeginPayload) => {
|
worker.on('stepBegin', (params: StepBeginPayload) => {
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Ignore test-related messages from failed workers, because timed out tests/fixtures
|
||||||
|
// may be triggering unexpected messages.
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { test, result, steps, stepStack } = this._testById.get(params.testId)!;
|
const { test, result, steps, stepStack } = this._testById.get(params.testId)!;
|
||||||
const parentStep = [...stepStack].pop();
|
const parentStep = [...stepStack].pop();
|
||||||
const step: TestStep = {
|
const step: TestStep = {
|
||||||
|
|
@ -301,6 +317,11 @@ export class Dispatcher {
|
||||||
this._reporter.onStepBegin?.(test, result, step);
|
this._reporter.onStepBegin?.(test, result, step);
|
||||||
});
|
});
|
||||||
worker.on('stepEnd', (params: StepEndPayload) => {
|
worker.on('stepEnd', (params: StepEndPayload) => {
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Ignore test-related messages from failed workers, because timed out tests/fixtures
|
||||||
|
// may be triggering unexpected messages.
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { test, result, steps, stepStack } = this._testById.get(params.testId)!;
|
const { test, result, steps, stepStack } = this._testById.get(params.testId)!;
|
||||||
const step = steps.get(params.stepId);
|
const step = steps.get(params.stepId);
|
||||||
if (!step) {
|
if (!step) {
|
||||||
|
|
@ -316,6 +337,13 @@ export class Dispatcher {
|
||||||
});
|
});
|
||||||
worker.on('stdOut', (params: TestOutputPayload) => {
|
worker.on('stdOut', (params: TestOutputPayload) => {
|
||||||
const chunk = chunkFromParams(params);
|
const chunk = chunkFromParams(params);
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Note: we keep reading stdout from workers that are currently stopping after failure,
|
||||||
|
// to debug teardown issues. However, we avoid spoiling the test result from
|
||||||
|
// the next retry.
|
||||||
|
this._reporter.onStdOut?.(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
||||||
if (pair)
|
if (pair)
|
||||||
pair.result.stdout.push(chunk);
|
pair.result.stdout.push(chunk);
|
||||||
|
|
@ -323,6 +351,13 @@ export class Dispatcher {
|
||||||
});
|
});
|
||||||
worker.on('stdErr', (params: TestOutputPayload) => {
|
worker.on('stdErr', (params: TestOutputPayload) => {
|
||||||
const chunk = chunkFromParams(params);
|
const chunk = chunkFromParams(params);
|
||||||
|
if (worker.didFail) {
|
||||||
|
// Note: we keep reading stderr from workers that are currently stopping after failure,
|
||||||
|
// to debug teardown issues. However, we avoid spoiling the test result from
|
||||||
|
// the next retry.
|
||||||
|
this._reporter.onStdErr?.(chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
||||||
if (pair)
|
if (pair)
|
||||||
pair.result.stderr.push(chunk);
|
pair.result.stderr.push(chunk);
|
||||||
|
|
@ -381,6 +416,7 @@ class Worker extends EventEmitter {
|
||||||
hash = '';
|
hash = '';
|
||||||
index: number;
|
index: number;
|
||||||
didSendStop = false;
|
didSendStop = false;
|
||||||
|
didFail = false;
|
||||||
|
|
||||||
constructor(runner: Dispatcher) {
|
constructor(runner: Dispatcher) {
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,28 @@ test('should report test.step', async ({ runInlineTest }) => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not have internal error when steps are finished after timeout', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const test = pwt.test.extend({
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
await use(page);
|
||||||
|
// Timeout in fixture teardown that will resolve on browser.close.
|
||||||
|
await page.waitForNavigation();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
// Timeout in the test.
|
||||||
|
await page.click('foo');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { workers: 1, timeout: 1000, reporter: 'dot', retries: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.output).not.toContain('Internal error');
|
||||||
|
});
|
||||||
|
|
||||||
test('should report api step hierarchy', async ({ runInlineTest }) => {
|
test('should report api step hierarchy', async ({ runInlineTest }) => {
|
||||||
const expectReporterJS = `
|
const expectReporterJS = `
|
||||||
class Reporter {
|
class Reporter {
|
||||||
|
|
@ -389,7 +411,7 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
||||||
steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined,
|
steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnd() {
|
async onEnd() {
|
||||||
const processSuite = (suite: Suite) => {
|
const processSuite = (suite: Suite) => {
|
||||||
for (const child of suite.suites)
|
for (const child of suite.suites)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue