From 70a78cf7db250b7bdce8b30f0fab2a5808f01ac6 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Fri, 13 May 2022 18:34:34 -0400 Subject: [PATCH] Add tests for load costume error handling. Fix issue where asset data was not getting saved out properly. --- package.json | 1 + src/import/load-costume.js | 12 ++- src/virtual-machine.js | 11 +-- test/fixtures/corrupt_svg.sb3 | Bin 0 -> 4856 bytes test/fixtures/corrupt_svg.sprite3 | Bin 0 -> 2713 bytes test/fixtures/default.sb3 | Bin 0 -> 41932 bytes test/fixtures/fake-renderer.js | 13 +++ test/fixtures/make-test-storage.js | 4 +- test/fixtures/missing_svg.sb3 | Bin 0 -> 3446 bytes test/fixtures/missing_svg.sprite3 | Bin 0 -> 1949 bytes test/fixtures/readProjectFile.js | 5 + test/integration/sb3_corrupted_svg.js | 107 ++++++++++++++++++++++ test/integration/sb3_missing_svg.js | 87 ++++++++++++++++++ test/integration/sprite3_corrupted_svg.js | 106 +++++++++++++++++++++ test/integration/sprite3_missing_svg.js | 85 +++++++++++++++++ 15 files changed, 418 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/corrupt_svg.sb3 create mode 100644 test/fixtures/corrupt_svg.sprite3 create mode 100644 test/fixtures/default.sb3 create mode 100644 test/fixtures/missing_svg.sb3 create mode 100644 test/fixtures/missing_svg.sprite3 create mode 100644 test/integration/sb3_corrupted_svg.js create mode 100644 test/integration/sb3_missing_svg.js create mode 100644 test/integration/sprite3_corrupted_svg.js create mode 100644 test/integration/sprite3_missing_svg.js diff --git a/package.json b/package.json index 136318ce..68d1ba16 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/import/load-costume.js b/src/import/load-costume.js index b3b73c59..58f3cacf 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -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); }); diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 9abbf679..3e67ec91 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -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 diff --git a/test/fixtures/corrupt_svg.sb3 b/test/fixtures/corrupt_svg.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..52fb93096f082385fc0aec5c32df6fff96e588b7 GIT binary patch literal 4856 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/corrupt_svg.sprite3 b/test/fixtures/corrupt_svg.sprite3 new file mode 100644 index 0000000000000000000000000000000000000000..1d9dd0b3dd08da0ca2ebdfd1c435b295e44d179e GIT binary patch literal 2713 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/fake-renderer.js b/test/fixtures/fake-renderer.js index dd086876..f8343692 100644 --- a/test/fixtures/fake-renderer.js +++ b/test/fixtures/fake-renderer.js @@ -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 () { diff --git a/test/fixtures/make-test-storage.js b/test/fixtures/make-test-storage.js index 0179ca34..1401a2c1 100644 --- a/test/fixtures/make-test-storage.js +++ b/test/fixtures/make-test-storage.js @@ -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; }; diff --git a/test/fixtures/missing_svg.sb3 b/test/fixtures/missing_svg.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e0956c36f06c05e56a9de97f38882654b9158ee7 GIT binary patch literal 3446 zcmb_e2{=@38$L6G83|L?8Zxqu#9(YCp(4iEP3em*-F|LVGW&UI$axz4%X=XvhuzR!DG;bC0D04Hky@U)w| z_}|Z301Xhlf`h&MJkUnIA?|3Cko{;ymmq&1cR!4(y&XRQkB?JunLk6q_y7nO8v+1x z>sigMivSSctvdj&EeozoA`uL?S=#PaRUs{z;=2q-!Aw77aCH`O^>B7^o+OwPCk2U# zR#DJn;xGjy3c^%E^Drl{NN$O3Mlt#3$5Jo`Z=%n|t&(Qs<>ZxS?zTFXejzjaWDZQ& zjr`Fr{KmL_QDmz;=_r4h=;0iC?@bl<8g!ZucNG5gcl z#3PYj5|%*EW-sk+Sh2Le3{%)bFu1&_wR3O6W34LfD$^Q7Qee-d{?;>e4@Fw@?l!Y zyWW=&vl~Zk7h@k7^2ntX#IJKjpD47s*qSXJ>u4#Cxtvd z9XF51*){957TU3t4|_Xw+e_Tl+mW(Vxmv^Pl|Q!?KO#NReyRN)K5)X>yXsVp@&P6}FBSDY0bB zY%o3-$JP^A!#oGdXKNJe8q~wZnfD zRV?ze1yL_UVr|AdHew#bDw{07)Z0u?1a@ z&nuY&sV0IA44Y|&e@8SaNwohg{7%}jLj!!3a)%BDHcx!u;aB21O1|gH!2fM*z0Tvf zvC9W8nr%dvBKLFU7mbaW#B;%YPI?HZhZ6P%(a+e*VN%;a>9szXAqR8{)l6It@46nC zAxut#!^@p=a!)5gyOc*&4QcI%8qM3S=uJYEeCKBaR~y$gGUGcorr?d%%BNpJoYCVu ztyI`D>bH0GsPaieLVZt})*t1ObCPLjt`4n-0su!Ci%SLzKGk1VGCCS$U2Pq0H=L`B z7DdBFTT4UT1?T2Qad)9;<21CiRsJN0ev=Xvl+hwmf?%N!KHHcY8!Lk8b{P_PQ1(&L zNKkeV@L0700M-)$016-hH!_vX1-{b-z+gAwLHrXUwvu`>*|NjvQqf=@dnT#RnVwwX zRgzZxl4jCmI5Y))E8k&aMvm~830Og9Y#)S;gz|YwmS!e zR_!nJ?AhTVX7<9jDm?8;v`<1{UP`m{r(tY_^sAoqUEy-Ln$QH8ndOES+G|2B}SoC zsXCoG)*xJ}t7y$6;!G_zz_^tA>#E-r3zT%!CJol|=&3~Eg&h=Z9Ca@WNsI+HUAlxh zUtNJ}zdU_*UHH~!>sD)@tr1cqGmjoe*GXNque;b)%T^MP(O?_K87Ql+6xzkg>#DfT zuBW!;JP(J4qYrB(Y<;M+N%l5R+6an~P>*O3d0_q82XRmiy$NAhrIR6J%cD95RA}XsbZO(Ql&!WFq^4>SJhAZpJ}|lH!aK4FT|2ClZsp0BKQ~N1xs^; ze<)!neAMH80aO`DT8orQl5`T?!bklw!fGAM7>RrRY+~kPCltYFhD=-4E1=E02D*PX zcRJ(!AiH};>T3#DCPHk*5rjP#`3vWppT~?Z(**gzq3PlTZUtGiJb_)^)YUZAaBf&l z4K<1zh2o;Fu8zgIW8Gaf-Q9F>WR>7hk8cF#Z10N@cpm{T5Q`QbIB=x<8k}B+yH#jv z*)CLR7H8MgP}<(#y(g1EQOoj2bwh|kg;NzqEpkRdTO8s=Vl@3OS3c^>qB0$%(=Qjq z5ZWH;n8aszdd2O;FsZNfR~9O@+4$ZzDQY>Bo%c27N|_I>rkF2knl8R~Y8aNaZk_-2 zkRgj)ZXOfuFI@V%!{;J-oz2DGe!%~u??eU?k`4mE0l^9nfeOQxwu9g+bJ1YP`CED` z|9@_0KF_>8Txhj8_HaHLqIZLrzr!s4m~o+V;aIr&D2kScEZ5o37G+8Q`wx`!EiQB( z978l8MbV}pFTb_8G^W2`j}|t|@iq%T;T&%>AC=L6gSnSZ`72K6$D9jQ$I;e&R7P(E sHA47mQJI9r@ zu;dt^b3slE1~jfj-g4^i6NjX}`k; z0)M7voe!K6{#eCbt8vFAZjQo;<9;a*j~wj35cKu_#@kaC#XZ<|e&)@M{0e8TC7oaR zGC)f1U`Mn613#;Mx9Tje)O>yLP2gZpl)}_I4#gI33auNxJ+?Ysdt^F&Rc-vEFkWVP z{khjN-)`s5wlyNWvTeqNMQMptoz~R++#}XgI;U*O3GX7??4ZkQ zXZX*lUAcRSqePT_v`FToxh57qlk#oWzF+1!@9(a^`u8jn*spHhuJ>WSKzd?JePPC$ zvVYlc7MwiI@$Ev&)aAnQ(=F9Z^3S(jSG;=Tdqq@?-PNN@*4}N`S};ZX%0UH-I%ZI8 z7BjP@!~oMKD9*6QrU6jK7GG>y7$;hqTbQSqBqy1r87G;W85<>;q@<*!CZ(C17@L{v zl_!?LqZt&%SfZKHfa(0-AWt_p4WMn|j$y88xg`n$z!b&^go4@(3`$`P3``6H3@M2v zi7Y@hR~eX@_p%r9z84LawNaj}@u=|=aXjt@UGy;ZyD z?v+@ZuUx>Dc+TaOtk!4#lUd9EPE_rEH^Xx4i@T3_=32QV6qIePT$FLcHC^26NzT@) zN%tDF+Y0CRolyMq$e=d%e&v&T^TL(B+hkTg_utVojkz;(qW`+DH)D7hYy#rn=Zj~C z`EjWfeu_MrzIfZ@ikUw)W!zfK*=Lp=u9>zdevjK%`4gX*Pkmi`ZjtbDZ;keaensP9k{)@2lT8Ca8 zin_@9%9DMH(sk1dHg8P5l%qJ^-$~ynKh1kO?OOh;jZBjyjkPKaR%zSFe&I;@y#L{j z>))=exi9ekETg7yjQk2^5Bb%?>zRW-7d+c_|MmmpmuJ4Iu-z8^B;zIDB6X6#hVjWK z)91Vo+a6ZENdK~sNk<@FNVPp4i@d*1T$!TX;-t}t item.entryName.match(assetFileName))[0]; + return assetEntry.getData(); } }; diff --git a/test/integration/sb3_corrupted_svg.js b/test/integration/sb3_corrupted_svg.js new file mode 100644 index 00000000..281c1c60 --- /dev/null +++ b/test/integration/sb3_corrupted_svg.js @@ -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(' { + 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); +}); diff --git a/test/integration/sb3_missing_svg.js b/test/integration/sb3_missing_svg.js new file mode 100644 index 00000000..9e1445f5 --- /dev/null +++ b/test/integration/sb3_missing_svg.js @@ -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); +}); diff --git a/test/integration/sprite3_corrupted_svg.js b/test/integration/sprite3_corrupted_svg.js new file mode 100644 index 00000000..9bbe96bc --- /dev/null +++ b/test/integration/sprite3_corrupted_svg.js @@ -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(' 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); +}); diff --git a/test/integration/sprite3_missing_svg.js b/test/integration/sprite3_missing_svg.js new file mode 100644 index 00000000..70e3ab04 --- /dev/null +++ b/test/integration/sprite3_missing_svg.js @@ -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); +});