Skip to content

Commit

Permalink
Fixes #12: Handle differently loop case 1, improve tests and test runner
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdima committed May 23, 2016
1 parent b14ff75 commit 8b9f58f
Show file tree
Hide file tree
Showing 8 changed files with 652 additions and 193 deletions.
10 changes: 6 additions & 4 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ gulp.task('watch', ['bundle'], function () {

gulp.task('test', function () {
var tests = require('./release/tests/tests');
tests.runDescriptiveTests(path.join(__dirname, './test-cases/first-mate/tests.json'));
tests.runDescriptiveTests(path.join(__dirname, './test-cases/suite1/tests.json'));

tests.runMatcherTests(path.join(__dirname, './test-cases/matcher/testData.json'));
tests.runTests([
path.join(__dirname, './test-cases/first-mate/tests.json'),
path.join(__dirname, './test-cases/suite1/tests.json')
], [
path.join(__dirname, './test-cases/matcher/testData.json')
]);
});
13 changes: 8 additions & 5 deletions release/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1312,10 +1312,13 @@ function _tokenizeString(grammar, lineText, isFirstLine, linePos, stack, lineTok
handleCaptures(grammar, lineText, isFirstLine, stack, lineTokens, poppedRule.endCaptures, captureIndices);
lineTokens.produce(stack, captureIndices[0].end);
// pop
stack.pop();
var popped = stack.pop();
if (!hasAdvanced && stackElement.enterPos === linePos) {
// Grammar pushed & popped a rule without advancing
console.error('Grammar is in an endless loop - case 1');
console.error('[1] - Grammar is in an endless loop - Grammar pushed & popped a rule without advancing');
// See https://github.com/Microsoft/vscode-textmate/issues/12
// Let's assume this was a mistake by the grammar author and the intent was to continue in this state
stack.push(popped);
lineTokens.produce(stack, lineLength);
linePos = lineLength;
return false;
Expand Down Expand Up @@ -1343,7 +1346,7 @@ function _tokenizeString(grammar, lineText, isFirstLine, linePos, stack, lineTok
}
if (!hasAdvanced && stackElement.ruleId === stack[stack.length - 1].ruleId) {
// Grammar pushed the same rule without advancing
console.error('Grammar is in an endless loop - case 2');
console.error('[2] - Grammar is in an endless loop - Grammar pushed the same rule without advancing');
stack.pop();
lineTokens.produce(stack, lineLength);
linePos = lineLength;
Expand All @@ -1361,7 +1364,7 @@ function _tokenizeString(grammar, lineText, isFirstLine, linePos, stack, lineTok
}
if (!hasAdvanced && stackElement.ruleId === stack[stack.length - 1].ruleId) {
// Grammar pushed the same rule without advancing
console.error('Grammar is in an endless loop - case 2');
console.error('[3] - Grammar is in an endless loop - Grammar pushed the same rule without advancing');
stack.pop();
lineTokens.produce(stack, lineLength);
linePos = lineLength;
Expand All @@ -1376,7 +1379,7 @@ function _tokenizeString(grammar, lineText, isFirstLine, linePos, stack, lineTok
stack.pop();
if (!hasAdvanced) {
// Grammar is not advancing, nor is it pushing/popping
console.error('Grammar is in an endless loop - case 3');
console.error('[4] - Grammar is in an endless loop - Grammar is not advancing, nor is it pushing/popping');
if (stack.length > 1) {
stack.pop();
}
Expand Down
215 changes: 134 additions & 81 deletions release/tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,136 @@ var fs = require('fs');
var path = require('path');
var main_1 = require('../main');
require('colors');
var errCnt = 0;
function runDescriptiveTests(testLocation) {
var TestResult;
(function (TestResult) {
TestResult[TestResult["Pending"] = 0] = "Pending";
TestResult[TestResult["Success"] = 1] = "Success";
TestResult[TestResult["Failed"] = 2] = "Failed";
})(TestResult || (TestResult = {}));
var Test = (function () {
function Test(testName, runner) {
this.testName = testName;
this._runner = runner;
this.result = TestResult.Pending;
this.failReason = null;
}
Test.prototype.run = function () {
var ctx = new TestContext(this);
try {
this.result = TestResult.Success;
this._runner(ctx);
}
catch (err) {
this.result = TestResult.Failed;
this.failReason = err;
}
};
Test.prototype.fail = function (message, actual, expected) {
this.result = TestResult.Failed;
var reason = [
message
];
if (actual) {
'ACTUAL: ' + reason.push(JSON.stringify(actual, null, '\t'));
}
if (expected) {
'EXPECTED: ' + reason.push(JSON.stringify(expected, null, '\t'));
}
this.failReason = reason.join('\n');
};
return Test;
}());
var TestContext = (function () {
function TestContext(test) {
this._test = test;
}
TestContext.prototype.fail = function (message, actual, expected) {
this._test.fail(message, actual, expected);
};
return TestContext;
}());
var TestManager = (function () {
function TestManager() {
this._tests = [];
}
TestManager.prototype.registerTest = function (testName, runner) {
this._tests.push(new Test(testName, runner));
};
TestManager.prototype.runTests = function () {
var len = this._tests.length;
for (var i = 0; i < len; i++) {
var test = this._tests[i];
var progress = (i + 1) + '/' + len;
console.log(progress.yellow, ': ' + test.testName);
test.run();
if (test.result === TestResult.Failed) {
console.log('FAILED'.red);
console.log(test.failReason);
}
}
var passed = this._tests.filter(function (t) { return t.result === TestResult.Success; });
if (passed.length === len) {
console.log((passed.length + '/' + len + ' PASSED.').green);
}
else {
console.log((passed.length + '/' + len + ' PASSED.').red);
}
};
return TestManager;
}());
function runTests(tokenizationTestPaths, matcherTestPaths) {
var manager = new TestManager();
tokenizationTestPaths.forEach(function (path) {
generateTokenizationTests(manager, path);
});
matcherTestPaths.forEach(function (path) {
generateMatcherTests(manager, path);
});
manager.runTests();
}
exports.runTests = runTests;
function generateTokenizationTests(manager, testLocation) {
var tests = JSON.parse(fs.readFileSync(testLocation).toString());
errCnt = 0;
var suiteName = path.join(path.basename(path.dirname(testLocation)), path.basename(testLocation));
tests.forEach(function (test, index) {
var desc = test.desc;
var noAsserts = (test.feature === 'endless-loop');
console.log(index + ' - RUNNING ' + desc);
var locator = {
getFilePath: function (scopeName) { return null; },
getInjections: function (scopeName) {
if (scopeName === test.grammarScopeName) {
return test.grammarInjections;
manager.registerTest(suiteName + ' > ' + test.desc, function (ctx) {
var locator = {
getFilePath: function (scopeName) { return null; },
getInjections: function (scopeName) {
if (scopeName === test.grammarScopeName) {
return test.grammarInjections;
}
return void 0;
}
return void 0;
};
var registry = new main_1.Registry(locator);
var grammar = null;
test.grammars.forEach(function (grammarPath) {
var tmpGrammar = registry.loadGrammarFromPathSync(path.join(path.dirname(testLocation), grammarPath));
if (test.grammarPath === grammarPath) {
grammar = tmpGrammar;
}
});
if (test.grammarScopeName) {
grammar = registry.grammarForScopeName(test.grammarScopeName);
}
};
var registry = new main_1.Registry(locator);
var grammar = null;
test.grammars.forEach(function (grammarPath) {
var tmpGrammar = registry.loadGrammarFromPathSync(path.join(path.dirname(testLocation), grammarPath));
if (test.grammarPath === grammarPath) {
grammar = tmpGrammar;
var prevState = null;
if (!grammar) {
ctx.fail('I HAVE NO GRAMMAR FOR TEST');
return;
}
for (var i = 0; i < test.lines.length; i++) {
prevState = assertTokenization(ctx, grammar, test.lines[i], prevState);
}
});
if (test.grammarScopeName) {
grammar = registry.grammarForScopeName(test.grammarScopeName);
}
var prevState = null;
if (!grammar) {
console.error('I HAVE NO GRAMMAR FOR TEST ' + desc);
return;
}
for (var i = 0; i < test.lines.length; i++) {
prevState = assertTokenization(noAsserts, grammar, test.lines[i], prevState, desc);
}
});
if (errCnt === 0) {
var msg = 'Test suite at ' + testLocation + ' finished ok';
console.log(msg.green);
}
else {
var msg = 'Test suite at ' + testLocation + ' finished with ' + errCnt + ' errors.';
console.log(msg.red);
}
}
exports.runDescriptiveTests = runDescriptiveTests;
function assertTokenization(noAsserts, grammar, testCase, prevState, desc) {
function assertTokenization(ctx, grammar, testCase, prevState) {
var r = grammar.tokenizeLine(testCase.line, prevState);
if (!noAsserts) {
assertTokens(r.tokens, testCase.line, testCase.tokens, desc);
}
assertTokens(ctx, r.tokens, testCase.line, testCase.tokens);
return r.ruleStack;
}
function fail(message, actual, expected) {
errCnt++;
console.error(message.red);
console.log(JSON.stringify(actual, null, '\t'));
console.log(JSON.stringify(expected, null, '\t'));
}
function assertTokens(actual, line, expected, desc) {
function assertTokens(ctx, actual, line, expected) {
var actualTokens = actual.map(function (token) {
return {
value: line.substring(token.startIndex, token.endIndex),
Expand All @@ -80,32 +149,32 @@ function assertTokens(actual, line, expected, desc) {
});
}
if (actualTokens.length !== expected.length) {
fail('test: GOT DIFFERENT LENGTHS FOR ' + desc, actualTokens, expected);
ctx.fail('GOT DIFFERENT LENGTHS FOR ', actualTokens, expected);
return;
}
for (var i = 0, len = actualTokens.length; i < len; i++) {
assertToken(actualTokens[i], expected[i], desc);
assertToken(ctx, actualTokens[i], expected[i]);
}
}
function assertToken(actual, expected, desc) {
function assertToken(ctx, actual, expected) {
if (actual.value !== expected.value) {
fail('test: GOT DIFFERENT VALUES FOR ' + desc, actual.value, expected.value);
ctx.fail('test: GOT DIFFERENT VALUES FOR ', actual.value, expected.value);
return;
}
if (actual.scopes.length !== expected.scopes.length) {
fail('test: GOT DIFFERENT scope lengths FOR ' + desc, actual.scopes, expected.scopes);
ctx.fail('test: GOT DIFFERENT scope lengths FOR ', actual.scopes, expected.scopes);
return;
}
for (var i = 0, len = actual.scopes.length; i < len; i++) {
if (actual.scopes[i] !== expected.scopes[i]) {
fail('test: GOT DIFFERENT scopes FOR ' + desc, actual.scopes, expected.scopes);
ctx.fail('test: GOT DIFFERENT scopes FOR ', actual.scopes, expected.scopes);
return;
}
}
}
function runMatcherTests(testLocation, testNum) {
if (testNum === void 0) { testNum = -1; }
function generateMatcherTests(manager, testLocation) {
var tests = JSON.parse(fs.readFileSync(testLocation).toString());
var suiteName = path.join(path.basename(path.dirname(testLocation)), path.basename(testLocation));
var nameMatcher = function (identifers, stackElements) {
var lastIndex = 0;
return identifers.every(function (identifier) {
Expand All @@ -118,29 +187,13 @@ function runMatcherTests(testLocation, testNum) {
return false;
});
};
var errCnt = 0;
tests.forEach(function (test, index) {
if (testNum !== -1 && testNum !== index) {
return;
}
var matcher = main_1.createMatcher(test.expression, nameMatcher);
var result = matcher(test.input);
if (result === test.result) {
console.log(index + ': passed');
}
else {
var message = index + ': failed , expected ' + test.result;
console.error(message.red);
errCnt++;
}
manager.registerTest(suiteName + ' > ' + index, function (ctx) {
var matcher = main_1.createMatcher(test.expression, nameMatcher);
var result = matcher(test.input);
if (result !== test.result) {
ctx.fail('matcher expected', result, test.result);
}
});
});
if (errCnt === 0) {
var msg = 'Test suite at ' + testLocation + ' finished ok';
console.log(msg.green);
}
else {
var msg = 'Test suite at ' + testLocation + ' finished with ' + errCnt + ' errors.';
console.log(msg.red);
}
}
exports.runMatcherTests = runMatcherTests;
16 changes: 11 additions & 5 deletions src/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,19 @@ function _tokenizeString(grammar: Grammar, lineText: OnigString, isFirstLine: bo
lineTokens.produce(stack, captureIndices[0].end);

// pop
stack.pop();
let popped = stack.pop();

if (!hasAdvanced && stackElement.enterPos === linePos) {
// Grammar pushed & popped a rule without advancing
console.error('Grammar is in an endless loop - case 1');
console.error('[1] - Grammar is in an endless loop - Grammar pushed & popped a rule without advancing');

// See https://github.com/Microsoft/vscode-textmate/issues/12
// Let's assume this was a mistake by the grammar author and the intent was to continue in this state
stack.push(popped);

lineTokens.produce(stack, lineLength);
linePos = lineLength;

return false;
}
} else if (matchedRuleId === -3) {
Expand Down Expand Up @@ -523,7 +529,7 @@ function _tokenizeString(grammar: Grammar, lineText: OnigString, isFirstLine: bo

if (!hasAdvanced && stackElement.ruleId === stack[stack.length - 1].ruleId) {
// Grammar pushed the same rule without advancing
console.error('Grammar is in an endless loop - case 2');
console.error('[2] - Grammar is in an endless loop - Grammar pushed the same rule without advancing');
stack.pop();
lineTokens.produce(stack, lineLength);
linePos = lineLength;
Expand All @@ -543,7 +549,7 @@ function _tokenizeString(grammar: Grammar, lineText: OnigString, isFirstLine: bo

if (!hasAdvanced && stackElement.ruleId === stack[stack.length - 1].ruleId) {
// Grammar pushed the same rule without advancing
console.error('Grammar is in an endless loop - case 2');
console.error('[3] - Grammar is in an endless loop - Grammar pushed the same rule without advancing');
stack.pop();
lineTokens.produce(stack, lineLength);
linePos = lineLength;
Expand All @@ -560,7 +566,7 @@ function _tokenizeString(grammar: Grammar, lineText: OnigString, isFirstLine: bo

if (!hasAdvanced) {
// Grammar is not advancing, nor is it pushing/popping
console.error('Grammar is in an endless loop - case 3');
console.error('[4] - Grammar is in an endless loop - Grammar is not advancing, nor is it pushing/popping');
if (stack.length > 1) {
stack.pop();
}
Expand Down
Loading

0 comments on commit 8b9f58f

Please sign in to comment.