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:
parent
21e4ea6044
commit
b7c938a963
11 changed files with 543 additions and 209 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,4 +13,5 @@ test/browser/diffs
|
|||
test/browser/refs
|
||||
test/node/diffs
|
||||
test/node/refs
|
||||
__snapshots__
|
||||
__snapshots__
|
||||
__compare__
|
|
@ -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
|
||||
|
|
|
@ -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
179
demo/js/Compare.js
Normal 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);
|
||||
})();
|
|
@ -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
12
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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": [
|
||||
|
|
119
test/Common.js
119
test/Common.js
|
@ -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 };
|
|
@ -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
257
test/TestTools.js
Normal 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
|
||||
};
|
|
@ -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: '',
|
||||
|
|
Loading…
Add table
Reference in a new issue