0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2025-01-20 17:10:11 -05:00

Added build comparison tools and tests

This commit is contained in:
liabru 2019-09-30 23:28:17 +01:00
parent 21e4ea6044
commit b7c938a963
11 changed files with 543 additions and 209 deletions

3
.gitignore vendored
View file

@ -13,4 +13,5 @@ test/browser/diffs
test/browser/refs
test/node/diffs
test/node/refs
__snapshots__
__snapshots__
__compare__

View file

@ -34,5 +34,7 @@ creates a release build
runs the linter
- **npm run test**
runs the tests
- **npm run compare**
compares the output of examples for current source against release build
- **npm run doc**
builds the documentation

View file

@ -82,6 +82,27 @@
margin: 0;
padding: 0;
}
.matter-js-compare-build {
position: absolute;
background: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
z-index: 1;
pointer-events: none;
}
.matter-js-compare-build .matter-header-outer {
display: none;
}
.matter-js-compare-build canvas {
opacity: 0.5;
background: transparent !important;
}
</style>
</head>
<body>

179
demo/js/Compare.js Normal file
View file

@ -0,0 +1,179 @@
/**
* A Matter.js build comparison testbed.
*
* Tool for Matter.js maintainers to compare results of
* the current source build with the release build in the browser.
*
* USAGE: open http://localhost:8000/?compare=120#mixed
*
* NOTE: For the actual example code, refer to the source files in `/examples/`.
*
* @class Compare
*/
(function() {
// maintain reference to dev version of Matter already loaded
var MatterDev = window.Matter;
// load the build version of Matter
var matterBuildScript = document.createElement('script');
matterBuildScript.src = '../build/matter.min.js';
// wait for load
matterBuildScript.addEventListener('load', function() {
var examples = window.MatterDemo.examples;
// maintain reference of build version and set dev version as main
var MatterBuild = window.Matter;
window.Matter = MatterDev;
var demo = MatterTools.Demo.create({
toolbar: {
title: 'matter-js',
url: 'https://github.com/liabru/matter-js',
reset: true,
source: true,
inspector: true,
tools: true,
fullscreen: true,
exampleSelect: true
},
tools: {
inspector: true,
gui: true
},
inline: false,
preventZoom: true,
resetOnOrientation: true,
routing: true,
startExample: 'mixed',
examples: examples
});
var demoBuild = MatterTools.Demo.create({
toolbar: {
title: 'matter-js-compare-build',
reset: false,
source: false,
inspector: false,
tools: false,
fullscreen: false,
exampleSelect: false
},
tools: {
inspector: false,
gui: false
},
inline: false,
preventZoom: true,
resetOnOrientation: true,
routing: false,
startExample: 'mixed',
examples: examples.map(function(example) {
return Matter.Common.extend({}, example);
})
});
/**
* NOTE: For the actual example code, refer to the source files in `/examples/`.
* The code below is tooling for Matter.js maintainers to compare versions of Matter.
*/
// build version should not run itself
MatterBuild.Runner.run = function() {};
MatterBuild.Render.run = function() {};
// maintain original references to patched methods
MatterDev.Runner._tick = MatterDev.Runner.tick;
MatterDev.Render._world = MatterDev.Render.world;
MatterBuild.Mouse._setElement = MatterBuild.Mouse.setElement;
// patch MatterTools to control both demo versions simultaneously
MatterTools.Demo._setExample = MatterTools.Demo.setExample;
MatterTools.Demo.setExample = function(_demo, example) {
MatterBuild.Common._nextId = MatterBuild.Common._seed = 0;
MatterDev.Common._nextId = MatterDev.Common._seed = 0;
MatterBuild.Plugin._registry = MatterDev.Plugin._registry;
MatterBuild.use.apply(null, MatterDev.used);
window.Matter = MatterDev;
MatterTools.Demo._setExample(
demo, demo.examples.find(function(e) { return e.name === example.name; })
);
var maxTicks = parseFloat(window.location.search.split('=')[1]);
var ticks = 0;
MatterDev.Runner.tick = function(runner, engine, time) {
if (ticks === -1) {
return;
}
if (ticks >= maxTicks) {
console.info(
'Demo.Compare: ran ' + ticks + ' ticks, timestamp is now '
+ engine.timing.timestamp.toFixed(2)
);
ticks = -1;
return;
}
ticks += 1;
var demoBuildInstance = demoBuild.example.instance;
runner.isFixed = demoBuildInstance.runner.isFixed = true;
runner.delta = demoBuildInstance.runner.delta = 1000 / 60;
window.Matter = MatterBuild;
MatterBuild.Runner.tick(demoBuildInstance.runner, demoBuildInstance.engine, time);
window.Matter = MatterDev;
return MatterDev.Runner._tick(runner, engine, time);
};
MatterDev.Render.world = function(render) {
window.Matter = MatterBuild;
MatterBuild.Render.world(demoBuild.example.instance.render);
window.Matter = MatterDev;
return MatterDev.Render._world(render);
};
MatterBuild.Mouse.setElement = function(mouse) {
return MatterBuild.Mouse._setElement(mouse, demo.example.instance.render.canvas);
};
window.Matter = MatterBuild;
MatterTools.Demo._setExample(
demoBuild, demoBuild.examples.find(function(e) { return e.name === example.name; })
);
window.Matter = MatterDev;
};
// reset both engine versions simultaneously
MatterTools.Demo._reset = MatterTools.Demo.reset;
MatterTools.Demo.reset = function(_demo) {
MatterBuild.Common._nextId = MatterBuild.Common._seed = 0;
MatterDev.Common._nextId = MatterDev.Common._seed = 0;
window.Matter = MatterBuild;
MatterTools.Demo._reset(demoBuild);
window.Matter = MatterDev;
MatterTools.Demo._reset(demo);
};
document.body.appendChild(demo.dom.root);
document.body.appendChild(demoBuild.dom.root);
MatterTools.Demo.start(demo);
console.info(
'Demo.Compare: comparing matter-js@' + MatterDev.version + ' with matter-js@' + MatterBuild.version
);
});
document.body.append(matterBuildScript);
})();

View file

@ -1,5 +1,8 @@
/**
* The Matter.js demo page controller and example runner.
* The Matter.js development demo and testing tool.
*
* This demo uses MatterTools, you can see the wiki for a simple example instead:
* https://github.com/liabru/matter-js/wiki/Getting-started
*
* NOTE: For the actual example code, refer to the source files in `/examples/`.
*
@ -65,6 +68,14 @@
}
}
if (window.location.search.indexOf('compare') >= 0) {
var compareScript = document.createElement('script');
compareScript.src = '../js/Compare.js';
window.MatterDemo = { examples: examples };
document.body.append(compareScript);
return;
}
var demo = MatterTools.Demo.create({
toolbar: {
title: 'matter-js',

12
package-lock.json generated
View file

@ -5562,9 +5562,9 @@
"dev": true
},
"handlebars": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.4.tgz",
"integrity": "sha512-vvpo6mpK4ScNC1DbGRZ2d5BznS6ht0r1hi20RivsibMc6jNvFAeZQ6qk5VNspo6SOwVOJQbjHyBCpuS7BzA1pw==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@ -7224,6 +7224,12 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"json-stringify-pretty-compact": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==",
"dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",

View file

@ -27,6 +27,7 @@
"gulp-tag-version": "^1.3.0",
"gulp-util": "^3.0.8",
"jest": "^24.9.0",
"json-stringify-pretty-compact": "^2.0.0",
"run-sequence": "^1.1.4",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.8",
@ -37,11 +38,10 @@
"build": "webpack --mode=production & webpack --mode=production --env.MINIMIZE",
"build-alpha": "webpack --mode=production --env.EDGE & webpack --mode=production --env.MINIMIZE --env.EDGE",
"build-examples": "webpack --config webpack.examples.config.js --mode=production --env.EDGE & webpack --config webpack.examples.config.js --mode=production --env.MINIMIZE --env.EDGE",
"lint": "eslint 'src/**/*.js' 'demo/js/Demo.js' 'examples/*.js' 'test/*.spec.js' 'webpack.*.js' 'Gulpfile.js'",
"lint": "eslint 'src/**/*.js' 'demo/js/Demo.js' 'demo/js/Compare.js' 'examples/*.js' 'test/*.spec.js' 'webpack.*.js' 'Gulpfile.js'",
"doc": "gulp doc",
"test": "jest",
"test-snapshot": "TEST_SNAPSHOTS=true jest --ci",
"test-snapshot-update": "TEST_SNAPSHOTS=true jest -u"
"compare": "COMPARE=true jest"
},
"dependencies": {},
"files": [

View file

@ -1,119 +0,0 @@
const { Composite, Constraint, Vertices } = require('../src/module/main');
const includeKeys = [
// Common
'id', 'label',
// Constraint
'angularStiffness', 'bodyA', 'bodyB', 'pointA', 'pointB', 'damping', 'length', 'stiffness',
// Body
'angle', 'anglePrev', 'area', 'axes', 'bounds', 'min', 'max', 'x', 'y', 'collisionFilter', 'category', 'mask',
'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor',
'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'position', 'positionPrev', 'restitution', 'sleepThreshold', 'slop',
'timeScale', 'vertices'
];
const limit = (val, precision=3) => {
if (typeof val === 'number') {
return parseFloat(val.toPrecision(precision));
}
return val;
};
const engineSnapshot = (engine, extended=false) => {
const {
positionIterations, velocityIterations,
constraintIterations, timing, world
} = engine;
const bodies = Composite.allBodies(world);
const constraints = Composite.allConstraints(world);
const composites = Composite.allComposites(world);
return {
positionIterations,
velocityIterations,
constraintIterations,
timing,
bodyCount: bodies.length,
constraintCount: constraints.length,
compositeCount: composites.length,
averageBodyPosition: Vertices.mean(bodies.map(body => body.position)),
averageBodyPositionPrev: Vertices.mean(bodies.map(body => body.positionPrev)),
averageBodyAngle: bodies.reduce((angle, body) => angle + body.angle, 0) / bodies.length,
averageBodyAnglePrev: bodies.reduce((angle, body) => angle + body.anglePrev, 0) / bodies.length,
averageConstraintPosition: Vertices.mean(
constraints.reduce((positions, constraint) => {
positions.push(
Constraint.pointAWorld(constraint),
Constraint.pointBWorld(constraint)
);
return positions;
}, []).concat({ x: 0, y: 0 })
),
world: extended ? worldSnapshotExtended(engine.world) : worldSnapshot(engine.world)
};
};
const worldSnapshot = world => ({
...Composite.allBodies(world).reduce((bodies, body) => {
bodies[`${body.id} ${body.label}`] =
`${limit(body.position.x)} ${limit(body.position.y)} ${limit(body.angle)}`
+ ` ${limit(body.position.x - body.positionPrev.x)} ${limit(body.position.y - body.positionPrev.y)}`
+ ` ${limit(body.angle - body.anglePrev)}`;
return bodies;
}, {}),
...Composite.allConstraints(world).reduce((constraints, constraint) => {
const positionA = Constraint.pointAWorld(constraint);
const positionB = Constraint.pointBWorld(constraint);
constraints[`${constraint.id} ${constraint.label}`] =
`${limit(positionA.x)} ${limit(positionA.y)} ${limit(positionB.x)} ${limit(positionB.y)}`
+ ` ${constraint.bodyA ? constraint.bodyA.id : null} ${constraint.bodyB ? constraint.bodyB.id : null}`;
return constraints;
}, {})
});
const worldSnapshotExtended = world => worldSnapshotExtendedBase({
...Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
...Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {})
});
const worldSnapshotExtendedBase = (obj, depth=0) => {
if (typeof obj === 'number') {
return limit(obj);
}
if (Array.isArray(obj)) {
return obj.map(item => worldSnapshotExtendedBase(item, depth + 1));
}
if (typeof obj !== 'object') {
return obj;
}
return Object.entries(obj)
.filter(([key]) => depth === 0 || includeKeys.includes(key))
.reduce((cleaned, [key, val]) => {
if (val && val.id && String(val.id) !== key) {
val = val.id;
}
if (Array.isArray(val)) {
val = `[${val.length}]`;
}
return { ...cleaned, [key]: worldSnapshotExtendedBase(val, depth + 1) };
}, {});
};
module.exports = { engineSnapshot };

View file

@ -2,97 +2,72 @@
/* eslint no-global-assign: 0 */
"use strict";
const Common = require('./Common');
const fs = require('fs');
const execSync = require('child_process').execSync;
const useSnapshots = process.env.TEST_SNAPSHOTS === 'true';
const useBuild = process.env.TEST_BUILD === 'true';
const {
stubBrowserFeatures, engineSnapshot, toMatchExtrinsics, toMatchIntrinsics
} = require('./TestTools');
console.info(`Testing Matter from ${useBuild ? `build '../build/matter'.` : `source '../src/module/main'.`}`);
const totalUpdates = 120;
const isCompare = process.env.COMPARE === 'true';
const excludeExamples = ['stress', 'stress2', 'svg', 'terrain'];
// mock modules
if (useBuild) {
jest.mock('matter-js', () => require('../build/matter'), { virtual: true });
} else {
jest.mock('matter-js', () => require('../src/module/main'), { virtual: true });
}
const MatterBuild = require('../build/matter');
const MatterDev = require('../src/module/main');
jest.mock('matter-wrap', () => require('../demo/lib/matter-wrap'), { virtual: true });
jest.mock('poly-decomp', () => require('../demo/lib/decomp'), { virtual: true });
// import mocked Matter and plugins
const Matter = global.Matter = require('matter-js');
Matter.Plugin.register(require('matter-wrap'));
const runExamples = (matter) => {
let snapshots = {};
matter = stubBrowserFeatures(matter);
global.Matter = matter;
matter.Plugin.register(require('matter-wrap'));
// import Examples after Matter
const Example = require('../examples/index');
const Example = require('../examples/index');
const examples = Object.keys(Example).filter(key => !excludeExamples.includes(key));
// stub out browser-only functions
const noop = () => ({ collisionFilter: {}, mouse: {} });
Matter.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}});
Matter.Render.run = Matter.Render.lookAt = noop;
Matter.Runner.create = Matter.Runner.run = noop;
Matter.MouseConstraint.create = Matter.Mouse.create = noop;
Matter.Common.log = Matter.Common.info = Matter.Common.warn = noop;
const consoleOriginal = global.console;
global.console = { log: () => {} };
// check initial snapshots if enabled (experimental)
if (useSnapshots && !fs.existsSync('./test/__snapshots__')) {
const gitState = execSync('git log -n 1 --pretty=%d HEAD').toString().trim();
const gitIsClean = execSync('git status --porcelain').toString().trim().length === 0;
const gitIsMaster = gitState.startsWith('(HEAD -> master, origin/master');
for (name of examples) {
matter.Common._nextId = matter.Common._seed = 0;
if (!gitIsMaster || !gitIsClean) {
throw `Snapshots are experimental and are not currently committed due to size.
Stash changes and switch to HEAD on origin/master.
Use 'npm run test-snapshot-update' to generate initial snapshots.
Then run 'npm run test-snapshot' to test against these snapshots.
Currently on ${gitState}.
`;
}
}
const example = Example[name]();
const engine = example.engine;
// prevent examples from logging
const consoleOriginal = console;
beforeEach(() => { global.console = { log: noop }; });
afterEach(() => { global.console = consoleOriginal; });
// list the examples to test
const examplesExtended = ['constraints'];
const examples = [
'airFriction', 'ballPool', 'bridge', 'broadphase', 'car', 'catapult', 'chains', 'circleStack',
'cloth', 'collisionFiltering', 'compositeManipulation', 'compound', 'compoundStack', 'concave',
'constraints', 'doublePendulum', 'events', 'friction', 'gravity', 'gyro', 'manipulation', 'mixed',
'newtonsCradle', 'ragdoll', 'pyramid', 'raycasting', 'restitution', 'rounded', 'sensors', 'sleeping',
'slingshot', 'softBody', 'sprites', 'stack', 'staticFriction', 'timescale', 'views', 'wreckingBall'
];
// perform integration tests using listed examples
const testName = `Example.%s simulates without throwing${useSnapshots ? ' and matches snapshot' : ''}`;
test.each(examples.map(key => [key]))(testName, exampleName => {
let engine, startSnapshot, endSnapshot;
const simulate = () => {
const example = Example[exampleName]();
const extended = examplesExtended.includes(exampleName);
engine = example.engine;
startSnapshot = Common.engineSnapshot(engine, extended);
for (let i = 0; i < 100; i += 1) {
Matter.Engine.update(engine, 1000 / 60);
}
endSnapshot = Common.engineSnapshot(engine, extended);
};
// simulate and assert nothing is thrown
expect(simulate).not.toThrow();
// assert there has been some change to the world
expect(startSnapshot.world).not.toEqual(endSnapshot.world);
// compare to stored snapshot (experimental)
if (useSnapshots) {
expect(endSnapshot).toMatchSnapshot();
for (let i = 0; i < totalUpdates; i += 1) {
matter.Engine.update(engine, 1000 / 60);
}
snapshots[name] = isCompare ? engineSnapshot(engine) : {};
}
);
global.console = consoleOriginal;
global.Matter = undefined;
return snapshots;
};
const snapshotsDev = runExamples(MatterDev);
const snapshotsBuild = runExamples(MatterBuild);
const examples = Object.keys(snapshotsDev);
describe(`Integration tests (${examples.length})`, () => {
test(`Examples run without throwing`, () => {
expect(Object.keys(snapshotsDev)).toEqual(examples);
expect(Object.keys(snapshotsBuild)).toEqual(examples);
});
});
if (isCompare) {
describe(`Regression tests (${examples.length})`, () => {
expect.extend(toMatchExtrinsics);
expect.extend(toMatchIntrinsics);
test(`Examples match properties with release build`, () => {
expect(snapshotsDev).toMatchIntrinsics(snapshotsBuild, totalUpdates);
});
test(`Examples match positions and velocities with release build`, () => {
expect(snapshotsDev).toMatchExtrinsics(snapshotsBuild, totalUpdates);
});
});
}

257
test/TestTools.js Normal file
View file

@ -0,0 +1,257 @@
/* eslint-env es6 */
"use strict";
const fs = require('fs');
const compactStringify = require('json-stringify-pretty-compact');
const { Composite, Constraint } = require('../src/module/main');
const comparePath = './test/__compare__';
const compareCommand = 'open http://localhost:8000/?compare'
const diffCommand = 'code -n -d test/__compare__/examples-dev.json test/__compare__/examples-build.json';
const intrinsicProps = [
// Common
'id', 'label',
// Constraint
'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness',
// Body
'area', 'axes', 'collisionFilter', 'category', 'mask',
'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor',
'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution', 'sleepThreshold', 'slop',
'timeScale', 'vertices',
// Composite
'bodies', 'constraints', 'composites'
];
const stubBrowserFeatures = M => {
const noop = () => ({ collisionFilter: {}, mouse: {} });
M.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}});
M.Render.run = M.Render.lookAt = noop;
M.Runner.create = M.Runner.run = noop;
M.MouseConstraint.create = M.Mouse.create = noop;
M.Common.log = M.Common.info = M.Common.warn = noop;
return M;
};
const colors = { White: 37, BrightWhite: 90, BrightCyan: 36 };
const color = (text, number) => `\x1b[${number}m${text}\x1b[0m`;
const limit = (val, precision=3) => parseFloat(val.toPrecision(precision));
const engineSnapshot = (engine) => ({
timestamp: limit(engine.timing.timestamp),
world: worldSnapshotExtrinsic(engine.world),
worldIntrinsic: worldSnapshotIntrinsic(engine.world)
});
const worldSnapshotExtrinsic = world => ({
bodies: Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = [
body.position.x,
body.position.y,
body.positionPrev.x,
body.positionPrev.y,
body.angle,
body.anglePrev,
...body.vertices.reduce((flat, vertex) => (flat.push(vertex.x, vertex.y), flat), [])
];
return bodies;
}, {}),
constraints: Composite.allConstraints(world).reduce((constraints, constraint) => {
const positionA = Constraint.pointAWorld(constraint);
const positionB = Constraint.pointBWorld(constraint);
constraints[constraint.id] = [
positionA.x,
positionA.y,
positionB.x,
positionB.y
];
return constraints;
}, {})
});
const worldSnapshotIntrinsic = world => worldSnapshotIntrinsicBase({
bodies: Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
constraints: Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {}),
composites: Composite.allComposites(world).reduce((composites, composite) => {
composites[composite.id] = {
bodies: Composite.allBodies(composite).map(body => body.id),
constraints: Composite.allConstraints(composite).map(constraint => constraint.id),
composites: Composite.allComposites(composite).map(composite => composite.id)
};
return composites;
}, {})
});
const worldSnapshotIntrinsicBase = (obj, depth=0) => {
if (obj === Infinity) {
return 'Infinity';
} else if (typeof obj === 'number') {
return limit(obj);
} else if (Array.isArray(obj)) {
return obj.map(item => worldSnapshotIntrinsicBase(item, depth + 1));
} else if (typeof obj !== 'object') {
return obj;
}
const result = Object.entries(obj)
.filter(([key]) => depth <= 1 || intrinsicProps.includes(key))
.reduce((cleaned, [key, val]) => {
if (val && val.id && String(val.id) !== key) {
val = val.id;
}
if (Array.isArray(val) && !['composites', 'constraints', 'bodies'].includes(key)) {
val = `[${val.length}]`;
}
cleaned[key] = worldSnapshotIntrinsicBase(val, depth + 1);
return cleaned;
}, {});
return Object.keys(result).sort()
.reduce((sorted, key) => (sorted[key] = result[key], sorted), {});
};
const similarity = (a, b) => {
const distance = Math.sqrt(a.reduce((sum, _val, i) => sum + Math.pow(a[i] - b[i], 2), 0));
return 1 / (1 + (distance / a.length));
};
const snapshotSimilarityExtrinsic = (currentSnapshots, referenceSnapshots) => {
const result = {};
Object.entries(currentSnapshots).forEach(([name, current]) => {
const reference = referenceSnapshots[name];
const worldVector = [];
const worldVectorRef = [];
Object.keys(current.world).forEach(objectType => {
Object.keys(current.world[objectType]).forEach(objectId => {
worldVector.push(...current.world[objectType][objectId]);
worldVectorRef.push(...reference.world[objectType][objectId]);
});
});
result[name] = similarity(worldVector, worldVectorRef);
});
return result;
};
const writeSnapshots = (name, obj) => {
try {
fs.mkdirSync(comparePath, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
fs.writeFileSync(`${comparePath}/${name}.json`, compactStringify(obj, { maxLength: 100 }), 'utf8');
};
const toMatchExtrinsics = {
toMatchExtrinsics(received, value, ticks) {
const changed = [];
const borderline = [];
const equal = [];
const similaritys = snapshotSimilarityExtrinsic(received, value);
const entries = Object.entries(similaritys);
entries.sort(([_nameA, similarityA], [_nameB, similarityB]) => similarityA - similarityB);
entries.forEach(([name, similarity], i) => {
const percentSimilar = similarity * 100;
if (percentSimilar < 99.99) {
const col = i < 5 ? colors.White : colors.BrightWhite;
changed.push(color(`${name}`, col) + ` ${percentSimilar.toFixed(2)}%`);
} else if (percentSimilar !== 100) {
borderline.push(`~ ${name}`);
} else {
equal.push(`${name}`);
}
});
const pass = equal.length === similaritys.length && changed.length === 0 && borderline.length === 0;
return {
message: () => `Expected positions and velocities to match between builds.
${color('▶', colors.White)} Debug using ${color(compareCommand + '=' + ticks + '#' + entries[0][0], colors.BrightCyan)}
(${changed.length}) Changed
${changed.join(' ')}
(${borderline.length}) Borderline (> 99.99%)
${borderline.join(' ').slice(0, 80)}...
(${equal.length}) Equal
${equal.join(' ').slice(0, 80)}...`,
pass
};
}
};
const toMatchIntrinsics = {
toMatchIntrinsics(currentSnapshots, referenceSnapshots) {
const changed = [];
const equal = [];
const currentChanged = {};
const referenceChanged = {};
const entries = Object.entries(currentSnapshots);
entries.forEach(([name, current]) => {
const reference = referenceSnapshots[name];
const endWorld = current.worldIntrinsic;
const endWorldRef = reference.worldIntrinsic;
if (this.equals(endWorld, endWorldRef)) {
equal.push(`${name}`);
} else {
changed.push(color(`${name}`, changed.length < 5 ? colors.White : colors.BrightWhite));
if (changed.length < 2) {
currentChanged[name] = endWorld;
referenceChanged[name] = endWorldRef;
}
}
});
const pass = equal.length === entries.length && changed.length === 0;
writeSnapshots('examples-dev', currentChanged);
writeSnapshots('examples-build', referenceChanged);
return {
message: () => `Expected intrinsic properties to match between builds.
(${changed.length}) Changed
${changed.join(' ')}
(${equal.length}) Equal
${equal.join(' ').slice(0, 80)}...
${color('▶', colors.White)} Inspect using ${color(diffCommand, colors.BrightCyan)}`,
pass
};
}
};
module.exports = {
stubBrowserFeatures, engineSnapshot, toMatchExtrinsics, toMatchIntrinsics
};

View file

@ -53,7 +53,8 @@ module.exports = (env = {}) => {
devServer: {
contentBase: [
path.resolve(__dirname, './demo'),
path.resolve(__dirname, './examples')
path.resolve(__dirname, './examples'),
path.resolve(__dirname, './build')
],
open: true,
openPage: '',