Merge pull request #60 from LLK/develop

Release 2022-05-27
This commit is contained in:
Colby Gutierrez-Kraybill 2022-05-27 16:12:51 -04:00 committed by GitHub
commit 1db9e69c90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 591 additions and 84 deletions

View file

@ -1,3 +1,7 @@
{
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false
},
"extends": ["scratch", "scratch/node"]
}

21
.travis.yml Normal file
View file

@ -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

8
Dockerfile Normal file
View file

@ -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

View file

@ -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` |

1
TRADEMARK Normal file
View file

@ -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.

30
docker-compose.yml Normal file
View file

@ -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"

View file

@ -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);
};

View file

@ -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);

View file

@ -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"
}
}

BIN
test/fixtures/sb2/cloud.sb2 vendored Normal file

Binary file not shown.

BIN
test/fixtures/sb2/cloud_complex.sb2 vendored Normal file

Binary file not shown.

BIN
test/fixtures/sb2/cloud_opcodes.sb2 vendored Normal file

Binary file not shown.

103
test/fixtures/sb3/badExtensions.json vendored Normal file
View file

@ -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"
}
}

BIN
test/fixtures/sb3/cloud.sb3 vendored Normal file

Binary file not shown.

BIN
test/fixtures/sb3/cloud_complex.sb3 vendored Normal file

Binary file not shown.

BIN
test/fixtures/sb3/cloud_opcodes.sb3 vendored Normal file

Binary file not shown.

View file

@ -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"
}
}

BIN
test/fixtures/sb3/extensions.sb3 vendored Normal file

Binary file not shown.

79
test/unit/cloud.js Normal file
View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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'
]);

View file

@ -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();
});
});

View file

@ -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