-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
225 additions
and
936 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,5 @@ examples/build | |
test/browser/diffs | ||
test/browser/refs | ||
test/node/diffs | ||
test/node/refs | ||
test/node/refs | ||
__snapshots__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
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, world | ||
} = engine; | ||
|
||
const bodies = Composite.allBodies(world); | ||
const constraints = Composite.allConstraints(world); | ||
const composites = Composite.allComposites(world); | ||
|
||
return { | ||
positionIterations, | ||
velocityIterations, | ||
constraintIterations, | ||
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
"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'; | ||
|
||
console.info(`Testing Matter from ${useBuild ? `build '../build/matter'.` : `source '../src/module/main'.`}`); | ||
|
||
// mock modules | ||
if (useBuild) { | ||
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('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')); | ||
|
||
// import Examples after Matter | ||
const Example = require('../examples/index'); | ||
|
||
// 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; | ||
|
||
// 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'); | ||
|
||
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}. | ||
`; | ||
} | ||
} | ||
|
||
// 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(); | ||
} | ||
} | ||
); |
Oops, something went wrong.