/** * This test mocks breaking on loading a corrupted sound. * The VM should handle this safely by replacing the sound data with the default (empty) sound, * but keeping track of the original sound data and serializing the * original sound data back out. The saved project.json should not * reflect that the sound is broken and should therefore re-attempt * to load the sound 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 {extractAsset, readFileToBuffer} = require('../fixtures/readProjectFile'); const VirtualMachine = require('../../src/index'); const {serializeSounds} = require('../../src/serialization/serialize-assets'); const projectUri = path.resolve(__dirname, '../fixtures/corrupt_sound.sb3'); const project = readFileToBuffer(projectUri); const soundFileName = '78618aadd225b1db7bf837fa17dc0568.wav'; const originalSound = extractAsset(projectUri, soundFileName); // We need to get the actual md5 because we hand modified the sound file to corrupt it // after we downloaded the project from Scratch // Loading the project back into the VM will correct the assetId and md5 const brokenSoundMd5 = md5(originalSound); let fakeId = -1; const FakeAudioEngine = function () { return { decodeSoundPlayer: soundData => { const soundDataString = soundData.asset.decodeText(); if (soundDataString.includes('here is some')) { return Promise.reject(new Error('mock audio engine broke')); } // Otherwise return fake data return Promise.resolve({ id: fakeId++, buffer: { sampleRate: 1, length: 1 } }); }, createBank: () => null }; }; let vm; let defaultSoundAssetId; tap.beforeEach(() => { const storage = makeTestStorage(); vm = new VirtualMachine(); vm.attachStorage(storage); defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound; vm.attachAudioEngine(FakeAudioEngine()); return vm.loadProject(project); }); const test = tap.test; test('load sb3 project with corrupted sound file', t => { t.equal(vm.runtime.targets.length, 2); const stage = vm.runtime.targets[0]; t.ok(stage.isStage); const catSprite = vm.runtime.targets[1]; t.equal(catSprite.getName(), 'Sprite1'); t.equal(catSprite.getSounds().length, 1); const corruptedSound = catSprite.getSounds()[0]; t.equal(corruptedSound.name, 'Boop Sound Recording'); t.equal(corruptedSound.assetId, defaultSoundAssetId); t.equal(corruptedSound.dataFormat, 'wav'); // Runtime should have info about broken asset t.ok(corruptedSound.broken); t.equal(corruptedSound.broken.assetId, brokenSoundMd5); // Verify that we saved the original asset data t.equal(md5(corruptedSound.broken.asset.data), brokenSoundMd5); t.end(); }); test('load and then save project with corrupted sound file', t => { const resavedProject = JSON.parse(vm.toJSON()); t.equal(resavedProject.targets.length, 2); const stage = resavedProject.targets[0]; t.ok(stage.isStage); const catSprite = resavedProject.targets[1]; t.equal(catSprite.name, 'Sprite1'); t.equal(catSprite.sounds.length, 1); const corruptedSound = catSprite.sounds[0]; t.equal(corruptedSound.name, 'Boop Sound Recording'); // Resaved project costume should have the metadata that corresponds to the original broken costume t.equal(corruptedSound.assetId, brokenSoundMd5); t.equal(corruptedSound.dataFormat, 'wav'); // Test that we didn't save any data about the costume being broken t.notOk(corruptedSound.broken); t.end(); }); test('serializeSounds saves orignal broken sound', t => { const soundDescs = serializeSounds(vm.runtime, vm.runtime.targets[1].id); t.equal(soundDescs.length, 1); const sound = soundDescs[0]; t.equal(sound.fileName, `${brokenSoundMd5}.wav`); t.equal(md5(sound.fileContent), brokenSoundMd5); t.end(); });