Skip to content

Commit

Permalink
Improved tests using BFS instead if DFS
Browse files Browse the repository at this point in the history
  • Loading branch information
RunDevelopment committed Jun 2, 2019
1 parent 476893f commit caae686
Showing 1 changed file with 108 additions and 20 deletions.
128 changes: 108 additions & 20 deletions tests/pattern-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@ const { assert } = require("chai");
const PrismLoader = require('./helper/prism-loader');
const { languages } = require('../components');


/**
* Performs a breadth-first search on the given start element.
*
* @param {any} start
* @param {(path: { key: string, value: any }[]) => void} callback
*/
function BFS(start, callback) {
const visited = new Set();
/** @type {{ key: string, value: any }[][]} */
let toVisit = [
[{ key: null, value: start }]
];

callback(toVisit[0]);

while (toVisit.length > 0) {
/** @type {{ key: string, value: any }[][]} */
const newToVisit = [];

for (const path of toVisit) {
const obj = path[path.length - 1].value;
if (!visited.has(obj)) {
visited.add(obj);

for (const key in obj) {
const value = obj[key];

path.push({ key, value });
callback(path);

if (Array.isArray(value) || Object.prototype.toString.call(value) == '[object Object]') {
newToVisit.push([...path]);
}

path.pop();
}
}
}

toVisit = newToVisit;
}
}


for (const lang in languages) {
if (lang === 'meta') {
continue;
Expand All @@ -13,44 +58,87 @@ for (const lang in languages) {

const Prism = PrismLoader.createInstance(lang);

it('- should not match the empty string', function () {
let lastToken = '<unknown>';
/**
* Invokes the given function on every pattern in `Prism.languages`.
*
* @param {(values: { pattern: RegExp, tokenPath: string, name: string, parent: any, path: { key: string, value: any }[] }) => void} callback
*/
function forEachPattern(callback) {
BFS(Prism.languages, path => {
const { key, value } = path[path.length - 1];

Prism.languages.DFS(Prism.languages, function (name, value) {
if (typeof this === 'object' && !Array.isArray(this) && name !== 'pattern') {
lastToken = name;
let tokenPath = '<languages>';
for (const { key } of path) {
if (!key) {
// do nothing
} else if (/^\d+$/.test(key)) {
tokenPath += `[${key}]`;
} else if (/^[a-z]\w*$/i.test(key)) {
tokenPath += `.${key}`;
} else {
tokenPath += `[${JSON.stringify(key)}]`;
}
}

if (Prism.util.type(value) === 'RegExp') {
assert.notMatch('', value, `Token '${lastToken}': ${value} should not match the empty string.`);
if (Object.prototype.toString.call(value) == '[object RegExp]') {
callback({
pattern: value,
tokenPath,
name: key,
parent: path.length > 1 ? path[path.length - 2].value : undefined,
path,
});
}
});
}


it('- should not match the empty string', function () {
forEachPattern(({ pattern, tokenPath }) => {
// test for empty string
assert.notMatch('', pattern, `Token ${tokenPath}: ${pattern} should not match the empty string.`);
});
});

it('- should have nice names and aliases', function () {
let lastToken = '<unknown>';
it('- should have a capturing group if lookbehind is set to true', function () {
forEachPattern(({ pattern, tokenPath, name, parent }) => {
if (name === 'pattern' && parent.lookbehind) {
const simplifiedSource = pattern.source.replace(/\\\D/g, '_').replace(/\[[^\]]*\]/g, '_');

if (!/\((?!\?)/.test(simplifiedSource)) {
assert.fail(`Token ${tokenPath}: The pattern is set to 'lookbehind: true' but does not have a capturing group.`);
}
}
});
});

it('- should have nice names and aliases', function () {
const niceName = /^[a-z][a-z\d]*(?:[-_][a-z\d]+)*$/;
function testName(name, desc = 'token name') {
if (!niceName.test(name)) {
assert.fail(`The ${desc} '${name}' does not match ${niceName}`);
}
}

Prism.languages.DFS(Prism.languages, function (name, value) {
if (typeof this === 'object' && !Array.isArray(this) && name !== 'pattern') {
lastToken = name;
forEachPattern(({ name, parent, tokenPath, path }) => {
// token name
let offset = 1;
if (name == 'pattern') { // regex can be inside an object
offset++;
}

if (Prism.util.type(value) === 'RegExp') {
testName(lastToken);
if (Array.isArray(path[path.length - 1 - offset].value)) { // regex/regex object can be inside an array
offset++;
}
const patternName = path[path.length - offset].key;
testName(patternName);

if (name === 'alias') {
if (typeof value === 'string') {
testName(value, `alias of '${lastToken}'`);
} else if (Array.isArray(value)) {
value.forEach(name => testName(name, `alias of '${lastToken}'`));
// check alias
if (name == 'pattern' && 'alias' in parent) {
const alias = parent.alias;
if (typeof alias === 'string') {
testName(alias, `alias of '${tokenPath}'`);
} else if (Array.isArray(alias)) {
alias.forEach(name => testName(name, `alias of '${tokenPath}'`));
}
}
});
Expand Down

0 comments on commit caae686

Please sign in to comment.