From 31fac37780313bc58f3f2b0d00e13e4a62d307e0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Aug 2020 20:35:26 -0700 Subject: [PATCH] test: allow overriding the worker fixtures (#3460) --- test/chromium/oopif.spec.ts | 97 ++++++++----------------------------- test/runner/fixtures.js | 11 ++++- test/runner/fixturesUI.js | 2 +- test/runner/index.js | 13 ++--- test/runner/runner.js | 50 +++++++++++++++++-- 5 files changed, 83 insertions(+), 90 deletions(-) diff --git a/test/chromium/oopif.spec.ts b/test/chromium/oopif.spec.ts index f9fd06a9f4..2f2e2b986f 100644 --- a/test/chromium/oopif.spec.ts +++ b/test/chromium/oopif.spec.ts @@ -13,20 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import '../base.fixture'; -import { registerFixture } from '../runner/fixtures'; -import { Page, Browser, BrowserContext } from '../..'; +import { registerWorkerFixture } from '../runner/fixtures'; -const {FFOX, CHROMIUM, WEBKIT} = testOptions; +const {CHROMIUM} = testOptions; -declare global { - interface FixtureState { - sppBrowser: Browser; - sppContext: BrowserContext; - sppPage: Page; - } -} -registerFixture('sppBrowser', async ({browserType, defaultBrowserOptions}, test) => { +registerWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test) => { const browser = await browserType.launch({ ...defaultBrowserOptions, args: (defaultBrowserOptions.args || []).concat(['--site-per-process']) @@ -35,30 +28,14 @@ registerFixture('sppBrowser', async ({browserType, defaultBrowserOptions}, test) await browser.close(); }); -registerFixture('sppContext', async ({sppBrowser}, test) => { - const context = await sppBrowser.newContext(); - await test(context); - await context.close(); -}); - -registerFixture('sppPage', async ({sppContext}, test) => { - const page = await sppContext.newPage(); - await test(page); -}); - - -it.skip(!CHROMIUM)('should report oopif frames', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should report oopif frames', async function({browser, page, server}) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); expect(page.frames().length).toBe(2); expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html'); }); -it.skip(!CHROMIUM)('should handle oopif detach', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should handle oopif detach', async function({browser, page, server}) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); expect(page.frames().length).toBe(2); @@ -71,9 +48,7 @@ it.skip(!CHROMIUM)('should handle oopif detach', async function({sppBrowser, spp expect(detachedFrame).toBe(frame); }); -it.skip(!CHROMIUM)('should handle remote -> local -> remote transitions', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should handle remote -> local -> remote transitions', async function({browser, page, server}) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); expect(await countOOPIFs(browser)).toBe(1); @@ -92,9 +67,7 @@ it.skip(!CHROMIUM)('should handle remote -> local -> remote transitions', async expect(await countOOPIFs(browser)).toBe(1); }); -it.fail(true)('should get the proper viewport', async({sppBrowser, sppPage, server}) => { - const browser = sppBrowser; - const page = sppPage; +it.fail(true)('should get the proper viewport', async({browser, page, server}) => { expect(page.viewportSize()).toEqual({width: 1280, height: 720}); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); @@ -113,9 +86,7 @@ it.fail(true)('should get the proper viewport', async({sppBrowser, sppPage, serv expect(await oopif.evaluate(() => 'ontouchstart' in window)).toBe(false); }); -it.skip(!CHROMIUM)('should expose function', async({sppBrowser, sppPage, server}) => { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should expose function', async({browser, page, server}) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); expect(await countOOPIFs(browser)).toBe(1); @@ -127,9 +98,7 @@ it.skip(!CHROMIUM)('should expose function', async({sppBrowser, sppPage, server} expect(result).toBe(36); }); -it.skip(!CHROMIUM)('should emulate media', async({sppBrowser, sppPage, server}) => { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should emulate media', async({browser, page, server}) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); expect(await countOOPIFs(browser)).toBe(1); @@ -139,10 +108,7 @@ it.skip(!CHROMIUM)('should emulate media', async({sppBrowser, sppPage, server}) expect(await oopif.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true); }); -it.skip(!CHROMIUM)('should emulate offline', async({sppBrowser, sppPage, sppContext, server}) => { - const browser = sppBrowser; - const context = sppContext; - const page = sppPage; +it.skip(!CHROMIUM)('should emulate offline', async({browser, page, context, server}) => { await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); expect(await countOOPIFs(browser)).toBe(1); @@ -152,8 +118,7 @@ it.skip(!CHROMIUM)('should emulate offline', async({sppBrowser, sppPage, sppCont expect(await oopif.evaluate(() => navigator.onLine)).toBe(false); }); -it.skip(!CHROMIUM)('should support context options', async({sppBrowser, server, playwright}) => { - const browser = sppBrowser; +it.skip(!CHROMIUM)('should support context options', async({browser, server, playwright}) => { const iPhone = playwright.devices['iPhone 6'] const context = await browser.newContext({ ...iPhone, timezoneId: 'America/Jamaica', locale: 'fr-CH', userAgent: 'UA' }); const page = await context.newPage(); @@ -175,9 +140,7 @@ it.skip(!CHROMIUM)('should support context options', async({sppBrowser, server, await context.close(); }); -it.skip(!CHROMIUM)('should respect route', async({sppBrowser, sppPage, server}) => { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should respect route', async({browser, page, server}) => { let intercepted = false; await page.route('**/digits/0.png', route => { intercepted = true; @@ -189,9 +152,7 @@ it.skip(!CHROMIUM)('should respect route', async({sppBrowser, sppPage, server}) expect(intercepted).toBe(true); }); -it.skip(!CHROMIUM)('should take screenshot', async({sppBrowser, sppPage, server, golden}) => { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should take screenshot', async({browser, page, server, golden}) => { await page.setViewportSize({width: 500, height: 500}); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(page.frames().length).toBe(2); @@ -199,17 +160,13 @@ it.skip(!CHROMIUM)('should take screenshot', async({sppBrowser, sppPage, server, expect(await page.screenshot()).toMatchImage(golden('screenshot-oopif.png')); }); -it.skip(!CHROMIUM)('should load oopif iframes with subresources and request interception', async function({sppBrowser, sppPage, server, context}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) { await page.route('**/*', route => route.continue()); await page.goto(server.PREFIX + '/dynamic-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); }); -it.skip(!CHROMIUM)('should report main requests', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should report main requests', async function({browser, page, server}) { const requestFrames = []; page.on('request', r => requestFrames.push(r.frame())); const finishedFrames = []; @@ -247,10 +204,7 @@ it.skip(!CHROMIUM)('should report main requests', async function({sppBrowser, sp expect(finishedFrames[2]).toBe(grandChild); }); -it.skip(!CHROMIUM)('should support exposeFunction', async function({sppBrowser, sppContext, sppPage, server}) { - const browser = sppBrowser; - const context = sppContext; - const page = sppPage; +it.skip(!CHROMIUM)('should support exposeFunction', async function({browser, context, page, server}) { await context.exposeFunction('dec', a => a - 1); await page.exposeFunction('inc', a => a + 1); await page.goto(server.PREFIX + '/dynamic-oopif.html'); @@ -262,10 +216,7 @@ it.skip(!CHROMIUM)('should support exposeFunction', async function({sppBrowser, expect(await page.frames()[1].evaluate(() => window['dec'](4))).toBe(3); }); -it.skip(!CHROMIUM)('should support addInitScript', async function({sppBrowser, sppContext, sppPage, server}) { - const browser = sppBrowser; - const context = sppContext; - const page = sppPage; +it.skip(!CHROMIUM)('should support addInitScript', async function({browser, context, page, server}) { await context.addInitScript(() => window['bar'] = 17); await page.addInitScript(() => window['foo'] = 42); await page.goto(server.PREFIX + '/dynamic-oopif.html'); @@ -277,9 +228,7 @@ it.skip(!CHROMIUM)('should support addInitScript', async function({sppBrowser, s expect(await page.frames()[1].evaluate(() => window['bar'])).toBe(17); }); // @see https://github.com/microsoft/playwright/issues/1240 -it.skip(!CHROMIUM)('should click a button when it overlays oopif', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should click a button when it overlays oopif', async function({browser, page, server}) { await page.goto(server.PREFIX + '/button-overlay-oopif.html'); expect(await countOOPIFs(browser)).toBe(1); await page.click('button'); @@ -311,9 +260,7 @@ it.skip(!CHROMIUM)('should report google.com frame with headful', async({browser await browser.close(); }); -it.skip(!CHROMIUM)('ElementHandle.boundingBox() should work', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('ElementHandle.boundingBox() should work', async function({browser, page, server}) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.$eval('iframe', iframe => { iframe.style.width = '500px'; @@ -336,9 +283,7 @@ it.skip(!CHROMIUM)('ElementHandle.boundingBox() should work', async function({sp expect(await handle2.boundingBox()).toEqual({ x: 100 + 42, y: 50 + 17, width: 50, height: 50 }); }); -it.skip(!CHROMIUM)('should click', async function({sppBrowser, sppPage, server}) { - const browser = sppBrowser; - const page = sppPage; +it.skip(!CHROMIUM)('should click', async function({browser, page, server}) { await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.$eval('iframe', iframe => { iframe.style.width = '500px'; diff --git a/test/runner/fixtures.js b/test/runner/fixtures.js index 3bf8b7a349..f86559fc3c 100644 --- a/test/runner/fixtures.js +++ b/test/runner/fixtures.js @@ -17,6 +17,7 @@ const debug = require('debug'); const registrations = new Map(); +const filesWithRegistrations = new Set(); class Fixture { constructor(pool, name, scope, fn) { @@ -139,7 +140,13 @@ function fixtureParameterNames(fn) { } function innerRegisterFixture(name, scope, fn) { - registrations.set(name, { scope, fn }); + const stackFrame = new Error().stack.split('\n').slice(1).filter(line => !line.includes(__filename))[0]; + const location = stackFrame.replace(/.*at Object. \((.*)\)/, '$1'); + const file = location.replace(/^(.+):\d+:\d+$/, '$1'); + const registration = { scope, fn, file, location }; + registrations.set(name, registration); + if (scope === 'worker') + filesWithRegistrations.add(file); }; function registerFixture(name, fn) { @@ -150,4 +157,4 @@ function registerWorkerFixture (name, fn) { innerRegisterFixture(name, 'worker', fn); }; -module.exports = { FixturePool, registerFixture, registerWorkerFixture }; +module.exports = { FixturePool, registerFixture, registerWorkerFixture, filesWithRegistrations }; diff --git a/test/runner/fixturesUI.js b/test/runner/fixturesUI.js index fdc0e534ee..c47ef24e6a 100644 --- a/test/runner/fixturesUI.js +++ b/test/runner/fixturesUI.js @@ -16,7 +16,7 @@ const { FixturePool, registerFixture, registerWorkerFixture } = require('./fixtures'); const { Test, Suite } = require('mocha'); -const {installTransform} = require('./transform'); +const { installTransform } = require('./transform'); const commonSuite = require('mocha/lib/interfaces/common'); Error.stackTraceLimit = 15; diff --git a/test/runner/index.js b/test/runner/index.js index 8c00688a66..eb3fc13d59 100644 --- a/test/runner/index.js +++ b/test/runner/index.js @@ -19,9 +19,7 @@ const path = require('path'); const program = require('commander'); const { Runner } = require('./runner'); const Mocha = require('mocha'); -const constants = require('mocha/lib/runner').constants; const { fixturesUI } = require('./fixturesUI'); -const colors = require('colors/safe'); class NullReporter {} @@ -36,7 +34,8 @@ program .option('--timeout ', 'Specify test timeout threshold (in milliseconds), default: 10000', 10000) .action(async (command) => { // Collect files - const files = collectFiles(path.join(process.cwd(), command.args[0]), command.args.slice(1)); + const files = []; + collectFiles(path.join(process.cwd(), command.args[0]), command.args.slice(1), files); const rootSuite = new Mocha.Suite('', new Mocha.Context(), true); let total = 0; @@ -62,7 +61,7 @@ program mocha.suite.title = path.basename(file); } - // Now run the tests. + // Filter tests. if (rootSuite.hasOnly()) rootSuite.filterOnly(); if (!command.reporter) { @@ -89,13 +88,12 @@ program program.parse(process.argv); -function collectFiles(dir, filters) { +function collectFiles(dir, filters, files) { if (fs.statSync(dir).isFile()) return [dir]; - const files = []; for (const name of fs.readdirSync(dir)) { if (fs.lstatSync(path.join(dir, name)).isDirectory()) { - files.push(...collectFiles(path.join(dir, name), filters)); + collectFiles(path.join(dir, name), filters, files); continue; } if (!name.includes('spec')) @@ -111,5 +109,4 @@ function collectFiles(dir, filters) { } } } - return files; } diff --git a/test/runner/runner.js b/test/runner/runner.js index 620363114f..199dd69361 100644 --- a/test/runner/runner.js +++ b/test/runner/runner.js @@ -15,11 +15,13 @@ */ const child_process = require('child_process'); +const crypto = require('crypto'); const path = require('path'); const { EventEmitter } = require('events'); const Mocha = require('mocha'); const builtinReporters = require('mocha/lib/reporters'); const DotRunner = require('./dotReporter'); +const { filesWithRegistrations } = require('./fixtures'); const constants = Mocha.Runner.constants; // Mocha runner does not remove uncaughtException listeners. @@ -63,11 +65,27 @@ class Runner extends EventEmitter { } } + _filesSortedByWorkerHash() { + const result = []; + for (const file of this._files.keys()) + result.push({ file, hash: computeWorkerHash(file) }); + result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1)); + return result; + } + async run() { this.emit(constants.EVENT_RUN_BEGIN, {}); - for (const file of this._files.keys()) { + const files = this._filesSortedByWorkerHash(); + while (files.length) { const worker = await this._obtainWorker(); - this._runJob(worker, file); + const requiredHash = files[0].hash; + if (worker.hash && worker.hash !== requiredHash) { + this._restartWorker(worker); + continue; + } + const entry = files.shift(); + worker.hash = requiredHash; + this._runJob(worker, entry.file); } await new Promise(f => this._runCompleteCallback = f); this.emit(constants.EVENT_RUN_END, {}); @@ -135,7 +153,7 @@ class Runner extends EventEmitter { worker.init().then(() => this._workerAvailable(worker)); } - async _restartWorker(worker) { + _restartWorker(worker) { worker.stop(); this._createWorker(); } @@ -214,4 +232,30 @@ class Worker extends EventEmitter { } } +function collectRequires(file, allDeps) { + if (allDeps.has(file)) + return; + allDeps.add(file); + const cache = require.cache[file]; + const deps = cache.children.map(m => m.id); + for (const dep of deps) + collectRequires(dep, allDeps); +} + +function computeWorkerHash(file) { + // At this point, filesWithRegistrations 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 deps = new Set(); + const hash = crypto.createHash('sha1'); + collectRequires(file, deps); + for (const dep of deps) { + if (!filesWithRegistrations.has(dep)) + continue; + hash.update(dep); + } + return hash.digest('hex'); +} + module.exports = { Runner };