mirror of
https://github.com/liabru/matter-js.git
synced 2025-01-21 17:14:38 -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
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,3 +14,4 @@ test/browser/refs
|
||||||
test/node/diffs
|
test/node/diffs
|
||||||
test/node/refs
|
test/node/refs
|
||||||
__snapshots__
|
__snapshots__
|
||||||
|
__compare__
|
|
@ -34,5 +34,7 @@ creates a release build
|
||||||
runs the linter
|
runs the linter
|
||||||
- **npm run test**
|
- **npm run test**
|
||||||
runs the tests
|
runs the tests
|
||||||
|
- **npm run compare**
|
||||||
|
compares the output of examples for current source against release build
|
||||||
- **npm run doc**
|
- **npm run doc**
|
||||||
builds the documentation
|
builds the documentation
|
||||||
|
|
|
@ -82,6 +82,27 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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/`.
|
* 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({
|
var demo = MatterTools.Demo.create({
|
||||||
toolbar: {
|
toolbar: {
|
||||||
title: 'matter-js',
|
title: 'matter-js',
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -5562,9 +5562,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.4.tgz",
|
||||||
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
|
"integrity": "sha512-vvpo6mpK4ScNC1DbGRZ2d5BznS6ht0r1hi20RivsibMc6jNvFAeZQ6qk5VNspo6SOwVOJQbjHyBCpuS7BzA1pw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
|
@ -7224,6 +7224,12 @@
|
||||||
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
|
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
|
||||||
"dev": true
|
"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": {
|
"json-stringify-safe": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
"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-tag-version": "^1.3.0",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
|
"json-stringify-pretty-compact": "^2.0.0",
|
||||||
"run-sequence": "^1.1.4",
|
"run-sequence": "^1.1.4",
|
||||||
"webpack": "^4.39.3",
|
"webpack": "^4.39.3",
|
||||||
"webpack-cli": "^3.3.8",
|
"webpack-cli": "^3.3.8",
|
||||||
|
@ -37,11 +38,10 @@
|
||||||
"build": "webpack --mode=production & webpack --mode=production --env.MINIMIZE",
|
"build": "webpack --mode=production & webpack --mode=production --env.MINIMIZE",
|
||||||
"build-alpha": "webpack --mode=production --env.EDGE & webpack --mode=production --env.MINIMIZE --env.EDGE",
|
"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",
|
"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",
|
"doc": "gulp doc",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test-snapshot": "TEST_SNAPSHOTS=true jest --ci",
|
"compare": "COMPARE=true jest"
|
||||||
"test-snapshot-update": "TEST_SNAPSHOTS=true jest -u"
|
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"files": [
|
"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 */
|
/* eslint no-global-assign: 0 */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const Common = require('./Common');
|
const {
|
||||||
const fs = require('fs');
|
stubBrowserFeatures, engineSnapshot, toMatchExtrinsics, toMatchIntrinsics
|
||||||
const execSync = require('child_process').execSync;
|
} = require('./TestTools');
|
||||||
const useSnapshots = process.env.TEST_SNAPSHOTS === 'true';
|
|
||||||
const useBuild = process.env.TEST_BUILD === 'true';
|
|
||||||
|
|
||||||
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
|
const MatterBuild = require('../build/matter');
|
||||||
if (useBuild) {
|
const MatterDev = require('../src/module/main');
|
||||||
jest.mock('matter-js', () => require('../build/matter'), { virtual: true });
|
|
||||||
} else {
|
|
||||||
jest.mock('matter-js', () => require('../src/module/main'), { virtual: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock('matter-wrap', () => require('../demo/lib/matter-wrap'), { virtual: true });
|
jest.mock('matter-wrap', () => require('../demo/lib/matter-wrap'), { virtual: true });
|
||||||
jest.mock('poly-decomp', () => require('../demo/lib/decomp'), { virtual: true });
|
jest.mock('poly-decomp', () => require('../demo/lib/decomp'), { virtual: true });
|
||||||
|
|
||||||
// import mocked Matter and plugins
|
const runExamples = (matter) => {
|
||||||
const Matter = global.Matter = require('matter-js');
|
let snapshots = {};
|
||||||
Matter.Plugin.register(require('matter-wrap'));
|
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 consoleOriginal = global.console;
|
||||||
const noop = () => ({ collisionFilter: {}, mouse: {} });
|
global.console = { log: () => {} };
|
||||||
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;
|
|
||||||
|
|
||||||
// check initial snapshots if enabled (experimental)
|
for (name of examples) {
|
||||||
if (useSnapshots && !fs.existsSync('./test/__snapshots__')) {
|
matter.Common._nextId = matter.Common._seed = 0;
|
||||||
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');
|
|
||||||
|
|
||||||
if (!gitIsMaster || !gitIsClean) {
|
const example = Example[name]();
|
||||||
throw `Snapshots are experimental and are not currently committed due to size.
|
const engine = example.engine;
|
||||||
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}.
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent examples from logging
|
for (let i = 0; i < totalUpdates; i += 1) {
|
||||||
const consoleOriginal = console;
|
matter.Engine.update(engine, 1000 / 60);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: {
|
devServer: {
|
||||||
contentBase: [
|
contentBase: [
|
||||||
path.resolve(__dirname, './demo'),
|
path.resolve(__dirname, './demo'),
|
||||||
path.resolve(__dirname, './examples')
|
path.resolve(__dirname, './examples'),
|
||||||
|
path.resolve(__dirname, './build')
|
||||||
],
|
],
|
||||||
open: true,
|
open: true,
|
||||||
openPage: '',
|
openPage: '',
|
||||||
|
|
Loading…
Add table
Reference in a new issue