muttley
Version:
Monitor Unit Test Tool
374 lines • 15.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const readline_1 = __importDefault(require("readline"));
const mocha_runner_1 = require("./mocha-runner");
const ps_1 = require("./ps");
const logger_1 = require("./logger");
const command_line_1 = require("./command-line");
const render_1 = require("./render");
const mutt = `
__,-----._ ,-.
,' ,-. \\\`---. ,-----<._/
(,.-. o:.\` )),"\\\\-._ ,' \`.
('"-\` .\\ \\\`:_ )\\ \`-;'-._ \\
,,-. \\\` ; : \\( \`-' ) -._ : \`:
( \\ \`._\\\\ \` ; ; \` : )
\\\`. \`-. __ , / \\ ;, (
\`.\`-.___--' \`- / ; | : |
\`-' \`-.\`--._ ' ; |
(\`--._\`. ; /\\ |
\\ ' \\ , ) :
| \`--::---- \\' ; ;|
\\ .__,- ( ) : :|
\\ : \`------; \\ | | ;
\\ : / , ) | | (
\\ \\ \`-^-| | / , ,\\
) ) | -^- ; \`-^-^'
_,' _ ; | |
/ , , ,' /---. :
\`-^-^' ( : :,'
\`-^--'
`;
var TestState;
(function (TestState) {
TestState[TestState["Ready"] = 3] = "Ready";
TestState[TestState["Running"] = 2] = "Running";
TestState[TestState["Passsed"] = 4] = "Passsed";
TestState[TestState["Failed"] = 1] = "Failed";
})(TestState || (TestState = {}));
class Testcase {
constructor(filename, stat, fixture, name) {
this.startTime = new Date();
this.endTime = new Date();
this.durationMs = 0;
this.state = TestState.Ready;
this.stack = [];
this.message = ''; // single line
this.fullMessage = '';
this.filename = filename;
this.mtime = stat.mtime;
this.suite = fixture;
this.name = name;
}
get basefilename() {
return path_1.default.basename(this.filename);
}
get key() {
return [this.filename, this.suite, this.name].join('-');
}
get runtimeInMs() {
if (this.state === TestState.Running)
return Date.now() - this.startTime.getTime();
else
return this.durationMs;
}
}
function onStart(filename) {
logger_1.logger.info('onStart', filename);
for (const [key, t] of allTests) {
logger_1.logger.debug(key, t.filename, filename);
if (t.filename === filename) {
logger_1.logger.debug('remove', t.filename, t.suite, t.name);
allTests.delete(key);
}
}
}
function onPass(filename, stat, suite, name, duration) {
logger_1.logger.info('onPass', filename, suite, name, duration);
const testcase = new Testcase(filename, stat, suite, name);
testcase.durationMs = duration;
testcase.state = TestState.Passsed;
allTests.set(testcase.key, testcase);
}
function onFail(filename, stat, { suite, name, fullMessage, message, stack }) {
logger_1.logger.error('onFail', filename, suite, name, message);
logger_1.logger.debug('onFail', filename, suite, name, fullMessage, message, stack);
const testcase = new Testcase(filename, stat, suite, name);
testcase.state = TestState.Failed;
testcase.message = message;
testcase.fullMessage = fullMessage;
testcase.stack = stack;
testcase.state = TestState.Failed;
allTests.set(testcase.key, testcase);
}
function onEnd(resolve, passed, failed) {
logger_1.logger.info('onEnd', passed, failed);
resolve();
}
const watchlist = new Map();
async function readTestCasesFromFile(filename, stat) {
logger_1.logger.info('readTestCasesFromFile', filename);
const theRunner = new mocha_runner_1.MochaTestRunner();
// var theRunner = new fakeTestRunner();
const tests = await theRunner.findTestsP(filename);
if (tests.length)
watchlist.set(filename, new Set()); // add the test file itself to the watchlist
if (tests.length && deps) {
// only find dependancies if there is a dependancy finder module
const files = deps.getFlat(filename); // all files this depends on
files.forEach(file => {
const newList = watchlist.get(file) || new Set();
newList.add(filename);
watchlist.set(file, newList);
});
}
tests.forEach(test => {
const testcase = new Testcase(filename, stat, test.suite, test.name);
testcase.state = TestState.Running;
allTests.set(testcase.key, testcase);
logger_1.logger.debug('adding', testcase.key, testcase.suite, testcase.name);
});
if (tests.length) {
// if no test runner, don't run it
if (command_line_1.config.testCmd === 'none')
return Promise.resolve();
return new Promise(resolve => {
return theRunner.runFileP(filename, onStart.bind(null, filename), onPass.bind(null, filename, stat), onFail.bind(null, filename, stat), onEnd.bind(null, resolve));
});
}
}
const allTests = new Map();
const allFiles = new Map();
const depsModule = command_line_1.config.dependencyModule;
const deps = depsModule === 'none' ? null : require(depsModule).tree;
function readFiles(folders) {
logger_1.logger.info('readFiles', folders);
return new Promise((resolve, reject) => {
folders.forEach(folder => {
fs_1.default.readdir(folder, async (error, files) => {
logger_1.logger.debug('readdir', folder);
if (error) {
reject(error);
}
else {
const promises = [];
const subFolders = [];
files.forEach(file => {
const filepath = path_1.default.resolve(folder, file);
const stat = fs_1.default.statSync(filepath);
if (stat.isFile() && file.endsWith('.js')) {
const lastModified = allFiles.get(filepath);
if (!lastModified || lastModified < stat.mtime) {
allFiles.set(filepath, stat.mtime);
promises.push(readTestCasesFromFile(filepath, stat));
// if first time found no need to test files that depend on it
if (lastModified) {
let x;
// l(Object.getOwnPropertyNames(require.cache));
const fullPath = path_1.default.resolve(__dirname, filepath);
logger_1.logger.info('File Changed. Looking for', fullPath, 'in', watchlist);
if ((x = watchlist.get(fullPath))) {
logger_1.logger.info('Found files that need re-run', x);
x.forEach(xx => {
const xstat = fs_1.default.statSync(xx);
promises.push(readTestCasesFromFile(xx, xstat));
});
}
}
}
}
else if (stat.isDirectory() && !file.startsWith('.') && file.indexOf('node_modules') == -1) {
subFolders.push(filepath);
}
});
await Promise.all(promises);
logger_1.logger.debug('subfolders', subFolders);
if (subFolders.length)
await readFiles(subFolders);
resolve();
}
});
});
});
}
let mode = 'd'; // i : bundle info, h : help, e - expanded errors
// columns name, file, time, status, error
const Label = {
[TestState.Ready]: 'Waiting',
[TestState.Running]: 'Running',
[TestState.Passsed]: 'Passed',
[TestState.Failed]: 'Failed',
};
// for colours see https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
const Colour = {
[TestState.Ready]: render_1.FgColour.blue,
[TestState.Running]: render_1.FgColour.yellow,
[TestState.Passsed]: render_1.FgColour.green,
[TestState.Failed]: render_1.FgColour.red,
};
function shortTextFromStack(stack) {
return stack.length ? `${stack[0].file}:${stack[0].lineno}` : '';
}
const testColumns = [
{ name: 'FILE', width: 20, just: 'l', func: (row) => path_1.default.basename(row.filename) },
{ name: 'SUITE', width: 20, just: 'l', func: (row) => row.suite },
{ name: 'NAME', width: 25, just: 'l', func: (row) => row.name },
{ name: 'STATUS', width: 8, just: 'l', func: (row) => Label[row.state] },
{ name: 'TIME(ms)', width: 8, just: 'l', func: (row) => row.runtimeInMs },
{ name: 'MSG', width: 40, just: 'l', func: (row) => row.message },
{ name: 'SOURCE', width: 36, just: 'l', func: (row) => shortTextFromStack(row.stack) },
];
function renderTestHeader() {
let failing = 0, running = 0;
allTests.forEach(test => {
failing += test.state === TestState.Failed ? 1 : 0;
running += test.state === TestState.Running ? 1 : 0;
});
render_1.renderHeader(allTests.size, failing, running, watchlist.size);
}
function renderAllTests() {
const sort = (lhs, rhs) => {
if (lhs.state === rhs.state)
return lhs.filename.localeCompare(rhs.filename);
return lhs.state - rhs.state;
};
const table = {
columns: testColumns,
rows: Array.from(allTests.values()).sort(sort),
rowColour: (row) => {
const test = row;
return Colour[test.state];
},
};
render_1.renderTable(table);
}
function renderFailures() {
Array.from(allTests)
.filter(([, t]) => t.state === TestState.Failed)
.forEach(([, t]) => {
process.stdout.write(['\x1b[31;1mFAILED:', t.suite, t.name, os_1.default.EOL].join(' '));
process.stdout.write(['\x1b[32m', t.fullMessage, '\x1b[0m', os_1.default.EOL].join(' '));
let pad = 0;
t.stack.forEach(frame => {
process.stdout.write(`\x1b[35m${' '.repeat(2 * pad++)}${frame.file}:${frame.lineno}\x1b[0m${os_1.default.EOL}`);
});
process.stdout.write(os_1.default.EOL);
});
}
function renderZoom(nth) {
logger_1.logger.debug('renderZoom', nth);
const columns = process.stdout.columns || 80;
const pair = Array.from(allTests).filter(([, t]) => t.state === TestState.Failed)[nth - 1];
if (!pair) {
logger_1.logger.error('renderZoom', nth, 'not found');
return;
}
const [, test] = pair;
let lines = 7;
lines += test.stack.length * 2; // the stack and the error take this
const windowLines = 14;
render_1.writeline('\x1b[31;1m', [test.suite, test.name, test.filename, test.fullMessage, '\x1b[0m'].join(' '));
const renderStack = [...test.stack].reverse();
let pad = 0;
renderStack.forEach(frame => {
render_1.writeline(`\x1b[35m'${' '.repeat(2 * pad++)}${frame.file}:${frame.lineno}\x1b[0m`);
lines++;
});
process.stdout.write(os_1.default.EOL);
//find first line in stack
renderStack.forEach(frame => {
if (lines < (process.stdout.rows || 24)) {
const filepath = frame.file;
const line = frame.lineno;
// inverse filename padded full width
render_1.writeline('\x1b[7m', `${filepath}:${line}`.padEnd(columns), '\x1b[0m');
if (fs_1.default.existsSync(filepath))
render_1.renderFileWindow(filepath, windowLines, line);
lines += windowLines + 1;
}
});
}
function renderHelp() {
render_1.write('Monitor Unit Testing Tool - MUTT', os_1.default.EOL);
[
`esc Default view`,
`l show info level log`,
`r re-run all tests`,
`z Zoom into test failures`,
`1-9 Zoom into test failure 1-9`,
`p process list`,
`q Quit`,
`h Help`,
].forEach(line => render_1.writeline(line));
render_1.writeline(mutt);
}
function render() {
if (logger_1.logger.type === 'stdout')
return; // don't render if logging to stdout
renderTestHeader();
process.stdout.write('\x1b[5;0H'); // row 5
if (mode >= '1' && mode <= '9')
return renderZoom(Number.parseInt(mode, 10));
else
switch (mode) {
case 'd':
return renderAllTests();
case 'z':
return renderFailures();
case 'p':
ps_1.renderProcessList();
break;
case 'h':
return renderHelp();
default:
render_1.writeline(`Nothing to show in mode '${mode}`);
}
}
function run(paths) {
readline_1.default.emitKeypressEvents(process.stdin);
process.stdin.setRawMode && process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key.name === 'q') {
render_1.write('\x1b[?1049l'); // revert alternative buffer
render_1.writeline('\x1b[?25h'); // show cursor
process.exit();
}
else if (key.name === 'r') {
allTests.clear();
allFiles.clear();
}
else if (key.name === 'l') {
logger_1.logger.level = 'info';
logger_1.logger.type = 'stdout'; // setting this will stop the render function
}
else if (key.name === 'd' || key.name === 'escape') {
mode = 'd';
logger_1.logger.level = 'off';
logger_1.logger.type = 'file';
render();
}
else {
mode = key.name;
render();
}
});
render_1.write('\x1b[?1049h'); // alternative buffer
render_1.write('\x1b[2J'); //clear
render_1.write('\x1b[H'); // home
render_1.write('\x1b[?25l'); // hide cursor
setInterval(async () => {
await readFiles(paths);
}, command_line_1.config.refreshIntervalMs);
setInterval(async () => {
await render();
}, command_line_1.config.refreshIntervalMs);
}
if (typeof command_line_1.argv.verbose !== 'undefined') {
if (!['error', 'warn', 'debug', 'info'].includes(command_line_1.argv.verbose)) {
logger_1.logger.type = 'stdout';
logger_1.logger.level = 'error';
logger_1.logger.error('-v parameter must be one of error|warn|debug|info.');
process.exit(12);
}
logger_1.logger.level = command_line_1.argv.verbose;
logger_1.logger.type = 'file';
}
const paths = Array.isArray(command_line_1.argv.paths) && command_line_1.argv.paths.length ? command_line_1.argv.paths : ['.'];
run(paths);
//# sourceMappingURL=mutt.js.map