diff --git a/.eslintrc b/.eslintrc index c893eab..4932f68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,7 @@ { + "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false + }, "extends": ["scratch", "scratch/node"] } diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..71ddb6c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: node_js +node_js: + - 16 + - lts/* +cache: + directories: + - node_modules +jobs: + include: + - stage: release + node_js: 16 + script: echo deploying... + deploy: + - provider: script + script: npx semantic-release --branches develop + on: + branch: develop +stages: +- test +- name: release + if: type != pull_request AND branch = develop diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b33e8cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:16 + +RUN mkdir -p /var/app/current +WORKDIR /var/app/current +COPY . ./ +RUN rm -rf ./node_modules +RUN npm install +RUN npm install -g nodemon tap diff --git a/README.md b/README.md index afb20c5..6923060 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ ## scratch-analysis #### Analysis tool for summarizing the structure, composition, and complexity of [Scratch](https://scratch.mit.edu) programs. +[![Build Status](https://travis-ci.org/LLK/scratch-analysis.svg?branch=develop)](https://travis-ci.org/LLK/scratch-analysis) +[![Greenkeeper badge](https://badges.greenkeeper.io/LLK/scratch-analysis.svg)](https://greenkeeper.io/) + ## Getting Started ```bash npm install scratch-analysis @@ -23,7 +26,8 @@ The `scratch-analysis` module will return an object containing high-level summar | `scripts` | `count` | | `blocks` | `count`, `unique`, `list`, `frequency` | | `sprites` | `count` | -| `variables` | `count` | +| `variables` | `count`, `id` | +| `cloud` | `count`, `id` | | `lists` | `count` | | `costumes` | `count`, `list`, `hash` | | `sounds` | `count`, `list`, `hash` | diff --git a/TRADEMARK b/TRADEMARK new file mode 100644 index 0000000..17b5d4c --- /dev/null +++ b/TRADEMARK @@ -0,0 +1 @@ +The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..361ab02 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.4' +volumes: + npm_data: + runtime_data: + +networks: + default: + external: + name: scratchapi_scratch_network + +services: + app: + container_name: scratch-analysis-lib + hostname: scratch-analysis + build: + context: ./ + dockerfile: Dockerfile + image: scratch-analysis:latest + command: node -e "require('http').createServer((req, res) => { res.end('OK'); }).listen(8080, () => {console.log('Listening on 8080'); } );" + volumes: + - type: bind + source: ./ + target: /var/app/current + consistency: cached + volume: + nocopy: true + - npm_data:/var/app/current/node_modules + - runtime_data:/runtime + ports: + - "9999:8080" diff --git a/lib/sb2.js b/lib/sb2.js index 4831957..9681c1b 100644 --- a/lib/sb2.js +++ b/lib/sb2.js @@ -77,7 +77,7 @@ const sprites = function (input) { let result = 0; for (let i in input.children) { - if (input.children[i].hasOwnProperty('spriteInfo')) result++; + if (Object.prototype.hasOwnProperty.call(input.children[i], 'spriteInfo')) result++; } return {count: result}; @@ -92,6 +92,24 @@ const blocks = function (project) { // Storage objects const result = []; + /** + * Determine if a argument is the name of a known cloud variable. + * @param {string} arg Argument (variable name) + * @return {boolean} Is cloud variable? + */ + const isArgCloudVar = function (arg) { + // Validate argument + // @note "Hacked" inputs here could be objects (arrays) + if (typeof arg !== 'string') return false; + + // Iterate over global variables and check to see if arg matches + for (let i in project.variables) { + const variable = project.variables[i]; + if (variable.name === arg && variable.isPersistent) return true; + } + return false; + }; + /** * Walk scripts array(s) and build block list. * @param {array} stack Stack of blocks @@ -108,11 +126,20 @@ const blocks = function (project) { continue; } + // Get opcode and check variable manipulation for the presence of + // cloud variables + let opcode = stack[i][0]; + if (opcode === 'setVar:to:' || opcode === 'changeVar:by:') { + if (isArgCloudVar(stack[i][1])) { + opcode += 'cloud:'; + } + } + // Add to block list - result.push(stack[i][0]); + result.push(opcode); // Don't pull in params from procedures - if (stack[i][0] === 'procDef') continue; + if (opcode === 'procDef') continue; // Move to next item and walk walk(stack[i].slice(1)); @@ -154,6 +181,35 @@ const extensions = function (project) { return result; }; +/** + * Extracts cloud variable information. + * @param {object} project Project object (SB2 format) + * @param {array} names Names of all variables in project + * @return {object} Cloud variable information + */ +const cloud = function (project, names) { + const obj = []; + + // Extract "isPersistent" parameter from all variables in project + const cloudyness = extract(project, 'variables', 'isPersistent').id; + + // Ensure that variable names and isPersistent parameter list are the same + // length + if (names.length !== cloudyness.length) return -1; + + // Iterate over isPersistent values, and extract names of any that are true + for (let i in cloudyness) { + if (cloudyness[i]) { + obj.push(names[i]); + } + } + + return { + count: obj.length, + id: obj + }; +}; + /** * Analyzes a project and returns summary information about the project. * @param {object} project Project object (SB2 format) @@ -171,6 +227,8 @@ module.exports = function (project, callback) { costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5') }; + meta.cloud = cloud(project, meta.variables.id); + // Sprites meta.sprites = sprites(project); @@ -180,6 +238,9 @@ module.exports = function (project, callback) { // Extensions meta.extensions = extensions(project); + // Metadata is only in sb3s so just fill in an empty object. + meta.meta = {}; + // Return all metadata return callback(null, meta); }; diff --git a/lib/sb3.js b/lib/sb3.js index b9c910c..cf3d87b 100644 --- a/lib/sb3.js +++ b/lib/sb3.js @@ -21,9 +21,14 @@ const variables = function (targets, attribute) { let occurrences = 0; let idList = []; + // Cloud variables are a type of variable + const isCloud = (attribute === 'cloud'); + if (isCloud) attribute = 'variables'; + for (let t in targets) { for (let a in targets[t][attribute]) { const variable = targets[t][attribute][a]; + if (isCloud && (variable.length !== 3 || !variable[2])) continue; occurrences++; idList.push(variable[0]); } @@ -67,10 +72,37 @@ const blocks = function (targets) { // Storage object let result = []; + /** + * Determine if a argument is the name of a known cloud variable. + * @param {string} arg Argument (variable name) + * @return {boolean} Is cloud variable? + */ + const isArgCloudVar = function (arg) { + // Validate argument + if (typeof arg !== 'string') return false; + + // Check first target (stage) to determine if arg is a cloud variable id + const stage = targets[0]; + if (typeof stage.variables[arg] !== 'undefined') { + return stage.variables[arg].length === 3 && stage.variables[arg][2]; + } + }; + + // Iterate over all targets and push block opcodes to storage object for (let t in targets) { for (let a in targets[t].blocks) { const block = targets[t].blocks[a]; - if (!block.shadow) result.push(block.opcode); + + // Get opcode and check variable manipulation for the presence of + // cloud variables + let opcode = block.opcode; + if (opcode === 'data_setvariableto' || opcode === 'data_changevariableby') { + if (isArgCloudVar(block.fields.VARIABLE[1])) { + opcode += '_cloud'; + } + } + + if (!block.shadow) result.push(opcode); } } @@ -88,22 +120,32 @@ const blocks = function (targets) { const extensions = function (list) { return { - count: list.length, - id: list + count: (typeof list === 'object' ? list.length : 0), + id: (typeof list === 'object' ? list : []) }; }; +const metadata = function (meta) { + let obj = {}; + if (meta.origin) { + obj.origin = meta.origin; + } + return obj; +}; + module.exports = function (project, callback) { const meta = { scripts: scripts(project.targets), variables: variables(project.targets, 'variables'), + cloud: variables(project.targets, 'cloud'), lists: variables(project.targets, 'lists'), comments: extract(project.targets, 'comments'), sounds: extract(project.targets, 'sounds', 'name', 'md5ext'), costumes: extract(project.targets, 'costumes', 'name', 'md5ext'), sprites: sprites(project.targets), blocks: blocks(project.targets), - extensions: extensions(project.extensions) + extensions: extensions(project.extensions), + meta: metadata(project.meta) }; callback(null, meta); diff --git a/package.json b/package.json index 5308d51..92847a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scratch-analysis", - "version": "1.0.0", + "version": "1.1.0", "description": "Analysis tool for summarizing the structure, composition, and complexity of Scratch programs.", "main": "lib/index.js", "directories": { @@ -8,21 +8,19 @@ "test": "test" }, "scripts": { - "test": "npm run test:lint && npm run test:unit && npm run test:integration", + "test": "npm run test:lint && npm run test:unit", "test:lint": "eslint .", - "test:unit": "tap test/unit/*.js", - "test:integration": "tap test/integration/*.js", - "test:coverage": "tap test/{unit,integration}/*.js --coverage --coverage-report=lcov" + "test:unit": "tap --reporter nyan test/unit/*.js --statements=97 --branches=97" }, "author": "Scratch Foundation", "license": "BSD-3-Clause", "dependencies": { - "scratch-parser": "4.3.2" + "scratch-parser": "5.0.0" }, "devDependencies": { - "babel-eslint": "^10.0.1", - "eslint": "^5.10.0", - "eslint-config-scratch": "^5.0.0", - "tap": "^12.1.1" + "@babel/eslint-parser": "^7.5.4", + "eslint": "^8.16.0", + "eslint-config-scratch": "^7.0.0", + "tap": "^16.2.0" } } diff --git a/test/fixtures/sb2/cloud.sb2 b/test/fixtures/sb2/cloud.sb2 new file mode 100644 index 0000000..bcab352 Binary files /dev/null and b/test/fixtures/sb2/cloud.sb2 differ diff --git a/test/fixtures/sb2/cloud_complex.sb2 b/test/fixtures/sb2/cloud_complex.sb2 new file mode 100644 index 0000000..976474e Binary files /dev/null and b/test/fixtures/sb2/cloud_complex.sb2 differ diff --git a/test/fixtures/sb2/cloud_opcodes.sb2 b/test/fixtures/sb2/cloud_opcodes.sb2 new file mode 100644 index 0000000..da1ab6f Binary files /dev/null and b/test/fixtures/sb2/cloud_opcodes.sb2 differ diff --git a/test/fixtures/sb3/badExtensions.json b/test/fixtures/sb3/badExtensions.json new file mode 100644 index 0000000..98a679a --- /dev/null +++ b/test/fixtures/sb3/badExtensions.json @@ -0,0 +1,103 @@ +{ + "targets": [ + { + "isStage": true, + "name": "Stage", + "variables": { + "`jEk@4|i[#Fk?(8x)AV.-my variable": [ + "my variable", + 0 + ] + }, + "lists": {}, + "broadcasts": {}, + "blocks": {}, + "comments": {}, + "currentCostume": 0, + "costumes": [ + { + "assetId": "cd21514d0531fdffb22204e0ec5ed84a", + "name": "backdrop1", + "md5ext": "cd21514d0531fdffb22204e0ec5ed84a.svg", + "dataFormat": "svg", + "rotationCenterX": 240, + "rotationCenterY": 180 + } + ], + "sounds": [ + { + "assetId": "83a9787d4cb6f3b7632b4ddfebf74367", + "name": "pop", + "dataFormat": "wav", + "format": "", + "rate": 44100, + "sampleCount": 1032, + "md5ext": "83a9787d4cb6f3b7632b4ddfebf74367.wav" + } + ], + "volume": 100, + "layerOrder": 0, + "tempo": 60, + "videoTransparency": 50, + "videoState": "on", + "textToSpeechLanguage": null + }, + { + "isStage": false, + "name": "Sprite1", + "variables": {}, + "lists": {}, + "broadcasts": {}, + "blocks": {}, + "comments": {}, + "currentCostume": 0, + "costumes": [ + { + "assetId": "b7853f557e4426412e64bb3da6531a99", + "name": "costume1", + "bitmapResolution": 1, + "md5ext": "b7853f557e4426412e64bb3da6531a99.svg", + "dataFormat": "svg", + "rotationCenterX": 48, + "rotationCenterY": 50 + }, + { + "assetId": "e6ddc55a6ddd9cc9d84fe0b4c21e016f", + "name": "costume2", + "bitmapResolution": 1, + "md5ext": "e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg", + "dataFormat": "svg", + "rotationCenterX": 46, + "rotationCenterY": 53 + } + ], + "sounds": [ + { + "assetId": "83c36d806dc92327b9e7049a565c6bff", + "name": "Meow", + "dataFormat": "wav", + "format": "", + "rate": 44100, + "sampleCount": 37376, + "md5ext": "83c36d806dc92327b9e7049a565c6bff.wav" + } + ], + "volume": 100, + "layerOrder": 1, + "visible": true, + "x": 0, + "y": 0, + "size": 100, + "direction": 90, + "draggable": false, + "rotationStyle": "all around" + } + ], + "monitors": [], + "meta": { + "semver": "3.0.0", + "vm": "0.2.0-prerelease.20181217191056", + "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "origin": "test.scratch.mit.edu" + } +} diff --git a/test/fixtures/sb3/cloud.sb3 b/test/fixtures/sb3/cloud.sb3 new file mode 100644 index 0000000..60d584a Binary files /dev/null and b/test/fixtures/sb3/cloud.sb3 differ diff --git a/test/fixtures/sb3/cloud_complex.sb3 b/test/fixtures/sb3/cloud_complex.sb3 new file mode 100644 index 0000000..c6522c9 Binary files /dev/null and b/test/fixtures/sb3/cloud_complex.sb3 differ diff --git a/test/fixtures/sb3/cloud_opcodes.sb3 b/test/fixtures/sb3/cloud_opcodes.sb3 new file mode 100644 index 0000000..77e2158 Binary files /dev/null and b/test/fixtures/sb3/cloud_opcodes.sb3 differ diff --git a/test/fixtures/sb3/default.json b/test/fixtures/sb3/default.json index 3235973..3c43b07 100644 --- a/test/fixtures/sb3/default.json +++ b/test/fixtures/sb3/default.json @@ -98,6 +98,7 @@ "meta": { "semver": "3.0.0", "vm": "0.2.0-prerelease.20181217191056", - "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" + "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "origin": "test.scratch.mit.edu" } } diff --git a/test/fixtures/sb3/extensions.sb3 b/test/fixtures/sb3/extensions.sb3 new file mode 100644 index 0000000..a92d9c8 Binary files /dev/null and b/test/fixtures/sb3/extensions.sb3 differ diff --git a/test/unit/cloud.js b/test/unit/cloud.js new file mode 100644 index 0000000..1780982 --- /dev/null +++ b/test/unit/cloud.js @@ -0,0 +1,79 @@ +const fs = require('fs'); +const path = require('path'); +const test = require('tap').test; +const analysis = require('../../lib/index'); + +const sb2 = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb2/cloud.sb2') +); +const sb3 = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb3/cloud.sb3') +); +const sb2Complex = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb2/cloud_complex.sb2') +); +const sb3Complex = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb3/cloud_complex.sb3') +); + +test('sb2', t => { + analysis(sb2, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.cloud, 'object'); + t.equal(result.cloud.count, 1); + t.same(result.cloud.id, ['☁ baz']); + t.end(); + }); +}); + +test('sb3', t => { + analysis(sb3, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.cloud, 'object'); + t.equal(result.cloud.count, 1); + t.same(result.cloud.id, ['☁ baz']); + t.end(); + }); +}); + +test('sb2 complex', t => { + analysis(sb2Complex, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.cloud, 'object'); + t.equal(result.cloud.count, 8); + t.same(result.cloud.id, [ + '☁ Player_1', + '☁ Player_2', + '☁ Player_3', + '☁ Player_4', + '☁ Player_5', + '☁ GameData', + '☁ Player_6', + '☁ SAVE_DATA2' + ]); + t.end(); + }); +}); + +test('sb3 complex', t => { + analysis(sb3Complex, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.cloud, 'object'); + t.equal(result.cloud.count, 8); + t.same(result.cloud.id, [ + '☁ Player_1', + '☁ Player_2', + '☁ Player_3', + '☁ Player_4', + '☁ Player_5', + '☁ GameData', + '☁ Player_6', + '☁ SAVE_DATA2' + ]); + t.end(); + }); +}); diff --git a/test/unit/cloud_opcodes.js b/test/unit/cloud_opcodes.js new file mode 100644 index 0000000..22bc613 --- /dev/null +++ b/test/unit/cloud_opcodes.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); +const test = require('tap').test; +const analysis = require('../../lib/index'); + +const sb2 = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb2/cloud_opcodes.sb2') +); +const sb3 = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb3/cloud_opcodes.sb3') +); + +test('sb2', t => { + analysis(sb2, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.blocks, 'object'); + t.type(result.blocks.id, 'object'); + t.same(result.blocks.id, [ + 'whenGreenFlag', + 'doForever', + 'setVar:to:', + 'randomFrom:to:', + 'changeVar:by:', + 'setVar:to:', + 'randomFrom:to:', + 'changeVar:by:', + 'setVar:to:cloud:', + 'randomFrom:to:', + 'changeVar:by:cloud:', + 'wait:elapsed:from:' + ]); + t.end(); + }); +}); + +test('sb3', t => { + analysis(sb3, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + t.type(result.blocks, 'object'); + t.type(result.blocks.id, 'object'); + t.same(result.blocks.id, [ + 'event_whenflagclicked', + 'control_forever', + 'control_wait', + 'data_setvariableto', + 'data_setvariableto', + 'data_setvariableto_cloud', + 'operator_random', + 'operator_random', + 'operator_random', + 'data_changevariableby', + 'data_changevariableby', + 'data_changevariableby_cloud' + ]); + t.end(); + }); +}); diff --git a/test/unit/sb2.js b/test/unit/sb2.js index 261211c..3c72e29 100644 --- a/test/unit/sb2.js +++ b/test/unit/sb2.js @@ -13,9 +13,9 @@ const complexBinary = fs.readFileSync( path.resolve(__dirname, '../fixtures/sb2/complex.sb2') ); -test('defalt (object)', t => { +test('default (object)', t => { analysis(defaultObject, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -23,34 +23,34 @@ test('defalt (object)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 0); - t.deepEqual(result.variables.id, []); + t.same(result.variables.id, []); t.type(result.lists, 'object'); t.equal(result.lists.count, 0); - t.deepEqual(result.lists.id, []); + t.same(result.lists.id, []); t.type(result.comments, 'object'); t.equal(result.comments.count, 0); t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ '739b5e2a2435f6e1ec2993791b423146.png', '09dc888b0b7df19f70d81588ae73420e.svg', '3696356a03a8d938318876a593572843.svg' @@ -62,20 +62,23 @@ test('defalt (object)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 0); t.equal(result.blocks.unique, 0); - t.deepEqual(result.blocks.id, []); - t.deepEqual(result.blocks.frequency, {}); + t.same(result.blocks.id, []); + t.same(result.blocks.frequency, {}); t.type(result.extensions, 'object'); t.equal(result.extensions.count, 0); - t.deepEqual(result.extensions.id, []); + t.same(result.extensions.id, []); + + t.type(result.meta, 'object'); + t.same(result.meta, {}); t.end(); }); }); -test('defalt (binary)', t => { +test('default (binary)', t => { analysis(defaultBinary, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -83,34 +86,34 @@ test('defalt (binary)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 0); - t.deepEqual(result.variables.id, []); + t.same(result.variables.id, []); t.type(result.lists, 'object'); t.equal(result.lists.count, 0); - t.deepEqual(result.lists.id, []); + t.same(result.lists.id, []); t.type(result.comments, 'object'); t.equal(result.comments.count, 0); t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ '739b5e2a2435f6e1ec2993791b423146.png', 'f9a1c175dbe2e5dee472858dd30d16bb.svg', '6e8bd9ae68fdb02b7e1e3df656a75635.svg' @@ -122,12 +125,12 @@ test('defalt (binary)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 0); t.equal(result.blocks.unique, 0); - t.deepEqual(result.blocks.id, []); - t.deepEqual(result.blocks.frequency, {}); + t.same(result.blocks.id, []); + t.same(result.blocks.frequency, {}); t.type(result.extensions, 'object'); t.equal(result.extensions.count, 0); - t.deepEqual(result.extensions.id, []); + t.same(result.extensions.id, []); t.end(); }); @@ -135,7 +138,7 @@ test('defalt (binary)', t => { test('complex (binary)', t => { analysis(complexBinary, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -143,14 +146,14 @@ test('complex (binary)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 2); - t.deepEqual(result.variables.id, [ + t.same(result.variables.id, [ 'global', 'local' ]); t.type(result.lists, 'object'); t.equal(result.lists.count, 2); - t.deepEqual(result.lists.id, [ + t.same(result.lists.id, [ 'globallist', 'locallist' ]); @@ -160,23 +163,23 @@ test('complex (binary)', t => { t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ '5b465b3b07d39019109d8dc6d6ee6593.svg', 'f9a1c175dbe2e5dee472858dd30d16bb.svg', '6e8bd9ae68fdb02b7e1e3df656a75635.svg' @@ -188,7 +191,7 @@ test('complex (binary)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 34); t.equal(result.blocks.unique, 18); - t.deepEqual(result.blocks.id, [ + t.same(result.blocks.id, [ 'whenGreenFlag', 'doForever', 'changeGraphicEffect:by:', @@ -224,7 +227,7 @@ test('complex (binary)', t => { 'LEGO WeDo 2.0\u001FsetLED', 'randomFrom:to:' ]); - t.deepEqual(result.blocks.frequency, { + t.same(result.blocks.frequency, { 'LEGO WeDo 2.0\u001FsetLED': 1, 'LEGO WeDo 2.0\u001FwhenTilted': 1, 'bounceOffEdge': 1, @@ -247,7 +250,7 @@ test('complex (binary)', t => { t.type(result.extensions, 'object'); t.equal(result.extensions.count, 1); - t.deepEqual(result.extensions.id, [ + t.same(result.extensions.id, [ 'LEGO WeDo 2.0' ]); diff --git a/test/unit/sb3.js b/test/unit/sb3.js index 8edf126..9f0bd17 100644 --- a/test/unit/sb3.js +++ b/test/unit/sb3.js @@ -13,9 +13,17 @@ const complexBinary = fs.readFileSync( path.resolve(__dirname, '../fixtures/sb3/complex.sb3') ); -test('defalt (object)', t => { +const extensionsBinary = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb3/extensions.sb3') +); + +const badExtensions = fs.readFileSync( + path.resolve(__dirname, '../fixtures/sb3/badExtensions.json') +); + +test('default (object)', t => { analysis(defaultObject, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -23,36 +31,36 @@ test('defalt (object)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 1); - t.deepEqual(result.variables.id, [ + t.same(result.variables.id, [ 'my variable' ]); t.type(result.lists, 'object'); t.equal(result.lists.count, 0); - t.deepEqual(result.lists.id, []); + t.same(result.lists.id, []); t.type(result.comments, 'object'); t.equal(result.comments.count, 0); t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'Meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ 'cd21514d0531fdffb22204e0ec5ed84a.svg', 'b7853f557e4426412e64bb3da6531a99.svg', 'e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg' @@ -64,20 +72,22 @@ test('defalt (object)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 0); t.equal(result.blocks.unique, 0); - t.deepEqual(result.blocks.id, []); - t.deepEqual(result.blocks.frequency, {}); + t.same(result.blocks.id, []); + t.same(result.blocks.frequency, {}); t.type(result.extensions, 'object'); t.equal(result.extensions.count, 0); - t.deepEqual(result.extensions.id, []); + t.same(result.extensions.id, []); + t.type(result.meta, 'object'); + t.equal(result.meta.origin, 'test.scratch.mit.edu'); t.end(); }); }); -test('defalt (binary)', t => { +test('default (binary)', t => { analysis(defaultBinary, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -85,36 +95,36 @@ test('defalt (binary)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 1); - t.deepEqual(result.variables.id, [ + t.same(result.variables.id, [ 'my variable' ]); t.type(result.lists, 'object'); t.equal(result.lists.count, 0); - t.deepEqual(result.lists.id, []); + t.same(result.lists.id, []); t.type(result.comments, 'object'); t.equal(result.comments.count, 0); t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'Meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ 'cd21514d0531fdffb22204e0ec5ed84a.svg', 'b7853f557e4426412e64bb3da6531a99.svg', 'e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg' @@ -126,12 +136,15 @@ test('defalt (binary)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 0); t.equal(result.blocks.unique, 0); - t.deepEqual(result.blocks.id, []); - t.deepEqual(result.blocks.frequency, {}); + t.same(result.blocks.id, []); + t.same(result.blocks.frequency, {}); t.type(result.extensions, 'object'); t.equal(result.extensions.count, 0); - t.deepEqual(result.extensions.id, []); + t.same(result.extensions.id, []); + + t.type(result.meta, 'object'); + t.same({}, result.meta); t.end(); }); @@ -139,7 +152,7 @@ test('defalt (binary)', t => { test('complex (binary)', t => { analysis(complexBinary, (err, result) => { - t.true(typeof err === 'undefined' || err === null); + t.ok(typeof err === 'undefined' || err === null); t.type(result, 'object'); t.type(result.scripts, 'object'); @@ -147,14 +160,14 @@ test('complex (binary)', t => { t.type(result.variables, 'object'); t.equal(result.variables.count, 2); - t.deepEqual(result.variables.id, [ + t.same(result.variables.id, [ 'global', 'local' ]); t.type(result.lists, 'object'); t.equal(result.lists.count, 2); - t.deepEqual(result.lists.id, [ + t.same(result.lists.id, [ 'globallist', 'locallist' ]); @@ -164,23 +177,23 @@ test('complex (binary)', t => { t.type(result.sounds, 'object'); t.equal(result.sounds.count, 2); - t.deepEqual(result.sounds.id, [ + t.same(result.sounds.id, [ 'pop', 'meow' ]); - t.deepEqual(result.sounds.hash, [ + t.same(result.sounds.hash, [ '83a9787d4cb6f3b7632b4ddfebf74367.wav', '83c36d806dc92327b9e7049a565c6bff.wav' ]); t.type(result.costumes, 'object'); t.equal(result.costumes.count, 3); - t.deepEqual(result.costumes.id, [ + t.same(result.costumes.id, [ 'backdrop1', 'costume1', 'costume2' ]); - t.deepEqual(result.costumes.hash, [ + t.same(result.costumes.hash, [ '7633d36de03d1df75808f581bbccc742.svg', 'e6bcb4046c157f60c9f5c3bb5f299fce.svg', '64208764c777be25d34d813dc0b743c7.svg' @@ -192,7 +205,7 @@ test('complex (binary)', t => { t.type(result.blocks, 'object'); t.equal(result.blocks.count, 34); t.equal(result.blocks.unique, 18); - t.deepEqual(result.blocks.id, [ + t.same(result.blocks.id, [ 'event_whenflagclicked', 'control_forever', 'looks_changeeffectby', @@ -228,7 +241,7 @@ test('complex (binary)', t => { 'wedo2_setLightHue', 'operator_random' ]); - t.deepEqual(result.blocks.frequency, { + t.same(result.blocks.frequency, { argument_reporter_string_number: 4, control_forever: 4, data_addtolist: 2, @@ -251,10 +264,90 @@ test('complex (binary)', t => { t.type(result.extensions, 'object'); t.equal(result.extensions.count, 1); - t.deepEqual(result.extensions.id, [ + t.same(result.extensions.id, [ 'wedo2' ]); t.end(); }); }); + +test('extensions', t => { + analysis(extensionsBinary, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + + t.type(result.extensions, 'object'); + t.equal(result.extensions.count, 2); + t.same(result.extensions.id, [ + 'translate', + 'text2speech' + ]); + + t.end(); + }); +}); + +test('regression test IBE-198, a bad list does not break library', t => { + analysis(badExtensions, (err, result) => { + t.ok(typeof err === 'undefined' || err === null); + t.type(result, 'object'); + + t.type(result.scripts, 'object'); + t.equal(result.scripts.count, 0); + + t.type(result.variables, 'object'); + t.equal(result.variables.count, 1); + t.same(result.variables.id, [ + 'my variable' + ]); + + t.type(result.lists, 'object'); + t.equal(result.lists.count, 0); + t.same(result.lists.id, []); + + t.type(result.comments, 'object'); + t.equal(result.comments.count, 0); + + t.type(result.sounds, 'object'); + t.equal(result.sounds.count, 2); + t.same(result.sounds.id, [ + 'pop', + 'Meow' + ]); + t.same(result.sounds.hash, [ + '83a9787d4cb6f3b7632b4ddfebf74367.wav', + '83c36d806dc92327b9e7049a565c6bff.wav' + ]); + + t.type(result.costumes, 'object'); + t.equal(result.costumes.count, 3); + t.same(result.costumes.id, [ + 'backdrop1', + 'costume1', + 'costume2' + ]); + t.same(result.costumes.hash, [ + 'cd21514d0531fdffb22204e0ec5ed84a.svg', + 'b7853f557e4426412e64bb3da6531a99.svg', + 'e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg' + ]); + + t.type(result.sprites, 'object'); + t.equal(result.sprites.count, 1); + + t.type(result.blocks, 'object'); + t.equal(result.blocks.count, 0); + t.equal(result.blocks.unique, 0); + t.same(result.blocks.id, []); + t.same(result.blocks.frequency, {}); + + t.type(result.extensions, 'object'); + t.equal(result.extensions.count, 0); + t.same(result.extensions.id, []); + + t.type(result.meta, 'object'); + t.equal(result.meta.origin, 'test.scratch.mit.edu'); + t.end(); + }); +}); diff --git a/test/unit/utility.js b/test/unit/utility.js index ee220bd..70b454d 100644 --- a/test/unit/utility.js +++ b/test/unit/utility.js @@ -10,7 +10,7 @@ test('spec', t => { test('frequency', t => { const input = ['foo', 'foo', 'foo', 'bar', 'bar', 'baz']; const result = utility.frequency(input); - t.deepEqual(result, { + t.same(result, { foo: 3, bar: 2, baz: 1