Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions src/tests/run_static_analysis_runtime_contracts_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function usage() {
return [
'Usage: node src/tests/run_static_analysis_runtime_contracts_node.js [--filter NAME]',
'',
'Replays src/tests/resources/testdata.js and checks static object occupancy invariants.',
'Replays src/tests/resources/testdata.js and checks static-analysis runtime contracts.',
].join('\n');
}

Expand Down Expand Up @@ -106,12 +106,19 @@ function staticContractForSource(source, testName) {
}
return {
objectNames: [],
staticLayerContracts: [],
constantQuantityObjectNames: [],
quantityContracts: [],
unavailableReason: `${report.status}: ${expected.diagnostic}`,
};
}
const objects = ((report.ps_tagged && report.ps_tagged.objects) || []);
const staticLayerContracts = ((report.ps_tagged && report.ps_tagged.collision_layers) || [])
.filter(layer => layer.tags && layer.tags.static === true)
.map(layer => ({
layerId: layer.id,
objectNames: layer.objects.slice(),
}));
const quantityContracts = objects
.filter(object => object.tags && object.tags.quantity)
.map(object => ({
Expand All @@ -124,6 +131,7 @@ function staticContractForSource(source, testName) {
objectNames: objects
.filter(object => object.tags && object.tags.static === true)
.map(object => object.name),
staticLayerContracts,
constantQuantityObjectNames: quantityContracts
.filter(contract => contract.neverIncreases && contract.neverDecreases)
.map(contract => contract.objectName),
Expand Down Expand Up @@ -195,6 +203,38 @@ function snapshotStaticObjects(objectNames) {
return snapshots;
}

function layerOccupancySnapshot(layerContract) {
if (!canSnapshotBoard()) {
throw new Error(`cannot snapshot layer ${layerContract.layerId}: no active board level`);
}
const layerObjects = layerContract.objectNames.map(objectName => {
const runtimeName = engineObjectName(objectName);
return {
displayName: objectName,
objectId: state.objects[runtimeName].id,
};
});
const cells = [];
for (let cellIndex = 0; cellIndex < level.n_tiles; cellIndex++) {
const cell = level.getCell(cellIndex);
cells.push(layerObjects
.filter(object => cell.get(object.objectId))
.map(object => object.displayName)
.sort()
.join('|'));
}
return cells;
}

function snapshotStaticLayers(layerContracts) {
const snapshots = new Map();
if (!canSnapshotBoard()) return snapshots;
for (const layerContract of layerContracts) {
snapshots.set(layerContract.layerId, layerOccupancySnapshot(layerContract));
}
return snapshots;
}

function objectCountSnapshot(displayName) {
return objectOccupancySnapshot(displayName).reduce((sum, present) => sum + present, 0);
}
Expand Down Expand Up @@ -229,6 +269,27 @@ function firstSnapshotDifference(beforeSnapshots, objectNames) {
return null;
}

function firstLayerSnapshotDifference(beforeSnapshots, layerContracts) {
for (const layerContract of layerContracts) {
const before = beforeSnapshots.get(layerContract.layerId) || [];
const after = layerOccupancySnapshot(layerContract);
const length = Math.max(before.length, after.length);
for (let cellIndex = 0; cellIndex < length; cellIndex++) {
const beforeValue = before[cellIndex] || '';
const afterValue = after[cellIndex] || '';
if (beforeValue !== afterValue) {
return {
layerId: layerContract.layerId,
cellIndex,
before: beforeValue,
after: afterValue,
};
}
}
}
return null;
}

function firstQuantityDifference(beforeCounts, quantityContracts) {
for (const contract of quantityContracts) {
const before = beforeCounts.get(contract.objectName) || 0;
Expand Down Expand Up @@ -318,6 +379,7 @@ function runSimulationWithStaticChecks(testName, dataarray) {
const expectedSounds = dataarray[5] === undefined ? null : dataarray[5];
const staticContract = staticContractForSource(source, testName);
const staticObjects = staticContract.objectNames;
const staticLayerContracts = staticContract.staticLayerContracts;
const constantQuantityObjects = staticContract.constantQuantityObjectNames;
const quantityContracts = staticContract.quantityContracts;
const countedObjects = quantityObjectNames(quantityContracts);
Expand All @@ -328,6 +390,7 @@ function runSimulationWithStaticChecks(testName, dataarray) {
lazyFunctionGeneration = false;

let objectBoundaryChecks = 0;
let layerBoundaryChecks = 0;
let quantityBoundaryChecks = 0;
let restartBoundaryTriggered = false;
const previousDoRestart = global.DoRestart;
Expand All @@ -341,7 +404,8 @@ function runSimulationWithStaticChecks(testName, dataarray) {
compileSimulationSource(testName, source, targetLevel, randomSeed);

let currentIdentity = boardIdentity();
let snapshots = snapshotStaticObjects(staticObjects);
let objectSnapshots = snapshotStaticObjects(staticObjects);
let layerSnapshots = snapshotStaticLayers(staticLayerContracts);
let countSnapshots = snapshotObjectCounts(countedObjects);

for (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
Expand All @@ -359,12 +423,13 @@ function runSimulationWithStaticChecks(testName, dataarray) {

if (resetBoundary) {
currentIdentity = nextIdentity;
snapshots = snapshotStaticObjects(staticObjects);
objectSnapshots = snapshotStaticObjects(staticObjects);
layerSnapshots = snapshotStaticLayers(staticLayerContracts);
countSnapshots = snapshotObjectCounts(countedObjects);
continue;
}

const diff = firstSnapshotDifference(snapshots, staticObjects);
const diff = firstSnapshotDifference(objectSnapshots, staticObjects);
if (diff) {
throw new Error([
`${testName}: static object occupancy changed`,
Expand All @@ -376,6 +441,18 @@ function runSimulationWithStaticChecks(testName, dataarray) {
].join('\n'));
}

const layerDiff = firstLayerSnapshotDifference(layerSnapshots, staticLayerContracts);
if (layerDiff) {
throw new Error([
`${testName}: static layer occupancy changed`,
` input ${inputIndex}: ${tokenLabel(inputToken)}`,
` layer: ${layerDiff.layerId}`,
` cell: ${layerDiff.cellIndex}`,
` before: ${layerDiff.before}`,
` after: ${layerDiff.after}`,
].join('\n'));
}

const countDiff = firstQuantityDifference(countSnapshots, quantityContracts);
if (countDiff) {
throw new Error([
Expand All @@ -389,6 +466,7 @@ function runSimulationWithStaticChecks(testName, dataarray) {
}

objectBoundaryChecks += staticObjects.length;
layerBoundaryChecks += staticLayerContracts.length;
quantityBoundaryChecks += quantityClaimCount(quantityContracts);
countSnapshots = snapshotObjectCounts(countedObjects);
currentIdentity = nextIdentity;
Expand All @@ -398,8 +476,10 @@ function runSimulationWithStaticChecks(testName, dataarray) {

return {
staticObjectCount: staticObjects.length,
staticLayerCount: staticLayerContracts.length,
constantQuantityObjectCount: constantQuantityObjects.length,
objectBoundaryChecks,
layerBoundaryChecks,
quantityBoundaryChecks,
analysisUnavailableReason: staticContract.unavailableReason,
};
Expand Down Expand Up @@ -429,8 +509,10 @@ function runAll(options = {}) {
const failures = [];
let caseCount = 0;
let casesWithStaticObjects = 0;
let casesWithStaticLayers = 0;
let casesWithConstantQuantityObjects = 0;
let objectBoundaryChecks = 0;
let layerBoundaryChecks = 0;
let quantityBoundaryChecks = 0;
let analysisUnavailableCount = 0;
const entries = global.testdata.filter(entry => testMatchesFilter(entry[0], options.filter || null));
Expand All @@ -454,10 +536,14 @@ function runAll(options = {}) {
if (result.staticObjectCount > 0) {
casesWithStaticObjects++;
}
if (result.staticLayerCount > 0) {
casesWithStaticLayers++;
}
if (result.constantQuantityObjectCount > 0) {
casesWithConstantQuantityObjects++;
}
objectBoundaryChecks += result.objectBoundaryChecks;
layerBoundaryChecks += result.layerBoundaryChecks;
quantityBoundaryChecks += result.quantityBoundaryChecks;
if (result.analysisUnavailableReason) {
analysisUnavailableCount++;
Expand All @@ -472,8 +558,10 @@ function runAll(options = {}) {
ok: failures.length === 0,
caseCount,
casesWithStaticObjects,
casesWithStaticLayers,
casesWithConstantQuantityObjects,
objectBoundaryChecks,
layerBoundaryChecks,
quantityBoundaryChecks,
analysisUnavailableCount,
failures,
Expand All @@ -497,7 +585,7 @@ function main() {
}

console.log(
`static_analysis_runtime_contracts: ok (${result.caseCount} cases, ${result.analysisUnavailableCount} analysis-unavailable, ${result.casesWithStaticObjects} with static objects, ${result.casesWithConstantQuantityObjects} with constant-quantity objects, ${result.objectBoundaryChecks} object-boundary checks, ${result.quantityBoundaryChecks} quantity-boundary checks)`
`static_analysis_runtime_contracts: ok (${result.caseCount} cases, ${result.analysisUnavailableCount} analysis-unavailable, ${result.casesWithStaticObjects} with static objects, ${result.casesWithStaticLayers} with static layers, ${result.casesWithConstantQuantityObjects} with constant-quantity objects, ${result.objectBoundaryChecks} object-boundary checks, ${result.layerBoundaryChecks} layer-boundary checks, ${result.quantityBoundaryChecks} quantity-boundary checks)`
);
return 0;
}
Expand All @@ -516,13 +604,16 @@ module.exports = {
MAX_AGAIN_DRAIN_STEPS,
boardIdentity,
engineObjectName,
firstLayerSnapshotDifference,
firstQuantityDifference,
firstSnapshotDifference,
layerOccupancySnapshot,
objectCountSnapshot,
parseArgs,
runAll,
runSimulationWithStaticChecks,
snapshotObjectCounts,
snapshotStaticLayers,
snapshotStaticObjects,
staticContractForSource,
};
5 changes: 5 additions & 0 deletions src/tests/run_static_analysis_runtime_contracts_node_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ assert.ok(sokoban, 'sokoban fixture should be available');
const result = runSimulationWithStaticChecks(sokoban[0], sokoban[1]);

assert.strictEqual(result.staticObjectCount, 3, 'sokoban should have three static objects');
assert.strictEqual(result.staticLayerCount, 2, 'sokoban should have two static layers');
assert.strictEqual(result.constantQuantityObjectCount, 5, 'sokoban should have five constant-quantity objects');
assert.ok(
result.quantityBoundaryChecks > result.objectBoundaryChecks,
'quantity checks should include movable constant-quantity objects'
);
assert.ok(
result.layerBoundaryChecks > 0,
'layer checks should include static collision layers'
);

const restartBoundarySource = [
'========',
Expand Down