vscode-tmgrammar-test
Version:
Test runner for VSCode textmate grammars
302 lines • 12.7 kB
JavaScript
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
;