chore(test runner): simplify some dispatcher logic (#27732)

- remove `onlyStartedTests` in favor of explicit branch with comments;
- produce one "test not found" error per test instead of a single large
error;
- extract `_failTestWithErrors` from `_massSkipTestsFromRemaining`.
This commit is contained in:
Dmitry Gozman 2023-10-23 09:15:13 -07:00 committed by GitHub
parent 85112be25c
commit b8678ef902
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 44 additions and 36 deletions

View file

@ -357,27 +357,29 @@ class JobDispatcher {
data.result.attachments.push(attachment); data.result.attachments.push(attachment);
} }
private _massSkipTestsFromRemaining(testIds: Set<string>, errors: TestError[], onlyStartedTests?: boolean) { private _failTestWithErrors(test: TestCase, errors: TestError[]) {
const runData = this._dataByTestId.get(test.id);
// There might be a single test that has started but has not finished yet.
let result: TestResult;
if (runData) {
result = runData.result;
} else {
result = test._appendTestResult();
this._reporter.onTestBegin(test, result);
}
result.errors = [...errors];
result.error = result.errors[0];
result.status = errors.length ? 'failed' : 'skipped';
this._reportTestEnd(test, result);
this._failedTests.add(test);
}
private _massSkipTestsFromRemaining(testIds: Set<string>, errors: TestError[]) {
for (const test of this._remainingByTestId.values()) { for (const test of this._remainingByTestId.values()) {
if (!testIds.has(test.id)) if (!testIds.has(test.id))
continue; continue;
if (!this._failureTracker.hasReachedMaxFailures()) { if (!this._failureTracker.hasReachedMaxFailures()) {
const runData = this._dataByTestId.get(test.id); this._failTestWithErrors(test, errors);
// There might be a single test that has started but has not finished yet.
let result: TestResult;
if (runData) {
result = runData.result;
} else {
if (onlyStartedTests && this._currentlyRunning)
continue;
result = test._appendTestResult();
this._reporter.onTestBegin(test, result);
}
result.errors = [...errors];
result.error = result.errors[0];
result.status = errors.length ? 'failed' : 'skipped';
this._reportTestEnd(test, result);
this._failedTests.add(test);
errors = []; // Only report errors for the first test. errors = []; // Only report errors for the first test.
} }
this._remainingByTestId.delete(test.id); this._remainingByTestId.delete(test.id);
@ -401,15 +403,14 @@ class JobDispatcher {
return; return;
} }
if (params.fatalUnknownTestIds) { for (const testId of params.fatalUnknownTestIds || []) {
const titles = params.fatalUnknownTestIds.map(testId => { const test = this._remainingByTestId.get(testId);
const test = this._remainingByTestId.get(testId); if (test) {
return test?.titlePath().slice(1).join(' > '); this._remainingByTestId.delete(testId);
}).filter(title => !!title); this._failTestWithErrors(test, [{ message: `Test not found in the worker process. Make sure test title does not change.` }]);
this._massSkipTestsFromRemaining(new Set(params.fatalUnknownTestIds), [{ }
message: `Test(s) not found in the worker process. Make sure test titles do not change:\n${titles.join('\n')}`
}]);
} }
if (params.fatalErrors.length) { if (params.fatalErrors.length) {
// In case of fatal errors, report first remaining test as failing with these errors, // In case of fatal errors, report first remaining test as failing with these errors,
// and all others as skipped. // and all others as skipped.
@ -417,9 +418,18 @@ class JobDispatcher {
} }
// Handle tests that should be skipped because of the setup failure. // Handle tests that should be skipped because of the setup failure.
this._massSkipTestsFromRemaining(new Set(params.skipTestsDueToSetupFailure), []); this._massSkipTestsFromRemaining(new Set(params.skipTestsDueToSetupFailure), []);
// Handle unexpected worker exit.
if (params.unexpectedExitError) if (params.unexpectedExitError) {
this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError], true /* onlyStartedTests */); // When worker exits during a test, we blame the test itself.
//
// The most common situation when worker exits while not running a test is:
// worker failed to require the test file (at the start) because of an exception in one of imports.
// In this case, "skip" all remaining tests, to avoid running into the same exception over and over.
if (this._currentlyRunning)
this._massSkipTestsFromRemaining(new Set([this._currentlyRunning.test.id]), [params.unexpectedExitError]);
else
this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError]);
}
const retryCandidates = new Set<TestCase>(); const retryCandidates = new Set<TestCase>();
const serialSuitesWithFailures = new Set<Suite>(); const serialSuitesWithFailures = new Set<Suite>();

View file

@ -16,7 +16,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures'; import { test, expect, parseTestRunnerOutput, countTimes } from './playwright-test-fixtures';
test('it should not allow multiple tests with the same name per suite', async ({ runInlineTest }) => { test('it should not allow multiple tests with the same name per suite', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
@ -377,8 +377,6 @@ test('should teardown workers that are redundant', async ({ runInlineTest }) =>
}); });
test('should not hang if test suites in worker are inconsistent with runner', async ({ runInlineTest }) => { test('should not hang if test suites in worker are inconsistent with runner', async ({ runInlineTest }) => {
const oldValue = process.env.TEST_WORKER_INDEX;
delete process.env.TEST_WORKER_INDEX;
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { name: 'project-name' }; module.exports = { name: 'project-name' };
@ -400,13 +398,13 @@ test('should not hang if test suites in worker are inconsistent with runner', as
}); });
} }
`, `,
}, { 'workers': 1 }); }, { 'workers': 1 }, { TEST_WORKER_INDEX: undefined });
process.env.TEST_WORKER_INDEX = oldValue;
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(2);
expect(result.skipped).toBe(1); expect(result.skipped).toBe(0);
expect(result.report.suites[0].specs[1].tests[0].results[0].error!.message).toBe('Test(s) not found in the worker process. Make sure test titles do not change:\nproject-name > a.spec.js > Test 1 - bar\nproject-name > a.spec.js > Test 2 - baz'); const expectedError = 'Test not found in the worker process. Make sure test title does not change.';
expect(countTimes(result.output, expectedError)).toBe(2); // Once per each test that was missing.
}); });
test('sigint should stop global setup', async ({ interactWithTestRunner }) => { test('sigint should stop global setup', async ({ interactWithTestRunner }) => {