adapt LastRunReporter

This commit is contained in:
Mathias Leppich 2024-09-11 20:23:24 +02:00
parent ea72517529
commit b78b88fbac
8 changed files with 40 additions and 107 deletions

View file

@ -24,7 +24,7 @@ import { getPackageJsonPath, mergeObjects } from '../util';
import type { Matcher } from '../util';
import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig, FullProject } from '../../types/testReporter';
import type { LastRunInfo } from '../runner/runner';
import { LastRunReporter } from '../runner/lastRun';
export type ConfigLocation = {
resolvedConfigFile?: string;
@ -59,7 +59,7 @@ export class FullConfigInternal {
defineConfigWasUsed = false;
shardingMode: ShardingMode;
lastRunFile: string | undefined;
lastRunInfo?: LastRunInfo;
readonly lastRunReporter: LastRunReporter;
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
if (configCLIOverrides.projects && userConfig.projects)
@ -135,6 +135,8 @@ export class FullConfigInternal {
resolveProjectDependencies(this.projects);
this._assignUniqueProjectIds(this.projects);
this.config.projects = this.projects.map(p => p.project);
this.lastRunReporter = new LastRunReporter(this);
}
private _assignUniqueProjectIds(projects: FullProjectInternal[]) {

View file

@ -1,73 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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, TestCase, TestResult } from '../../types/testReporter';
import type { LastRunInfo } from '../runner/runner';
import { BaseReporter, resolveOutputFile } from './base';
type LastRunOptions = {
outputFile?: string,
configDir: string,
_mode: 'list' | 'test' | 'merge',
};
class LastRunReporter extends BaseReporter {
private lastRun: LastRunInfo = {
failedTests: [],
status: 'passed',
testDurations: {},
};
private resolvedOutputFile: string | undefined;
constructor(options: LastRunOptions) {
super();
this.resolvedOutputFile = resolveOutputFile('LASTRUN', { fileName: '.last-run.json', ...options })?.outputFile;
}
override printsToStdio() {
return !this.resolvedOutputFile;
}
override onTestEnd(test: TestCase, result: TestResult): void {
super.onTestEnd(test, result);
this.lastRun.testDurations![test.id] = result.duration;
if (result.status === 'failed')
this.lastRun.failedTests.push(test.id);
}
override async onEnd(result: FullResult) {
await super.onEnd(result);
this.lastRun.status = result.status;
await this.outputReport(this.lastRun, this.resolvedOutputFile);
}
async outputReport(lastRun: LastRunInfo, resolvedOutputFile: string | undefined) {
const reportString = JSON.stringify(lastRun, undefined, 2);
if (resolvedOutputFile) {
await fs.promises.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
await fs.promises.writeFile(resolvedOutputFile, reportString);
} else {
console.log(reportString);
}
}
}
export default LastRunReporter;

View file

@ -39,7 +39,7 @@ type ReportData = {
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], rootDirOverride: string | undefined) {
const reporters = await createReporters(config, 'merge', false, reporterDescriptions);
const multiplexer = new Multiplexer(reporters);
const multiplexer = new Multiplexer([...reporters, config.lastRunReporter]);
const stringPool = new StringInternPool();
let printStatus: StatusCallback = () => {};

View file

@ -21,9 +21,10 @@ import { filterProjects } from './projectUtils';
import type { FullConfigInternal } from '../common/config';
import type { ReporterV2 } from '../reporters/reporterV2';
type LastRunInfo = {
export type LastRunInfo = {
status: FullResult['status'];
failedTests: string[];
testDurations?: { [testId: string]: number };
};
export class LastRunReporter implements ReporterV2 {
@ -33,21 +34,32 @@ export class LastRunReporter implements ReporterV2 {
constructor(config: FullConfigInternal) {
this._config = config;
if (config.lastRunFile) {
// specified via command line argument
this._lastRunFile = config.lastRunFile;
} else {
const [project] = filterProjects(config.projects, config.cliProjectFilter);
if (project)
this._lastRunFile = path.join(project.project.outputDir, '.last-run.json');
}
}
async filterLastFailed() {
async lastRunInfo(): Promise<LastRunInfo | undefined> {
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);
return JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8'));
} catch {
}
}
async filterLastFailed() {
const lastRunInfo = await this.lastRunInfo();
if (!lastRunInfo)
return;
this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
}
version(): 'v2' {
return 'v2';
}
@ -65,7 +77,11 @@ export class LastRunReporter implements ReporterV2 {
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);
const testDurations = this._suite?.allTests().reduce((map, t) => {
map[t.id] = t.results.map(r => r.duration).reduce((a, b) => a + b, 0);
return map;
}, {} as { [key: string]: number });
const lastRunReport = JSON.stringify({ status: result.status, failedTests, testDurations }, undefined, 2);
await fs.promises.writeFile(this._lastRunFile, lastRunReport);
}
}

View file

@ -180,7 +180,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
testGroups.push(...createTestGroups(projectSuite, config.config.workers));
// Shard test groups.
const testGroupsInThisShard = filterForShard(config.shardingMode, config.config.shard, testGroups, config.lastRunInfo);
const testGroupsInThisShard = await filterForShard(config, testGroups);
const testsInThisShard = new Set<TestCase>();
for (const group of testGroupsInThisShard) {
for (const test of group.tests)

View file

@ -23,7 +23,6 @@ import GitHubReporter from '../reporters/github';
import HtmlReporter from '../reporters/html';
import JSONReporter from '../reporters/json';
import JUnitReporter from '../reporters/junit';
import LastRunReporter from '../reporters/lastrun';
import LineReporter from '../reporters/line';
import ListReporter from '../reporters/list';
import MarkdownReporter from '../reporters/markdown';
@ -77,13 +76,6 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
else if (mode !== 'merge')
reporters.unshift(!process.env.CI ? new LineReporter({ omitFailures: true }) : new DotReporter());
}
if (!isTestServer && (mode === 'test' || mode === 'merge')) {
// If we are not in the test server, always add a last-run reporter.
const lastRunOutputFile = config.lastRunFile ?? config.projects.length >= 0 ? path.join(config.projects[0].project.outputDir, '.last-run.json') : undefined;
reporters.push(new LastRunReporter({ ...runOptions, outputFile: lastRunOutputFile }));
}
return reporters;
}

View file

@ -24,7 +24,6 @@ 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;
@ -75,11 +74,10 @@ export class Runner {
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const reporters = await createReporters(config, listOnly ? 'list' : 'test', false);
const lastRun = new LastRunReporter(config);
if (config.cliLastFailed)
await lastRun.filterLastFailed();
await config.lastRunReporter.filterLastFailed();
const reporter = new InternalReporter([...reporters, lastRun]);
const reporter = new InternalReporter([...reporters, config.lastRunReporter]);
const taskRunner = listOnly ? createTaskRunnerForList(
config,
reporter,

View file

@ -14,9 +14,9 @@
* limitations under the License.
*/
import type { PlaywrightTestConfig } from '../../types/test';
import type { FullConfigInternal } from '../common/config';
import type { Suite, TestCase } from '../common/test';
import type { LastRunInfo } from './runner';
import type { LastRunInfo } from './lastRun';
export type TestGroup = {
workerHash: string;
@ -132,23 +132,21 @@ export function createTestGroups(projectSuite: Suite, workers: number): TestGrou
return result;
}
export function filterForShard(
mode: PlaywrightTestConfig['shardingMode'],
shard: { total: number, current: number },
testGroups: TestGroup[],
lastRunInfo?: LastRunInfo,
): Set<TestGroup> {
export async function filterForShard(config: FullConfigInternal, testGroups: TestGroup[]): Promise<Set<TestGroup>> {
// Note that sharding works based on test groups.
// This means parallel files will be sharded by single tests,
// while non-parallel files will be sharded by the whole file.
//
// Shards are still balanced by the number of tests, not files,
// even in the case of non-paralleled files.
const mode = config.shardingMode;
const shard = config.config.shard!;
if (mode === 'round-robin')
return filterForShardRoundRobin(shard, testGroups);
if (mode === 'duration-round-robin')
if (mode === 'duration-round-robin') {
const lastRunInfo = await config.lastRunReporter.lastRunInfo();
return filterForShardRoundRobin(shard, testGroups, lastRunInfo);
}
return filterForShardPartition(shard, testGroups);
}