playwright/test-runner/src/reporters/pytest.ts
2020-08-28 00:32:00 -07:00

255 lines
7.6 KiB
TypeScript

/**
* 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 colors from 'colors/safe';
import milliseconds from 'ms';
import * as path from 'path';
import { Test, Suite, Configuration, TestResult } from '../test';
import { BaseReporter } from './base';
import { RunnerConfig } from '../runnerConfig';
const cursorPrevLine = '\u001B[F';
const eraseLine = '\u001B[2K';
type Row = {
id: string;
relativeFile: string;
configuration: string;
ordinal: number;
track: string[];
total: number;
failed: boolean;
startTime: number;
finishTime: number;
};
const statusRows = 2;
class PytestReporter extends BaseReporter {
private _rows = new Map<string, Row>();
private _suiteIds = new Map<Suite, string>();
private _lastOrdinal = 0;
private _visibleRows: number;
private _total: number;
private _progress: string[] = [];
private _throttler = new Throttler(250, () => this._repaint());
onBegin(config: RunnerConfig, rootSuite: Suite) {
super.onBegin(config, rootSuite);
this._total = rootSuite.total();
const jobs = Math.min(config.jobs, rootSuite.suites.length);
this._visibleRows = jobs + Math.min(jobs, 3); // 3 buffer rows for completed (green) workers.
for (let i = 0; i < this._visibleRows + statusRows; ++i) // 4 rows for status
process.stdout.write('\n');
for (const s of rootSuite.suites) {
const relativeFile = path.relative(this.config.testDir, s.file);
const configurationString = serializeConfiguration(s.configuration);
const id = relativeFile + `::[${configurationString}]`;
this._suiteIds.set(s, id);
const row = {
id,
relativeFile,
configuration: configurationString,
ordinal: this._lastOrdinal++,
track: [],
total: s.total(),
failed: false,
startTime: 0,
finishTime: 0,
};
this._rows.set(id, row);
}
}
onTestBegin(test: Test) {
super.onTestBegin(test);
const row = this._rows.get(this._id(test));
if (!row.startTime)
row.startTime = Date.now();
}
onTestStdOut(test: Test, chunk: string | Buffer) {
this._repaint(chunk);
}
onTestStdErr(test: Test, chunk: string | Buffer) {
this._repaint(chunk);
}
onTestEnd(test: Test, result: TestResult) {
super.onTestEnd(test, result);
switch (result.status) {
case 'skipped': {
this._append(test, colors.yellow('∘'));
this._progress.push('S');
this._throttler.schedule();
break;
}
case 'passed': {
this._append(test, colors.green('✓'));
this._progress.push('P');
this._throttler.schedule();
break;
}
case 'failed':
// fall through
case 'timedOut': {
const title = result.status === 'timedOut' ? colors.red('T') : colors.red('F');
const row = this._append(test, title);
row.failed = true;
this._progress.push('F');
this._repaint(this.formatFailure(test) + '\n');
break;
}
}
}
private _append(test: Test, s: string): Row {
const testId = this._id(test);
const row = this._rows.get(testId);
row.track.push(s);
if (row.track.length === row.total)
row.finishTime = Date.now();
return row;
}
private _repaint(prependChunk?: string | Buffer) {
const rowList = [...this._rows.values()];
const running = rowList.filter(r => r.startTime && !r.finishTime);
const finished = rowList.filter(r => r.finishTime).sort((a, b) => b.finishTime - a.finishTime);
const finishedToPrint = finished.slice(0, this._visibleRows - running.length);
const lines = [];
for (const row of finishedToPrint.concat(running)) {
const remaining = row.total - row.track.length;
const remainder = '·'.repeat(remaining);
let title = row.relativeFile;
if (row.finishTime) {
if (row.failed)
title = colors.red(row.relativeFile);
else
title = colors.green(row.relativeFile);
}
const configuration = ` [${colors.gray(row.configuration)}]`;
lines.push(' ' + title + configuration + ' ' + row.track.join('') + colors.gray(remainder));
}
const status = [];
if (this.asExpected.length)
status.push(colors.green(`${this.asExpected.length} as expected`));
if (this.skipped.length)
status.push(colors.yellow(`${this.skipped.length} skipped`));
const timedOut = [...this.unexpected].filter(t => t._hasResultWithStatus('timedOut'));
if (this.unexpected.size - timedOut.length)
status.push(colors.red(`${this.unexpected.size - timedOut.length} unexpected failures`));
if (timedOut.length)
status.push(colors.red(`${timedOut.length} timed out`));
status.push(colors.dim(`(${milliseconds(Date.now() - this.startTime)})`));
for (let i = lines.length; i < this._visibleRows; ++i)
lines.push('');
lines.push(this._paintProgress(this._progress.length, this._total));
lines.push(status.join(' '));
lines.push('');
process.stdout.write((cursorPrevLine + eraseLine).repeat(this._visibleRows + statusRows));
if (prependChunk)
process.stdout.write(prependChunk);
process.stdout.write(lines.join('\n'));
}
private _id(test: Test): string {
for (let suite = test.suite; suite; suite = suite.parent) {
if (this._suiteIds.has(suite))
return this._suiteIds.get(suite);
}
return '';
}
private _paintProgress(worked: number, total: number) {
const length = Math.min(total, 80);
const cellSize = Math.ceil(total / length);
const cellNum = (total / cellSize) | 0;
const bars: string[] = [];
for (let i = 0; i < cellNum; ++i) {
let bar = blankBar;
if (worked < cellSize * i) {
bars.push(bar);
continue;
}
bar = greenBar;
for (let j = i * cellSize; j < worked && j < (i + 1) * cellSize; ++j) {
if (worked < j)
continue;
if (this._progress[j] === 'F') {
bar = redBar;
break;
}
if (this._progress[j] === 'S') {
bar = yellowBar;
break;
}
}
bars.push(bar);
}
return '[' + bars.join('') + '] ' + worked + '/' + total;
}
}
const blankBar = '-';
const redBar = colors.red('▇');
const greenBar = colors.green('▇');
const yellowBar = colors.yellow('▇');
function serializeConfiguration(configuration: Configuration): string {
const tokens = [];
for (const { name, value } of configuration)
tokens.push(`${name}=${value}`);
return tokens.join(', ');
}
class Throttler {
private _timeout: number;
private _callback: () => void;
private _lastFire = 0;
private _timer: NodeJS.Timeout | null = null;
constructor(timeout: number, callback: () => void) {
this._timeout = timeout;
this._callback = callback;
}
schedule() {
const time = Date.now();
const timeRemaining = this._lastFire + this._timeout - time;
if (timeRemaining <= 0) {
this._fire();
return;
}
if (!this._timer)
this._timer = setTimeout(() => this._fire(), timeRemaining);
}
private _fire() {
this._timer = null;
this._lastFire = Date.now();
this._callback();
}
}
export default PytestReporter;