chore: iterate towards tele reporter reuse in vscode (2) (#29812)

This commit is contained in:
Pavel Feldman 2024-03-04 19:52:20 -08:00 committed by GitHub
parent 5eb8fea616
commit e314b83e56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 52 additions and 58 deletions

View file

@ -122,10 +122,9 @@ export type JsonEvent = {
}; };
type TeleReporterReceiverOptions = { type TeleReporterReceiverOptions = {
pathSeparator: string;
mergeProjects: boolean; mergeProjects: boolean;
mergeTestCases: boolean; mergeTestCases: boolean;
internString?: StringIntern; resolvePath: (rootDir: string, relativePath: string) => string;
configOverrides?: Pick<reporterTypes.FullConfig, 'configFile' | 'quiet' | 'reportSlowTests' | 'reporter'>; configOverrides?: Pick<reporterTypes.FullConfig, 'configFile' | 'quiet' | 'reportSlowTests' | 'reporter'>;
}; };
@ -242,7 +241,6 @@ export class TeleReporterReceiver {
testResult.workerIndex = payload.workerIndex; testResult.workerIndex = payload.workerIndex;
testResult.parallelIndex = payload.parallelIndex; testResult.parallelIndex = payload.parallelIndex;
testResult.setStartTimeNumber(payload.startTime); testResult.setStartTimeNumber(payload.startTime);
testResult.statusEx = 'running';
this._reporter.onTestBegin?.(test, testResult); this._reporter.onTestBegin?.(test, testResult);
} }
@ -251,22 +249,21 @@ export class TeleReporterReceiver {
test.timeout = testEndPayload.timeout; test.timeout = testEndPayload.timeout;
test.expectedStatus = testEndPayload.expectedStatus; test.expectedStatus = testEndPayload.expectedStatus;
test.annotations = testEndPayload.annotations; test.annotations = testEndPayload.annotations;
const result = test.resultsMap.get(payload.id)!; const result = test._resultsMap.get(payload.id)!;
result.duration = payload.duration; result.duration = payload.duration;
result.status = payload.status; result.status = payload.status;
result.statusEx = payload.status;
result.errors = payload.errors; result.errors = payload.errors;
result.error = result.errors?.[0]; result.error = result.errors?.[0];
result.attachments = this._parseAttachments(payload.attachments); result.attachments = this._parseAttachments(payload.attachments);
this._reporter.onTestEnd?.(test, result); this._reporter.onTestEnd?.(test, result);
// Free up the memory as won't see these step ids. // Free up the memory as won't see these step ids.
result.stepMap = new Map(); result._stepMap = new Map();
} }
private _onStepBegin(testId: string, resultId: string, payload: JsonTestStepStart) { private _onStepBegin(testId: string, resultId: string, payload: JsonTestStepStart) {
const test = this._tests.get(testId)!; const test = this._tests.get(testId)!;
const result = test.resultsMap.get(resultId)!; const result = test._resultsMap.get(resultId)!;
const parentStep = payload.parentStepId ? result.stepMap.get(payload.parentStepId) : undefined; const parentStep = payload.parentStepId ? result._stepMap.get(payload.parentStepId) : undefined;
const location = this._absoluteLocation(payload.location); const location = this._absoluteLocation(payload.location);
const step = new TeleTestStep(payload, parentStep, location); const step = new TeleTestStep(payload, parentStep, location);
@ -274,14 +271,14 @@ export class TeleReporterReceiver {
parentStep.steps.push(step); parentStep.steps.push(step);
else else
result.steps.push(step); result.steps.push(step);
result.stepMap.set(payload.id, step); result._stepMap.set(payload.id, step);
this._reporter.onStepBegin?.(test, result, step); this._reporter.onStepBegin?.(test, result, step);
} }
private _onStepEnd(testId: string, resultId: string, payload: JsonTestStepEnd) { private _onStepEnd(testId: string, resultId: string, payload: JsonTestStepEnd) {
const test = this._tests.get(testId)!; const test = this._tests.get(testId)!;
const result = test.resultsMap.get(resultId)!; const result = test._resultsMap.get(resultId)!;
const step = result.stepMap.get(payload.id)!; const step = result._stepMap.get(payload.id)!;
step.duration = payload.duration; step.duration = payload.duration;
step.error = payload.error; step.error = payload.error;
this._reporter.onStepEnd?.(test, result, step); this._reporter.onStepEnd?.(test, result, step);
@ -294,7 +291,7 @@ export class TeleReporterReceiver {
private _onStdIO(type: JsonStdIOType, testId: string | undefined, resultId: string | undefined, data: string, isBase64: boolean) { private _onStdIO(type: JsonStdIOType, testId: string | undefined, resultId: string | undefined, data: string, isBase64: boolean) {
const chunk = isBase64 ? ((globalThis as any).Buffer ? Buffer.from(data, 'base64') : atob(data)) : data; const chunk = isBase64 ? ((globalThis as any).Buffer ? Buffer.from(data, 'base64') : atob(data)) : data;
const test = testId ? this._tests.get(testId) : undefined; const test = testId ? this._tests.get(testId) : undefined;
const result = test && resultId ? test.resultsMap.get(resultId) : undefined; const result = test && resultId ? test._resultsMap.get(resultId) : undefined;
if (type === 'stdout') { if (type === 'stdout') {
result?.stdout.push(chunk); result?.stdout.push(chunk);
this._reporter.onStdOut?.(chunk, test, result); this._reporter.onStdOut?.(chunk, test, result);
@ -407,11 +404,7 @@ export class TeleReporterReceiver {
private _absolutePath(relativePath?: string): string | undefined { private _absolutePath(relativePath?: string): string | undefined {
if (relativePath === undefined) if (relativePath === undefined)
return; return;
return this._internString(this._rootDir + this._options.pathSeparator + relativePath); return this._options.resolvePath(this._rootDir, relativePath);
}
private _internString(s: string): string {
return this._options.internString ? this._options.internString(s) : s;
} }
} }
@ -475,7 +468,7 @@ export class TeleTestCase implements reporterTypes.TestCase {
repeatEachIndex = 0; repeatEachIndex = 0;
id: string; id: string;
resultsMap = new Map<string, TeleTestResult>(); _resultsMap = new Map<string, TeleTestResult>();
constructor(id: string, title: string, location: reporterTypes.Location, repeatEachIndex: number) { constructor(id: string, title: string, location: reporterTypes.Location, repeatEachIndex: number) {
this.id = id; this.id = id;
@ -515,13 +508,13 @@ export class TeleTestCase implements reporterTypes.TestCase {
_clearResults() { _clearResults() {
this.results = []; this.results = [];
this.resultsMap.clear(); this._resultsMap.clear();
} }
_createTestResult(id: string): TeleTestResult { _createTestResult(id: string): TeleTestResult {
const result = new TeleTestResult(this.results.length); const result = new TeleTestResult(this.results.length);
this.results.push(result); this.results.push(result);
this.resultsMap.set(id, result); this._resultsMap.set(id, result);
return result; return result;
} }
} }
@ -571,8 +564,7 @@ class TeleTestResult implements reporterTypes.TestResult {
errors: reporterTypes.TestResult['errors'] = []; errors: reporterTypes.TestResult['errors'] = [];
error: reporterTypes.TestResult['error']; error: reporterTypes.TestResult['error'];
stepMap: Map<string, reporterTypes.TestStep> = new Map(); _stepMap: Map<string, reporterTypes.TestStep> = new Map();
statusEx: reporterTypes.TestResult['status'] | 'scheduled' | 'running' = 'scheduled';
private _startTime: number = 0; private _startTime: number = 0;

View file

@ -50,7 +50,7 @@ export class BlobReporter extends TeleReporterEmitter {
private _reportName!: string; private _reportName!: string;
constructor(options: BlobReporterOptions) { constructor(options: BlobReporterOptions) {
super(message => this._messages.push(message), false); super(message => this._messages.push(message));
this._options = options; this._options = options;
if (this._options.fileName && !this._options.fileName.endsWith('.zip')) if (this._options.fileName && !this._options.fileName.endsWith('.zip'))
throw new Error(`Blob report file name must end with .zip extension: ${this._options.fileName}`); throw new Error(`Blob report file name must end with .zip extension: ${this._options.fileName}`);

View file

@ -54,10 +54,9 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
// If explicit config is provided, use platform path separator, otherwise use the one from the report (if any). // If explicit config is provided, use platform path separator, otherwise use the one from the report (if any).
const pathSeparator = rootDirOverride ? path.sep : (eventData.pathSeparatorFromMetadata ?? path.sep); const pathSeparator = rootDirOverride ? path.sep : (eventData.pathSeparatorFromMetadata ?? path.sep);
const receiver = new TeleReporterReceiver(multiplexer, { const receiver = new TeleReporterReceiver(multiplexer, {
pathSeparator,
mergeProjects: false, mergeProjects: false,
mergeTestCases: false, mergeTestCases: false,
internString: s => stringPool.internString(s), resolvePath: (rootDir, relativePath) => stringPool.internString(rootDir + pathSeparator + relativePath),
configOverrides: config.config, configOverrides: config.config,
}); });
printStatus(`processing test events`); printStatus(`processing test events`);

View file

@ -21,14 +21,19 @@ import type * as teleReceiver from '../isomorphic/teleReceiver';
import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
import type { ReporterV2 } from './reporterV2'; import type { ReporterV2 } from './reporterV2';
export type TeleReporterEmitterOptions = {
omitOutput?: boolean;
omitBuffers?: boolean;
};
export class TeleReporterEmitter implements ReporterV2 { export class TeleReporterEmitter implements ReporterV2 {
private _messageSink: (message: teleReceiver.JsonEvent) => void; private _messageSink: (message: teleReceiver.JsonEvent) => void;
private _rootDir!: string; private _rootDir!: string;
private _skipBuffers: boolean; private _emitterOptions: TeleReporterEmitterOptions;
constructor(messageSink: (message: teleReceiver.JsonEvent) => void, skipBuffers: boolean) { constructor(messageSink: (message: teleReceiver.JsonEvent) => void, options: TeleReporterEmitterOptions = {}) {
this._messageSink = messageSink; this._messageSink = messageSink;
this._skipBuffers = skipBuffers; this._emitterOptions = options;
} }
version(): 'v2' { version(): 'v2' {
@ -113,6 +118,8 @@ export class TeleReporterEmitter implements ReporterV2 {
} }
private _onStdIO(type: teleReceiver.JsonStdIOType, chunk: string | Buffer, test: void | reporterTypes.TestCase, result: void | reporterTypes.TestResult): void { private _onStdIO(type: teleReceiver.JsonStdIOType, chunk: string | Buffer, test: void | reporterTypes.TestCase, result: void | reporterTypes.TestResult): void {
if (this._emitterOptions.omitOutput)
return;
const isBase64 = typeof chunk !== 'string'; const isBase64 = typeof chunk !== 'string';
const data = isBase64 ? chunk.toString('base64') : chunk; const data = isBase64 ? chunk.toString('base64') : chunk;
this._messageSink({ this._messageSink({
@ -224,7 +231,7 @@ export class TeleReporterEmitter implements ReporterV2 {
return { return {
...a, ...a,
// There is no Buffer in the browser, so there is no point in sending the data there. // There is no Buffer in the browser, so there is no point in sending the data there.
base64: (a.body && !this._skipBuffers) ? a.body.toString('base64') : undefined, base64: (a.body && !this._emitterOptions.omitBuffers) ? a.body.toString('base64') : undefined,
}; };
}); });
} }

View file

@ -88,7 +88,7 @@ export async function createReporterForTestServer(config: FullConfigInternal, fi
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', send?: (message: any) => void) { function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', send?: (message: any) => void) {
return { return {
configDir: config.configDir, configDir: config.configDir,
send, _send: send,
_mode: mode, _mode: mode,
}; };
} }

View file

@ -114,13 +114,13 @@ class Dispatcher implements TestServerInterface {
async listTests(params: { async listTests(params: {
configFile: string; configFile: string;
locations: string[]; locations: string[];
reporters: { file: string, event: string }[]; reporter: string;
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
}) { }) {
const config = await this._loadConfig(params.configFile); const config = await this._loadConfig(params.configFile);
config.cliArgs = params.locations || []; config.cliArgs = params.locations || [];
const wireReporters = await this._wireReporters(config, 'list', params.reporters); const wireReporter = await createReporterForTestServer(config, params.reporter, 'list', message => this._dispatchEvent('report', message));
const reporter = new InternalReporter(new Multiplexer(wireReporters)); const reporter = new InternalReporter(new Multiplexer([wireReporter]));
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true }); const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: true });
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config); reporter.onConfigure(config.config);
@ -138,7 +138,7 @@ class Dispatcher implements TestServerInterface {
async test(params: { async test(params: {
configFile: string; configFile: string;
locations: string[]; locations: string[];
reporters: { file: string, event: string }[]; reporter: string;
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
headed?: boolean; headed?: boolean;
oneWorker?: boolean; oneWorker?: boolean;
@ -169,9 +169,9 @@ class Dispatcher implements TestServerInterface {
config.cliGrep = params.grep; config.cliGrep = params.grep;
config.cliProjectFilter = params.projects?.length ? params.projects : undefined; config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
const wireReporters = await this._wireReporters(config, 'test', params.reporters); const wireReporter = await createReporterForTestServer(config, params.reporter, 'test', message => this._dispatchEvent('report', message));
const configReporters = await createReporters(config, 'test'); const configReporters = await createReporters(config, 'test');
const reporter = new InternalReporter(new Multiplexer([...configReporters, ...wireReporters])); const reporter = new InternalReporter(new Multiplexer([...configReporters, wireReporter]));
const taskRunner = createTaskRunnerForTestServer(config, reporter); const taskRunner = createTaskRunnerForTestServer(config, reporter);
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config); reporter.onConfigure(config.config);
@ -186,14 +186,6 @@ class Dispatcher implements TestServerInterface {
await run; await run;
} }
private async _wireReporters(config: FullConfigInternal, mode: 'test' | 'list', reporters: { file: string, event: string }[]) {
return await Promise.all(reporters.map(r => {
return createReporterForTestServer(config, r.file, mode, message => {
this._dispatchEvent(r.event, message);
});
}));
}
async findRelatedTestFiles(params: { async findRelatedTestFiles(params: {
configFile: string; configFile: string;
files: string[]; files: string[];

View file

@ -33,13 +33,13 @@ export interface TestServerInterface {
listTests(params: { listTests(params: {
configFile: string; configFile: string;
locations: string[]; locations: string[];
reporters: { file: string, event: string }[]; reporter: string;
}): Promise<void>; }): Promise<void>;
test(params: { test(params: {
configFile: string; configFile: string;
locations: string[]; locations: string[];
reporters: { file: string, event: string }[]; reporter: string;
headed?: boolean; headed?: boolean;
oneWorker?: boolean; oneWorker?: boolean;
trace?: 'on' | 'off'; trace?: 'on' | 'off';

View file

@ -167,7 +167,7 @@ class UIMode {
} }
private async _listTests() { private async _listTests() {
const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), true)); const reporter = new InternalReporter(new TeleReporterEmitter(e => this._dispatchEvent('listReport', e), { omitBuffers: true }));
this._config.cliListOnly = true; this._config.cliListOnly = true;
this._config.testIdMatcher = undefined; this._config.testIdMatcher = undefined;
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false }); const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
@ -195,7 +195,7 @@ class UIMode {
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id); this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
const reporters = await createReporters(this._config, 'ui'); const reporters = await createReporters(this._config, 'ui');
reporters.push(new TeleReporterEmitter(e => this._dispatchEvent('testReport', e), true)); reporters.push(new TeleReporterEmitter(e => this._dispatchEvent('testReport', e), { omitBuffers: true }));
const reporter = new InternalReporter(new Multiplexer(reporters)); const reporter = new InternalReporter(new Multiplexer(reporters));
const taskRunner = createTaskRunnerForWatch(this._config, reporter); const taskRunner = createTaskRunnerForWatch(this._config, reporter);
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);

View file

@ -22,7 +22,7 @@ import { TreeView } from '@web/components/treeView';
import type { TreeState } from '@web/components/treeView'; import type { TreeState } from '@web/components/treeView';
import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver'; import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver';
import type { TeleTestCase } from '@testIsomorphic/teleReceiver'; import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
import type { FullConfig, Suite, TestCase, Location, TestError } from 'playwright/types/testReporter'; import type { FullConfig, Suite, TestCase, Location, TestError, TestResult } from 'playwright/types/testReporter';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
import { idForAction, MultiTraceModel } from './modelUtil'; import { idForAction, MultiTraceModel } from './modelUtil';
import type { SourceLocation } from './modelUtil'; import type { SourceLocation } from './modelUtil';
@ -151,7 +151,8 @@ export const UIModeView: React.FC<{}> = ({
for (const test of testModel.rootSuite?.allTests() || []) { for (const test of testModel.rootSuite?.allTests() || []) {
if (testIds.has(test.id)) { if (testIds.has(test.id)) {
(test as TeleTestCase)._clearResults(); (test as TeleTestCase)._clearResults();
(test as TeleTestCase)._createTestResult('pending'); const result = (test as TeleTestCase)._createTestResult('pending');
(result as any)[statusEx] = 'scheduled';
} }
} }
setTestModel({ ...testModel }); setTestModel({ ...testModel });
@ -655,9 +656,9 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
lastRunReceiver = undefined; lastRunReceiver = undefined;
} }
}, { }, {
pathSeparator,
mergeProjects: true, mergeProjects: true,
mergeTestCases: false mergeTestCases: false,
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
}); });
}, },
@ -675,17 +676,19 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true); throttleUpdateRootSuite(config, rootSuite, loadErrors, progress, true);
}, },
onTestBegin: () => { onTestBegin: (test: TestCase, testResult: TestResult) => {
(testResult as any)[statusEx] = 'running';
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress); throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
}, },
onTestEnd: (test: TestCase) => { onTestEnd: (test: TestCase, testResult: TestResult) => {
if (test.outcome() === 'skipped') if (test.outcome() === 'skipped')
++progress.skipped; ++progress.skipped;
else if (test.outcome() === 'unexpected') else if (test.outcome() === 'unexpected')
++progress.failed; ++progress.failed;
else else
++progress.passed; ++progress.passed;
(testResult as any)[statusEx] = testResult.status;
throttleUpdateRootSuite(config, rootSuite, loadErrors, progress); throttleUpdateRootSuite(config, rootSuite, loadErrors, progress);
}, },
@ -705,9 +708,9 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
onStepBegin: () => {}, onStepBegin: () => {},
onStepEnd: () => {}, onStepEnd: () => {},
}, { }, {
pathSeparator,
mergeProjects: true, mergeProjects: true,
mergeTestCases: true, mergeTestCases: true,
resolvePath: (rootDir, relativePath) => rootDir + pathSeparator + relativePath,
}); });
receiver._setClearPreviousResultsWhenTestBegins(); receiver._setClearPreviousResultsWhenTestBegins();
return sendMessage('list', {}); return sendMessage('list', {});
@ -906,11 +909,11 @@ function createTree(rootSuite: Suite | undefined, loadErrors: TestError[], proje
parentGroup.children.push(testCaseItem); parentGroup.children.push(testCaseItem);
} }
const result = (test as TeleTestCase).results[0]; const result = test.results[0];
let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none'; let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
if (result?.statusEx === 'scheduled') if ((result as any)?.[statusEx] === 'scheduled')
status = 'scheduled'; status = 'scheduled';
else if (result?.statusEx === 'running') else if ((result as any)?.[statusEx] === 'running')
status = 'running'; status = 'running';
else if (result?.status === 'skipped') else if (result?.status === 'skipped')
status = 'skipped'; status = 'skipped';
@ -1053,3 +1056,4 @@ async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
} }
const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/'; const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';
const statusEx = Symbol('statusEx');