peter
Version:
Peter Test Framework
156 lines (137 loc) • 5.21 kB
JavaScript
/**
* @author Wes Garland, wes@kingsds.network
* @date July 2020
* @file src/drivers/bash.js Test driver which executes .bash tests.
*
* @note These tests must not make use of /dev/fd, /dev/stderr, /dev/stdout, etc.
* Redirection should be done via >&2 etc; this is to work around bugs
* with NodeJS and pipes and ENXIO. If dependent scripts use these, the test
* should create a dummy subshell which redirects these resources to files,
* polling them in the background as necessary.
*
* Tests with lines matching '# peter-timeout: NUMBER" will have the timeout set to NUMBER seconds.
*/
;
const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
const chalk = require('chalk');
const { registerDriver } = require('.');
const logStream = require('../utils/log-stream');
const { setTestTimeout } = require('../utils/test-timer');
const isWin = require('os').platform() === 'win32';
const bashPath = process.env.BASH || (isWin ? 'bash' : '/bin/bash');
const myPackage = require('../../package.json');
const debug = require('debug');
registerDriver('bash', {
async run(testPath, options) {
let fullTestPath = (isWin ? path.win32 : path).resolve(testPath);
let testContents = await fs.promises.readFile(fullTestPath, { encoding: 'utf-8' });
let testExitCode;
var cleanupHandler;
const childEnv = {
PETER_VERSION: myPackage.version,
PETER: require.main.filename,
PETER_TEST: fullTestPath,
PETER_VERBOSE: String(options.verbose)
};
Object.assign(childEnv, options.env);
delete childEnv.DEBUG; /* don't pass along debugging of the parent to the children */
/* Figure out the timeouts; test can override command line, env vars override everything */
options = Object.assign({}, options);
if ((/^#\s*peter-timeout:\s*/m).test(testContents))
options.timeout = testContents.match(/^#\s*peter-timeout:\s*(.*)/m)[1];
options.timeout = 0
|| Number(process.env.PETER_BASH_TIMEOUT)
|| Number(process.env.PETER_TIMEOUT)
|| Number(options.timeout);
const timeoutMs = (options.timeout * 1000) || undefined; /* 0 => no timeout */
const testProcess = childProcess.execFile(bashPath, [ fullTestPath ], {
env: childEnv,
timeout: timeoutMs,
killSignal: 1,
});
let timeoutExceeded = null;
const testTimeoutStart = Date.now();
setTestTimeout(testProcess, testPath, options.timeout, testTimeoutStart).then((time) => { timeoutExceeded = time });
const streams = logStream.captureTest(testProcess, testPath, options);
let stderr = '';
testProcess.stderr.on('readable', () => {
let chunk;
while ((chunk = testProcess.stderr.read()) !== null)
stderr += chunk.toString('utf8');
});
await new Promise((resolve, reject) => {
var done = false;
cleanupHandler = function cleanupHandler()
{
if (testProcess)
{
testProcess.exitCode !== null && testProcess.kill();
setImmediate(() => testProcess.exitCode !== null && testProcess.kill(9));
}
}
function handleError(error)
{
if (done)
return;
testExitCode = 1;
done = true;
reject(error);
}
function handleExit(exitCode)
{
var success;
if (done)
return;
done = true;
success = (!timeoutExceeded && exitCode === 0);
if (options.invert)
success = !success;
testExitCode = success ? 0 : 1;
resolve();
}
process.on('CLEANUP', cleanupHandler);
testProcess.on('error', handleError);
testProcess.on('exit', handleExit);
const stdinFilename = path.join(fullTestPath + '.stdin');
if (fs.existsSync(stdinFilename))
testProcess.stdin.write(fs.readFileSync(stdinFilename), (err) => err && reject(err));
testProcess.stdin.end();
});
const ansiCsRe = /\u001b\[[\u0030-\u003f]*[\u0020-\u002f]*[\u0040-u\007e]/g; /* Matches ANSI Control Sequences */
const failures = [];
if (timeoutExceeded)
failures.push({
name: 'Test failed',
stderr: stderr.length ? stderr.replace(ansiCsRe, '').split(/\r?\n/) : undefined,
diag: {
at: 'timeout',
operator: 'lt',
expected: `${options.timeout}s`,
actual: timeoutExceeded + 's',
},
});
else if (testExitCode)
failures.push({
name: 'Test failed' + (options.failing ? '(expected)' : ''),
stderr: stderr.length ? stderr.replace(ansiCsRe, '').split(/\r?\n/) : undefined,
diag: {
at: 'exit code',
operator: 'eq',
expected: 0,
actual: testExitCode,
},
});
const result = {
type: 'bash',
count: 1,
pass: testExitCode === 0 ? 1 : 0,
fail: testExitCode === 0 ? 0 : 1,
skip: 0,
ok: testExitCode === 0,
failures,
};
return { result, streams, failureTypes: { timeoutExceeded, testExitCode } };
},
});