From 356517cddbd8f71946065da19b3f7a64b5d0c4df Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 10 Sep 2024 12:14:44 -0700 Subject: [PATCH] chore(test runner): extract LastRunReporter (#32540) --- packages/playwright/src/common/config.ts | 1 + packages/playwright/src/program.ts | 8 +-- packages/playwright/src/runner/lastRun.ts | 71 +++++++++++++++++++++++ packages/playwright/src/runner/runner.ts | 42 ++------------ 4 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 packages/playwright/src/runner/lastRun.ts diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index d9e218a3da..d7fb499645 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -53,6 +53,7 @@ export class FullConfigInternal { cliListOnly = false; cliPassWithNoTests?: boolean; cliFailOnFlakyTests?: boolean; + cliLastFailed?: boolean; testIdMatcher?: Matcher; defineConfigWasUsed = false; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index e57c0a3328..1bf2fb42b2 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -19,7 +19,7 @@ import type { Command } from 'playwright-core/lib/utilsBundle'; import fs from 'fs'; import path from 'path'; -import { Runner, readLastRunInfo } from './runner/runner'; +import { Runner } from './runner/runner'; import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; import { serializeError } from './util'; import { showHTMLReport } from './reporters/html'; @@ -207,11 +207,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) { if (!config) return; - if (opts.lastFailed) { - const lastRunInfo = await readLastRunInfo(config); - config.testIdMatcher = id => lastRunInfo.failedTests.includes(id); - } - config.cliArgs = args; config.cliGrep = opts.grep as string | undefined; config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged; @@ -220,6 +215,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliProjectFilter = opts.project || undefined; config.cliPassWithNoTests = !!opts.passWithNoTests; config.cliFailOnFlakyTests = !!opts.failOnFlakyTests; + config.cliLastFailed = !!opts.lastFailed; const runner = new Runner(config); const status = await runner.runAllTests(); diff --git a/packages/playwright/src/runner/lastRun.ts b/packages/playwright/src/runner/lastRun.ts new file mode 100644 index 0000000000..407543041e --- /dev/null +++ b/packages/playwright/src/runner/lastRun.ts @@ -0,0 +1,71 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import type { FullResult, Suite } from '../../types/testReporter'; +import { filterProjects } from './projectUtils'; +import type { FullConfigInternal } from '../common/config'; +import type { ReporterV2 } from '../reporters/reporterV2'; + +type LastRunInfo = { + status: FullResult['status']; + failedTests: string[]; +}; + +export class LastRunReporter implements ReporterV2 { + private _config: FullConfigInternal; + private _lastRunFile: string | undefined; + private _suite: Suite | undefined; + + constructor(config: FullConfigInternal) { + this._config = config; + const [project] = filterProjects(config.projects, config.cliProjectFilter); + if (project) + this._lastRunFile = path.join(project.project.outputDir, '.last-run.json'); + } + + async filterLastFailed() { + if (!this._lastRunFile) + return; + try { + const lastRunInfo = JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8')) as LastRunInfo; + this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id); + } catch { + } + } + + version(): 'v2' { + return 'v2'; + } + + printsToStdio() { + return false; + } + + onBegin(suite: Suite) { + this._suite = suite; + } + + async onEnd(result: FullResult) { + if (!this._lastRunFile || this._config.cliListOnly) + return; + await fs.promises.mkdir(path.dirname(this._lastRunFile), { recursive: true }); + const failedTests = this._suite?.allTests().filter(t => !t.ok()).map(t => t.id); + const lastRunReport = JSON.stringify({ status: result.status, failedTests }, undefined, 2); + await fs.promises.writeFile(this._lastRunFile, lastRunReport); + } +} diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index 2744fd2730..22f8204592 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -15,8 +15,6 @@ * limitations under the License. */ -import fs from 'fs'; -import path from 'path'; import { monotonicTime } from 'playwright-core/lib/utils'; import type { FullResult, TestError } from '../../types/testReporter'; import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; @@ -26,6 +24,7 @@ import { TestRun, createTaskRunner, createTaskRunnerForClearCache, createTaskRun import type { FullConfigInternal } from '../common/config'; import { affectedTestFiles } from '../transform/compilationCache'; import { InternalReporter } from '../reporters/internalReporter'; +import { LastRunReporter } from './lastRun'; type ProjectConfigWithFiles = { name: string; @@ -76,7 +75,11 @@ export class Runner { webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); const reporters = await createReporters(config, listOnly ? 'list' : 'test', false); - const reporter = new InternalReporter(reporters); + const lastRun = new LastRunReporter(config); + if (config.cliLastFailed) + await lastRun.filterLastFailed(); + + const reporter = new InternalReporter([...reporters, lastRun]); const taskRunner = listOnly ? createTaskRunnerForList( config, reporter, @@ -94,9 +97,6 @@ export class Runner { if (modifiedResult && modifiedResult.status) status = modifiedResult.status; - if (!listOnly) - await writeLastRunInfo(testRun, status); - await reporter.onExit(); // Calling process.exit() might truncate large stdout/stderr output. @@ -143,33 +143,3 @@ export class Runner { return { status }; } } - -export type LastRunInfo = { - status: FullResult['status']; - failedTests: string[]; -}; - -async function writeLastRunInfo(testRun: TestRun, status: FullResult['status']) { - const [project] = filterProjects(testRun.config.projects, testRun.config.cliProjectFilter); - if (!project) - return; - const outputDir = project.project.outputDir; - await fs.promises.mkdir(outputDir, { recursive: true }); - const lastRunReportFile = path.join(outputDir, '.last-run.json'); - const failedTests = testRun.rootSuite?.allTests().filter(t => !t.ok()).map(t => t.id); - const lastRunReport = JSON.stringify({ status, failedTests }, undefined, 2); - await fs.promises.writeFile(lastRunReportFile, lastRunReport); -} - -export async function readLastRunInfo(config: FullConfigInternal): Promise { - const [project] = filterProjects(config.projects, config.cliProjectFilter); - if (!project) - return { status: 'passed', failedTests: [] }; - const outputDir = project.project.outputDir; - try { - const lastRunReportFile = path.join(outputDir, '.last-run.json'); - return JSON.parse(await fs.promises.readFile(lastRunReportFile, 'utf8')) as LastRunInfo; - } catch { - } - return { status: 'passed', failedTests: [] }; -}