0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2024-12-25 13:39:06 -05:00
liabru-matter-js/test/TestTools.js
2024-03-03 21:01:45 +00:00

335 lines
No EOL
12 KiB
JavaScript

/* eslint-env es6 */
"use strict";
const fs = require('fs');
const compactStringify = require('json-stringify-pretty-compact');
const comparePath = './test/__compare__';
const compareCommand = 'open http://localhost:8000/?compare';
const diffSaveCommand = 'npm run test-save';
const diffCommand = 'code -n -d test/__compare__/examples-build.json test/__compare__/examples-dev.json';
const equalityThreshold = 1;
const colors = { Red: 31, Green: 32, Yellow: 33, White: 37, BrightWhite: 90, BrightCyan: 36 };
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save, benchmark) => {
const performanceDev = capturePerformanceTotals(capturesDev);
const performanceBuild = capturePerformanceTotals(capturesBuild);
const perfChange = noiseThreshold(1 - (performanceDev.duration / performanceBuild.duration), 0.01);
const memoryChange = noiseThreshold((performanceDev.memory / performanceBuild.memory) - 1, 0.01);
const overlapChange = (performanceDev.overlap / (performanceBuild.overlap || 1)) - 1;
const filesizeChange = (devSize / buildSize) - 1;
const similaritys = extrinsicSimilarity(capturesDev, capturesBuild);
const similarityAverage = extrinsicSimilarityAverage(similaritys);
const similarityEntries = Object.entries(similaritys);
similarityEntries.sort((a, b) => a[1] - b[1]);
const firstCapture = Object.entries(capturesDev)[0][1];
const updates = firstCapture.extrinsic.updates;
const similarityAveragePerUpdate = Math.pow(1, -1 / updates) * Math.pow(similarityAverage, 1 / updates);
const devIntrinsicsChanged = {};
const buildIntrinsicsChanged = {};
let intrinsicChangeCount = 0;
const captureSummary = Object.entries(capturesDev)
.map(([name]) => {
const changedIntrinsics = !equals(capturesDev[name].intrinsic, capturesBuild[name].intrinsic);
if (changedIntrinsics) {
capturesDev[name].changedIntrinsics = true;
if (intrinsicChangeCount < 1) {
devIntrinsicsChanged[name] = capturesDev[name].state;
buildIntrinsicsChanged[name] = capturesBuild[name].state;
intrinsicChangeCount += 1;
}
}
return { name };
})
.sort((a, b) => a.name.localeCompare(b.name));
const formatColor = green => green ? colors.Green : colors.Yellow;
const report = (breakEvery, format) => [
[`Output sample comparison estimates of ${similarityEntries.length} examples`,
`against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}:`
].join(' '),
`\n\n${format(`Similarity`, colors.White)} `,
`${format(formatPercent(similarityAveragePerUpdate, false, true), formatColor(similarityAveragePerUpdate === 1))}% `,
` ${format('Overlap', colors.White)}`,
` ${format(formatPercent(overlapChange), formatColor(overlapChange <= 0))}%`,
` ${format('Filesize', colors.White)}`,
`${format(formatPercent(filesizeChange), formatColor(filesizeChange <= 0))}%`,
`${format(`${(devSize / 1024).toPrecision(4)} KB`, colors.White)}`,
...(benchmark ? [
`\n${format('Performance', colors.White)}`,
` ${format(formatPercent(perfChange), formatColor(perfChange >= 0))}%`,
` ${format('Memory', colors.White)} `,
` ${format(formatPercent(memoryChange), formatColor(memoryChange <= 0))}%`,
] : []),
captureSummary.reduce((output, p, i) => {
output += `${p.name} `;
output += `${similarityRatings(similaritys[p.name])} `;
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
if (i > 0 && i < captureSummary.length && breakEvery > 0 && i % breakEvery === 0) {
output += '\n';
}
return output;
}, '\n\n'),
`\n\nwhere for the sample · no change detected ● extrinsics changed ◆ intrinsics changed\n`,
similarityAverage < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 150 + '#' + similarityEntries[0][0], colors.BrightCyan)}` : '',
intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : ''
].join(' ');
if (save) {
writeResult('examples-dev', devIntrinsicsChanged);
writeResult('examples-build', buildIntrinsicsChanged);
writeResult('examples-report', report(5, s => s));
}
return report(5, color);
};
const similarityRatings = similarity => similarity < equalityThreshold ? color('●', colors.Yellow) : '·';
const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·';
const color = (text, number) => number ? `\x1b[${number}m${text}\x1b[0m` : text;
const formatPercent = (val, showSign=true, showFractional=false, padStart=6) => {
let fractionalSign = '';
if (showFractional && val > 0.9999 && val < 1) {
val = 0.9999;
fractionalSign = '>';
} else if (showFractional && val > 0 && val < 0.0001) {
val = 0.0001;
fractionalSign = '<';
}
const percentFixed = Math.abs(100 * val).toFixed(2);
const sign = parseFloat((100 * val).toFixed(2)) >= 0 ? '+' : '-';
return ((showFractional ? fractionalSign : '') + (showSign ? sign : '') + percentFixed).padStart(padStart, ' ');
};
const noiseThreshold = (val, threshold) => {
const sign = val < 0 ? -1 : 1;
const magnitude = Math.abs(val);
return sign * Math.max(0, magnitude - threshold) / (1 - threshold);
};
const smoothExp = (last, current) => {
const delta = current - last;
const sign = delta < 0 ? -1 : 1;
const magnitude = Math.abs(delta);
return last + Math.sqrt(magnitude) * sign;
};
const equals = (a, b) => {
try {
expect(a).toEqual(b);
} catch (e) {
return false;
}
return true;
};
const capturePerformanceTotals = (captures) => {
const totals = {
duration: 0,
overlap: 0,
memory: 0
};
for (const [ name ] of Object.entries(captures)) {
totals.duration += captures[name].duration;
totals.overlap += captures[name].overlap;
totals.memory += captures[name].memory;
};
return totals;
};
const extrinsicSimilarity = (currentCaptures, referenceCaptures, key='extrinsic') => {
const result = {};
const zeroVector = { x: 0, y: 0 };
for (const name in currentCaptures) {
const currentExtrinsic = currentCaptures[name][key];
const referenceExtrinsic = referenceCaptures[name][key];
let totalCount = 0;
let totalSimilarity = 0;
for (const objectType in currentExtrinsic) {
for (const objectId in currentExtrinsic[objectType]) {
const currentObject = currentExtrinsic[objectType][objectId];
const referenceObject = referenceExtrinsic[objectType][objectId];
for (let i = 0; i < currentObject.vertices.length; i += 1) {
const currentPosition = currentObject.position;
const currentVertex = currentObject.vertices[i];
const referenceVertex = referenceObject.vertices[i] ? referenceObject.vertices[i] : zeroVector;
const radius = Math.sqrt(
Math.pow(currentVertex.x - currentPosition.x, 2)
+ Math.pow(currentVertex.y - currentPosition.y, 2)
);
const distance = Math.sqrt(
Math.pow(currentVertex.x - referenceVertex.x, 2)
+ Math.pow(currentVertex.y - referenceVertex.y, 2)
);
totalSimilarity += Math.min(1, distance / (2 * radius)) / currentObject.vertices.length;
}
totalCount += 1;
}
}
result[name] = 1 - (totalSimilarity / totalCount);
}
return result;
};
const extrinsicSimilarityAverage = (similaritys) => {
const entries = Object.entries(similaritys);
let average = 0;
entries.forEach(([_, similarity]) => average += similarity);
return average /= entries.length;
};
const serialize = (obj, exclude=()=>false, precision=4, path='$', visited=[], paths=[]) => {
if (typeof obj === 'number') {
return parseFloat(obj.toPrecision(precision));
} else if (typeof obj === 'string' || typeof obj === 'boolean') {
return obj;
} else if (obj === null) {
return 'null';
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (obj === Infinity) {
return 'Infinity';
} else if (obj === -Infinity) {
return '-Infinity';
} else if (typeof obj === 'function') {
return 'function';
} else if (Array.isArray(obj)) {
return obj.map(
(item, index) => serialize(item, exclude, precision, path + '.' + index, visited, paths)
);
}
const visitedIndex = visited.indexOf(obj);
if (visitedIndex !== -1) {
return paths[visitedIndex];
}
visited.push(obj);
paths.push(path);
const result = {};
for (const key of Object.keys(obj).sort()) {
if (!exclude(key, obj[key], path + '.' + key)) {
result[key] = serialize(obj[key], exclude, precision, path + '.' + key, visited, paths);
}
}
return result;
};
const writeResult = (name, obj) => {
try {
fs.mkdirSync(comparePath, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
if (typeof obj === 'string') {
fs.writeFileSync(`${comparePath}/${name}.md`, obj, 'utf8');
} else {
fs.writeFileSync(`${comparePath}/${name}.json`, compactStringify(obj, { maxLength: 100 }), 'utf8');
}
};
const logReport = (captures, version) => {
let report = '';
for (const capture of Object.values(captures)) {
if (!capture.logs.length) {
continue;
}
report += ` ${capture.name}\n`;
for (const log of capture.logs) {
report += ` ${log}\n`;
}
}
return `Output logs from ${color(version, colors.Yellow)} build on last run\n\n`
+ (report ? report : ' None\n');
};
const requireUncached = path => {
delete require.cache[require.resolve(path)];
const module = require(path);
delete require.cache[require.resolve(path)];
return module;
};
const getArg = (name, defaultValue=null, parser=(v) => v) => {
const value = process.argv.find(arg => arg.startsWith(`--${name}=`));
return value ? parser(value.split('=')[1]) : defaultValue;
};
const toMatchExtrinsics = {
toMatchExtrinsics(received, value) {
const similaritys = extrinsicSimilarity(received, value, 'extrinsic');
const pass = Object.values(similaritys).every(similarity => similarity >= equalityThreshold);
return {
message: () => 'Expected positions and velocities to match between builds.',
pass
};
}
};
const toMatchIntrinsics = {
toMatchIntrinsics(currentCaptures, referenceCaptures) {
const entries = Object.entries(currentCaptures);
let changed = false;
entries.forEach(([name, current]) => {
const reference = referenceCaptures[name];
if (!this.equals(current.intrinsic, reference.intrinsic)) {
changed = true;
}
});
return {
message: () => 'Expected intrinsic properties to match between builds.',
pass: !changed
};
}
};
module.exports = {
requireUncached, comparisonReport, logReport, getArg, smoothExp,
serialize, toMatchExtrinsics, toMatchIntrinsics
};