feat(testrunner): expose test and runner config to fixtures (#3580)

This commit is contained in:
Pavel Feldman 2020-08-22 16:44:56 -07:00 committed by GitHub
parent f4e8f34c96
commit 4025f9f1ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 196 additions and 156 deletions

View File

@ -17,6 +17,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import program from 'commander'; import program from 'commander';
import { reporters } from './reporters';
import { installTransform } from './transform'; import { installTransform } from './transform';
import { Runner } from './runner'; import { Runner } from './runner';
import { TestCollector } from './testCollector'; import { TestCollector } from './testCollector';
@ -90,13 +91,15 @@ program
grep: command.grep, grep: command.grep,
jobs, jobs,
outputDir: command.output, outputDir: command.output,
reporter: command.reporter,
snapshotDir: path.join(testDir, '__snapshots__'), snapshotDir: path.join(testDir, '__snapshots__'),
testDir, testDir,
timeout: command.timeout, timeout: command.timeout,
trialRun: command.trialRun, trialRun: command.trialRun,
updateSnapshots: command.updateSnapshots updateSnapshots: command.updateSnapshots
}); });
const reporterFactory = reporters[command.reporter || 'dot'];
new reporterFactory(runner);
try { try {
if (beforeFunction) if (beforeFunction)
await beforeFunction(); await beforeFunction();

View File

@ -39,32 +39,21 @@ export function setParameters(params: any) {
registerWorkerFixture(name as keyof WorkerState, async ({}, test) => await test(parameters[name] as never)); registerWorkerFixture(name as keyof WorkerState, async ({}, test) => await test(parameters[name] as never));
} }
type TestConfig = { class Fixture<Config> {
outputDir: string; pool: FixturePool<Config>;
testDir: string;
};
type TestResult = {
success: boolean;
test: Test;
config: TestConfig;
error?: Error;
};
class Fixture {
pool: FixturePool;
name: string; name: string;
scope: any; scope: string;
fn: any; fn: Function;
deps: any; deps: string[];
usages: Set<unknown>; usages: Set<string>;
hasGeneratorValue: boolean; hasGeneratorValue: boolean;
value: any; value: any;
_teardownFenceCallback: (value?: unknown) => void; _teardownFenceCallback: (value?: unknown) => void;
_tearDownComplete: any; _tearDownComplete: Promise<void>;
_setup: boolean; _setup = false;
_teardown: any; _teardown = false;
constructor(pool: FixturePool, name: string, scope: any, fn: any) {
constructor(pool: FixturePool<Config>, name: string, scope: string, fn: any) {
this.pool = pool; this.pool = pool;
this.name = name; this.name = name;
this.scope = scope; this.scope = scope;
@ -75,11 +64,11 @@ class Fixture {
this.value = this.hasGeneratorValue ? parameters[name] : null; this.value = this.hasGeneratorValue ? parameters[name] : null;
} }
async setup() { async setup(config: Config, test?: Test) {
if (this.hasGeneratorValue) if (this.hasGeneratorValue)
return; return;
for (const name of this.deps) { for (const name of this.deps) {
await this.pool.setupFixture(name); await this.pool.setupFixture(name, config, test);
this.pool.instances.get(name).usages.add(this.name); this.pool.instances.get(name).usages.add(this.name);
} }
@ -95,12 +84,12 @@ class Fixture {
this.value = value; this.value = value;
setupFenceFulfill(); setupFenceFulfill();
return await teardownFence; return await teardownFence;
}).catch((e: any) => setupFenceReject(e)); }, config, test).catch((e: any) => setupFenceReject(e));
await setupFence; await setupFence;
this._setup = true; this._setup = true;
} }
async teardown(testResult: TestResult) { async teardown() {
if (this.hasGeneratorValue) if (this.hasGeneratorValue)
return; return;
if (this._teardown) if (this._teardown)
@ -110,24 +99,24 @@ class Fixture {
const fixture = this.pool.instances.get(name); const fixture = this.pool.instances.get(name);
if (!fixture) if (!fixture)
continue; continue;
await fixture.teardown(testResult); await fixture.teardown();
} }
if (this._setup) { if (this._setup) {
debug('pw:test:hook')(`teardown "${this.name}"`); debug('pw:test:hook')(`teardown "${this.name}"`);
this._teardownFenceCallback(testResult); this._teardownFenceCallback();
} }
await this._tearDownComplete; await this._tearDownComplete;
this.pool.instances.delete(this.name); this.pool.instances.delete(this.name);
} }
} }
export class FixturePool { export class FixturePool<Config> {
instances: Map<any, any>; instances: Map<string, Fixture<Config>>;
constructor() { constructor() {
this.instances = new Map(); this.instances = new Map();
} }
async setupFixture(name: string) { async setupFixture(name: string, config: Config, test?: Test) {
let fixture = this.instances.get(name); let fixture = this.instances.get(name);
if (fixture) if (fixture)
return fixture; return fixture;
@ -137,21 +126,21 @@ export class FixturePool {
const { scope, fn } = registrations.get(name); const { scope, fn } = registrations.get(name);
fixture = new Fixture(this, name, scope, fn); fixture = new Fixture(this, name, scope, fn);
this.instances.set(name, fixture); this.instances.set(name, fixture);
await fixture.setup(); await fixture.setup(config, test);
return fixture; return fixture;
} }
async teardownScope(scope: string, testResult?: TestResult) { async teardownScope(scope: string) {
for (const [name, fixture] of this.instances) { for (const [name, fixture] of this.instances) {
if (fixture.scope === scope) if (fixture.scope === scope)
await fixture.teardown(testResult); await fixture.teardown();
} }
} }
async resolveParametersAndRun(fn: (arg0: {}) => any, timeout: number) { async resolveParametersAndRun(fn: (arg0: {}) => any, timeout: number, config: Config, test?: Test) {
const names = fixtureParameterNames(fn); const names = fixtureParameterNames(fn);
for (const name of names) for (const name of names)
await this.setupFixture(name); await this.setupFixture(name, config, test);
const params = {}; const params = {};
for (const n of names) for (const n of names)
params[n] = this.instances.get(n).value; params[n] = this.instances.get(n).value;
@ -167,19 +156,14 @@ export class FixturePool {
]); ]);
} }
wrapTestCallback(callback: any, timeout: number, test: Test, config: TestConfig) { wrapTestCallback(callback: any, timeout: number, config: Config, test: Test) {
if (!callback) if (!callback)
return callback; return callback;
const testResult: TestResult = { success: true, test, config };
return async() => { return async() => {
try { try {
await this.resolveParametersAndRun(callback, timeout); await this.resolveParametersAndRun(callback, timeout, config, test);
} catch (e) {
testResult.success = false;
testResult.error = e;
throw e;
} finally { } finally {
await this.teardownScope('test', testResult); await this.teardownScope('test');
} }
}; };
} }
@ -205,7 +189,7 @@ export function fixturesForCallback(callback: any): string[] {
return result; return result;
} }
function fixtureParameterNames(fn: { toString: () => any; }) { function fixtureParameterNames(fn: { toString: () => any; }): string[] {
const text = fn.toString(); const text = fn.toString();
const match = text.match(/async(?:\s+function)?\s*\(\s*{\s*([^}]*)\s*}/); const match = text.match(/async(?:\s+function)?\s*\(\s*{\s*([^}]*)\s*}/);
if (!match || !match[1].trim()) if (!match || !match[1].trim())
@ -214,10 +198,10 @@ function fixtureParameterNames(fn: { toString: () => any; }) {
return signature.split(',').map((t: string) => t.trim()); return signature.split(',').map((t: string) => t.trim());
} }
function innerRegisterFixture(name: any, scope: string, fn: any, caller: Function) { function innerRegisterFixture(name: string, scope: string, fn: Function, caller: Function) {
const obj = {stack: ''}; const obj = {stack: ''};
Error.captureStackTrace(obj, caller); Error.captureStackTrace(obj, caller);
const stackFrame = obj.stack.split('\n')[1]; const stackFrame = obj.stack.split('\n')[2];
const location = stackFrame.replace(/.*at Object.<anonymous> \((.*)\)/, '$1'); const location = stackFrame.replace(/.*at Object.<anonymous> \((.*)\)/, '$1');
const file = location.replace(/^(.+):\d+:\d+$/, '$1'); const file = location.replace(/^(.+):\d+:\d+$/, '$1');
const registration = { name, scope, fn, file, location }; const registration = { name, scope, fn, file, location };
@ -227,11 +211,11 @@ function innerRegisterFixture(name: any, scope: string, fn: any, caller: Functio
registrationsByFile.get(file).push(registration); registrationsByFile.get(file).push(registration);
}; };
export function registerFixture<T extends keyof TestState>(name: T, fn: (params: FixtureParameters & WorkerState & TestState, test: (arg: TestState[T]) => Promise<TestResult>) => Promise<void>) { export function registerFixture<Config, T extends keyof TestState>(name: T, fn: (params: FixtureParameters & WorkerState & TestState, runTest: (arg: TestState[T]) => Promise<void>, config: Config, test: Test) => Promise<void>) {
innerRegisterFixture(name, 'test', fn, registerFixture); innerRegisterFixture(name, 'test', fn, registerFixture);
}; };
export function registerWorkerFixture<T extends keyof (WorkerState & FixtureParameters)>(name: T, fn: (params: FixtureParameters & WorkerState, test: (arg: (WorkerState & FixtureParameters)[T]) => Promise<void>) => Promise<void>) { export function registerWorkerFixture<Config, T extends keyof (WorkerState & FixtureParameters)>(name: T, fn: (params: FixtureParameters & WorkerState, runTest: (arg: (WorkerState & FixtureParameters)[T]) => Promise<void>, config: Config) => Promise<void>) {
innerRegisterFixture(name, 'worker', fn, registerWorkerFixture); innerRegisterFixture(name, 'worker', fn, registerWorkerFixture);
}; };

View File

@ -17,4 +17,15 @@
import './builtin.fixtures'; import './builtin.fixtures';
import './expect'; import './expect';
export {registerFixture, registerWorkerFixture, registerParameter, parameters} from './fixtures'; import { registerFixture as registerFixtureT, registerWorkerFixture as registerWorkerFixtureT } from './fixtures';
import { RunnerConfig } from './runnerConfig';
import { Test } from './test';
export { parameters, registerParameter } from './fixtures';
export function registerFixture<T extends keyof TestState>(name: T, fn: (params: FixtureParameters & WorkerState & TestState, runTest: (arg: TestState[T]) => Promise<void>, config: RunnerConfig, test: Test) => Promise<void>) {
registerFixtureT<RunnerConfig, T>(name, fn);
};
export function registerWorkerFixture<T extends keyof (WorkerState & FixtureParameters)>(name: T, fn: (params: FixtureParameters & WorkerState, runTest: (arg: (WorkerState & FixtureParameters)[T]) => Promise<void>, config: RunnerConfig) => Promise<void>) {
registerWorkerFixtureT<RunnerConfig, T>(name, fn);
};

View File

@ -22,8 +22,9 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import terminalLink from 'terminal-link'; import terminalLink from 'terminal-link';
import StackUtils from 'stack-utils'; import StackUtils from 'stack-utils';
import { Test } from './test'; import { Test, Suite } from './test';
import { EventEmitter } from 'ws'; import { EventEmitter } from 'ws';
import { RunnerConfig } from './runnerConfig';
const stackUtils = new StackUtils(); const stackUtils = new StackUtils();
@ -33,6 +34,8 @@ class BaseReporter {
failures: Test[] = []; failures: Test[] = [];
duration = 0; duration = 0;
startTime: number; startTime: number;
config: RunnerConfig;
suite: Suite;
constructor(runner: EventEmitter) { constructor(runner: EventEmitter) {
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
@ -52,8 +55,10 @@ class BaseReporter {
this.failures.push(test); this.failures.push(test);
}); });
runner.once('begin', () => { runner.once('begin', (options: { config: RunnerConfig, suite: Suite }) => {
this.startTime = Date.now(); this.startTime = Date.now();
this.config = options.config;
this.suite = options.suite;
}); });
runner.once('end', () => { runner.once('end', () => {
@ -167,6 +172,44 @@ export class ListReporter extends BaseReporter {
} }
} }
export class JSONReporter extends BaseReporter {
constructor(runner: EventEmitter) {
super(runner);
runner.once('end', () => {
const result = {
config: this.config,
tests: this.suite.tests.map(test => this._serializeTest(test)),
suites: this.suite.suites.map(suite => this._serializeSuite(suite))
};
console.log(JSON.stringify(result, undefined, 2));
});
}
private _serializeSuite(suite: Suite): any {
return {
title: suite.title,
file: suite.file,
configuration: suite.configuration,
tests: suite.tests.map(test => this._serializeTest(test)),
suites: suite.suites.map(suite => this._serializeSuite(suite))
};
}
private _serializeTest(test: Test): any {
return {
title: test.title,
file: test.file,
only: test.only,
pending: test.pending,
slow: test.slow,
duration: test.duration,
timeout: test.timeout,
error: test.error
};
}
}
function indent(lines: string, tab: string) { function indent(lines: string, tab: string) {
return lines.replace(/^/gm, tab); return lines.replace(/^/gm, tab);
} }
@ -181,3 +224,9 @@ function positionInFile(stack: string, file: string): { column: number; line: nu
} }
return null; return null;
} }
export const reporters = {
'dot': DotReporter,
'list': ListReporter,
'json': JSONReporter
};

View File

@ -18,28 +18,13 @@ import child_process from 'child_process';
import crypto from 'crypto'; import crypto from 'crypto';
import path from 'path'; import path from 'path';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { DotReporter, ListReporter} from './reporters';
import { lookupRegistrations, FixturePool } from './fixtures'; import { lookupRegistrations, FixturePool } from './fixtures';
import { Suite } from './test'; import { Suite } from './test';
import { TestRunnerEntry } from './testRunner'; import { TestRunnerEntry } from './testRunner';
import { RunnerConfig } from './runnerConfig';
type RunnerOptions = {
jobs: number;
reporter: any;
outputDir: string;
snapshotDir: string;
testDir: string;
timeout: number;
debug?: boolean;
quiet?: boolean;
grep?: string;
trialRun?: boolean;
updateSnapshots?: boolean;
};
export class Runner extends EventEmitter { export class Runner extends EventEmitter {
readonly _options: RunnerOptions; private _workers = new Set<Worker>();
private _workers =new Set<Worker>();
private _freeWorkers: Worker[] = []; private _freeWorkers: Worker[] = [];
private _workerClaimers: (() => void)[] = []; private _workerClaimers: (() => void)[] = [];
stats: { duration: number; failures: number; passes: number; pending: number; tests: number; }; stats: { duration: number; failures: number; passes: number; pending: number; tests: number; };
@ -48,10 +33,13 @@ export class Runner extends EventEmitter {
private _testsByConfiguredFile = new Map<any, any>(); private _testsByConfiguredFile = new Map<any, any>();
private _queue: TestRunnerEntry[] = []; private _queue: TestRunnerEntry[] = [];
private _stopCallback: () => void; private _stopCallback: () => void;
readonly _config: RunnerConfig;
private _suite: Suite;
constructor(suite: Suite, total: number, options: RunnerOptions) { constructor(suite: Suite, total: number, config: RunnerConfig) {
super(); super();
this._options = options;
this._config = config;
this.stats = { this.stats = {
duration: 0, duration: 0,
failures: 0, failures: 0,
@ -59,13 +47,11 @@ export class Runner extends EventEmitter {
pending: 0, pending: 0,
tests: 0, tests: 0,
}; };
const reporterFactory = options.reporter === 'list' ? ListReporter : DotReporter;
new reporterFactory(this);
this._testById = new Map(); this._testById = new Map();
this._testsByConfiguredFile = new Map(); this._testsByConfiguredFile = new Map();
this._suite = suite;
suite.eachTest(test => { this._suite.eachTest(test => {
const configuredFile = `${test.file}::[${test._configurationString}]`; const configuredFile = `${test.file}::[${test._configurationString}]`;
if (!this._testsByConfiguredFile.has(configuredFile)) { if (!this._testsByConfiguredFile.has(configuredFile)) {
this._testsByConfiguredFile.set(configuredFile, { this._testsByConfiguredFile.set(configuredFile, {
@ -83,7 +69,7 @@ export class Runner extends EventEmitter {
if (process.stdout.isTTY) { if (process.stdout.isTTY) {
console.log(); console.log();
const jobs = Math.min(options.jobs, this._testsByConfiguredFile.size); const jobs = Math.min(config.jobs, this._testsByConfiguredFile.size);
console.log(`Running ${total} test${ total > 1 ? 's' : '' } using ${jobs} worker${ jobs > 1 ? 's' : ''}`); console.log(`Running ${total} test${ total > 1 ? 's' : '' } using ${jobs} worker${ jobs > 1 ? 's' : ''}`);
} }
} }
@ -97,7 +83,7 @@ export class Runner extends EventEmitter {
} }
async run() { async run() {
this.emit('begin', {}); this.emit('begin', { config: this._config, suite: this._suite });
this._queue = this._filesSortedByWorkerHash(); this._queue = this._filesSortedByWorkerHash();
// Loop in case job schedules more jobs // Loop in case job schedules more jobs
while (this._queue.length) while (this._queue.length)
@ -144,7 +130,7 @@ export class Runner extends EventEmitter {
if (this._freeWorkers.length) if (this._freeWorkers.length)
return this._freeWorkers.pop(); return this._freeWorkers.pop();
// If we can create worker, create it. // If we can create worker, create it.
if (this._workers.size < this._options.jobs) if (this._workers.size < this._config.jobs)
this._createWorker(); this._createWorker();
// Wait for the next available worker. // Wait for the next available worker.
await new Promise(f => this._workerClaimers.push(f)); await new Promise(f => this._workerClaimers.push(f));
@ -160,7 +146,7 @@ export class Runner extends EventEmitter {
} }
_createWorker() { _createWorker() {
const worker = this._options.debug ? new InProcessWorker(this) : new OopWorker(this); const worker = this._config.debug ? new InProcessWorker(this) : new OopWorker(this);
worker.on('test', params => { worker.on('test', params => {
++this.stats.tests; ++this.stats.tests;
this.emit('test', this._updateTest(params.test)); this.emit('test', this._updateTest(params.test));
@ -216,7 +202,7 @@ export class Runner extends EventEmitter {
let lastWorkerId = 0; let lastWorkerId = 0;
class Worker extends EventEmitter { class Worker extends EventEmitter {
runner: any; runner: Runner;
hash: string; hash: string;
constructor(runner) { constructor(runner) {
@ -254,26 +240,26 @@ class OopWorker extends Worker {
this.stderr = []; this.stderr = [];
this.on('stdout', params => { this.on('stdout', params => {
const chunk = chunkFromParams(params); const chunk = chunkFromParams(params);
if (!runner._options.quiet) if (!runner._config.quiet)
process.stdout.write(chunk); process.stdout.write(chunk);
this.stdout.push(chunk); this.stdout.push(chunk);
}); });
this.on('stderr', params => { this.on('stderr', params => {
const chunk = chunkFromParams(params); const chunk = chunkFromParams(params);
if (!runner._options.quiet) if (!runner._config.quiet)
process.stderr.write(chunk); process.stderr.write(chunk);
this.stderr.push(chunk); this.stderr.push(chunk);
}); });
} }
async init() { async init() {
this.process.send({ method: 'init', params: { workerId: lastWorkerId++, ...this.runner._options } }); this.process.send({ method: 'init', params: { workerId: lastWorkerId++, ...this.runner._config } });
await new Promise(f => this.process.once('message', f)); // Ready ack await new Promise(f => this.process.once('message', f)); // Ready ack
} }
run(entry) { run(entry) {
this.hash = entry.hash; this.hash = entry.hash;
this.process.send({ method: 'run', params: { entry, options: this.runner._options } }); this.process.send({ method: 'run', params: { entry, config: this.runner._config } });
} }
stop() { stop() {
@ -294,22 +280,22 @@ class OopWorker extends Worker {
} }
class InProcessWorker extends Worker { class InProcessWorker extends Worker {
fixturePool: FixturePool; fixturePool: FixturePool<RunnerConfig>;
constructor(runner: Runner) { constructor(runner: Runner) {
super(runner); super(runner);
this.fixturePool = require('./testRunner').fixturePool; this.fixturePool = require('./testRunner').fixturePool as FixturePool<RunnerConfig>;
} }
async init() { async init() {
const { initializeImageMatcher } = require('./expect'); const { initializeImageMatcher } = require('./expect');
initializeImageMatcher(this.runner._options); initializeImageMatcher(this.runner._config);
} }
async run(entry) { async run(entry) {
delete require.cache[entry.file]; delete require.cache[entry.file];
const { TestRunner } = require('./testRunner'); const { TestRunner } = require('./testRunner');
const testRunner = new TestRunner(entry, this.runner._options, 0); const testRunner = new TestRunner(entry, this.runner._config, 0);
for (const event of ['test', 'pending', 'pass', 'fail', 'done']) for (const event of ['test', 'pending', 'pass', 'fail', 'done'])
testRunner.on(event, this.emit.bind(this, event)); testRunner.on(event, this.emit.bind(this, event));
testRunner.run(); testRunner.run();

View File

@ -0,0 +1,28 @@
/**
* 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.
*/
export type RunnerConfig = {
jobs: number;
outputDir: string;
snapshotDir: string;
testDir: string;
timeout: number;
debug?: boolean;
quiet?: boolean;
grep?: string;
trialRun?: boolean;
updateSnapshots?: boolean;
};

View File

@ -22,6 +22,7 @@ export class Test {
file: string; file: string;
only = false; only = false;
pending = false; pending = false;
slow = false;
duration = 0; duration = 0;
timeout = 0; timeout = 0;
fn: Function; fn: Function;
@ -56,10 +57,6 @@ export class Test {
fullTitle(): string { fullTitle(): string {
return this.titlePath().join(' '); return this.titlePath().join(' ');
} }
slow(): number {
return 10000;
}
} }
export class Suite { export class Suite {
@ -70,6 +67,7 @@ export class Suite {
only = false; only = false;
pending = false; pending = false;
file: string; file: string;
configuration: Configuration;
_hooks: { type: string, fn: Function } [] = []; _hooks: { type: string, fn: Function } [] = [];
_entries: (Suite | Test)[] = []; _entries: (Suite | Test)[] = [];

View File

@ -15,7 +15,7 @@
*/ */
import path from 'path'; import path from 'path';
import { fixturesForCallback, registerWorkerFixture } from './fixtures'; import { fixturesForCallback } from './fixtures';
import { Configuration, Test, Suite } from './test'; import { Configuration, Test, Suite } from './test';
import { fixturesUI } from './fixturesUI'; import { fixturesUI } from './fixturesUI';
@ -29,9 +29,6 @@ export class TestCollector {
constructor(files: string[], matrix: { [key: string] : string }, options) { constructor(files: string[], matrix: { [key: string] : string }, options) {
this._matrix = matrix; this._matrix = matrix;
for (const name of Object.keys(matrix))
//@ts-ignore
registerWorkerFixture(name, async ({}, test) => test());
this._options = options; this._options = options;
this.suite = new Suite(''); this.suite = new Suite('');
if (options.grep) { if (options.grep) {
@ -106,6 +103,7 @@ export class TestCollector {
_cloneSuite(suite: Suite, configurationObject: Configuration, configurationString: string, tests: Set<Test>) { _cloneSuite(suite: Suite, configurationObject: Configuration, configurationString: string, tests: Set<Test>) {
const copy = suite.clone(); const copy = suite.clone();
copy.only = suite.only; copy.only = suite.only;
copy.configuration = configurationObject;
for (const entry of suite._entries) { for (const entry of suite._entries) {
if (entry instanceof Suite) { if (entry instanceof Suite) {
copy.addSuite(this._cloneSuite(entry, configurationObject, configurationString, tests)); copy.addSuite(this._cloneSuite(entry, configurationObject, configurationString, tests));

View File

@ -15,13 +15,14 @@
*/ */
import path from 'path'; import path from 'path';
import { FixturePool, registerWorkerFixture, rerunRegistrations, setParameters } from './fixtures'; import { FixturePool, rerunRegistrations, setParameters } from './fixtures';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { setCurrentTestFile } from './expect'; import { setCurrentTestFile } from './expect';
import { Test, Suite } from './test'; import { Test, Suite } from './test';
import { fixturesUI } from './fixturesUI'; import { fixturesUI } from './fixturesUI';
import { RunnerConfig } from './runnerConfig';
export const fixturePool = new FixturePool(); export const fixturePool = new FixturePool<RunnerConfig>();
export type TestRunnerEntry = { export type TestRunnerEntry = {
file: string; file: string;
@ -40,30 +41,23 @@ export class TestRunner extends EventEmitter {
private _remaining: Set<number>; private _remaining: Set<number>;
private _trialRun: any; private _trialRun: any;
private _configuredFile: any; private _configuredFile: any;
private _configurationObject: any;
private _parsedGeneratorConfiguration: any = {}; private _parsedGeneratorConfiguration: any = {};
private _outDir: string; private _config: RunnerConfig;
private _timeout: number; private _timeout: number;
private _testDir: string;
constructor(entry: TestRunnerEntry, options, workerId) { constructor(entry: TestRunnerEntry, config: RunnerConfig, workerId: number) {
super(); super();
this._file = entry.file; this._file = entry.file;
this._ordinals = new Set(entry.ordinals); this._ordinals = new Set(entry.ordinals);
this._remaining = new Set(entry.ordinals); this._remaining = new Set(entry.ordinals);
this._trialRun = options.trialRun; this._trialRun = config.trialRun;
this._timeout = options.timeout; this._timeout = config.timeout;
this._testDir = options.testDir; this._config = config;
this._outDir = options.outputDir;
this._configuredFile = entry.configuredFile; this._configuredFile = entry.configuredFile;
this._configurationObject = entry.configurationObject; for (const {name, value} of entry.configurationObject)
for (const {name, value} of this._configurationObject) {
this._parsedGeneratorConfiguration[name] = value; this._parsedGeneratorConfiguration[name] = value;
// @ts-ignore
registerWorkerFixture(name, async ({}, test) => await test(value));
}
this._parsedGeneratorConfiguration['parallelIndex'] = workerId; this._parsedGeneratorConfiguration['parallelIndex'] = workerId;
setCurrentTestFile(path.relative(options.testDir, this._file)); setCurrentTestFile(path.relative(config.testDir, this._file));
} }
stop() { stop() {
@ -146,7 +140,7 @@ export class TestRunner extends EventEmitter {
if (dir === 'before') if (dir === 'before')
all.reverse(); all.reverse();
for (const hook of all) for (const hook of all)
await fixturePool.resolveParametersAndRun(hook, 0); await fixturePool.resolveParametersAndRun(hook, 0, this._config);
} }
private _reportDone() { private _reportDone() {
@ -159,10 +153,7 @@ export class TestRunner extends EventEmitter {
private _testWrapper(test: Test) { private _testWrapper(test: Test) {
const timeout = test.slow ? this._timeout * 3 : this._timeout; const timeout = test.slow ? this._timeout * 3 : this._timeout;
return fixturePool.wrapTestCallback(test.fn, timeout, test, { return fixturePool.wrapTestCallback(test.fn, timeout, { ...this._config }, test);
outputDir: this._outDir,
testDir: this._testDir,
});
} }
private _serializeTest(test) { private _serializeTest(test) {

View File

@ -14,10 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
const { initializeImageMatcher } = require('./expect'); import { initializeImageMatcher } from './expect';
const { TestRunner, fixturePool } = require('./testRunner'); import { TestRunner, fixturePool } from './testRunner';
import * as util from 'util';
const util = require('util');
let closed = false; let closed = false;
@ -45,8 +44,8 @@ process.on('disconnect', gracefullyCloseAndExit);
process.on('SIGINT',() => {}); process.on('SIGINT',() => {});
process.on('SIGTERM',() => {}); process.on('SIGTERM',() => {});
let workerId; let workerId: number;
let testRunner; let testRunner: TestRunner;
process.on('message', async message => { process.on('message', async message => {
if (message.method === 'init') { if (message.method === 'init') {
@ -59,7 +58,7 @@ process.on('message', async message => {
return; return;
} }
if (message.method === 'run') { if (message.method === 'run') {
testRunner = new TestRunner(message.params.entry, message.params.options, workerId); testRunner = new TestRunner(message.params.entry, message.params.config, workerId);
for (const event of ['test', 'pending', 'pass', 'fail', 'done']) for (const event of ['test', 'pending', 'pass', 'fail', 'done'])
testRunner.on(event, sendMessageToParent.bind(null, event)); testRunner.on(event, sendMessageToParent.bind(null, event));
await testRunner.run(); await testRunner.run();

View File

@ -25,12 +25,16 @@ import { Transport } from '../lib/protocol/transport';
import { setUnderTest } from '../lib/utils/utils'; import { setUnderTest } from '../lib/utils/utils';
import { installCoverageHooks } from './coverage'; import { installCoverageHooks } from './coverage';
import { parameters, registerFixture, registerWorkerFixture } from '../test-runner'; import { parameters, registerFixture, registerWorkerFixture } from '../test-runner';
import {mkdtempAsync, removeFolderAsync} from './utils'; import {mkdtempAsync, removeFolderAsync} from './utils';
setUnderTest(); // Note: we must call setUnderTest before requiring Playwright export const options = {
CHROMIUM: parameters.browserName === 'chromium',
const platform = os.platform(); FIREFOX: parameters.browserName === 'firefox',
WEBKIT: parameters.browserName === 'webkit',
HEADLESS : !!valueFromEnv('HEADLESS', true),
WIRE: !!process.env.PWWIRE,
SLOW_MO: valueFromEnv('SLOW_MO', 0),
}
declare global { declare global {
interface WorkerState { interface WorkerState {
@ -52,9 +56,6 @@ declare global {
} }
interface FixtureParameters { interface FixtureParameters {
browserName: string; browserName: string;
headless: boolean;
wire: boolean;
slowMo: number;
} }
} }
@ -63,6 +64,7 @@ declare global {
const LINUX: boolean; const LINUX: boolean;
const WIN: boolean; const WIN: boolean;
} }
const platform = os.platform();
global['MAC'] = platform === 'darwin'; global['MAC'] = platform === 'darwin';
global['LINUX'] = platform === 'linux'; global['LINUX'] = platform === 'linux';
global['WIN'] = platform === 'win32'; global['WIN'] = platform === 'win32';
@ -96,22 +98,24 @@ const getExecutablePath = (browserName) => {
return process.env.WKPATH; return process.env.WKPATH;
} }
registerWorkerFixture('defaultBrowserOptions', async({browserName, headless, slowMo}, test) => { registerWorkerFixture('defaultBrowserOptions', async({browserName}, test) => {
let executablePath = getExecutablePath(browserName); let executablePath = getExecutablePath(browserName);
if (executablePath) if (executablePath)
console.error(`Using executable at ${executablePath}`); console.error(`Using executable at ${executablePath}`);
await test({ await test({
handleSIGINT: false, handleSIGINT: false,
slowMo, slowMo: options.SLOW_MO,
headless, headless: options.HEADLESS,
executablePath executablePath
}); });
}); });
registerWorkerFixture('playwright', async({browserName, wire}, test) => { registerWorkerFixture('playwright', async({browserName}, test) => {
setUnderTest(); // Note: we must call setUnderTest before requiring Playwright
const {coverage, uninstall} = installCoverageHooks(browserName); const {coverage, uninstall} = installCoverageHooks(browserName);
if (wire) { if (options.WIRE) {
const connection = new Connection(); const connection = new Connection();
const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'rpc', 'server'), [], { const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'rpc', 'server'), [], {
stdio: 'pipe', stdio: 'pipe',
@ -184,10 +188,10 @@ registerFixture('context', async ({browser}, test) => {
await context.close(); await context.close();
}); });
registerFixture('page', async ({context}, runTest) => { registerFixture('page', async ({context}, runTest, config, test) => {
const page = await context.newPage(); const page = await context.newPage();
const { success, test, config } = await runTest(page); await runTest(page);
if (!success) { if (test.error) {
const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, ''); const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, '');
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_'); const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png'; const assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png';
@ -211,10 +215,8 @@ registerFixture('tmpDir', async ({}, test) => {
await removeFolderAsync(tmpDir).catch(e => {}); await removeFolderAsync(tmpDir).catch(e => {});
}); });
export const options = { function valueFromEnv(name, defaultValue) {
CHROMIUM: parameters.browserName === 'chromium', if (!(name in process.env))
FIREFOX: parameters.browserName === 'firefox', return defaultValue;
WEBKIT: parameters.browserName === 'webkit', return JSON.parse(process.env[name]);
HEADLESS : parameters.headless,
WIRE: parameters.wire,
} }

View File

@ -20,19 +20,10 @@ declare const matrix: (m: any) => void;
matrix({ matrix({
'browserName': process.env.BROWSER ? [process.env.BROWSER] : ['chromium', 'webkit', 'firefox'], 'browserName': process.env.BROWSER ? [process.env.BROWSER] : ['chromium', 'webkit', 'firefox'],
'headless': [!!valueFromEnv('HEADLESS', true)],
'wire': [!!process.env.PWWIRE],
'slowMo': [valueFromEnv('SLOW_MO', 0)]
}); });
before(async () => { before(async () => {
}); });
after(async () => { after(async () => {
}); });
function valueFromEnv(name, defaultValue) {
if (!(name in process.env))
return defaultValue;
return JSON.parse(process.env[name]);
}