261 lines
6.5 KiB
JavaScript
261 lines
6.5 KiB
JavaScript
'use strict';
|
|
|
|
const TestDiscovery = require('./helper/test-discovery');
|
|
const TestCase = require('./helper/test-case');
|
|
const PrismLoader = require('./helper/prism-loader');
|
|
const { BFS, BFSPathToPrismTokenPath } = require('./helper/util');
|
|
const { assert } = require('chai');
|
|
const components = require('../components.json');
|
|
const ALL_LANGUAGES = [...Object.keys(components.languages).filter(k => k !== 'meta')];
|
|
|
|
|
|
describe('Pattern test coverage', function () {
|
|
/**
|
|
* @type {Map<string, PatternData>}
|
|
* @typedef PatternData
|
|
* @property {RegExp} pattern
|
|
* @property {string} language
|
|
* @property {Set<string>} from
|
|
* @property {RegExpExecArray[]} matches
|
|
*/
|
|
const patterns = new Map();
|
|
|
|
/**
|
|
* @param {string | string[]} languages
|
|
* @returns {import("./helper/prism-loader").Prism}
|
|
*/
|
|
function createInstance(languages) {
|
|
const Prism = PrismLoader.createInstance(languages);
|
|
|
|
BFS(Prism.languages, (path, object) => {
|
|
const { key, value } = path[path.length - 1];
|
|
const tokenPath = BFSPathToPrismTokenPath(path);
|
|
|
|
if (Object.prototype.toString.call(value) == '[object RegExp]') {
|
|
const regex = makeGlobal(value);
|
|
object[key] = regex;
|
|
|
|
const patternKey = String(regex);
|
|
let data = patterns.get(patternKey);
|
|
if (!data) {
|
|
data = {
|
|
pattern: regex,
|
|
language: path[1].key,
|
|
from: new Set([tokenPath]),
|
|
matches: []
|
|
};
|
|
patterns.set(patternKey, data);
|
|
} else {
|
|
data.from.add(tokenPath);
|
|
}
|
|
|
|
regex.exec = string => {
|
|
let match = RegExp.prototype.exec.call(regex, string);
|
|
if (match) {
|
|
data.matches.push(match);
|
|
}
|
|
return match;
|
|
};
|
|
}
|
|
});
|
|
|
|
return Prism;
|
|
}
|
|
|
|
describe('Register all patterns', function () {
|
|
it('all', function () {
|
|
this.slow(10 * 1000);
|
|
// This will cause ALL regexes of Prism to be registered in the patterns map.
|
|
// (Languages that don't have any tests can't be caught otherwise.)
|
|
createInstance(ALL_LANGUAGES);
|
|
});
|
|
});
|
|
|
|
describe('Run all language tests', function () {
|
|
// define tests for all tests in all languages in the test suite
|
|
for (const [languageIdentifier, files] of TestDiscovery.loadAllTests()) {
|
|
it(languageIdentifier, function () {
|
|
this.timeout(10 * 1000);
|
|
|
|
for (const filePath of files) {
|
|
try {
|
|
TestCase.run({
|
|
languageIdentifier,
|
|
filePath,
|
|
updateMode: 'none',
|
|
createInstance
|
|
});
|
|
} catch (error) {
|
|
// we don't case about whether the test succeeds,
|
|
// we just want to gather usage data
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('Coverage', function () {
|
|
for (const language of ALL_LANGUAGES) {
|
|
describe(language, function () {
|
|
it(`- should cover all patterns`, function () {
|
|
const untested = getAllOf(language).filter(d => d.matches.length === 0);
|
|
if (untested.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const problems = untested.map(data => {
|
|
return formatProblem(data, [
|
|
'This pattern is completely untested. Add test files that match this pattern.'
|
|
]);
|
|
});
|
|
|
|
assert.fail([
|
|
`${problems.length} pattern(s) are untested:\n`
|
|
+ 'You can learn more about writing tests at https://prismjs.com/test-suite.html#writing-tests',
|
|
...problems
|
|
].join('\n\n'));
|
|
});
|
|
|
|
it(`- should exhaustively cover all keywords in keyword lists`, function () {
|
|
const problems = [];
|
|
|
|
for (const data of getAllOf(language)) {
|
|
if (data.matches.length === 0) {
|
|
// don't report the same pattern twice
|
|
continue;
|
|
}
|
|
|
|
const keywords = getKeywordList(data.pattern);
|
|
if (!keywords) {
|
|
continue;
|
|
}
|
|
const keywordCount = keywords.size;
|
|
|
|
data.matches.forEach(([m]) => {
|
|
if (data.pattern.ignoreCase) {
|
|
m = m.toUpperCase();
|
|
}
|
|
keywords.delete(m);
|
|
});
|
|
|
|
if (keywords.size > 0) {
|
|
problems.push(formatProblem(data, [
|
|
`Add test files to test all keywords. The following keywords (${keywords.size}/${keywordCount}) are untested:`,
|
|
...[...keywords].map(k => ` ${k}`)
|
|
]));
|
|
}
|
|
}
|
|
|
|
if (problems.length === 0) {
|
|
return;
|
|
}
|
|
|
|
assert.fail([
|
|
`${problems.length} keyword list(s) are not exhaustively tested:\n`
|
|
+ 'You can learn more about writing tests at https://prismjs.com/test-suite.html#writing-tests',
|
|
...problems
|
|
].join('\n\n'));
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @param {string} language
|
|
* @returns {PatternData[]}
|
|
*/
|
|
function getAllOf(language) {
|
|
return [...patterns.values()].filter(d => d.language === language);
|
|
}
|
|
|
|
/**
|
|
* @param {string} string
|
|
* @param {number} maxLength
|
|
* @returns {string}
|
|
*/
|
|
function short(string, maxLength) {
|
|
if (string.length > maxLength) {
|
|
return string.slice(0, maxLength - 1) + '…';
|
|
} else {
|
|
return string;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the given pattern string describes a keyword list, all keyword will be returned. Otherwise, `null` will be
|
|
* returned.
|
|
*
|
|
* @param {RegExp} pattern
|
|
* @returns {Set<string> | null}
|
|
*/
|
|
function getKeywordList(pattern) {
|
|
// Right now, only keyword lists of the form /\b(?:foo|bar)\b/ are supported.
|
|
// In the future, we might want to convert these regexes to NFAs and iterate all words to cover more complex
|
|
// keyword lists and even operator and punctuation lists.
|
|
|
|
let source = pattern.source.replace(/^\\b|\\b$/g, '');
|
|
if (source.startsWith('(?:') && source.endsWith(')')) {
|
|
source = source.slice('(?:'.length, source.length - ')'.length);
|
|
}
|
|
|
|
if (/^\w+(?:\|\w+)*$/.test(source)) {
|
|
if (pattern.ignoreCase) {
|
|
source = source.toUpperCase();
|
|
}
|
|
return new Set(source.split(/\|/g));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Iterable<string>} occurrences
|
|
* @returns {{ origin: string; otherOccurrences: string[] }}
|
|
*/
|
|
function splitOccurrences(occurrences) {
|
|
const all = [...occurrences];
|
|
return {
|
|
origin: all[0],
|
|
otherOccurrences: all.slice(1),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {PatternData} data
|
|
* @param {string[]} messageLines
|
|
* @returns {string}
|
|
*/
|
|
function formatProblem(data, messageLines) {
|
|
const { origin, otherOccurrences } = splitOccurrences(data.from);
|
|
|
|
const lines = [
|
|
`${origin}:`,
|
|
short(String(data.pattern), 100),
|
|
'',
|
|
...messageLines,
|
|
];
|
|
|
|
if (otherOccurrences.length) {
|
|
lines.push(
|
|
'',
|
|
'Other occurrences of this pattern:',
|
|
...otherOccurrences.map(o => `- ${o}`)
|
|
);
|
|
}
|
|
|
|
return lines.join('\n ');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @param {RegExp} regex
|
|
* @returns {RegExp}
|
|
*/
|
|
function makeGlobal(regex) {
|
|
if (regex.global) {
|
|
return regex;
|
|
} else {
|
|
return RegExp(regex.source, regex.flags + 'g');
|
|
}
|
|
}
|