scratch-vm/test/integration/sb3_corrupted_sound.js

120 lines
4.1 KiB
JavaScript

/**
* 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();
process.nextTick(process.exit);
});