chore: project.id, configFile in reporter apis (#17346)

This commit is contained in:
Pavel Feldman 2022-09-14 14:56:28 -07:00 committed by GitHub
parent 59c32bf2c6
commit 854c783019
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 101 additions and 50 deletions

View file

@ -84,6 +84,12 @@ const config: PlaywrightTestConfig = {
export default config; export default config;
``` ```
## property: TestConfig.configFile
* since: v1.27
- type: ?<[string]>
Path to config file, if any.
## property: TestConfig.forbidOnly ## property: TestConfig.forbidOnly
* since: v1.10 * since: v1.10
- type: ?<[boolean]> - type: ?<[boolean]>

View file

@ -150,6 +150,13 @@ Filter to only run tests with a title **not** matching one of the patterns. This
`grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests). `grepInvert` option is also useful for [tagging tests](../test-annotations.md#tag-tests).
## property: TestProject.id
* since: v1.27
- type: ?<[string]>
Unique project id within this config.
## property: TestProject.metadata ## property: TestProject.metadata
* since: v1.10 * since: v1.10
- type: ?<[Metadata]> - type: ?<[Metadata]>

View file

@ -125,6 +125,7 @@ export class Loader {
config.snapshotDir = path.resolve(configDir, config.snapshotDir); config.snapshotDir = path.resolve(configDir, config.snapshotDir);
this._fullConfig._configDir = configDir; this._fullConfig._configDir = configDir;
this._fullConfig.configFile = this._configFile;
this._fullConfig.rootDir = config.testDir || this._configDir; this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir); this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir);
this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly); this._fullConfig.forbidOnly = takeFirst(config.forbidOnly, baseFullConfig.forbidOnly);
@ -165,7 +166,7 @@ export class Loader {
const candidate = name + (i ? i : ''); const candidate = name + (i ? i : '');
if (usedNames.has(candidate)) if (usedNames.has(candidate))
continue; continue;
p._id = candidate; p.id = candidate;
usedNames.add(candidate); usedNames.add(candidate);
break; break;
} }
@ -277,7 +278,7 @@ export class Loader {
process.env.PWTEST_USE_SCREENSHOTS_DIR = '1'; process.env.PWTEST_USE_SCREENSHOTS_DIR = '1';
} }
return { return {
_id: '', id: '',
_fullConfig: fullConfig, _fullConfig: fullConfig,
_fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), _fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
_expect: takeFirst(projectConfig.expect, config.expect, {}), _expect: takeFirst(projectConfig.expect, config.expect, {}),
@ -391,20 +392,20 @@ class ProjectSuiteBuilder {
test.retries = this._project.retries; test.retries = this._project.retries;
const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : ''; const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles. // At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
const testIdExpression = `[project=${this._project._id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`; const testIdExpression = `[project=${this._project.id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
const testId = to._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20); const testId = to._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
test.id = testId; test.id = testId;
test.repeatEachIndex = repeatEachIndex; test.repeatEachIndex = repeatEachIndex;
test._projectId = this._project._id; test._projectId = this._project.id;
if (!filter(test)) { if (!filter(test)) {
to._entries.pop(); to._entries.pop();
to.tests.pop(); to.tests.pop();
} else { } else {
const pool = this._buildPool(entry); const pool = this._buildPool(entry);
if (this._project._fullConfig._workerIsolation === 'isolate-pools') if (this._project._fullConfig._workerIsolation === 'isolate-pools')
test._workerHash = `run${this._project._id}-${pool.digest}-repeat${repeatEachIndex}`; test._workerHash = `run${this._project.id}-${pool.digest}-repeat${repeatEachIndex}`;
else else
test._workerHash = `run${this._project._id}-repeat${repeatEachIndex}`; test._workerHash = `run${this._project.id}-repeat${repeatEachIndex}`;
test._pool = pool; test._pool = pool;
} }
} }
@ -436,7 +437,7 @@ class ProjectSuiteBuilder {
(originalFixtures as any)[key] = value; (originalFixtures as any)[key] = value;
} }
if (Object.entries(optionsFromConfig).length) if (Object.entries(optionsFromConfig).length)
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 } }); result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project.id}`, line: 1, column: 1 } });
if (Object.entries(originalFixtures).length) if (Object.entries(originalFixtures).length)
result.push({ fixtures: originalFixtures, location: f.location }); result.push({ fixtures: originalFixtures, location: f.location });
} }
@ -647,6 +648,7 @@ export const baseFullConfig: FullConfigInternal = {
projects: [], projects: [],
reporter: [[process.env.CI ? 'dot' : 'list']], reporter: [[process.env.CI ? 'dot' : 'list']],
reportSlowTests: { max: 5, threshold: 15000 }, reportSlowTests: { max: 5, threshold: 15000 },
configFile: '',
rootDir: path.resolve(process.cwd()), rootDir: path.resolve(process.cwd()),
quiet: false, quiet: false,
shard: null, shard: null,

View file

@ -18,6 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, Reporter, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, Reporter, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep } from '../../types/testReporter';
import { prepareErrorStack } from './base'; import { prepareErrorStack } from './base';
import { MultiMap } from 'playwright-core/lib/utils/multimap';
export function toPosixPath(aPath: string): string { export function toPosixPath(aPath: string): string {
return aPath.split(path.sep).join(path.posix.sep); return aPath.split(path.sep).join(path.posix.sep);
@ -53,7 +54,7 @@ class JSONReporter implements Reporter {
private _serializeReport(): JSONReport { private _serializeReport(): JSONReport {
return { return {
config: { config: {
...this.config, ...removePrivateFields(this.config),
rootDir: toPosixPath(this.config.rootDir), rootDir: toPosixPath(this.config.rootDir),
projects: this.config.projects.map(project => { projects: this.config.projects.map(project => {
return { return {
@ -61,6 +62,7 @@ class JSONReporter implements Reporter {
repeatEach: project.repeatEach, repeatEach: project.repeatEach,
retries: project.retries, retries: project.retries,
metadata: project.metadata, metadata: project.metadata,
id: project.id,
name: project.name, name: project.name,
testDir: toPosixPath(project.testDir), testDir: toPosixPath(project.testDir),
testIgnore: serializePatterns(project.testIgnore), testIgnore: serializePatterns(project.testIgnore),
@ -75,23 +77,32 @@ class JSONReporter implements Reporter {
} }
private _mergeSuites(suites: Suite[]): JSONReportSuite[] { private _mergeSuites(suites: Suite[]): JSONReportSuite[] {
const fileSuites = new Map<string, JSONReportSuite>(); const fileSuites = new MultiMap<string, JSONReportSuite>();
const result: JSONReportSuite[] = [];
for (const projectSuite of suites) { for (const projectSuite of suites) {
const projectId = projectSuite.project()!.id;
const projectName = projectSuite.project()!.name;
for (const fileSuite of projectSuite.suites) { for (const fileSuite of projectSuite.suites) {
const file = fileSuite.location!.file; const file = fileSuite.location!.file;
if (!fileSuites.has(file)) { const serialized = this._serializeSuite(projectId, projectName, fileSuite);
const serialized = this._serializeSuite(fileSuite); if (serialized)
if (serialized) { fileSuites.set(file, serialized);
fileSuites.set(file, serialized);
result.push(serialized);
}
} else {
this._mergeTestsFromSuite(fileSuites.get(file)!, fileSuite);
}
} }
} }
return result;
const results: JSONReportSuite[] = [];
for (const [, suites] of fileSuites) {
const result: JSONReportSuite = {
title: suites[0].title,
file: suites[0].file,
column: 0,
line: 0,
specs: [],
};
for (const suite of suites)
this._mergeTestsFromSuite(result, suite);
results.push(result);
}
return results;
} }
private _relativeLocation(location: Location | undefined): Location { private _relativeLocation(location: Location | undefined): Location {
@ -104,63 +115,61 @@ class JSONReporter implements Reporter {
}; };
} }
private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location | undefined) { private _locationMatches(s1: JSONReportSuite | JSONReportSpec, s2: JSONReportSuite | JSONReportSpec) {
const relative = this._relativeLocation(location); return s1.file === s2.file && s1.line === s2.line && s1.column === s2.column;
return s.file === relative.file && s.line === relative.line && s.column === relative.column;
} }
private _mergeTestsFromSuite(to: JSONReportSuite, from: Suite) { private _mergeTestsFromSuite(to: JSONReportSuite, from: JSONReportSuite) {
for (const fromSuite of from.suites) { for (const fromSuite of from.suites || []) {
const toSuite = (to.suites || []).find(s => s.title === fromSuite.title && this._locationMatches(s, from.location)); const toSuite = (to.suites || []).find(s => s.title === fromSuite.title && this._locationMatches(s, fromSuite));
if (toSuite) { if (toSuite) {
this._mergeTestsFromSuite(toSuite, fromSuite); this._mergeTestsFromSuite(toSuite, fromSuite);
} else { } else {
const serialized = this._serializeSuite(fromSuite); if (!to.suites)
if (serialized) { to.suites = [];
if (!to.suites) to.suites.push(fromSuite);
to.suites = [];
to.suites.push(serialized);
}
} }
} }
for (const test of from.tests) {
const toSpec = to.specs.find(s => s.title === test.title && s.file === toPosixPath(path.relative(this.config.rootDir, test.location.file)) && s.line === test.location.line && s.column === test.location.column); for (const spec of from.specs || []) {
const toSpec = to.specs.find(s => s.title === spec.title && s.file === toPosixPath(path.relative(this.config.rootDir, spec.file)) && s.line === spec.line && s.column === spec.column);
if (toSpec) if (toSpec)
toSpec.tests.push(this._serializeTest(test)); toSpec.tests.push(...spec.tests);
else else
to.specs.push(this._serializeTestSpec(test)); to.specs.push(spec);
} }
} }
private _serializeSuite(suite: Suite): null | JSONReportSuite { private _serializeSuite(projectId: string, projectName: string, suite: Suite): null | JSONReportSuite {
if (!suite.allTests().length) if (!suite.allTests().length)
return null; return null;
const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as JSONReportSuite[]; const suites = suite.suites.map(suite => this._serializeSuite(projectId, projectName, suite)).filter(s => s) as JSONReportSuite[];
return { return {
title: suite.title, title: suite.title,
...this._relativeLocation(suite.location), ...this._relativeLocation(suite.location),
specs: suite.tests.map(test => this._serializeTestSpec(test)), specs: suite.tests.map(test => this._serializeTestSpec(projectId, projectName, test)),
suites: suites.length ? suites : undefined, suites: suites.length ? suites : undefined,
}; };
} }
private _serializeTestSpec(test: TestCase): JSONReportSpec { private _serializeTestSpec(projectId: string, projectName: string, test: TestCase): JSONReportSpec {
return { return {
title: test.title, title: test.title,
ok: test.ok(), ok: test.ok(),
tags: (test.title.match(/@[\S]+/g) || []).map(t => t.substring(1)), tags: (test.title.match(/@[\S]+/g) || []).map(t => t.substring(1)),
tests: [this._serializeTest(test)], tests: [this._serializeTest(projectId, projectName, test)],
id: test.id, id: test.id,
...this._relativeLocation(test.location), ...this._relativeLocation(test.location),
}; };
} }
private _serializeTest(test: TestCase): JSONReportTest { private _serializeTest(projectId: string, projectName: string, test: TestCase): JSONReportTest {
return { return {
timeout: test.timeout, timeout: test.timeout,
annotations: test.annotations, annotations: test.annotations,
expectedStatus: test.expectedStatus, expectedStatus: test.expectedStatus,
projectName: test.titlePath()[1], projectId,
projectName,
results: test.results.map(r => this._serializeTestResult(r, test)), results: test.results.map(r => this._serializeTestResult(r, test)),
status: test.outcome(), status: test.outcome(),
}; };
@ -217,6 +226,10 @@ function stdioEntry(s: string | Buffer): any {
return { buffer: s.toString('base64') }; return { buffer: s.toString('base64') };
} }
function removePrivateFields(config: FullConfig): FullConfig {
return Object.fromEntries(Object.entries(config).filter(([name, value]) => !name.startsWith('_'))) as FullConfig;
}
export function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] { export function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] {
if (!Array.isArray(patterns)) if (!Array.isArray(patterns))
patterns = [patterns]; patterns = [patterns];

View file

@ -512,10 +512,10 @@ export class Runner {
for (const [project, files] of filesByProject) { for (const [project, files] of filesByProject) {
for (const file of files) { for (const file of files) {
const group: TestGroup = { const group: TestGroup = {
workerHash: `run${project._id}-repeat${repeatEachIndex}`, workerHash: `run${project.id}-repeat${repeatEachIndex}`,
requireFile: file, requireFile: file,
repeatEachIndex, repeatEachIndex,
projectId: project._id, projectId: project.id,
tests: [], tests: [],
watchMode: true, watchMode: true,
}; };

View file

@ -118,8 +118,8 @@ export class TestInfoImpl implements TestInfo {
const fullTitleWithoutSpec = test.titlePath().slice(1).join(' '); const fullTitleWithoutSpec = test.titlePath().slice(1).join(' ');
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec)); let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
if (project._id) if (project.id)
testOutputDir += '-' + sanitizeForFilePath(project._id); testOutputDir += '-' + sanitizeForFilePath(project.id);
if (this.retry) if (this.retry)
testOutputDir += '-retry' + this.retry; testOutputDir += '-retry' + this.retry;
if (this.repeatEachIndex) if (this.repeatEachIndex)

View file

@ -63,7 +63,6 @@ export interface FullConfigInternal extends FullConfigPublic {
* increasing the surface area of the public API type called FullProject. * increasing the surface area of the public API type called FullProject.
*/ */
export interface FullProjectInternal extends FullProjectPublic { export interface FullProjectInternal extends FullProjectPublic {
_id: string;
_fullConfig: FullConfigInternal; _fullConfig: FullConfigInternal;
_fullyParallel: boolean; _fullyParallel: boolean;
_expect: Project['expect']; _expect: Project['expect'];

View file

@ -160,7 +160,7 @@ export class WorkerRunner extends EventEmitter {
return; return;
this._loader = await Loader.deserialize(this._params.loader); this._loader = await Loader.deserialize(this._params.loader);
this._project = this._loader.fullConfig().projects.find(p => p._id === this._params.projectId)!; this._project = this._loader.fullConfig().projects.find(p => p.id === this._params.projectId)!;
} }
async runTestGroup(runPayload: RunPayload) { async runTestGroup(runPayload: RunPayload) {

View file

@ -198,6 +198,10 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
* Metadata that will be put directly to the test report serialized as JSON. * Metadata that will be put directly to the test report serialized as JSON.
*/ */
metadata: Metadata; metadata: Metadata;
/**
* Unique project id within this config.
*/
id: string;
/** /**
* Project name is visible in the report and during test execution. * Project name is visible in the report and during test execution.
*/ */
@ -578,6 +582,11 @@ interface TestConfig {
}; };
}; };
/**
* Path to config file, if any.
*/
configFile?: string;
/** /**
* Whether to exit with an error if any tests or groups are marked as * Whether to exit with an error if any tests or groups are marked as
* [test.only(title, testFunction)](https://playwright.dev/docs/api/class-test#test-only) or * [test.only(title, testFunction)](https://playwright.dev/docs/api/class-test#test-only) or
@ -1298,6 +1307,10 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
* *
*/ */
webServer: TestConfigWebServer | null; webServer: TestConfigWebServer | null;
/**
* Path to config file, if any.
*/
configFile?: string;
} }
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'; export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
@ -4402,6 +4415,11 @@ interface TestProject {
*/ */
grepInvert?: RegExp|Array<RegExp>; grepInvert?: RegExp|Array<RegExp>;
/**
* Unique project id within this config.
*/
id?: string;
/** /**
* Metadata that will be put directly to the test report serialized as JSON. * Metadata that will be put directly to the test report serialized as JSON.
*/ */

View file

@ -444,6 +444,7 @@ export interface JSONReport {
repeatEach: number, repeatEach: number,
retries: number, retries: number,
metadata: Metadata, metadata: Metadata,
id: string,
name: string, name: string,
testDir: string, testDir: string,
testIgnore: string[], testIgnore: string[],
@ -480,6 +481,7 @@ export interface JSONReportTest {
annotations: { type: string, description?: string }[], annotations: { type: string, description?: string }[],
expectedStatus: TestStatus; expectedStatus: TestStatus;
projectName: string; projectName: string;
projectId: string;
results: JSONReportTestResult[]; results: JSONReportTestResult[];
status: 'skipped' | 'expected' | 'unexpected' | 'flaky'; status: 'skipped' | 'expected' | 'unexpected' | 'flaky';
} }

View file

@ -41,6 +41,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
grep: RegExp | RegExp[]; grep: RegExp | RegExp[];
grepInvert: RegExp | RegExp[] | null; grepInvert: RegExp | RegExp[] | null;
metadata: Metadata; metadata: Metadata;
id: string;
name: string; name: string;
snapshotDir: string; snapshotDir: string;
outputDir: string; outputDir: string;
@ -92,6 +93,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
updateSnapshots: 'all' | 'none' | 'missing'; updateSnapshots: 'all' | 'none' | 'missing';
workers: number; workers: number;
webServer: TestConfigWebServer | null; webServer: TestConfigWebServer | null;
configFile?: string;
// [internal] !!! DO NOT ADD TO THIS !!! See prior note. // [internal] !!! DO NOT ADD TO THIS !!! See prior note.
} }

View file

@ -55,6 +55,7 @@ export interface JSONReport {
repeatEach: number, repeatEach: number,
retries: number, retries: number,
metadata: Metadata, metadata: Metadata,
id: string,
name: string, name: string,
testDir: string, testDir: string,
testIgnore: string[], testIgnore: string[],
@ -91,6 +92,7 @@ export interface JSONReportTest {
annotations: { type: string, description?: string }[], annotations: { type: string, description?: string }[],
expectedStatus: TestStatus; expectedStatus: TestStatus;
projectName: string; projectName: string;
projectId: string;
results: JSONReportTestResult[]; results: JSONReportTestResult[];
status: 'skipped' | 'expected' | 'unexpected' | 'flaky'; status: 'skipped' | 'expected' | 'unexpected' | 'flaky';
} }

View file

@ -95,7 +95,7 @@ az storage blob upload --connection-string "${FLAKINESS_CONNECTION_STRING}" -c u
UTC_DATE=$(cat <<EOF | node UTC_DATE=$(cat <<EOF | node
const date = new Date(); const date = new Date();
console.log([date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()].join('-')); console.log(date.toISOString().substring(0, 10).replace(/-/g, ''));
EOF EOF
) )