195 lines
7.1 KiB
JavaScript
195 lines
7.1 KiB
JavaScript
import path from 'path';
|
|
import * as diff from 'diff';
|
|
import fs from 'fs';
|
|
import harness from './harness';
|
|
import compactStringify from 'json-stringify-pretty-compact';
|
|
|
|
// we have to handle this edge case here because we have test fixtures for this
|
|
// edge case, and we don't want UPDATE=1 to mess with them
|
|
function stringify(v) {
|
|
let s = compactStringify(v);
|
|
// http://timelessrepo.com/json-isnt-a-javascript-subset
|
|
if (s.indexOf('\u2028') >= 0) {
|
|
s = s.replace(/\u2028/g, '\\u2028');
|
|
}
|
|
if (s.indexOf('\u2029') >= 0) {
|
|
s = s.replace(/\u2029/g, '\\u2029');
|
|
}
|
|
return s;
|
|
}
|
|
|
|
const decimalSigFigs = 6;
|
|
|
|
function stripPrecision(x) {
|
|
// Intended for test output serialization:
|
|
// strips down to 6 decimal sigfigs but stops at decimal point
|
|
if (typeof x === 'number') {
|
|
if (x === 0) { return x; }
|
|
|
|
const multiplier = Math.pow(10,
|
|
Math.max(0,
|
|
decimalSigFigs - Math.ceil(Math.log10(Math.abs(x)))));
|
|
|
|
// We strip precision twice in a row here to avoid cases where
|
|
// stripping an already stripped number will modify its value
|
|
// due to bad floating point precision luck
|
|
// eg `Math.floor(8.16598 * 100000) / 100000` -> 8.16597
|
|
const firstStrip = Math.floor(x * multiplier) / multiplier;
|
|
return Math.floor(firstStrip * multiplier) / multiplier;
|
|
} else if (typeof x !== 'object') {
|
|
return x;
|
|
} else if (Array.isArray(x)) {
|
|
return x.map(stripPrecision);
|
|
} else {
|
|
const stripped = {};
|
|
for (const key of Object.keys(x)) {
|
|
stripped[key] = stripPrecision(x[key]);
|
|
}
|
|
return stripped;
|
|
}
|
|
}
|
|
|
|
function deepEqual(a, b) {
|
|
if (typeof a !== typeof b)
|
|
return false;
|
|
if (typeof a === 'number') {
|
|
return stripPrecision(a) === stripPrecision(b);
|
|
}
|
|
if (a === null || b === null || typeof a !== 'object')
|
|
return a === b;
|
|
|
|
const ka = Object.keys(a);
|
|
const kb = Object.keys(b);
|
|
|
|
if (ka.length !== kb.length)
|
|
return false;
|
|
|
|
ka.sort();
|
|
kb.sort();
|
|
|
|
for (let i = 0; i < ka.length; i++)
|
|
if (ka[i] !== kb[i] || !deepEqual(a[ka[i]], b[ka[i]]))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Run the expression suite.
|
|
*
|
|
* @param {string} implementation - identify the implementation under test; used to
|
|
* deal with implementation-specific test exclusions and fudge-factors
|
|
* @param {Object} options
|
|
* @param {Array<string>} [options.tests] - array of test names to run; tests not in the array will be skipped
|
|
* @param {Array<string>} [options.ignores] - array of test names to ignore.
|
|
* @param {} runExpressionTest - a function that runs a single expression test fixture
|
|
* @returns {undefined} terminates the process when testing is complete
|
|
*/
|
|
export function run(implementation, options, runExpressionTest) {
|
|
const directory = path.join(__dirname, '../expression-tests');
|
|
options.fixtureFilename = 'test.json';
|
|
harness(directory, implementation, options, (fixture, params, done) => {
|
|
try {
|
|
const result = runExpressionTest(fixture, params);
|
|
const dir = path.join(directory, params.id);
|
|
|
|
if (process.env.UPDATE) {
|
|
fixture.expected = {
|
|
compiled: result.compiled,
|
|
outputs: stripPrecision(result.outputs),
|
|
serialized: result.serialized
|
|
};
|
|
|
|
delete fixture.metadata;
|
|
|
|
fs.writeFile(path.join(dir, 'test.json'), `${stringify(fixture, null, 2)}\n`, done);
|
|
return;
|
|
}
|
|
|
|
const expected = fixture.expected;
|
|
const compileOk = deepEqual(result.compiled, expected.compiled);
|
|
const evalOk = compileOk && deepEqual(result.outputs, expected.outputs);
|
|
|
|
let recompileOk = true;
|
|
let roundTripOk = true;
|
|
let serializationOk = true;
|
|
if (expected.compiled.result !== 'error') {
|
|
serializationOk = compileOk && deepEqual(expected.serialized, result.serialized);
|
|
recompileOk = compileOk && deepEqual(result.recompiled, expected.compiled);
|
|
roundTripOk = recompileOk && deepEqual(result.roundTripOutputs, expected.outputs);
|
|
}
|
|
|
|
params.ok = compileOk && evalOk && recompileOk && roundTripOk && serializationOk;
|
|
|
|
const diffOutput = {
|
|
text: '',
|
|
html: ''
|
|
};
|
|
|
|
const diffJson = (label, expectedJson, actualJson) => {
|
|
let text = '';
|
|
let html = '';
|
|
diff.diffJson(expectedJson, actualJson)
|
|
.forEach((hunk) => {
|
|
if (hunk.added) {
|
|
text += `+ ${hunk.value}`;
|
|
html += `<ins> ${hunk.value}</ins>`;
|
|
} else if (hunk.removed) {
|
|
text += `- ${hunk.value}`;
|
|
html += `<del> ${hunk.value}</del>`;
|
|
} else {
|
|
text += ` ${hunk.value}`;
|
|
html += `<span> ${hunk.value}</span>`;
|
|
}
|
|
});
|
|
if (text) {
|
|
diffOutput.text += `${label}\n${text}`;
|
|
diffOutput.html += `<h3>${label}</h3>\n${html}`;
|
|
}
|
|
};
|
|
|
|
if (!compileOk) {
|
|
diffJson('Compiled', expected.compiled, result.compiled);
|
|
}
|
|
if (compileOk && !serializationOk) {
|
|
diffJson('Serialized', expected.serialized, result.serialized);
|
|
}
|
|
if (compileOk && !recompileOk) {
|
|
diffJson('Serialized and re-compiled', expected.compiled, result.recompiled);
|
|
}
|
|
|
|
const diffOutputs = (testOutputs) => {
|
|
return expected.outputs.map((expectedOutput, i) => {
|
|
if (!deepEqual(expectedOutput, testOutputs[i])) {
|
|
return `f(${JSON.stringify(fixture.inputs[i])})\nExpected: ${JSON.stringify(expectedOutput)}\nActual: ${JSON.stringify(testOutputs[i])}`;
|
|
}
|
|
return false;
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
};
|
|
|
|
if (compileOk && !evalOk) {
|
|
const differences = `Original\n${diffOutputs(result.outputs)}\n`;
|
|
diffOutput.text += differences;
|
|
diffOutput.html += differences;
|
|
}
|
|
if (recompileOk && !roundTripOk) {
|
|
const differences = `\nRoundtripped through serialize()\n${diffOutputs(result.roundTripOutputs)}\n`;
|
|
diffOutput.text += differences;
|
|
diffOutput.html += differences;
|
|
}
|
|
|
|
params.difference = diffOutput.html;
|
|
if (diffOutput.text) { console.log(diffOutput.text); }
|
|
|
|
params.expression = compactStringify(fixture.expression);
|
|
params.serialized = compactStringify(result.serialized);
|
|
|
|
done();
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
});
|
|
}
|