diff --git a/test/fixtures/corrupt_png.sb3 b/test/fixtures/corrupt_png.sb3 new file mode 100644 index 000000000..acc482216 Binary files /dev/null and b/test/fixtures/corrupt_png.sb3 differ diff --git a/test/fixtures/corrupt_png.sprite3 b/test/fixtures/corrupt_png.sprite3 new file mode 100644 index 000000000..fc1d61356 Binary files /dev/null and b/test/fixtures/corrupt_png.sprite3 differ diff --git a/test/fixtures/missing_png.sb3 b/test/fixtures/missing_png.sb3 new file mode 100644 index 000000000..40260a33b Binary files /dev/null and b/test/fixtures/missing_png.sb3 differ diff --git a/test/fixtures/missing_png.sprite3 b/test/fixtures/missing_png.sprite3 new file mode 100644 index 000000000..0702cdcd3 Binary files /dev/null and b/test/fixtures/missing_png.sprite3 differ diff --git a/test/integration/sb3_corrupted_png.js b/test/integration/sb3_corrupted_png.js new file mode 100644 index 000000000..dd02ca4fd --- /dev/null +++ b/test/integration/sb3_corrupted_png.js @@ -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.sb3'); +const project = readFileToBuffer(projectUri); +const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.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 sb3 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(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + 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, 'Green Guy'); + t.equal(greenGuySprite.costumes.length, 1); + + const corruptedCostume = greenGuySprite.costumes[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + // 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); +}); diff --git a/test/integration/sb3_missing_png.js b/test/integration/sb3_missing_png.js new file mode 100644 index 000000000..a16e17a66 --- /dev/null +++ b/test/integration/sb3_missing_png.js @@ -0,0 +1,111 @@ +/** + * This test ensures that the VM gracefully handles an sb3 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.sb3'); +const project = readFileToBuffer(projectUri); + + +const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b'; + +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 sb3 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(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Green Guy'); + // 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 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 greenGuySprite = resavedProject.targets[1]; + t.equal(greenGuySprite.name, 'Green Guy'); + t.equal(greenGuySprite.costumes.length, 1); + + const missingCostume = greenGuySprite.costumes[0]; + t.equal(missingCostume.name, 'Green Guy'); + // 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); +}); diff --git a/test/integration/sprite3_corrupted_png.js b/test/integration/sprite3_corrupted_png.js new file mode 100644 index 000000000..2898fa82a --- /dev/null +++ b/test/integration/sprite3_corrupted_png.js @@ -0,0 +1,125 @@ +/** + * This test mocks render breaking on loading a sprite3 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.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const costumeFileName = 'e1320c21995dcf6de10119be7f08c26b.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 sprite3 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(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const corruptedCostume = greenGuySprite.getCostumes()[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + 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, 'Green Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const corruptedCostume = resavedSprite.costumes[0]; + t.equal(corruptedCostume.name, 'Green Guy'); + // 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); +}); diff --git a/test/integration/sprite3_missing_png.js b/test/integration/sprite3_missing_png.js new file mode 100644 index 000000000..bbc7a816c --- /dev/null +++ b/test/integration/sprite3_missing_png.js @@ -0,0 +1,107 @@ +/** + * This test ensures that the VM gracefully handles a sprite3 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.sprite3'); +const sprite = readFileToBuffer(spriteUri); + +const missingCostumeAssetId = 'e1320c21995dcf6de10119be7f08c26b'; + +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 sprite3 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(), 'Green Guy'); + t.equal(greenGuySprite.getCostumes().length, 1); + + const missingCostume = greenGuySprite.getCostumes()[0]; + t.equal(missingCostume.name, 'Green Guy'); + // 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 sprite3 with missing bitmap costume file', t => { + const resavedSprite = JSON.parse(vm.toJSON(vm.runtime.targets[2].id)); + + t.equal(resavedSprite.name, 'Green Guy'); + t.equal(resavedSprite.costumes.length, 1); + + const missingCostume = resavedSprite.costumes[0]; + t.equal(missingCostume.name, 'Green Guy'); + // 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); +});