Add tests for load costume error handling. Fix issue where asset data was not getting saved out properly.

This commit is contained in:
Karishma Chadha committed 2022-05-13 19:07:34 -04:00
1 parent a8618b378f
commit 70a78cf7db
15 files changed
+418 -13

No files matched your search

+1
View File
@@ -67,6 +67,7 @@
"gh-pages": "1.2.0",
"in-publish": "2.0.1",
"jsdoc": "3.6.6",
"js-md5": "0.7.3",
"json": "^9.0.4",
"lodash.defaultsdeep": "4.6.1",
"pngjs": "3.3.3",
+7 -5
View File
@@ -250,9 +250,9 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) {
// Handle all manner of costume errors with a Gray Question Mark (default costume)
// and preserve as much of the original costume data as possible
// Returns a promise of a costume
const handleCostumeLoadError = function (costume, runtime) {
// Keep track of the old assetId until we're done loading the default costume
// const oldAsset = costume.asset; // could be null
const handleCostumeLoadError = function (costume, runtime) {
// Keep track of the old asset information until we're done loading the default costume
const oldAsset = costume.asset; // could be null
const oldAssetId = costume.assetId;
const oldRotationX = costume.rotationCenterX;
const oldRotationY = costume.rotationCenterY;
@@ -270,8 +270,10 @@ const handleCostumeLoadError = function (costume, runtime) {
loadedCostume.broken = {};
loadedCostume.broken.assetId = oldAssetId;
loadedCostume.broken.md5 = `${oldAssetId}.${costume.dataFormat}`;
// Should be null if we got here because the costume was missing
loadedCostume.broken.asset = runtime.storage.get(oldAssetId);
loadedCostume.broken.asset = oldAsset;
loadedCostume.broken.rotationCenterX = oldRotationX;
loadedCostume.broken.rotationCenterY = oldRotationY;
return loadedCostume;
@@ -310,7 +312,7 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) {
if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
return loadVector_(costume, runtime, rotationCenter, optVersion)
.catch(error => {
log.warn(`Error loading vector image: ${error.name}: ${error.message}`);
log.warn(`Error loading vector image: ${error}`);
return handleCostumeLoadError(costume, runtime);
});
+5 -6
View File
@@ -427,11 +427,9 @@ class VirtualMachine extends EventEmitter {
* specified by optZipType or blob by default.
*/
exportSprite (targetId, optZipType) {
const sb3 = require('./serialization/sb3');
const soundDescs = serializeSounds(this.runtime, targetId);
const costumeDescs = serializeCostumes(this.runtime, targetId);
const spriteJson = StringUtil.stringify(sb3.serialize(this.runtime, targetId));
const spriteJson = this.toJSON(targetId);
const zip = new JSZip();
zip.file('sprite.json', spriteJson);
@@ -448,12 +446,13 @@ class VirtualMachine extends EventEmitter {
}
/**
* Export project as a Scratch 3.0 JSON representation.
* Export project or sprite as a Scratch 3.0 JSON representation.
* @param {string=} optTargetId - Optional id of a sprite to serialize
* @return {string} Serialized state of the runtime.
*/
toJSON () {
toJSON (optTargetId) {
const sb3 = require('./serialization/sb3');
return StringUtil.stringify(sb3.serialize(this.runtime));
return StringUtil.stringify(sb3.serialize(this.runtime, optTargetId));
}
// TODO do we still need this function? Keeping it here so as not to introduce
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+13
View File
@@ -4,6 +4,19 @@ const FakeRenderer = function () {
this.y = 0;
this.order = 0;
this.spriteCount = 5;
this._nextSkinId = -1;
};
FakeRenderer.prototype.createSVGSkin = function () {
return this._nextSkinId++;
};
FakeRenderer.prototype.getSkinSize = function (d) { // eslint-disable-line no-unused-vars
return [0, 0];
};
FakeRenderer.prototype.getSkinRotationCenter = function (d) { // eslint-disable-line no-unused-vars
return [0, 0];
};
FakeRenderer.prototype.createDrawable = function () {
+2 -2
View File
@@ -39,8 +39,8 @@ const getAssetUrl = function (asset) {
const makeTestStorage = function () {
const storage = new ScratchStorage();
const AssetType = storage.AssetType;
storage.addWebSource([AssetType.Project], getProjectUrl);
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
storage.addWebStore([AssetType.Project], getProjectUrl);
storage.addWebStore([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
return storage;
};
BIN
View File
Binary file not shown.
Binary file not shown.
+5
View File
@@ -12,5 +12,10 @@ module.exports = {
return JSON.parse(zip.readAsText(projectEntry.entryName, 'utf8'));
}
return null;
},
extractAsset: function (path, assetFileName) {
const zip = new AdmZip(path);
const assetEntry = zip.getEntries().filter(item => item.entryName.match(assetFileName))[0];
return assetEntry.getData();
}
};
+107
View File
@@ -0,0 +1,107 @@
/**
* This test mocks render breaking on loading a corrupted vector 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 {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile');
const VirtualMachine = require('../../src/index');
const {serializeCostumes} = require('../../src/serialization/serialize-assets');
const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3');
const project = readFileToBuffer(projectUri);
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
const originalCostume = extractAsset(projectUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg 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);
let vm;
let defaultVectorAssetId;
tap.beforeEach(() => {
const storage = makeTestStorage();
vm = new VirtualMachine();
vm.attachStorage(storage);
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer._nextSkinId++;
};
vm.attachRenderer(new FakeRenderer());
return vm.loadProject(project);
});
const test = tap.test;
test('load sb3 project with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'costume1');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
t.equal(corruptedCostume.dataFormat, 'svg');
// 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 vector 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 blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Square Guy');
t.equal(blueGuySprite.costumes.length, 1);
const corruptedCostume = blueGuySprite.costumes[0];
t.equal(corruptedCostume.name, 'costume1');
// Resaved project costume should have the metadata that corresponds to the original broken costume
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
t.equal(corruptedCostume.dataFormat, 'svg');
// 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}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+87
View File
@@ -0,0 +1,87 @@
/**
* This test ensures that the VM gracefully handles an sb3 project with
* a missing vector 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 readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');
const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3');
const project = readFileToBuffer(projectUri);
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
let vm;
tap.beforeEach(() => {
const storage = makeTestStorage();
// This line removes the webhelper from the list of available helpers.
// W/o the following line, this fails because storage doesn't handle the case
// where none of the tools have isGetSupported: true
// TODO: Remove this line when the related storage bug is resolved so that
// storage gracefully handles non-browser situations where assets are missing.
storage._helpers = [storage._helpers[0]];
vm = new VirtualMachine();
vm.attachStorage(storage);
vm.attachRenderer(new FakeRenderer());
return vm.loadProject(project);
});
const test = tap.test;
test('loading sb3 project with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 2);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[1];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
t.equal(missingCostume.assetId, defaultVectorAssetId);
t.equal(missingCostume.dataFormat, 'svg');
// Runtime should have info about broken asset
t.ok(missingCostume.broken);
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
t.end();
});
test('load and then save sb3 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 blueGuySprite = resavedProject.targets[1];
t.equal(blueGuySprite.name, 'Blue Square Guy');
t.equal(blueGuySprite.costumes.length, 1);
const missingCostume = blueGuySprite.costumes[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
t.equal(missingCostume.assetId, missingCostumeAssetId);
t.equal(missingCostume.dataFormat, 'svg');
// Test that we didn't save any data about the costume being broken
t.notOk(missingCostume.broken);
t.end();
process.nextTick(process.exit);
});
+106
View File
@@ -0,0 +1,106 @@
/**
* This test mocks render breaking on loading a sprite with a
* corrupted vector 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 {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_svg.sprite3');
const sprite = readFileToBuffer(spriteUri);
const costumeFileName = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb.svg';
const originalCostume = extractAsset(spriteUri, costumeFileName);
// We need to get the actual md5 because we hand modified the svg 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);
let vm;
let defaultVectorAssetId;
tap.beforeEach(() => {
const storage = makeTestStorage();
vm = new VirtualMachine();
vm.attachStorage(storage);
defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
// Mock renderer breaking on loading a corrupt costume
FakeRenderer.prototype.createSVGSkin = function (svgString) {
// Look for text added to costume to make it a corrupt svg
if (svgString.includes('<here is some')) {
throw new Error('mock createSVGSkin broke');
}
return FakeRenderer.prototype._nextSkinId++;
};
vm.attachRenderer(new FakeRenderer());
return vm.loadProject(project).then(() => vm.addSprite(sprite));
});
const test = tap.test;
test('load sprite3 with corrupted vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const corruptedCostume = blueGuySprite.getCostumes()[0];
t.equal(corruptedCostume.name, 'costume1');
t.equal(corruptedCostume.assetId, defaultVectorAssetId);
t.equal(corruptedCostume.dataFormat, 'svg');
// 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, 'Blue Square Guy');
t.equal(resavedSprite.costumes.length, 1);
const corruptedCostume = resavedSprite.costumes[0];
t.equal(corruptedCostume.name, 'costume1');
// Resaved project costume should have the metadata that corresponds to the original broken costume
t.equal(corruptedCostume.assetId, brokenCostumeMd5);
t.equal(corruptedCostume.dataFormat, 'svg');
// 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}.svg`);
t.equal(md5(costume.fileContent), brokenCostumeMd5);
t.end();
process.nextTick(process.exit);
});
+85
View File
@@ -0,0 +1,85 @@
/**
* This test ensures that the VM gracefully handles a sprite3 file with
* a missing vector 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 readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');
// 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_svg.sprite3');
const sprite = readFileToBuffer(spriteUri);
const missingCostumeAssetId = 'a267f8b97ee9cf8aa9832aa0b4cfd9eb';
let vm;
tap.beforeEach(() => {
const storage = makeTestStorage();
// This line removes the webhelper from the list of available helpers.
// W/o the following line, this fails because storage doesn't handle the case
// where none of the tools have isGetSupported: true
// TODO: Remove this line when the related storage bug is resolved so that
// storage gracefully handles non-browser situations where assets are missing.
storage._helpers = [storage._helpers[0]];
vm = new VirtualMachine();
vm.attachStorage(storage);
vm.attachRenderer(new FakeRenderer());
return vm.loadProject(project).then(() => vm.addSprite(sprite));
});
const test = tap.test;
test('loading sprite3 with missing vector costume file', t => {
t.equal(vm.runtime.targets.length, 3);
const stage = vm.runtime.targets[0];
t.ok(stage.isStage);
const blueGuySprite = vm.runtime.targets[2];
t.equal(blueGuySprite.getName(), 'Blue Square Guy');
t.equal(blueGuySprite.getCostumes().length, 1);
const missingCostume = blueGuySprite.getCostumes()[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
const defaultVectorAssetId = vm.runtime.storage.defaultAssetId.ImageVector;
t.equal(missingCostume.assetId, defaultVectorAssetId);
t.equal(missingCostume.dataFormat, 'svg');
// Runtime should have info about broken asset
t.ok(missingCostume.broken);
t.equal(missingCostume.broken.assetId, missingCostumeAssetId);
t.end();
});
test('load and then save sprite3 with missing vector costume file', t => {
const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id));
t.equal(resavedSprite.name, 'Blue Square Guy');
t.equal(resavedSprite.costumes.length, 1);
const missingCostume = resavedSprite.costumes[0];
t.equal(missingCostume.name, 'costume1');
// Costume should have both default cosutme (e.g. Gray Question Mark) data and original data
t.equal(missingCostume.assetId, missingCostumeAssetId);
t.equal(missingCostume.dataFormat, 'svg');
// Test that we didn't save any data about the costume being broken
t.notOk(missingCostume.broken);
t.end();
process.nextTick(process.exit);
});