UNPKG

muttley

Version:
374 lines 15.3 kB
"use strict"; 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