playwright/test/runner/fixtures.js
2020-08-17 10:33:42 -07:00

211 lines
6.3 KiB
JavaScript

/**
* 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.
*/
const crypto = require('crypto');
const debug = require('debug');
const registrations = new Map();
const registrationsByFile = new Map();
class Fixture {
constructor(pool, name, scope, fn) {
this.pool = pool;
this.name = name;
this.scope = scope;
this.fn = fn;
this.deps = fixtureParameterNames(this.fn);
this.usages = new Set();
this.value = null;
}
async setup() {
for (const name of this.deps) {
await this.pool.setupFixture(name);
this.pool.instances.get(name).usages.add(this.name);
}
const params = {};
for (const n of this.deps)
params[n] = this.pool.instances.get(n).value;
let setupFenceFulfill;
let setupFenceReject;
const setupFence = new Promise((f, r) => { setupFenceFulfill = f; setupFenceReject = r; });
const teardownFence = new Promise(f => this._teardownFenceCallback = f);
debug('pw:test:hook')(`setup "${this.name}"`);
this._tearDownComplete = this.fn(params, async value => {
this.value = value;
setupFenceFulfill();
await teardownFence;
}).catch(e => setupFenceReject(e));
await setupFence;
this._setup = true;
}
async teardown() {
if (this._teardown)
return;
this._teardown = true;
for (const name of this.usages) {
const fixture = this.pool.instances.get(name);
if (!fixture)
continue;
await fixture.teardown();
}
if (this._setup) {
debug('pw:test:hook')(`teardown "${this.name}"`);
this._teardownFenceCallback();
}
await this._tearDownComplete;
this.pool.instances.delete(this.name);
}
}
class FixturePool {
constructor() {
this.instances = new Map();
}
async setupFixture(name) {
let fixture = this.instances.get(name);
if (fixture)
return fixture;
if (!registrations.has(name))
throw new Error('Unknown fixture: ' + name);
const { scope, fn } = registrations.get(name);
fixture = new Fixture(this, name, scope, fn);
this.instances.set(name, fixture);
await fixture.setup();
return fixture;
}
async teardownScope(scope) {
for (const [name, fixture] of this.instances) {
if (fixture.scope === scope)
await fixture.teardown();
}
}
async resolveParametersAndRun(fn) {
const names = fixtureParameterNames(fn);
for (const name of names)
await this.setupFixture(name);
const params = {};
for (const n of names)
params[n] = this.instances.get(n).value;
return fn(params);
}
wrapTestCallback(callback) {
if (!callback)
return callback;
return async() => {
try {
return await this.resolveParametersAndRun(callback);
} finally {
await this.teardownScope('test');
}
};
}
fixtures(callback) {
const result = new Set();
const visit = (callback) => {
for (const name of fixtureParameterNames(callback)) {
if (name in result)
continue;
result.add(name);
const { fn } = registrations.get(name)
visit(fn);
}
};
visit(callback);
return result;
}
}
function fixtureParameterNames(fn) {
const text = fn.toString();
const match = text.match(/async(?:\s+function)?\s*\(\s*{\s*([^}]*)\s*}/);
if (!match || !match[1].trim())
return [];
let signature = match[1];
return signature.split(',').map(t => t.trim());
}
function innerRegisterFixture(name, scope, fn) {
const stackFrame = new Error().stack.split('\n').slice(1).filter(line => !line.includes(__filename))[0];
const location = stackFrame.replace(/.*at Object.<anonymous> \((.*)\)/, '$1');
const file = location.replace(/^(.+):\d+:\d+$/, '$1');
const registration = { name, scope, fn, file, location };
registrations.set(name, registration);
if (!registrationsByFile.has(file))
registrationsByFile.set(file, []);
registrationsByFile.get(file).push(registration);
};
function registerFixture(name, fn) {
innerRegisterFixture(name, 'test', fn);
};
function registerWorkerFixture(name, fn) {
innerRegisterFixture(name, 'worker', fn);
};
function collectRequires(file, result) {
if (result.has(file))
return;
result.add(file);
const cache = require.cache[file];
const deps = cache.children.map(m => m.id).slice().reverse();
for (const dep of deps)
collectRequires(dep, result);
}
function lookupRegistrations(file, scope) {
const deps = new Set();
collectRequires(file, deps);
const allDeps = [...deps].reverse();
let result = [];
for (const dep of allDeps) {
const registrationList = registrationsByFile.get(dep);
if (!registrationList)
continue;
result = result.concat(registrationList.filter(r => r.scope === scope));
}
return result;
}
function rerunRegistrations(file, scope) {
// When we are running several tests in the same worker, we should re-run registrations before
// each file. That way we erase potential fixture overrides from the previous test runs.
for (const registration of lookupRegistrations(file, scope))
registrations.set(registration.name, registration);
}
function computeWorkerHash(file) {
// At this point, registrationsByFile contains all the files with worker fixture registrations.
// For every test, build the require closure and map each file to fixtures declared in it.
// This collection of fixtures is the fingerprint of the worker setup, a "worker hash".
// Tests with the matching "worker hash" will reuse the same worker.
const hash = crypto.createHash('sha1');
for (const registration of lookupRegistrations(file, 'worker'))
hash.update(registration.location);
return hash.digest('hex');
}
module.exports = { FixturePool, registerFixture, registerWorkerFixture, computeWorkerHash, rerunRegistrations };