Skip to content

Commit

Permalink
Test: Speed up CLI main tests with concurrentMap() function
Browse files Browse the repository at this point in the history
Closes #1684.
  • Loading branch information
Krinkle committed Apr 26, 2022
1 parent 70cab87 commit 9245ec1
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 12 deletions.
15 changes: 10 additions & 5 deletions test/cli/cli-main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const expectedOutput = require('./fixtures/expected/tap-outputs');
const { execute, prettyPrintCommand } = require('./helpers/execute');
const { execute, prettyPrintCommand, concurrentMapKeys } = require('./helpers/execute');
const semver = require('semver');

const skipOnWinTest = (process.platform === 'win32' ? 'skip' : 'test');
Expand Down Expand Up @@ -72,10 +72,15 @@ const fixtureCases = {
};

QUnit.module('CLI Main', () => {
QUnit.test.each('fixtures', fixtureCases, async (assert, command) => {
const execution = await execute(command);
assert.equal(execution.snapshot, getExpected(command));
});
QUnit.test.each('fixtures',
// Faster testing: Let the commands run in the background with concurrency,
// and only await/assert the already-started command.
concurrentMapKeys(fixtureCases, 0, (command) => execute(command)),
async (assert, execution) => {
const result = await execution;
assert.equal(result.snapshot, getExpected(result.command));
}
);

// TODO: Figure out why trace isn't trimmed on Windows. https://github.com/qunitjs/qunit/issues/1359
QUnit[skipOnWinTest]('report assert.throws() failures properly', async assert => {
Expand Down
60 changes: 54 additions & 6 deletions test/cli/helpers/execute.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use strict';

const path = require('path');
const cp = require('child_process');
const os = require('os');
const path = require('path');

const reEscape = /([\\{}()|.?*+\-^$[\]])/g;

// Apply light normalization to CLI output to allow strict string
Expand Down Expand Up @@ -65,7 +67,7 @@ function normalize (actual) {
* @param {Object} [options.env]
* @param {Function} [hook]
*/
module.exports.execute = async function execute (command, options = {}, hook) {
async function execute (command, options = {}, hook) {
options.cwd = path.join(__dirname, '..', 'fixtures');

// Inherit no environment by default
Expand Down Expand Up @@ -138,18 +140,64 @@ module.exports.execute = async function execute (command, options = {}, hook) {
if (result.code) {
result.snapshot += (result.snapshot ? '\n\n' : '') + '# exit code: ' + result.code;
}
result.command = command;

return result;
};
}

// Very loose command formatter.
// Not for the purpose of execution, but for the purpose
// of formatting the string key in fixtures/ files.
module.exports.prettyPrintCommand = function prettyPrintCommand (command) {
function prettyPrintCommand (command) {
return command.map(arg => {
// Quote spaces and stars
return /[ *]/.test(arg) ? `'${arg}'` : arg;
}).join(' ');
};
}

module.exports.normalize = normalize;
/**
* @param {Array<number,any>} input
* @param {number} [concurrency=0]
* @return {Array<number,Promise>}
*/
function concurrentMap (input, concurrency, asyncFn) {
const ret = [];
if (!concurrency) {
concurrency = os.cpus().length;
}
if (concurrency < 1) {
throw new Error('Concurrency must be non-zero');
}
for (let i = 0; i < input.length; i++) {
const val = input[i];
ret[i] = (i < concurrency)
? Promise.resolve(asyncFn(val))
: ret[i - concurrency].then(asyncFn.bind(null, val));
}
return ret;
}

/**
* @param {Object<string,any>} input
* @param {number} [concurrency=0]
* @return {Object<string,Promise>}
*/
function concurrentMapKeys (input, concurrency, asyncFn) {
const keys = Object.keys(input);
const values = concurrentMap(keys, concurrency, async function (key) {
return await asyncFn(input[key]);
});
const ret = {};
for (var i = 0; i < keys.length; i++) {
ret[keys[i]] = values[i];
}
return ret;
}

module.exports = {
normalize,
execute,
prettyPrintCommand,
concurrentMap,
concurrentMapKeys
};
128 changes: 127 additions & 1 deletion test/cli/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const path = require('path');

const { getIgnoreList } = require('../../src/cli/utils');
const { normalize } = require('./helpers/execute');
const { normalize, concurrentMap, concurrentMapKeys } = require('./helpers/execute');

QUnit.module('CLI utils', function () {
QUnit.test('getIgnoreList()', function (assert) {
Expand Down Expand Up @@ -81,4 +81,130 @@ QUnit.module('CLI utils', function () {
'strip frames for node_modules/append-transform'
);
});

QUnit.test.each('concurrentMap()', {
'concurrency=1': [1, [
'prepare',
'start A',
'init',
'finish A',
'start B',
'finish B',
'start C',
'finish C',
'start D',
'finish D',
'start E',
'finish E',
'start F',
'finish F'
]],
'concurrency=2': [2, [
'prepare',
'start A',
'start B',
'init',
'finish A',
'finish B',
'start C',
'start D',
'finish C',
'finish D',
'start E',
'start F',
'finish E',
'finish F'
]],
'concurrency=3': [3, [
'prepare',
'start A',
'start B',
'start C',
'init',
'finish A',
'finish B',
'finish C',
'start D',
'start E',
'start F',
'finish D',
'finish E',
'finish F'
]]
}, async function (assert, [concurrency, expected]) {
const steps = [];

steps.push('prepare');

const stream = concurrentMap(
['A', 'B', 'C', 'D', 'E', 'F'],
concurrency,
async function (val) {
steps.push(`start ${val}`);
await Promise.resolve();
steps.push(`finish ${val}`);
return val;
}
);

steps.push('init');

const end = stream.length - 1;
await stream[end];

assert.deepEqual(steps, expected);
});

function waitMicroTicks (count) {
let prom = Promise.resolve();
for (let i = 1; i < count; i++) {
prom = prom.then(() => {
return new Promise(resolve => {
setImmediate(resolve);
});
});
}
return prom;
}

QUnit.test('concurrentMapKeys()', async function (assert) {
let actual = [];
let running = 0;

const stream = concurrentMapKeys({
one: 10,
two: 5,
three: 7,
four: 1,
five: 29,
six: 15,
seven: 17,
eight: 8,
nine: 9
}, 4, async function (int) {
running++;
actual.push(running);
await waitMicroTicks(int);
running--;
return 1000 + int;
});

assert.strictEqual(await stream.one, 1010);
assert.strictEqual(await stream.seven, 1017);
assert.strictEqual(await stream.nine, 1009);
assert.deepEqual(actual, [
// ramp up
1,
2,
3,
// keep up
4,
4,
4,
4,
// no more remaining
3,
1
]);
});
});

0 comments on commit 9245ec1

Please sign in to comment.