mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-07 11:34:00 -04:00
Hook up error handling to bitmap loading flow and add tests for loading missing and corrupt bitmpas from sb2 and sprite2 files
This commit is contained in:
parent
0d3a72ec9b
commit
3e193565ee
11 changed files with 505 additions and 14 deletions
|
@ -99,7 +99,7 @@ const canvasPool = (function () {
|
||||||
* assetMatchesBase is true if the asset matches the base layer; false if it required adjustment
|
* assetMatchesBase is true if the asset matches the base layer; false if it required adjustment
|
||||||
*/
|
*/
|
||||||
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
if (!costume || !costume.asset) {
|
if (!costume || !costume.asset) { // TODO: We can probably remove this check...
|
||||||
return Promise.reject('Costume load failed. Assets were missing.');
|
return Promise.reject('Costume load failed. Assets were missing.');
|
||||||
}
|
}
|
||||||
if (!runtime.v2BitmapAdapter) {
|
if (!runtime.v2BitmapAdapter) {
|
||||||
|
@ -176,7 +176,7 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
assetMatchesBase: scale === 1 && !textImageElement
|
assetMatchesBase: scale === 1 && !textImageElement
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.finally(() => {
|
||||||
// Clean up the text layer properties if it fails to load
|
// Clean up the text layer properties if it fails to load
|
||||||
delete costume.textLayerMD5;
|
delete costume.textLayerMD5;
|
||||||
delete costume.textLayerAsset;
|
delete costume.textLayerAsset;
|
||||||
|
@ -325,7 +325,11 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return loadBitmap_(costume, runtime, rotationCenter, optVersion);
|
return loadBitmap_(costume, runtime, rotationCenter, optVersion)
|
||||||
|
.catch(error => {
|
||||||
|
log.warn(`Error loading bitmap image: ${error}`);
|
||||||
|
return handleCostumeLoadError(costume, runtime);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -377,18 +381,25 @@ const loadCostume = function (md5ext, costume, runtime, optVersion) {
|
||||||
textLayerPromise = Promise.resolve(null);
|
textLayerPromise = Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all([costumePromise, textLayerPromise]).then(assetArray => {
|
return Promise.all([costumePromise, textLayerPromise])
|
||||||
if (assetArray[0]) {
|
.then(assetArray => {
|
||||||
costume.asset = assetArray[0];
|
if (assetArray[0]) {
|
||||||
} else {
|
costume.asset = assetArray[0];
|
||||||
return handleCostumeLoadError(costume, runtime);
|
} else {
|
||||||
}
|
return handleCostumeLoadError(costume, runtime);
|
||||||
|
}
|
||||||
|
|
||||||
if (assetArray[1]) {
|
if (assetArray[1]) {
|
||||||
costume.textLayerAsset = assetArray[1];
|
costume.textLayerAsset = assetArray[1];
|
||||||
}
|
}
|
||||||
return loadCostumeFromAsset(costume, runtime, optVersion);
|
return loadCostumeFromAsset(costume, runtime, optVersion);
|
||||||
});
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Handle case where storage.load rejects with errors
|
||||||
|
// instead of resolving null
|
||||||
|
log.warn('Error loading costume: ', error);
|
||||||
|
return handleCostumeLoadError(costume, runtime);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -877,6 +877,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) {
|
updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) {
|
||||||
const costume = this.editingTarget.getCostumes()[costumeIndex];
|
const costume = this.editingTarget.getCostumes()[costumeIndex];
|
||||||
if (!(costume && this.runtime && this.runtime.renderer)) return;
|
if (!(costume && this.runtime && this.runtime.renderer)) return;
|
||||||
|
if (costume && costume.broken) delete costume.broken;
|
||||||
|
|
||||||
costume.rotationCenterX = rotationCenterX;
|
costume.rotationCenterX = rotationCenterX;
|
||||||
costume.rotationCenterY = rotationCenterY;
|
costume.rotationCenterY = rotationCenterY;
|
||||||
|
|
BIN
test/fixtures/corrupt_png.sb2
vendored
Normal file
BIN
test/fixtures/corrupt_png.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/corrupt_png.sprite2
vendored
Normal file
BIN
test/fixtures/corrupt_png.sprite2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/missing_png.sb2
vendored
Normal file
BIN
test/fixtures/missing_png.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/missing_png.sprite2
vendored
Normal file
BIN
test/fixtures/missing_png.sprite2
vendored
Normal file
Binary file not shown.
126
test/integration/sb2_corrupted_png.js
Normal file
126
test/integration/sb2_corrupted_png.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* This test mocks render breaking on loading a corrupted bitmap costume.
|
||||||
|
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||||
|
* but keeping track of the original costume data and serializing the
|
||||||
|
* original costume data back out. The saved project.json should not
|
||||||
|
* reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const md5 = require('js-md5');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||||
|
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb2');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
const costumeFileName = '1.png';
|
||||||
|
const originalCostume = extractAsset(projectUri, costumeFileName);
|
||||||
|
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||||
|
// after we downloaded the project from Scratch
|
||||||
|
// Loading the project back into the VM will correct the assetId and md5
|
||||||
|
const brokenCostumeMd5 = md5(originalCostume);
|
||||||
|
|
||||||
|
global.Image = function () {
|
||||||
|
const image = {
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
const base64Image = image.src.split(',')[1];
|
||||||
|
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||||
|
if (decodedText.includes('Here is some')) {
|
||||||
|
image.onerror();
|
||||||
|
} else {
|
||||||
|
image.onload();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
global.document = {
|
||||||
|
createElement: () => ({
|
||||||
|
// Create mock canvas
|
||||||
|
getContext: () => ({
|
||||||
|
drawImage: () => ({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
let defaultBitmapAssetId;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||||
|
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||||
|
|
||||||
|
return vm.loadProject(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('load sb2 project with corrupted bitmap costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = vm.runtime.targets[1];
|
||||||
|
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||||
|
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||||
|
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'png');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(corruptedCostume.broken);
|
||||||
|
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||||
|
// Verify that we saved the original asset data
|
||||||
|
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save project with corrupted bitmap costume file', t => {
|
||||||
|
const resavedProject = JSON.parse(vm.toJSON());
|
||||||
|
|
||||||
|
t.equal(resavedProject.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = resavedProject.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = resavedProject.targets[1];
|
||||||
|
t.equal(greenGuySprite.name, 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = greenGuySprite.costumes[0];
|
||||||
|
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||||
|
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||||
|
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'png');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(corruptedCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume saves orignal broken costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[1].id);
|
||||||
|
t.equal(costumeDescs.length, 1);
|
||||||
|
const costume = costumeDescs[0];
|
||||||
|
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||||
|
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
111
test/integration/sb2_missing_png.js
Normal file
111
test/integration/sb2_missing_png.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* This test ensures that the VM gracefully handles an sb2 project with
|
||||||
|
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||||
|
* a Gray Question Mark, but keeping track of the original costume data
|
||||||
|
* and serializing the original costume data back out. The saved project.json
|
||||||
|
* should not reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb2');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
|
||||||
|
const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa';
|
||||||
|
|
||||||
|
global.Image = function () {
|
||||||
|
const image = {
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
};
|
||||||
|
setTimeout(() => image.onload(), 1000);
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
global.document = {
|
||||||
|
createElement: () => ({
|
||||||
|
// Create mock canvas
|
||||||
|
getContext: () => ({
|
||||||
|
drawImage: () => ({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||||
|
|
||||||
|
return vm.loadProject(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('loading sb2 project with missing bitmap costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = vm.runtime.targets[1];
|
||||||
|
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||||
|
t.equal(missingCostume.name, 'GreenGuy');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||||
|
t.equal(missingCostume.assetId, defaultVectorAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'png');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(missingCostume.broken);
|
||||||
|
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sb2 project with missing costume file', t => {
|
||||||
|
const resavedProject = JSON.parse(vm.toJSON());
|
||||||
|
|
||||||
|
t.equal(resavedProject.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = resavedProject.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = resavedProject.targets[1];
|
||||||
|
t.equal(greenGuySprite.name, 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const missingCostume = greenGuySprite.costumes[0];
|
||||||
|
t.equal(missingCostume.name, 'GreenGuy');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'png');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(missingCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume does not save data for missing costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime);
|
||||||
|
|
||||||
|
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||||
|
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.png`);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
|
@ -13,6 +13,7 @@ const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||||
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
const VirtualMachine = require('../../src/index');
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2');
|
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2');
|
||||||
const project = readFileToBuffer(projectUri);
|
const project = readFileToBuffer(projectUri);
|
||||||
|
@ -95,6 +96,15 @@ test('load and then save sb2 project with missing costume file', t => {
|
||||||
// Test that we didn't save any data about the costume being broken
|
// Test that we didn't save any data about the costume being broken
|
||||||
t.notOk(missingCostume.broken);
|
t.notOk(missingCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume does not save data for missing costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime);
|
||||||
|
|
||||||
|
t.equal(costumeDescs.length, 1); // Should only have one costume, the backdrop
|
||||||
|
t.not(costumeDescs[0].fileName, `${missingCostumeAssetId}.svg`);
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
process.nextTick(process.exit);
|
process.nextTick(process.exit);
|
||||||
});
|
});
|
||||||
|
|
125
test/integration/sprite2_corrupted_png.js
Normal file
125
test/integration/sprite2_corrupted_png.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/**
|
||||||
|
* This test mocks render breaking on loading a sprite2 with a
|
||||||
|
* corrupted bitmap costume.
|
||||||
|
* The VM should handle this safely by displaying a Gray Question Mark,
|
||||||
|
* but keeping track of the original costume data and serializing the
|
||||||
|
* original costume data back out. The saved project.json should not
|
||||||
|
* reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const md5 = require('js-md5');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||||
|
const {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const spriteUri = path.resolve(__dirname, '../fixtures/corrupt_png.sprite2');
|
||||||
|
const sprite = readFileToBuffer(spriteUri);
|
||||||
|
|
||||||
|
const costumeFileName = '0.png';
|
||||||
|
const originalCostume = extractAsset(spriteUri, costumeFileName);
|
||||||
|
// We need to get the actual md5 because we hand modified the png to corrupt it
|
||||||
|
// after we downloaded the project from Scratch
|
||||||
|
// Loading the project back into the VM will correct the assetId and md5
|
||||||
|
const brokenCostumeMd5 = md5(originalCostume);
|
||||||
|
|
||||||
|
global.Image = function () {
|
||||||
|
const image = {
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
const base64Image = image.src.split(',')[1];
|
||||||
|
const decodedText = Buffer.from(base64Image, 'base64').toString();
|
||||||
|
if (decodedText.includes('Here is some')) {
|
||||||
|
image.onerror();
|
||||||
|
} else {
|
||||||
|
image.onload();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
global.document = {
|
||||||
|
createElement: () => ({
|
||||||
|
// Create mock canvas
|
||||||
|
getContext: () => ({
|
||||||
|
drawImage: () => ({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
let defaultBitmapAssetId;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||||
|
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||||
|
|
||||||
|
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('load sprite2 with corrupted bitmap costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 3);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = vm.runtime.targets[2];
|
||||||
|
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = greenGuySprite.getCostumes()[0];
|
||||||
|
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||||
|
t.equal(corruptedCostume.assetId, defaultBitmapAssetId);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'png');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(corruptedCostume.broken);
|
||||||
|
t.equal(corruptedCostume.broken.assetId, brokenCostumeMd5);
|
||||||
|
// Verify that we saved the original asset data
|
||||||
|
t.equal(md5(corruptedCostume.broken.asset.data), brokenCostumeMd5);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sprite with corrupted costume file', t => {
|
||||||
|
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||||
|
|
||||||
|
t.equal(resavedSprite.name, 'GreenGuy');
|
||||||
|
t.equal(resavedSprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const corruptedCostume = resavedSprite.costumes[0];
|
||||||
|
t.equal(corruptedCostume.name, 'GreenGuy');
|
||||||
|
// Resaved project costume should have the metadata that corresponds to the original broken costume
|
||||||
|
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
|
||||||
|
t.equal(corruptedCostume.dataFormat, 'png');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(corruptedCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume saves orignal broken costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||||
|
t.equal(costumeDescs.length, 1);
|
||||||
|
const costume = costumeDescs[0];
|
||||||
|
t.equal(costume.fileName, `${brokenCostumeMd5}.png`);
|
||||||
|
t.equal(md5(costume.fileContent), brokenCostumeMd5);
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
107
test/integration/sprite2_missing_png.js
Normal file
107
test/integration/sprite2_missing_png.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* This test ensures that the VM gracefully handles a sprite2 file with
|
||||||
|
* a missing bitmap costume. The VM should handle this safely by displaying
|
||||||
|
* a Gray Question Mark, but keeping track of the original costume data
|
||||||
|
* and serializing the original costume data back out. The saved project.json
|
||||||
|
* should not reflect that the costume is broken and should therefore re-attempt
|
||||||
|
* to load the costume if the saved project is re-loaded.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
const FakeBitmapAdapter = require('../fixtures/fake-bitmap-adapter');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
// The particular project that we're loading doesn't matter for this test
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/default.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const spriteUri = path.resolve(__dirname, '../fixtures/missing_png.sprite2');
|
||||||
|
const sprite = readFileToBuffer(spriteUri);
|
||||||
|
|
||||||
|
const missingCostumeAssetId = 'aadce129bfe4e57f0dd81478f3ed82aa';
|
||||||
|
|
||||||
|
global.Image = function () {
|
||||||
|
const image = {
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
};
|
||||||
|
setTimeout(() => image.onload(), 1000);
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
global.document = {
|
||||||
|
createElement: () => ({
|
||||||
|
// Create mock canvas
|
||||||
|
getContext: () => ({
|
||||||
|
drawImage: () => ({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
vm.attachRenderer(new FakeRenderer());
|
||||||
|
vm.attachV2BitmapAdapter(new FakeBitmapAdapter());
|
||||||
|
|
||||||
|
return vm.loadProject(project).then(() => vm.addSprite(sprite));
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('loading sprite2 with missing bitmap costume file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 3);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const greenGuySprite = vm.runtime.targets[2];
|
||||||
|
t.equal(greenGuySprite.getName(), 'GreenGuy');
|
||||||
|
t.equal(greenGuySprite.getCostumes().length, 1);
|
||||||
|
|
||||||
|
const missingCostume = greenGuySprite.getCostumes()[0];
|
||||||
|
t.equal(missingCostume.name, 'GreenGuy');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
const defaultBitmapAssetId = vm.runtime.storage.defaultAssetId.ImageBitmap;
|
||||||
|
t.equal(missingCostume.assetId, defaultBitmapAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'png');
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(missingCostume.broken);
|
||||||
|
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sprite2 with missing bitmap costume file', t => {
|
||||||
|
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
|
||||||
|
|
||||||
|
t.equal(resavedSprite.name, 'GreenGuy');
|
||||||
|
t.equal(resavedSprite.costumes.length, 1);
|
||||||
|
|
||||||
|
const missingCostume = resavedSprite.costumes[0];
|
||||||
|
t.equal(missingCostume.name, 'GreenGuy');
|
||||||
|
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
|
||||||
|
t.equal(missingCostume.assetId, missingCostumeAssetId);
|
||||||
|
t.equal(missingCostume.dataFormat, 'png');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(missingCostume.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume does not save data for missing costume', t => {
|
||||||
|
const costumeDescs = serializeCostumes(vm.runtime, vm.runtime.targets[2].id);
|
||||||
|
|
||||||
|
t.equal(costumeDescs.length, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue