UNPKG

vscode-tmgrammar-test

Version:
302 lines 12.7 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs")); const tty = __importStar(require("tty")); const chalk_1 = __importDefault(require("chalk")); const commander_1 = require("commander"); const glob_1 = __importDefault(require("glob")); const index_1 = require("./common/index"); const os_1 = require("os"); const index_2 = require("./snapshot/index"); const diff = __importStar(require("diff")); const path = __importStar(require("path")); const bottleneck_1 = __importDefault(require("bottleneck")); let packageJson = require('../package.json'); commander_1.program .description('Run VSCode textmate grammar snapshot tests') .option('-u, --updateSnapshot', 'overwrite all snap files with new changes') .option('--config <configuration.json>', 'Path to the language configuration, package.json by default') .option('--printNotModified', 'include not modified scopes in the output', false) .option('--expandDiff', 'produce each diff on two lines prefixed with "++" and "--"', false) .option('-g, --grammar <grammar>', "Path to a grammar file. Multiple options supported. 'scopeName' is taken from the grammar", (x, xs) => xs.concat([x]), []) .option('-s, --scope <scope>', 'Explicitly specify scope of testcases, e.g. source.dhall') .version(packageJson.version) .argument('<testcases...>', 'A glob pattern(s) which specifies testcases to run, e.g. "./tests/**/test*.dhall". Quotes are important!') .parse(process.argv); const options = commander_1.program.opts(); let isatty = tty.isatty(1) && tty.isatty(2); const symbols = { ok: '✓', err: '✖', dot: '․', comma: ',', bang: '!' }; if (process.platform === 'win32') { symbols.ok = '\u221A'; symbols.err = '\u00D7'; symbols.dot = '.'; } let terminalWidth = 75; if (isatty) { terminalWidth = process.stdout.getWindowSize()[0]; } const TestFailed = -1; const TestSuccessful = 0; const Padding = ' '; const rawTestCases = commander_1.program.args.map((x) => glob_1.default.sync(x)).flat(); const testCases = rawTestCases.filter((x) => !x.endsWith('.snap')); if (testCases.length === 0) { console.log(chalk_1.default.red('ERROR') + " No testcases found. Got: '" + chalk_1.default.gray(commander_1.program.args.join(',')) + "'"); process.exit(-1); } let { grammars, extensionToScope } = (0, index_1.loadConfiguration)(options.config, options.scope, options.grammar); const limiter = new bottleneck_1.default({ maxConcurrent: 8, minTime: 0 }); const registry = (0, index_1.createRegistry)(grammars); const testResults = Promise.all(testCases.map((filename) => { const src = fs.readFileSync(filename).toString(); const scope = extensionToScope(path.extname(filename)); if (scope === undefined) { console.log(chalk_1.default.red('ERROR') + " can't run testcase: " + chalk_1.default.whiteBright(filename)); console.log('No scope is associated with the file.'); return TestFailed; } return limiter.schedule(() => (0, index_2.getVSCodeTokens)(registry, scope, src)) .then((tokens) => { if (fs.existsSync(filename + '.snap')) { if (options.updateSnapshot) { console.log(chalk_1.default.yellowBright('Updating snapshot for ') + chalk_1.default.whiteBright(filename + '.snap')); fs.writeFileSync(filename + '.snap', (0, index_2.renderSnap)(tokens), 'utf8'); return TestSuccessful; } else { const expectedTokens = (0, index_2.parseSnap)(fs.readFileSync(filename + '.snap').toString()); return renderTestResult(filename, expectedTokens, tokens); } } else { console.log(chalk_1.default.yellowBright('Generating snapshot ') + chalk_1.default.whiteBright(filename + '.snap')); fs.writeFileSync(filename + '.snap', (0, index_2.renderSnap)(tokens)); return TestSuccessful; } }) .catch((error) => { console.log(chalk_1.default.red('ERROR') + " can't run testcase: " + chalk_1.default.whiteBright(filename)); console.log(error); return TestFailed; }); })); testResults.then((xs) => { const result = xs.reduce((a, b) => a + b, 0); if (result === TestSuccessful) { process.exit(0); } else { process.exit(-1); } }); function renderTestResult(filename, expected, actual) { if (expected.length !== actual.length) { console.log(chalk_1.default.red('ERROR running testcase ') + chalk_1.default.whiteBright(filename) + chalk_1.default.red(` snapshot and actual file contain different number of lines.${os_1.EOL}`)); return TestFailed; } for (let i = 0; i < expected.length; i++) { const exp = expected[i]; const act = actual[i]; if (exp.src !== act.src) { console.log(chalk_1.default.red('ERROR running testcase ') + chalk_1.default.whiteBright(filename) + chalk_1.default.red(` source different snapshot at line ${i + 1}.${os_1.EOL} expected: ${exp.src}${os_1.EOL} actual: ${act.src}${os_1.EOL}`)); return TestFailed; } } // renderSnap won't produce assertions for empty lines, so we'll remove them here // for both actual end expected let actual1 = actual.filter((a) => a.src.trim().length > 0); let expected1 = expected.filter((a) => a.src.trim().length > 0); const wrongLines = flatten(expected1.map((exp, i) => { const act = actual1[i]; const expTokenMap = toMap((t) => `${t.startIndex}:${t.startIndex}`, exp.tokens); const actTokenMap = toMap((t) => `${t.startIndex}:${t.startIndex}`, act.tokens); const removed = exp.tokens .filter((t) => actTokenMap[`${t.startIndex}:${t.startIndex}`] === undefined) .map((t) => { return { changes: [ { text: t.scopes.join(' '), changeType: Removed } ], from: t.startIndex, to: t.endIndex }; }); const added = act.tokens .filter((t) => expTokenMap[`${t.startIndex}:${t.startIndex}`] === undefined) .map((t) => { return { changes: [ { text: t.scopes.join(' '), changeType: Added } ], from: t.startIndex, to: t.endIndex }; }); const modified = flatten(act.tokens.map((a) => { const e = expTokenMap[`${a.startIndex}:${a.startIndex}`]; if (e !== undefined) { const changes = diff.diffArrays(e.scopes, a.scopes); if (changes.length === 1 && !changes[0].added && !changes[0].removed) { return []; } const tchanges = changes.map((change) => { let changeType = change.added ? Added : change.removed ? Removed : NotModified; return { text: change.value.join(' '), changeType: changeType }; }); return [ { changes: tchanges, from: a.startIndex, to: a.endIndex } ]; } else { return []; } })); const allChanges = modified .concat(added) .concat(removed) .sort((x, y) => (x.from - y.from) * 10000 + (x.to - y.to)); if (allChanges.length > 0) { return [[allChanges, exp.src, i]]; } else { return []; } })); if (wrongLines.length > 0) { console.log(chalk_1.default.red('ERROR in test case ') + chalk_1.default.whiteBright(filename)); console.log(Padding + Padding + chalk_1.default.red('-- existing snapshot')); console.log(Padding + Padding + chalk_1.default.green('++ new changes')); console.log(); if (options.expandDiff) { printDiffOnTwoLines(wrongLines); } else { printDiffInline(wrongLines); } console.log(); return TestFailed; } else { console.log(chalk_1.default.green(symbols.ok) + ' ' + chalk_1.default.whiteBright(filename) + ' run successfully.'); return TestSuccessful; } } function printDiffInline(wrongLines) { wrongLines.forEach(([changes, src, i]) => { const lineNumberOffset = printSourceLine(src, i); changes.forEach((tchanges) => { const change = tchanges.changes .filter((c) => options.printNotModified || c.changeType !== NotModified) .map((c) => { let color = c.changeType === Added ? chalk_1.default.green : c.changeType === Removed ? chalk_1.default.red : chalk_1.default.gray; return color(c.text); }) .join(' '); printAccents(lineNumberOffset, tchanges.from, tchanges.to, change); }); console.log(); }); } function printDiffOnTwoLines(wrongLines) { wrongLines.forEach(([changes, src, i]) => { const lineNumberOffset = printSourceLine(src, i); changes.forEach((tchanges) => { const removed = tchanges.changes .filter((c) => c.changeType === Removed || (c.changeType === NotModified && options.printNotModified)) .map((c) => { return chalk_1.default.red(c.text); }) .join(' '); const added = tchanges.changes .filter((c) => c.changeType === Added || (c.changeType === NotModified && options.printNotModified)) .map((c) => { return chalk_1.default.green(c.text); }) .join(' '); printAccents1(lineNumberOffset, tchanges.from, tchanges.to, chalk_1.default.red('-- ') + removed, Removed); printAccents1(lineNumberOffset, tchanges.from, tchanges.to, chalk_1.default.green('++ ') + added, Added); }); console.log(); }); } function toMap(f, xs) { return xs.reduce((m, x) => { m[f(x)] = x; return m; }, {}); } function flatten(arr) { return arr.reduce((acc, val) => acc.concat(val), []); } const NotModified = 0; const Removed = 1; const Added = 2; function printSourceLine(line, n) { const pos = n + 1 + ': '; console.log(Padding + chalk_1.default.gray(pos) + line); return pos.length; } function printAccents(offset, from, to, diff) { const accents = ' '.repeat(from) + '^'.repeat(to - from); console.log(Padding + ' '.repeat(offset) + accents + ' ' + diff); } function printAccents1(offset, from, to, diff, change) { let color = change === Added ? chalk_1.default.green : change === Removed ? chalk_1.default.red : chalk_1.default.gray; let prefix = change === Added ? '++' : change === Removed ? '--' : ' '; const accents = color(' '.repeat(from) + '^'.repeat(to - from)); console.log(color(prefix) + ' '.repeat(offset) + accents + ' ' + diff); } //# sourceMappingURL=snapshot.js.map