From d09c4688130e322e83c3511872ee616889b5e3b8 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Fri, 17 Sep 2021 19:25:51 +0200 Subject: [PATCH] Allow reading bundle from stdin (#269) * Allow to read bundled tests from stdin * Validate config right after merging * Factor out config validation into own module * Improve naming around ambiguous items * Consolidate option validation logic * Throw directly when config validation fails --- cli/README.md | 18 +++++++++++++ cli/index.js | 27 ++++++++++++------- cli/test/jsdom.integration.js | 22 +++++++++++++++ cli/test/playwright.integration.js | 26 ++++++++++++++++++ cli/test/puppeteer.integration.js | 22 +++++++++++++++ mochify/index.js | 14 ++++------ mochify/lib/load-config.js | 4 ++- mochify/lib/resolve-bundle.js | 22 ++++++++++++--- mochify/lib/resolve-spec.js | 4 +++ mochify/lib/resolve-spec.test.js | 7 +++++ mochify/lib/validate-config.js | 21 +++++++++++++++ mochify/lib/validate-config.test.js | 42 +++++++++++++++++++++++++++++ 12 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 mochify/lib/validate-config.js create mode 100644 mochify/lib/validate-config.test.js diff --git a/cli/README.md b/cli/README.md index 5df12889..c3b99e52 100644 --- a/cli/README.md +++ b/cli/README.md @@ -64,3 +64,21 @@ Files in the given directory will be served as static assets. Options to pass to the server in case `--serve` or `--esm` is being used. Currently only `--server-option.port` for passing the port to use is supported. + +## Spec + +The `spec` argument can be a list of files or a glob pattern that will be resolved by Mochify. + +``` +mochify ./src/foo.test.js ./src/bar.test.js +mochify ./src/*.test.js # Let the shell handle glob expansion +mochify "./src/*.test.js" # Let Mochify handle glob expansion +``` + +### Reading a bundle from `stdin` + +When given `-` as the spec, Mochify expects to read a bundled test suite from `stdin`: + +``` +browserify -t babelify ./src/*.test.js | mochify - +``` diff --git a/cli/index.js b/cli/index.js index 5c8d59f8..71389857 100755 --- a/cli/index.js +++ b/cli/index.js @@ -72,6 +72,10 @@ const opts = yargs(hideBin(process.argv)) '$0 --config mochify.webdriver.js "./src/**/*.test.js" ', 'Run all tests matching the given spec using the configuration from mochify.webdriver.js.' ) + .example( + 'browserify "./src/**/*.test.js" | $0 -', + 'Read a bundled test suite from stdin.' + ) .epilogue( `Mochify Resources: GitHub: https://github.com/mantoni/mochify.js` @@ -86,16 +90,19 @@ if (opts['server-option']) { opts.server_options = opts['server-option']; } -(async () => { - if (opts._.length) { +if (opts._.length) { + if (opts._[0] === '-') { + opts.spec = process.stdin; + } else { opts.spec = opts._; } - delete opts._; - try { - const { exit_code } = await mochify(opts); +} +delete opts._; +mochify(opts) + .catch((err) => { + console.error(err.stack); + return { exit_code: 1 }; + }) + .then(({ exit_code }) => { process.exitCode = exit_code; - } catch (e) { - console.error(e.stack); - process.exitCode = 1; - } -})(); + }); diff --git a/cli/test/jsdom.integration.js b/cli/test/jsdom.integration.js index a110f10b..2ef67181 100644 --- a/cli/test/jsdom.integration.js +++ b/cli/test/jsdom.integration.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const { assert } = require('@sinonjs/referee-sinon'); const execa = require('execa'); @@ -28,6 +29,27 @@ describe('jsdom', () => { assert.equals(json.tests[0].fullTitle, 'test passes'); }); + it('reads from stdin', async () => { + let result; + try { + const cp = execa('../../index.js', ['--driver', 'jsdom', '-'], { + cwd: path.join(__dirname, 'fixture') + }); + const fixture = fs.createReadStream( + path.resolve(__dirname, './fixture/passes.js') + ); + fixture.pipe(cp.stdin); + result = await cp; + } catch (err) { + result = err; + } + + assert.isFalse(result.failed); + const json = JSON.parse(result.stdout); + assert.equals(json.tests.length, 1); + assert.equals(json.tests[0].fullTitle, 'test passes'); + }); + it('fails', async () => { const result = await run('fails.js'); diff --git a/cli/test/playwright.integration.js b/cli/test/playwright.integration.js index 7c19ec54..d48292d9 100644 --- a/cli/test/playwright.integration.js +++ b/cli/test/playwright.integration.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const { assert } = require('@sinonjs/referee-sinon'); const execa = require('execa'); @@ -35,6 +36,31 @@ describe('playwright', () => { assert.equals(json.tests[0].fullTitle, 'test passes'); }); + it('reads from stdin', async () => { + let result; + try { + const cp = execa( + '../../index.js', + ['--driver', 'playwright', '--driver-option.engine', 'firefox', '-'], + { + cwd: path.join(__dirname, 'fixture') + } + ); + const fixture = fs.createReadStream( + path.resolve(__dirname, './fixture/passes.js') + ); + fixture.pipe(cp.stdin); + result = await cp; + } catch (err) { + result = err; + } + + assert.isFalse(result.failed); + const json = JSON.parse(result.stdout); + assert.equals(json.tests.length, 1); + assert.equals(json.tests[0].fullTitle, 'test passes'); + }); + it('fails', async () => { const result = await run('fails.js'); diff --git a/cli/test/puppeteer.integration.js b/cli/test/puppeteer.integration.js index f917b839..1a396d68 100644 --- a/cli/test/puppeteer.integration.js +++ b/cli/test/puppeteer.integration.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const { assert } = require('@sinonjs/referee-sinon'); const execa = require('execa'); @@ -28,6 +29,27 @@ describe('puppeteer', () => { assert.equals(json.tests[0].fullTitle, 'test passes'); }); + it('reads from stdin', async () => { + let result; + try { + const cp = execa('../../index.js', ['--driver', 'puppeteer', '-'], { + cwd: path.join(__dirname, 'fixture') + }); + const fixture = fs.createReadStream( + path.resolve(__dirname, './fixture/passes.js') + ); + fixture.pipe(cp.stdin); + result = await cp; + } catch (err) { + result = err; + } + + assert.isFalse(result.failed); + const json = JSON.parse(result.stdout); + assert.equals(json.tests.length, 1); + assert.equals(json.tests[0].fullTitle, 'test passes'); + }); + it('fails', async () => { const result = await run('fails.js'); diff --git a/mochify/index.js b/mochify/index.js index b787463b..2b323f49 100644 --- a/mochify/index.js +++ b/mochify/index.js @@ -2,6 +2,7 @@ const { readFile } = require('fs').promises; const { loadConfig } = require('./lib/load-config'); +const { validateConfig } = require('./lib/validate-config'); const { setupClient } = require('./lib/setup-client'); const { createMochaRunner } = require('./lib/mocha-runner'); const { resolveBundle } = require('./lib/resolve-bundle'); @@ -13,12 +14,13 @@ exports.mochify = mochify; async function mochify(options = {}) { const config = await loadConfig(options); + validateConfig(config); // Create runner early to verify the reporter exists: const mocha_runner = createMochaRunner(config.reporter || 'spec'); const { mochifyDriver } = resolveMochifyDriver(config.driver); - const [mocha, client, files] = await Promise.all([ + const [mocha, client, resolved_spec] = await Promise.all([ readFile(require.resolve('mocha/mocha.js'), 'utf8'), readFile(require.resolve('./client'), 'utf8'), resolveSpec(config.spec) @@ -30,7 +32,7 @@ async function mochify(options = {}) { let server = null; if (config.serve || config.esm) { const _scripts = [mocha, configured_client]; - const _modules = config.esm ? files : []; + const _modules = config.esm ? resolved_spec : []; server = await startServer( config.serve || process.cwd(), Object.assign({ _scripts, _modules }, config.server_options) @@ -41,7 +43,7 @@ async function mochify(options = {}) { const driver_promise = mochifyDriver(driver_options); const bundler_promise = config.esm ? Promise.resolve('') - : resolveBundle(config.bundle, files); + : resolveBundle(config.bundle, resolved_spec); let driver, bundle; try { @@ -72,12 +74,6 @@ async function mochify(options = {}) { } function resolveMochifyDriver(name) { - if (!name) { - throw new Error( - 'Specifying a driver option is required. Mochify drivers need to be installed separately from the API or the CLI.' - ); - } - let driverModule; try { // eslint-disable-next-line node/global-require diff --git a/mochify/lib/load-config.js b/mochify/lib/load-config.js index 7cc3b907..1b39890a 100644 --- a/mochify/lib/load-config.js +++ b/mochify/lib/load-config.js @@ -22,7 +22,9 @@ async function loadConfig(options) { async function mergeWithDefault(default_config_promise, config) { const default_config = await default_config_promise; if (default_config) { - return deepmerge(default_config.config, config); + return deepmerge(default_config.config, config, { + clone: false + }); } return config; } diff --git a/mochify/lib/resolve-bundle.js b/mochify/lib/resolve-bundle.js index f5777c74..7f01ec4d 100644 --- a/mochify/lib/resolve-bundle.js +++ b/mochify/lib/resolve-bundle.js @@ -6,14 +6,21 @@ const { parseArgsStringToArgv } = require('string-argv'); exports.resolveBundle = resolveBundle; -async function resolveBundle(command, files) { +async function resolveBundle(command, resolved_spec) { + if ( + typeof resolved_spec === 'object' && + typeof resolved_spec.pipe === 'function' + ) { + return bufferStream(resolved_spec); + } + if (!command) { - return concatFiles(files); + return concatFiles(resolved_spec); } const [cmd, ...args] = parseArgsStringToArgv(command); - const result = await execa(cmd, args.concat(files), { + const result = await execa(cmd, args.concat(resolved_spec), { preferLocal: true }); @@ -28,3 +35,12 @@ async function concatFiles(files) { const buffers = await Promise.all(files.map((file) => fs.readFile(file))); return Buffer.concat(buffers).toString('utf8'); } + +function bufferStream(stream) { + return new Promise((resolve, reject) => { + const buffers = []; + stream.on('data', (chunk) => buffers.push(chunk)); + stream.on('error', (err) => reject(err)); + stream.on('end', () => resolve(Buffer.concat(buffers).toString('utf8'))); + }); +} diff --git a/mochify/lib/resolve-spec.js b/mochify/lib/resolve-spec.js index f71581a8..e3dd0a04 100644 --- a/mochify/lib/resolve-spec.js +++ b/mochify/lib/resolve-spec.js @@ -6,6 +6,10 @@ const glob = promisify(require('glob')); exports.resolveSpec = resolveSpec; async function resolveSpec(spec = 'test/**/*.js') { + if (typeof spec === 'object' && typeof spec.pipe === 'function') { + return spec; + } + const patterns = Array.isArray(spec) ? spec : [spec]; const matches = await Promise.all(patterns.map((pattern) => glob(pattern))); return matches.reduce((all, match) => all.concat(match), []); diff --git a/mochify/lib/resolve-spec.test.js b/mochify/lib/resolve-spec.test.js index 6c2c0224..4d90d305 100644 --- a/mochify/lib/resolve-spec.test.js +++ b/mochify/lib/resolve-spec.test.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const proxyquire = require('proxyquire'); const { assert, sinon } = require('@sinonjs/referee-sinon'); @@ -75,4 +76,10 @@ describe('mochify/lib/resolve-spec', () => { await assert.rejects(promise, error); }); + + it('passes through streams', async () => { + const stream = fs.createReadStream(__filename); + const promise = resolveSpec(stream); + await assert.resolves(promise, stream); + }); }); diff --git a/mochify/lib/validate-config.js b/mochify/lib/validate-config.js new file mode 100644 index 00000000..0223bf7d --- /dev/null +++ b/mochify/lib/validate-config.js @@ -0,0 +1,21 @@ +'use strict'; + +exports.validateConfig = validateConfig; + +function validateConfig(config) { + if (!config.driver) { + throw new Error( + 'Specifying a `driver` is required. Mochify drivers need to be installed separately from the API or the CLI.' + ); + } + if (config.esm && config.bundle) { + throw new Error('`esm` cannot be used in conjunction with `bundle`'); + } + if ( + config.bundle && + typeof config.spec === 'object' && + typeof config.spec.pipe === 'function' + ) { + throw new Error('`bundle` cannot be used when `spec` is a stream.'); + } +} diff --git a/mochify/lib/validate-config.test.js b/mochify/lib/validate-config.test.js new file mode 100644 index 00000000..f6a6cfd6 --- /dev/null +++ b/mochify/lib/validate-config.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const fs = require('fs'); +const { assert, refute } = require('@sinonjs/referee-sinon'); +const { validateConfig } = require('./validate-config'); + +describe('mochify/lib/validate-config', () => { + it('returns an error when esm and bundle are given', () => { + assert.exception(() => { + validateConfig({ + driver: 'puppeteer', + esm: true, + bundle: 'browserify', + spec: './test.js' + }); + }); + }); + + it('returns an error when bundle and a stream spec are given', () => { + assert.exception(() => { + validateConfig({ + driver: 'puppeteer', + bundle: 'browserify', + spec: fs.createReadStream(__filename) + }); + }); + }); + + it('returns an error on an empty config', () => { + assert.exception(() => validateConfig({})); + }); + + it('returns null on a valid config', () => { + refute.exception(() => { + validateConfig({ + bundle: 'browserify -t babelify', + spec: './test.js', + driver: 'puppeteer' + }); + }); + }); +});