diff --git a/test/fixtures/variable_characters.sb2 b/test/fixtures/variable_characters.sb2 new file mode 100644 index 000000000..301860488 Binary files /dev/null and b/test/fixtures/variable_characters.sb2 differ diff --git a/test/fixtures/variable_characters.sb3 b/test/fixtures/variable_characters.sb3 new file mode 100644 index 000000000..10720e5ee Binary files /dev/null and b/test/fixtures/variable_characters.sb3 differ diff --git a/test/integration/variable_special_chars_sb2.js b/test/integration/variable_special_chars_sb2.js new file mode 100644 index 000000000..19086e345 --- /dev/null +++ b/test/integration/variable_special_chars_sb2.js @@ -0,0 +1,135 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); +const StringUtil = require('../../src/util/string-util'); +const VariableUtil = require('../../src/util/variable-util'); + +const projectUri = path.resolve(__dirname, '../fixtures/variable_characters.sb2'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with special chars in variable names', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + t.equal(threads.length, 0); + + // we care that the last step updated the right number of monitors + // we don't care whether the last step ran other threads or not + const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor); + t.equal(lastStepUpdatedMonitorThreads.length, 3); + + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + const cat = vm.runtime.targets[1]; + const bananas = vm.runtime.targets[2]; + + const allVarListFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets); + + const abVarId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0]; + const abVar = stage.variables[abVarId]; + const abMonitor = vm.runtime._monitorState.get(abVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abVarId), abVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abMonitor.id), abMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(abVar.id), abVar.id); + t.equal(abVar.id, abVarId); + t.equal(abVar.type, Variable.LIST_TYPE); + t.equal(abVar.value[0], 'thing'); + t.equal(abVar.value[1], 'thing\'1'); + + // Find all the references for this list, and verify they have the correct ID + // There should be 3 fields, 2 on the stage, and one on the cat + t.equal(allVarListFields[abVarId].length, 3); + const stageBlocks = Object.keys(stage.blocks._blocks).map(blockId => stage.blocks._blocks[blockId]); + const stageListBlocks = stageBlocks.filter(block => block.fields.hasOwnProperty('LIST')); + t.equal(stageListBlocks.length, 2); + t.equal(stageListBlocks[0].fields.LIST.id, abVarId); + t.equal(stageListBlocks[1].fields.LIST.id, abVarId); + const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]); + const catListBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('LIST')); + t.equal(catListBlocks.length, 1); + t.equal(catListBlocks[0].fields.LIST.id, abVarId); + + const fooVarId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '"foo')[0]; + const fooVar = stage.variables[fooVarId]; + const fooMonitor = vm.runtime._monitorState.get(fooVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(fooVarId), fooVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(fooMonitor.id), fooMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(fooVar.id), fooVar.id); + t.equal(fooVar.id, fooVarId); + t.equal(fooVar.type, Variable.SCALAR_TYPE); + t.equal(fooVar.value, 'foo'); + + // Find all the references for this variable, and verify they have the correct ID + // There should be only two, one on the stage and one on bananas + t.equal(allVarListFields[fooVarId].length, 2); + const stageVarBlocks = stageBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(stageVarBlocks.length, 1); + t.equal(stageVarBlocks[0].fields.VARIABLE.id, fooVarId); + const catVarBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(catVarBlocks.length, 1); + t.equal(catVarBlocks[0].fields.VARIABLE.id, fooVarId); + + const ltPerfectVarId = Object.keys(bananas.variables).filter(k => bananas.variables[k].name === '< Perfect')[0]; + const ltPerfectVar = bananas.variables[ltPerfectVarId]; + const ltPerfectMonitor = vm.runtime._monitorState.get(ltPerfectVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectVarId), ltPerfectVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMonitor.id), ltPerfectMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(ltPerfectVar.id), ltPerfectVar.id); + t.equal(ltPerfectVar.id, ltPerfectVarId); + t.equal(ltPerfectVar.type, Variable.SCALAR_TYPE); + t.equal(ltPerfectVar.value, '> perfect'); + + // Find all the references for this variable, and verify they have the correct ID + // There should be one + t.equal(allVarListFields[ltPerfectVarId].length, 1); + const bananasBlocks = Object.keys(bananas.blocks._blocks).map(blockId => bananas.blocks._blocks[blockId]); + const bananasVarBlocks = bananasBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(bananasVarBlocks.length, 1); + t.equal(bananasVarBlocks[0].fields.VARIABLE.id, ltPerfectVarId); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/test/integration/variable_special_chars_sb3.js b/test/integration/variable_special_chars_sb3.js new file mode 100644 index 000000000..1c506406d --- /dev/null +++ b/test/integration/variable_special_chars_sb3.js @@ -0,0 +1,135 @@ +const path = require('path'); +const test = require('tap').test; +const makeTestStorage = require('../fixtures/make-test-storage'); +const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer; +const VirtualMachine = require('../../src/index'); +const Variable = require('../../src/engine/variable'); +const StringUtil = require('../../src/util/string-util'); +const VariableUtil = require('../../src/util/variable-util'); + +const projectUri = path.resolve(__dirname, '../fixtures/variable_characters.sb3'); +const project = readFileToBuffer(projectUri); + +test('importing sb2 project with special chars in variable names', t => { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + // All monitors should create threads that finish during the step and + // are revoved from runtime.threads. + t.equal(threads.length, 0); + + // we care that the last step updated the right number of monitors + // we don't care whether the last step ran other threads or not + const lastStepUpdatedMonitorThreads = vm.runtime._lastStepDoneThreads.filter(thread => thread.updateMonitor); + t.equal(lastStepUpdatedMonitorThreads.length, 3); + + t.equal(vm.runtime.targets.length, 3); + + const stage = vm.runtime.targets[0]; + const cat = vm.runtime.targets[1]; + const bananas = vm.runtime.targets[2]; + + const allVarListFields = VariableUtil.getAllVarRefsForTargets(vm.runtime.targets); + + const abVarId = Object.keys(stage.variables).filter(k => stage.variables[k].name === 'a&b')[0]; + const abVar = stage.variables[abVarId]; + const abMonitor = vm.runtime._monitorState.get(abVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abVarId), abVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(abMonitor.id), abMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(abVar.id), abVar.id); + t.equal(abVar.id, abVarId); + t.equal(abVar.type, Variable.LIST_TYPE); + t.equal(abVar.value[0], 'thing'); + t.equal(abVar.value[1], 'thing\'1'); + + // Find all the references for this list, and verify they have the correct ID + // There should be 3 fields, 2 on the stage, and one on the cat + t.equal(allVarListFields[abVarId].length, 3); + const stageBlocks = Object.keys(stage.blocks._blocks).map(blockId => stage.blocks._blocks[blockId]); + const stageListBlocks = stageBlocks.filter(block => block.fields.hasOwnProperty('LIST')); + t.equal(stageListBlocks.length, 2); + t.equal(stageListBlocks[0].fields.LIST.id, abVarId); + t.equal(stageListBlocks[1].fields.LIST.id, abVarId); + const catBlocks = Object.keys(cat.blocks._blocks).map(blockId => cat.blocks._blocks[blockId]); + const catListBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('LIST')); + t.equal(catListBlocks.length, 1); + t.equal(catListBlocks[0].fields.LIST.id, abVarId); + + const fooVarId = Object.keys(stage.variables).filter(k => stage.variables[k].name === '"foo')[0]; + const fooVar = stage.variables[fooVarId]; + const fooMonitor = vm.runtime._monitorState.get(fooVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(fooVarId), fooVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(fooMonitor.id), fooMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(fooVar.id), fooVar.id); + t.equal(fooVar.id, fooVarId); + t.equal(fooVar.type, Variable.SCALAR_TYPE); + t.equal(fooVar.value, 'foo'); + + // Find all the references for this variable, and verify they have the correct ID + // There should be only two, one on the stage and one on bananas + t.equal(allVarListFields[fooVarId].length, 2); + const stageVarBlocks = stageBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(stageVarBlocks.length, 1); + t.equal(stageVarBlocks[0].fields.VARIABLE.id, fooVarId); + const catVarBlocks = catBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(catVarBlocks.length, 1); + t.equal(catVarBlocks[0].fields.VARIABLE.id, fooVarId); + + const ltPerfectVarId = Object.keys(bananas.variables).filter(k => bananas.variables[k].name === '< Perfect')[0]; + const ltPerfectVar = bananas.variables[ltPerfectVarId]; + const ltPerfectMonitor = vm.runtime._monitorState.get(ltPerfectVarId); + // Check for unsafe characters, replaceUnsafeChars should just result in the original string + // (e.g. there was nothing to replace) + // Check that the variable ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectVarId), ltPerfectVarId); + // Check that the monitor record ID does not have any unsafe characters + t.equal(StringUtil.replaceUnsafeChars(ltPerfectMonitor.id), ltPerfectMonitor.id); + + // Check that the variable still has the correct info + t.equal(StringUtil.replaceUnsafeChars(ltPerfectVar.id), ltPerfectVar.id); + t.equal(ltPerfectVar.id, ltPerfectVarId); + t.equal(ltPerfectVar.type, Variable.SCALAR_TYPE); + t.equal(ltPerfectVar.value, '> perfect'); + + // Find all the references for this variable, and verify they have the correct ID + // There should be one + t.equal(allVarListFields[ltPerfectVarId].length, 1); + const bananasBlocks = Object.keys(bananas.blocks._blocks).map(blockId => bananas.blocks._blocks[blockId]); + const bananasVarBlocks = bananasBlocks.filter(block => block.fields.hasOwnProperty('VARIABLE')); + t.equal(bananasVarBlocks.length, 1); + t.equal(bananasVarBlocks[0].fields.VARIABLE.id, ltPerfectVarId); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/test/unit/util_string.js b/test/unit/util_string.js index 0b21e24f3..7fc88332a 100644 --- a/test/unit/util_string.js +++ b/test/unit/util_string.js @@ -84,3 +84,28 @@ test('stringify', t => { t.equal(parsed.f.nested, 0); t.end(); }); + +test('replaceUnsafeChars', t => { + const empty = ''; + t.equal(StringUtil.replaceUnsafeChars(empty), empty); + + const safe = 'hello'; + t.equal(StringUtil.replaceUnsafeChars(safe), safe); + + const unsafe = '< > & \' "'; + t.equal(StringUtil.replaceUnsafeChars(unsafe), 'lt gt amp apos quot'); + + const single = '&'; + t.equal(StringUtil.replaceUnsafeChars(single), 'amp'); + + const mix = 'b& c\'def_-"'; + t.equal(StringUtil.replaceUnsafeChars(mix), 'ltagtbamp caposdef_-quot'); + + const dupes = '<<&_"_"_&>>'; + t.equal(StringUtil.replaceUnsafeChars(dupes), 'ltltamp_quot_quot_ampgtgt'); + + const emoji = '(>^_^)>'; + t.equal(StringUtil.replaceUnsafeChars(emoji), '(gt^_^)gt'); + + t.end(); +}); diff --git a/test/unit/util_variable.js b/test/unit/util_variable.js new file mode 100644 index 000000000..18b0e768c --- /dev/null +++ b/test/unit/util_variable.js @@ -0,0 +1,83 @@ +const tap = require('tap'); +const Target = require('../../src/engine/target'); +const Runtime = require('../../src/engine/runtime'); +const VariableUtil = require('../../src/util/variable-util'); + +let target1; +let target2; + +tap.beforeEach(() => { + const runtime = new Runtime(); + target1 = new Target(runtime); + target1.blocks.createBlock({ + id: 'a block', + fields: { + VARIABLE: { + id: 'id1', + value: 'foo' + } + } + }); + target1.blocks.createBlock({ + id: 'another block', + fields: { + TEXT: { + value: 'not a variable' + } + } + }); + + target2 = new Target(runtime); + target2.blocks.createBlock({ + id: 'a different block', + fields: { + VARIABLE: { + id: 'id2', + value: 'bar' + } + } + }); + target2.blocks.createBlock({ + id: 'another var block', + fields: { + VARIABLE: { + id: 'id1', + value: 'foo' + } + } + }); + + return Promise.resolve(null); +}); + +const test = tap.test; + +test('get all var refs', t => { + const allVarRefs = VariableUtil.getAllVarRefsForTargets([target1, target2]); + t.equal(Object.keys(allVarRefs).length, 2); + t.equal(allVarRefs.id1.length, 2); + t.equal(allVarRefs.id2.length, 1); + t.equal(allVarRefs['not a variable'], undefined); + + t.end(); +}); + +test('merge variable ids', t => { + // Redo the id for the variable with 'id1' + VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id'); + const varField = target1.blocks.getBlock('a block').fields.VARIABLE; + t.equals(varField.id, 'renamed id'); + t.equals(varField.value, 'foo'); + + t.end(); +}); + +test('merge variable ids but with new name too', t => { + // Redo the id for the variable with 'id1' + VariableUtil.updateVariableIdentifiers(target1.blocks.getAllVariableAndListReferences().id1, 'renamed id', 'baz'); + const varField = target1.blocks.getBlock('a block').fields.VARIABLE; + t.equals(varField.id, 'renamed id'); + t.equals(varField.value, 'baz'); + + t.end(); +});