@zoroaster/fork
Advanced tools
+114
| import { fork } from 'spawncommand' | ||
| import forkFeed from 'forkfeed' | ||
| import { getForkArguments, assertForkOutput } from './lib' | ||
| import getArgs from './lib/get-args' | ||
| import { PassThrough } from 'stream' | ||
| import Catchment from 'catchment' | ||
| /** | ||
| * Run a fork. | ||
| * @param {Run} config Options for the run method. | ||
| * @param {(string|ForkConfig)} config.forkConfig Either the config, or the path to the module to fork. | ||
| * @param {string} config.input The input to the test from the test mask. | ||
| * @param {*} [config.props="{}"] Other Properties Of The Test, Such As `stdout` And `stderr`. Default `{}`. | ||
| * @param {Array<Context>} [config.contexts="[]"] The contexts for the test to be passed to `getArgs` and `getOptions`. Default `[]`. | ||
| * @returns {Promise<{stdout: string, stderr: string, code: number}>} The result of the work, updated to contain answers in the interactive mode. | ||
| */ | ||
| const run = async (config) => { | ||
| const { | ||
| forkConfig, | ||
| input, | ||
| props = {}, | ||
| contexts = [], | ||
| } = config | ||
| const a = input ? getArgs(input) : [] | ||
| const { | ||
| mod, args, options, | ||
| } = await getForkArguments(forkConfig, a, contexts, { | ||
| ...props, | ||
| input, | ||
| }) | ||
| const { promise, stdout, stdin, stderr } = fork(mod, args, options) | ||
| const { includeAnswers = true, log, inputs, stderrInputs } = forkConfig | ||
| const stdoutLog = new PassThrough() | ||
| const stderrLog = new PassThrough() | ||
| if (log === true) { | ||
| stdoutLog.pipe(process.stdout) | ||
| stderrLog.pipe(process.stderr) | ||
| } else if (log) { | ||
| log.stdout && stdoutLog.pipe(log.stdout) | ||
| log.stderr && stderrLog.pipe(log.stderr) | ||
| } | ||
| const needsStdoutAnswers = includeAnswers && inputs | ||
| const needsStderrAnswers = includeAnswers && stderrInputs | ||
| let co, ce | ||
| if (needsStdoutAnswers) co = new Catchment({ rs: stdoutLog }) | ||
| if (needsStderrAnswers) ce = new Catchment({ rs: stderrLog }) | ||
| forkFeed(stdout, stdin, inputs, stdoutLog) | ||
| forkFeed(stderr, stdin, stderrInputs, stderrLog) | ||
| const res = await promise | ||
| // override process's outputs with outputs with answers | ||
| if (needsStdoutAnswers) { | ||
| co.end(); const stdoutWithAnswers = await co.promise | ||
| Object.assign(res, { | ||
| stdout: stdoutWithAnswers, | ||
| }) | ||
| } | ||
| if (needsStderrAnswers) { | ||
| ce.end(); const stderrWithAnswers = await ce.promise | ||
| Object.assign(res, { | ||
| stderr: stderrWithAnswers, | ||
| }) | ||
| } | ||
| assertFork(res, props) | ||
| return res | ||
| } | ||
| const assertFork = ({ code, stdout, stderr }, props) => { | ||
| assertForkOutput(stdout, props.stdout) | ||
| assertForkOutput(stderr, props.stderr) | ||
| if (props.code && code != props.code) | ||
| throw new Error(`Fork exited with code ${code} != ${props.code}`) | ||
| } | ||
| export default run | ||
| /* documentary types/context.xml */ | ||
| /** | ||
| * @typedef {Object} Context A context made with a constructor. | ||
| * @prop {() => void} [_init] A function to initialise the context. | ||
| * @prop {() => void} [_destroy] A function to destroy the context. | ||
| */ | ||
| /* documentary types/index.xml */ | ||
| /** | ||
| * @typedef {import('child_process').ForkOptions} ForkOptions | ||
| * | ||
| * @typedef {Object} ForkConfig Parameters for forking. | ||
| * @prop {string} module The path to the module to fork. | ||
| * @prop {(args: string[], ...contexts?: Context[]) => string[]|Promise.<string[]>} [getArgs] The function to get arguments to pass the fork based on the parsed mask input and contexts. | ||
| * @prop {(...contexts?: Context[]) => ForkOptions} [getOptions] The function to get options for the fork, such as `ENV` and `cwd`, based on contexts. | ||
| * @prop {ForkOptions} [options] Options for the forked processed, such as `ENV` and `cwd`. | ||
| * @prop {[RegExp, string][]} [inputs] Inputs to push to `stdin` when `stdout` writes data. The inputs are kept on stack, and taken off the stack when the RegExp matches the written data. | ||
| * @prop {[RegExp, string][]} [stderrInputs] Inputs to push to `stdin` when `stderr` writes data (similar to `inputs`). | ||
| * @prop {boolean|{stderr: Writable, stdout: Writable}} [log=false] Whether to pipe data from `stdout`, `stderr` to the process's streams. If an object is passed, the output will be piped to streams specified as its `stdout` and `stderr` properties. Default `false`. | ||
| * @prop {boolean} [includeAnswers=true] Whether to add the answers to the `stderr` and `stdout` output. Default `true`. | ||
| */ | ||
| /* documentary types/run.xml */ | ||
| /** | ||
| * @typedef {Object} Run Options for the run method. | ||
| * @prop {(string|ForkConfig)} forkConfig Either the config, or the path to the module to fork. | ||
| * @prop {string} input The input to the test from the test mask. | ||
| * @prop {*} [props="{}"] Other Properties Of The Test, Such As `stdout` And `stderr`. Default `{}`. | ||
| * @prop {Array<Context>} [contexts="[]"] The contexts for the test to be passed to `getArgs` and `getOptions`. Default `[]`. | ||
| */ |
| import mismatch from 'mismatch' | ||
| /** | ||
| * Return shell arguments from a string. | ||
| * @param {string} input | ||
| */ | ||
| const getArgs = (input) => { | ||
| const res = mismatch(/(['"])?([\s\S]+?)\1(\s+|$)/g, input, ['q', 'a']) | ||
| .map(({ a }) => a) | ||
| return res | ||
| } | ||
| export default getArgs |
| import { deepEqual } from 'assert-diff' | ||
| import erte from 'erte' | ||
| import { equal } from 'assert' | ||
| export const assertExpected = (result, expected) => { | ||
| try { | ||
| equal(result, expected) | ||
| } catch (err) { | ||
| const e = erte(result, expected) | ||
| console.log(e) // eslint-disable-line no-console | ||
| throw err | ||
| } | ||
| } | ||
| /** | ||
| * @param {string|ForkConfig} forkConfig Parameters for forking. | ||
| * @param {string} forkConfig.module The path to the module to fork. | ||
| * @param {(args: string[], ...contexts?: Context[]) => string[]|Promise.<string[]>} [forkConfig.getArgs] The function to get arguments to pass the forked processed based on parsed masks input and contexts. | ||
| * @param {(...contexts?: Context[]) => ForkOptions} [forkConfig.getOptions] The function to get options for the forked processed, such as `ENV` and `cwd`, based on contexts. | ||
| * @param {ForkOptions} [forkConfig.options] Options for the forked processed, such as `ENV` and `cwd`. | ||
| * @param {string[]} args | ||
| * @param {Context[]} contexts | ||
| * @param {*} props The props found in the mask. | ||
| */ | ||
| export const getForkArguments = async (forkConfig, args = [], context = [], props = {}) => { | ||
| /** | ||
| * @type {ForkOptions} | ||
| */ | ||
| const stdioOpts = { | ||
| stdio: 'pipe', | ||
| execArgv: [], | ||
| } | ||
| if (typeof forkConfig == 'string') { | ||
| return { | ||
| mod: forkConfig, | ||
| args, | ||
| options: stdioOpts, | ||
| } | ||
| } | ||
| const { | ||
| module: mod, | ||
| getArgs, | ||
| options, | ||
| getOptions, | ||
| } = forkConfig | ||
| const a = getArgs ? await getArgs.call(props, args, ...context) : args | ||
| let opt = stdioOpts | ||
| if (options) { | ||
| opt = { | ||
| ...stdioOpts, | ||
| ...options, | ||
| } | ||
| } else if (getOptions) { | ||
| const o = await getOptions.call(props, ...context) | ||
| opt = { | ||
| ...stdioOpts, | ||
| ...o, | ||
| } | ||
| } | ||
| return { | ||
| mod, | ||
| args: a, | ||
| options: opt, | ||
| } | ||
| } | ||
| export const assertForkOutput = (actual, expected) => { | ||
| if (typeof expected == 'string') { | ||
| assertExpected(actual, expected) | ||
| } else if (expected) { | ||
| const a = JSON.parse(actual) | ||
| deepEqual(a, expected) | ||
| } | ||
| } | ||
| /** | ||
| * @typedef {import('..').Context} Context | ||
| * @typedef {import('..').ForkOptions} ForkOptions | ||
| * @typedef {import('..').ForkConfig} ForkConfig | ||
| */ |
| <types> | ||
| <type name="Run" desc="Options for the run method."> | ||
| <prop type="(string|ForkConfig)" name="forkConfig"> | ||
| Either the config, or the path to the module to fork. | ||
| </prop> | ||
| <prop string name="input"> | ||
| The input to the test from the test mask to set on the `this.input` property of the `getArgs` and `getOptions`. | ||
| </prop> | ||
| <prop name="props" default="{}"> | ||
| The properties to pass to the `getArgs` and `getOptions` as their this context. | ||
| </prop> | ||
| <prop type="Array<Context>" name="contexts" default="[]"> | ||
| The contexts for the test to be passed to `getArgs` and `getOptions`. | ||
| </prop> | ||
| </type> | ||
| </types> |
+26
-8
@@ -10,14 +10,23 @@ const { fork } = require('spawncommand'); | ||
| * Run a fork. | ||
| * @param {{forkConfig: string|ForkConfig, input: string, props?: *, contexts?: Context[] }} | ||
| * @param {Run} config Options for the run method. | ||
| * @param {(string|ForkConfig)} config.forkConfig Either the config, or the path to the module to fork. | ||
| * @param {string} config.input The input to the test from the test mask. | ||
| * @param {*} [config.props="{}"] Other Properties Of The Test, Such As `stdout` And `stderr`. Default `{}`. | ||
| * @param {Array<Context>} [config.contexts="[]"] The contexts for the test to be passed to `getArgs` and `getOptions`. Default `[]`. | ||
| * @returns {Promise<{stdout: string, stderr: string, code: number}>} The result of the work, updated to contain answers in the interactive mode. | ||
| */ | ||
| const run = async ({ | ||
| forkConfig, | ||
| input, | ||
| props = {}, | ||
| contexts = [], | ||
| }) => { | ||
| const run = async (config) => { | ||
| const { | ||
| forkConfig, | ||
| input, | ||
| props = {}, | ||
| contexts = [], | ||
| } = config | ||
| const a = input ? getArgs(input) : [] | ||
| const { | ||
| mod, args, options, | ||
| } = await getForkArguments(forkConfig, a, contexts) | ||
| } = await getForkArguments(forkConfig, a, contexts, { | ||
| ...props, | ||
| input, | ||
| }) | ||
| const { promise, stdout, stdin, stderr } = fork(mod, args, options) | ||
@@ -98,1 +107,10 @@ | ||
| */ | ||
| /* documentary types/run.xml */ | ||
| /** | ||
| * @typedef {Object} Run Options for the run method. | ||
| * @prop {(string|ForkConfig)} forkConfig Either the config, or the path to the module to fork. | ||
| * @prop {string} input The input to the test from the test mask. | ||
| * @prop {*} [props="{}"] Other Properties Of The Test, Such As `stdout` And `stderr`. Default `{}`. | ||
| * @prop {Array<Context>} [contexts="[]"] The contexts for the test to be passed to `getArgs` and `getOptions`. Default `[]`. | ||
| */ |
@@ -23,4 +23,5 @@ const { deepEqual } = require('assert-diff'); | ||
| * @param {Context[]} contexts | ||
| * @param {*} props The props found in the mask. | ||
| */ | ||
| const getForkArguments = async (forkConfig, args = [], context = []) => { | ||
| const getForkArguments = async (forkConfig, args = [], context = [], props = {}) => { | ||
| /** | ||
@@ -46,3 +47,3 @@ * @type {ForkOptions} | ||
| } = forkConfig | ||
| const a = getArgs ? await getArgs(args, ...context) : args | ||
| const a = getArgs ? await getArgs.call(props, args, ...context) : args | ||
| let opt = stdioOpts | ||
@@ -55,3 +56,3 @@ if (options) { | ||
| } else if (getOptions) { | ||
| const o = await getOptions(...context) | ||
| const o = await getOptions.call(props, ...context) | ||
| opt = { | ||
@@ -58,0 +59,0 @@ ...stdioOpts, |
+8
-0
@@ -0,1 +1,9 @@ | ||
| ## 16 February 2019 | ||
| ### 1.1.0 | ||
| - [doc] Document example. | ||
| - [package] Export the module. | ||
| - [feature] Pass the `input` and other properties in the context. | ||
| ## 24 October 2018 | ||
@@ -2,0 +10,0 @@ |
+11
-9
| { | ||
| "name": "@zoroaster/fork", | ||
| "version": "1.0.0", | ||
| "version": "1.1.0", | ||
| "description": "Test forks.", | ||
| "main": "build", | ||
| "main": "build/index.js", | ||
| "module": "src/index.js", | ||
| "scripts": { | ||
@@ -24,3 +25,4 @@ "t": "zoroaster -a", | ||
| "build", | ||
| "types" | ||
| "types", | ||
| "src" | ||
| ], | ||
@@ -45,13 +47,13 @@ "repository": { | ||
| "devDependencies": { | ||
| "alamode": "1.6.0", | ||
| "documentary": "1.20.1", | ||
| "alamode": "1.8.1", | ||
| "documentary": "1.21.2", | ||
| "eslint-config-artdeco": "1.0.1", | ||
| "reloquent": "1.2.3", | ||
| "reloquent": "1.2.4", | ||
| "yarn-s": "1.1.0", | ||
| "zoroaster": "3.6.2" | ||
| "zoroaster": "3.6.7" | ||
| }, | ||
| "dependencies": { | ||
| "assert-diff": "2.0.3", | ||
| "catchment": "3.1.1", | ||
| "erte": "1.1.4", | ||
| "catchment": "3.2.2", | ||
| "erte": "1.1.6", | ||
| "forkfeed": "1.0.0", | ||
@@ -58,0 +60,0 @@ "mismatch": "1.0.3", |
+44
-3
@@ -50,2 +50,45 @@ # @zoroaster/fork | ||
| _For example, to test the fork with the next code:_ | ||
| ```js | ||
| const [,, ...args] = process.argv | ||
| console.log(args) | ||
| console.error(process.env.EXAMPLE) | ||
| process.exit(5) | ||
| ``` | ||
| _The ContextTesting/Fork can be used:_ | ||
| ```js | ||
| /* yarn example/ */ | ||
| import fork from '@zoroaster/fork' | ||
| (async () => { | ||
| const res = await fork({ | ||
| contexts: ['CONTEXT'], | ||
| forkConfig: { | ||
| module: 'example/fork', | ||
| getArgs(inputs) { | ||
| return [...inputs, this.prop1] | ||
| }, | ||
| getOptions(CONTEXT) { | ||
| return { | ||
| env: { | ||
| EXAMPLE: `${CONTEXT} - ${this.input}`, | ||
| }, | ||
| } | ||
| }, | ||
| }, | ||
| input: 'hello world', | ||
| props: { | ||
| prop1: '999', | ||
| }, | ||
| }) | ||
| console.log(res) | ||
| })() | ||
| ``` | ||
| ```js | ||
| { code: 5, | ||
| stdout: '[ \'hello\', \'world\', \'999\' ]\n', | ||
| stderr: 'CONTEXT - hello world\n' } | ||
| ``` | ||
| <p align="center"><a href="#table-of-contents"><img src=".documentary/section-breaks/2.svg?sanitize=true"></a></p> | ||
@@ -55,6 +98,4 @@ | ||
| (c) [Context Testing][1] 2018 | ||
| (c) [Context Testing](https://contexttesting.com) 2019 | ||
| [1]: https://contexttesting.com | ||
| <p align="center"><a href="#table-of-contents"><img src=".documentary/section-breaks/-1.svg?sanitize=true"></a></p> |
25778
63.4%13
44.44%371
119.53%99
70.69%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
Updated
Updated