citeproc-test-runner
Version:
CSL test runner with style support
885 lines (845 loc) • 33.1 kB
JavaScript
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const tmp = require("tmp");
const clear = require("cross-clear");
const chokidar = require("chokidar");
const normalizeNewline = require("normalize-newline");
const fetchURL = require("fetch-promise");
const zoteroToCSLM = require('zotero2jurismcsl').convert;
const zoteroToCSL = require('zotero-to-csl');
var getAbbrevPath = require("citeproc-abbrevs").getAbbrevPath;
const config = require("./lib/configs.js");
const reporters = require("./lib/reporters.js").get(config);
const parseFixture = require("./lib/fixture-parser.js").parseFixture;
const sources = require("./lib/sources.js");
const options = require("./lib/options.js").options;
const usage = require("./lib/options.js").usage;
const errors = require("./lib/errors.js");
const Sys = require(path.join(config.path.scriptdir, "lib", "sys.js"));
const { styleCapabilities } = require("./lib/style-capabilities");
const {version} = require("./package.json");
var ksTimeout;
var cdTimeout;
var skipNames = {};
var TRAVIS = process.env.TRAVIS;
const groupIdMap = {
final: 2319948,
draft: 2339078
}
/*
* Console
*/
if (process.stdin.setRawMode) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
// The console needs to run in binary mode, to give the fancy reporters
// control over the terminal
//process.stdin.setEncoding( 'utf8' );
process.stdin.on('data', function( key ){
// ctrl-c ( end of text )
if ( key.toString("hex") === "03" ) {
console.log("\n");
process.exit();
}
});
/*
* Functions
*/
function Stripper(fn, noStrip) {
this.fn = fn;
this.noStrip = noStrip;
this.arr = [];
this.area = "code";
this.state = "reading";
this.skipStarRex = new RegExp("^\\s*(\\/\\*.*?\\*\\/)\\r?$", "m");
this.skipSlashRex = new RegExp("^\\s*(\\/\\/.*)\\r?$");
this.openRex = new RegExp("^\\s*(\\/\\*|\\/\\/SNIP-START)");
this.closeRex = new RegExp("^\\s*(\\*\\/|\\/\\/SNIP-END)\\s*\\r?$");
this.checkRex = new RegExp("");
this.dumpArr = function() {
return this.arr.join("\n");
};
this.checkLine = function (line) {
if (line.match(/^.use strict.;?\r?$/)) {
return;
}
if (this.noStrip) {
this.arr.push(line);
} else {
var m = null;
if (this.skipStarRex.test(line)) {
return;
} else if (this.openRex.test(line)) {
m = this.openRex.exec(line);
this.area = "comment";
this.state = "opening";
} else if (this.closeRex.test(line)) {
m = this.closeRex.exec(line);
this.state = "closing";
} else if (this.skipSlashRex.test(line)) {
return;
} else {
if (this.state === "opening") {
this.state = "skipping";
} else if (this.state === "closing") {
this.state = "reading";
this.area = "code";
}
}
if (this.state === "reading") {
if (line.trim()) {
this.arr.push(line);
}
}
}
};
}
function checkSanity() {
if (options.h) {
console.log(usage);
process.exit();
}
if (options.version) {
console.log(`cslrun version: ${version}`);
process.exit();
}
if (TRAVIS) {
options.r = "spec";
} else if (!options.r) {
options.r = "landing";
}
if (config.mode === "styleMode") {
if (!options.watch) {
throw new Error("Running in styleMode. The -w option is required. Add -h for help.");
}
}
if (options.C) {
throw new Error("The -C option has been discontinued. See cslrun --help for details.");
}
if (!options.U && !options.V) {
if (["s", "g", "a", "l"].filter(o => options[o]).length > 1) {
throw new Error("Only one of -s, -g, -a, or -l may be invoked.");
}
if (["s", "g", "a", "l"].filter(o => options[o]).length === 0) {
console.log(usage);
throw new Error("Use one of -s, -g, -a, or -l.");
}
}
if (options.U && !options.watch) {
throw new Error("The -U option requires -w.");
}
if (options.U) {
if (["final", "draft"].indexOf(options.U) === -1) {
if (!options.U.toString().match(/^[0-9]+$/)) {
throw new Error("Option 'update' [U] must be 'final', 'draft' or a valid Zotero group ID");
}
}
}
if (options.k && !options.watch) {
throw new Error("The -k option requires -w.");
}
}
function setLocalPathToStyleTestPath() {
var styleTestsPth = null;
if (!fs.existsSync(config.path.styletests)) {
throw new Error("The configured style tests directory must exist: " + config.path.styletests);
}
try {
styleTestsPth = path.join(config.path.styletests, options.S);
if (!fs.existsSync(styleTestsPth)) {
fs.mkdirSync(styleTestsPth);
}
config.path.local = path.join(config.path.styletests, options.S);
} catch (err) {
throw err;
throw new Error("Unable to create style tests directory: " + styleTestsPth);
}
}
function setWatchFiles(options) {
var arr = options.watch;
if ("string" === typeof arr) {
arr = [arr];
}
for (var i in arr) {
if (!path.isAbsolute(arr[i])) {
arr[i] = path.join(config.path.cwd, arr[i]);
}
if (!fs.existsSync(arr[i])) {
throw new Error("CSL file or directory to be watched does not exist: " + arr[i]);
}
}
options.watch = arr;
options.w = arr;
}
function checkOverlap(tn) {
if (config.testData[tn]) {
throw new Error("Fixture name exists in local and std: " + tn);
}
}
function checkSingle() {
var tn = options.single.replace(/.txt~?\r?$/, "");
var fn = tn + ".txt";
if (fn.split("_").length !== 2) {
throw new Error("Single test fixture must be specified as [group]_[name]");
}
var lpth = path.join(config.path.local, fn);
if (config.path.std) {
var spth = path.join(config.path.std, fn);
}
if (!fs.existsSync(lpth) && (options.style || !fs.existsSync(spth))) {
console.log("Looked for " + lpth);
console.log("Looked for " + spth);
throw new Error("Test fixture \"" + options.single + "\" not found.");
}
if (fs.existsSync(lpth)) {
config.testData[tn] = parseFixture(options, tn, lpth);
}
if (!options.style) {
if (fs.existsSync(spth)) {
checkOverlap(tn);
config.testData[tn] = parseFixture(options, tn, spth);
}
}
}
function checkGroup() {
var fail = true;
var rex = new RegExp("^" + options.group + "_.*\.txt\\r?$");
for (var line of fs.readdirSync(config.path.local)) {
if (rex.test(line)) {
fail = false;
var lpth = path.join(config.path.local, line);
var tn = line.replace(/.txt\r?$/, "");
if (!skipNames[tn]) {
config.testData[tn] = parseFixture(options, tn, lpth);
}
}
}
if (!options.style) {
for (var line of fs.readdirSync(config.path.std)) {
if (rex.test(line)) {
fail = false;
var spth = path.join(config.path.std, line);
var tn = line.replace(/.txt\r?$/, "");
if (!skipNames[tn]) {
if (fs.existsSync(spth)) {
checkOverlap(tn);
config.testData[tn] = parseFixture(options, tn, spth);
}
}
}
}
}
if (fail) {
throw new Error("No fixtures found for group \"" + options.group + "\".");
}
}
function checkAll() {
var rex = new RegExp("^.*_.*\.txt\\r?$");
for (var line of fs.readdirSync(config.path.local)) {
if (rex.test(line)) {
var lpth = path.join(config.path.local, line);
var tn = line.replace(/.txt\r?$/, "");
if (!skipNames[tn]) {
config.testData[tn] = parseFixture(options, tn, lpth);
}
}
}
if (!options.style) {
for (var line of fs.readdirSync(config.path.std)) {
if (rex.test(line)) {
var spth = path.join(config.path.std, line);
var tn = line.replace(/.txt\r?$/, "");
if (!skipNames[tn]) {
if (fs.existsSync(spth)) {
checkOverlap(tn);
config.testData[tn] = parseFixture(options, tn, spth);
}
}
}
}
}
}
function setGroupList() {
var rex = new RegExp("^([^_]+)_.*\.txt\\r?$");
for (var line of fs.readdirSync(config.path.local)) {
if (rex.test(line)) {
var m = rex.exec(line);
if (!config.testData[m[1]]) {
config.testData[m[1]] = [];
}
config.testData[m[1]].push(line);
}
}
for (var line of fs.readdirSync(config.path.std)) {
if (rex.test(line)) {
var m = rex.exec(line);
if (!config.testData[m[1]]) {
config.testData[m[1]] = [];
}
config.testData[m[1]].push(line);
}
}
}
// Is this initialization needed?
config.testData = {};
function fetchTestData() {
try {
config.testData = {};
if (options.single) {
checkSingle();
}
if (options.group) {
checkGroup();
}
if (options.all) {
checkAll();
}
} catch (err) {
errors.errorHandler(err);
}
}
function Bundle(noStrip) {
if (!config.path.src) {
console.log("Using processor from package");
return;
} else {
console.log("Rebundling processor");
}
// The markup of the code is weird, so we do weird things to strip
// comments.
// The noStrip option is not yet used, but will dump the processor
// with comments and skipped blocks intact when set to a value.
var ret = "";
for (var fn of sources) {
var txt = fs.readFileSync(path.join(config.path.src, fn + ".js")).toString();
/*
var stripper = new Stripper(fn, noStrip);
for (var line of txt.split(/(?:\r\n|\n)/)) {
stripper.checkLine(line);
}
ret += stripper.dumpArr() + "\n";
*/
ret += txt + "\n";
}
var license = fs.readFileSync(path.join(config.path.src, "..", "LICENSE")).toString().trim();
license = "/*\n" + license + "\n*/\n";
fs.writeFileSync(path.join(config.path.src, "..", "citeproc.js"), license + ret);
fs.writeFileSync(path.join(config.path.src, "..", "citeproc_commonjs.js"), license + ret + "\nmodule.exports = CSL");
}
function runJingAsync(validationCount, validationGoal, schema, test) {
var jingPromise = new Promise((resolve, reject) => {
var tmpobj = tmp.fileSync();
fs.writeFileSync(tmpobj.name, test.CSL);
var buf = [];
//console.log("java -client -jar " + config.path.jing + " -c " + schema + " " + tmpobj.name);
var jing = spawn(
"java",
[
"-client",
"-jar",
config.path.jing,
"-c",
schema,
tmpobj.name
],
{});
jing.stderr.on('data', (data) => {
reject(data.toString());
});
jing.stdout.on('data', (data) => {
process.stdout.write("\n");
buf.push(data);
});
jing.on('close', async function(code) {
validationCount++;
// If we are watching and code is 0, chain to integration tests.
// Otherwise stop here.
if (code == 0 && options.watch) {
// XXX Control this with the -c option?
if (options.c) {
process.exit();
} else {
await runFixturesAsync();
}
resolve();
} else if (code == 0) {
if (!options.validate || options.validate === "all") {
process.stdout.write("+");
}
if (validationCount === validationGoal) {
console.log("\nDone.");
process.exit(0);
}
resolve();
} else {
var txt = Buffer.concat(buf).toString();
var lines = txt.split(/(?:\r\n|\n)/);
for (var line of lines) {
console.log(line.toString().replace(/^.*?:([0-9]+):([0-9]+):\s*(.*)$/m, "[$1] : $3"));
}
console.log("\nValidation failure for " + test.NAME);
if (options.watch && options.c) {
process.exit();
} else if (!options.watch) {
validationCount--;
fs.writeFileSync(path.join(config.path.configdir, ".cslValidationPos"), "" + validationCount);
process.exit(0);
}
resolve();
}
});
});
return jingPromise;
}
async function runValidationsAsync() {
var validationCount = 0;
//console.log(config.testData)
var validationGoal = Object.keys(config.testData).length;
var startPos = 0;
if (options.w) {
console.log("Watching: " + options.watch[0]);
console.log("Validating CSL.");
} else {
console.log("Validating CSL in " + validationGoal + " fixtures.");
}
if (!options.w && !options.l && !options.U) {
if (options.a && fs.existsSync(path.join(config.path.configdir, ".cslValidationPos"))) {
startPos = fs.readFileSync(path.join(config.path.configdir, ".cslValidationPos")).toString();
startPos = parseInt(startPos, 10);
} else {
fs.writeFileSync(path.join(config.path.configdir, ".cslValidationPos"), "0");
}
}
for (var key in config.testData) {
if (startPos > validationCount) {
process.stdout.write(".");
validationCount++;
continue;
}
var test = config.testData[key];
var schema = config.path.cslschema;
var lineList = test.CSL.split(/(?:\r\n|\n)/);
var inStyle = false;
var m = null; // for version match
for (var line of lineList) {
if (line.indexOf("<style") > -1) {
inStyle = true;
}
if (inStyle && !m) {
m = line.match(/version=[\"\']([^\"\']+)[\"\']/);
}
if (inStyle && line.indexOf(">") > -1) {
break;
}
}
if (m) {
if (m[1].indexOf("mlz") > -1) {
schema = config.path.cslmschema;
}
} else {
throw new Error("Version not found in CSL for fixture: " + key);
}
await runJingAsync(validationCount, validationGoal, schema, test);
process.stdout.write("+");
validationCount++;
if (options.watch) {
// If in watch mode, all validations will be of the
// same CSL, so break after launching the first.
break;
}
}
}
function runFixturesAsync() {
var fixturesPromise = new Promise((resolve, reject) => {
console.log("Testing CSL.");
if (TRAVIS) {
if (!fs.existsSync(config.path.fixturedir)) {
fs.mkdirSync(config.path.fixturedir);
}
}
if (options.r) {
if (reporters[options.r]) {
if (reporters[options.r].path) {
options.r = reporters[options.r].path;
} else {
console.log("Reporter not found, defaulting to \"landing.\" Install \"" + options.r + "\" with:\n");
console.log(" npm install " + reporters[options.r].npmname);
console.log("or")
console.log(" npm install -g " + reporters[options.r].npmname);
options.r = "landing";
}
} else {
console.log("Unknown reporter \"" + options.r + ",\" defaulting to \"landing.\"");
options.r = "landing";
}
}
var args = [];
if (options.b) {
args.push("--no-color");
} else {
args.push("--color");
}
args.push("-R");
args.push(options.r);
if (options.k) {
args.push("--bail");
}
args.push(path.join(config.path.fixturedir, "fixtures.js"));
var mocha = spawn("mocha", args, {
shell: process.platform == 'win32'
});
mocha.on("error", function(err) {
var error = new Error("Failure running \"mocha.\" If the command \"mocha\" is not found,\ninstall it globally with:\n\n npm install -g mocha");
errors.errorHandler(error);
});
mocha.stdout.on('data', (data) => {
var lines = data.toString();
process.stdout.write(lines);
if (options.w && options.k) {
var m = lines.match(/.*AssertionError:\s*([^\n]+)\.txt/m);
if (m) {
console.log("Adopt this output as correct test RESULT? (y/n)");
process.stdin.once('data', function (key) {
if (!ksTimeout) {
ksTimeout = setTimeout(function() { ksTimeout=null }, 100) // block for 0.1 second to avoid stutter
var fn = path.basename(m[1]);
var test = config.testData[fn];
if (key == "y" || key == "Y") {
var sys = new Sys(config, test, []);
sys.preloadAbbreviationSets(config);
var result = sys.run();
var input = JSON.stringify(test.INPUT, null, 2);
var txt = fs.readFileSync(path.join(config.path.scriptdir, "lib", "templateTXT.txt")).toString();
var mode = [test.MODE];
for (var submode in test.submode) {
mode.push(submode);
}
mode = mode.join("-");
txt = txt.replace("%%MODE%%", mode);
txt = txt.replace("%%KEYS%%", JSON.stringify(test.KEYS, null, 2));
txt = txt.replace("%%DESCRIPTION%%", test.DESCRIPTION);
txt = txt.replace("%%INPUT%%", input);
txt = txt.replace("%%RESULT%%", result)
for (var key in test) {
if (["MODE", "INPUT", "RESULT", "NAME", "PATH", "CSL", "KEYS", "DESCRIPTION"].indexOf(key) > -1) {
continue;
}
if (key.toUpperCase() !== key) {
continue;
}
var testKey = typeof test[key] == "object" ? JSON.stringify(test[key], null, 2) : test[key];
var block = "\n\n>>===== " + key + " =====>>\n" + testKey.trim() + "\n<<===== " + key + " =====<<\n";
txt += block;
}
fs.writeFileSync(path.join(config.path.styletests, options.S, fn + ".txt"), txt);
// XXXZ Arg forces removal of this fixture
// Should this be promisified?
bundleValidateTest(fn).catch(err => errors.errorHandler(err));
resolve();
}
if (key == "n" || key == "N") {
skipNames[test.NAME] = true;
// XXXZ Arg forces removal of this fixture
// Should this be promisified?
bundleValidateTest(fn).catch(err => errors.errorHandler(err));
resolve();
}
}
});
}
}
});
mocha.stderr.on('data', (data) => {
console.log(data.toString().replace(/\s+\r?$/, ""));
reject();
});
mocha.on('close', (code) => {
resolve();
if (!options.watch) {
console.log("\n");
process.exit();
}
});
});
return fixturesPromise;
}
function buildTests() {
var fixtures = fs.readFileSync(path.join(config.path.scriptdir, "lib", "templateJS.js")).toString();
if (Object.keys(config.testData).length === 0) {
errors.setupGuidance("No tests to run.");
}
fixtures = fixtures.replace("%%CONFIG%%", JSON.stringify(config, null, 2));
fixtures = fixtures.replace("%%RUNPREP_PATH%%", JSON.stringify(path.join(config.path.scriptdir, "lib", "sys.js")));
fixtures = normalizeNewline(fixtures);
if (!fs.existsSync(config.path.fixturedir)) {
fs.mkdirSync(config.path.fixturedir);
}
fs.writeFileSync(path.join(config.path.fixturedir, "fixtures.js"), fixtures);
}
async function bundleValidateTest(continueAfter) {
// Remove test if continuing
if (continueAfter) {
for (var key of Object.keys(config.testData)) {
delete config.testData[key];
if (key === continueAfter) break;
}
}
// Bundle, load, and run tests if -s, -g, or -a
// Bundle the processor code.
if (options.watch && !options.once) {
clear();
}
Bundle();
// Build and run tests
if (options.watch) {
if (!continueAfter) {
fetchTestData();
}
buildTests();
if (options.novalidation) {
await runFixturesAsync();
} else if (!options.validationonly) {
await runValidationsAsync().catch(err => errors.errorHandlerNonFatal(err));
}
if (options.once || options.validationonly) {
process.exit();
}
var watcher = chokidar.watch(options.watch[0]);
watcher.on("change", (event, filename) => {
if (!cdTimeout) {
cdTimeout = setTimeout(function() { cdTimeout=null }, 200) // block for 0.1 second to avoid stutter
clear();
Bundle();
fetchTestData();
buildTests();
if (options.novalidation) {
runFixturesAsync();
} else {
runValidationsAsync().catch(err => errors.errorHandlerNonFatal(err));
}
}
});
for (var pth of options.watch.slice(1)) {
watcher.add(pth);
}
} else if (options.cranky) {
fetchTestData();
buildTests();
await runValidationsAsync().catch(err => errors.errorHandlerNonFatal(err));
} else {
fetchTestData();
buildTests();
await runFixturesAsync();
}
}
/*
* Do stuff
*/
(async function() {
try {
checkSanity();
if (options.validate) {
var filenames = null;
if (options.validate === "all") {
filenames = fs.readdirSync(config.path.modules).filter(o => o.match(/\.csl$/) ? o : false);
} else {
var m = options.validate.match(/^juris-([^-]+)(?:-[^.]+)*\.csl$/);
if (m) {
var rex = new RegExp(options.validate.replace(/\./g, "\\."));
} else {
var country = options.validate;
var rex = new RegExp(`^juris-${country}.*\.csl$`);
}
filenames = fs.readdirSync(config.path.modules).filter(o => o.match(rex) ? o : false);
}
if (filenames.length === 0) {
console.log(`Oops: nothing found for ${options.validate}`);
process.exit();
}
if (filenames) {
var goal = filenames.length;
var count = 0;
console.log(`Validating modules in ${config.path.modules}`);
for (var fn of filenames) {
if (options.validate !== "all") {
console.log(fn);
}
var filePath = path.join(config.path.modules, fn);
var csl = fs.readFileSync(filePath).toString();
if (csl.match(/^<\?.*\?>/)) {
csl = csl.split("\n").slice(1).join("\n");
}
// console.log(csl)
var res = await runJingAsync(count, goal, config.path.cslmschema, {CSL:csl, NAME:fn});
count++;
}
process.exit();
}
console.log("MISSED");
process.exit();
}
if (options.watch) {
setWatchFiles(options);
if (options.A) {
config.path.jurisAbbrevPath = options.A;
} else {
config.path.jurisAbbrevPath = getAbbrevPath();
}
}
if (options.list) {
setGroupList();
}
// -i and -s cannot be used together
if (options.items && options.submissions) {
errors.setupGuidance("The -i and -s options cannot be used together");
}
if (!options.items && !options.submissions) {
options.items = true;
}
if (options.U === "final") {
config.groupID = groupIdMap.final;
} else if (options.U === "draft") {
config.groupID = groupIdMap.draft;
} else if (options.U) {
config.groupID = options.U;
}
// If we are using -w and -S is not set, sniff out the style name and set it on
// options, so legacy code will do its thing.
if (options.watch && !options.style) {
var txt = fs.readFileSync(options.watch[0]).toString();
config.styleCapabilities = styleCapabilities(txt);
options.style = config.styleCapabilities.styleName;
options.S = config.styleCapabilities.styleName;
}
if (options.style) {
setLocalPathToStyleTestPath(options.style);
}
if (options.U) {
// Contact server, analyze return, check current fixtures,
// update with any missing fixtures. Big one.
// (1) Get collections from API
console.log(config.groupID)
// Need to page if more than 100 items
var json = await fetchURL("https://api.zotero.org/groups/" + config.groupID + "/collections/top?limit=100");
var obj = JSON.parse(json.buf.toString());
var collectionKey = obj.filter(o => (o.data.name === options.S))
.map(o => o.data.key);
if (!collectionKey || !collectionKey[0]) {
errors.setupGuidance("No collection found for style \"" + options.S + "\" in library of test items.");
}
collectionKey = collectionKey[0];
var obj = [];
var url = "https://api.zotero.org/groups/" + config.groupID + "/collections/" + collectionKey + "/items/top?limit=100";
while (url) {
json = await fetchURL(url);
obj = obj.concat(JSON.parse(json.buf.toString()));
// console.log(json.res.responseHeaders.link);
var m = json.res.responseHeaders.link.match(/<(https:\/\/[^>]+)>;\s+rel=\"next\"/);
if (m) {
url = m[1];
} else {
url = false;
}
}
// Need to read off the keys of the existing tests before building the data array.
// Can get the highest test number, or the holes in existing numbers, while we're at it.
var styleTestDir = path.join(config.path.styletests, options.S);
var doneKeys = {};
var doneNums = {};
var rex = new RegExp("^.*_.*\.txt\\r?$");
for (var fileName of fs.readdirSync(styleTestDir)) {
if (!rex.test(fileName)) continue;
var fixture = parseFixture(options, fileName, path.join(styleTestDir, fileName));
for (var key of fixture.KEYS) {
doneKeys[key] = true;
}
var m = fileName.match(/[^0-9]*([0-9]+)/);
if (m) {
doneNums[parseInt(m[1], 10)] = true;
}
}
var max = 0;
var doneNumsLst = Object.keys(doneNums);
if (doneNumsLst.length > 0) {
var max = Object.keys(doneNums).map(o => parseInt(o)).reduce(function(a, b) {
return Math.max(a, b);
});
}
var newNums = [];
for (var i=1,ilen=(max + obj.length + 1); i<ilen; i++) {
if (!doneNums[i]) {
newNums.push(i);
if (newNums.length === obj.length) {
break;
}
}
}
newNums.reverse();
var arr = [];
for (var o of obj) {
var key = o.data.key;
if (doneKeys[key]) {
continue;
}
delete o.data.key;
var description = o.data.abstractNote;
if (description) {
description = description.slice(0, 50).replace(/\n+/g, " ");
}
delete o.data.abstractNote;
delete o.data.version;
delete o.data.dateAdded;
delete o.data.dateModified;
var cslData = zoteroToCSL(o.data);
var cslItem = zoteroToCSLM(o, cslData);
arr.push({
key: key,
item: cslItem,
description: description
});
}
// Templates get some big changes here.
for (var i in arr) {
arr[i].id = "ITEM-1";
var item = JSON.stringify([arr[i]], null, 2);
var txt = fs.readFileSync(path.join(config.path.scriptdir, "lib", "templateTXT.txt")).toString();
txt = txt.replace("%%MODE%%", "all");
txt = txt.replace("%%KEYS%%", JSON.stringify([arr[i].key], null, 2));
txt = txt.replace("%%INPUT%%", JSON.stringify([arr[i].item], null, 2));
// Can we do something just a little more elegant with file naming?
var pos = "" + newNums.pop();
while (pos.length < 3) {
pos = "0" + pos;
}
var fileStub = "style_test" + pos;
if (arr[i].description) {
txt = txt.replace("%%DESCRIPTION%%", arr[i].description);
} else {
txt = txt.replace("%%DESCRIPTION%%", "should pass test " + fileStub)
}
fs.writeFileSync(path.join(config.path.styletests, options.S, fileStub + ".txt"), txt);
}
if (arr.length > 0) {
console.log("Maybe wrote draft tests to "+path.join(config.path.styletests, options.S));
} else {
console.log("No tests to write this time");
}
process.exit(0);
} else if (options.single || options.group || options.all) {
bundleValidateTest().catch(err => errors.errorHandler(err));
} else if (options.l) {
// Otherwise we've collected a list of group names.
var ret = Object.keys(config.testData);
ret.sort();
for (var key of ret) {
console.log(key + " (" + config.testData[key].length + ")");
}
process.exit(0);
}
} catch (err) {
errors.errorHandler(err);
}
})();