Initial working version

This commit is contained in:
Andrew Sliwinski 2018-12-17 18:11:08 -05:00
parent 4f28eeef52
commit 590b71a0aa
22 changed files with 1414 additions and 0 deletions

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
/node_modules
/.nyc_output
/coverage

3
.eslintrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": ["scratch", "scratch/node"]
}

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
## OSX
.DS_Store
## NPM
/node_modules
npm-*
package-lock.json
## Code Coverage
.nyc_output/
coverage/

45
README.md Normal file
View file

@ -0,0 +1,45 @@
## scratch-analysis
#### Analysis tool for summarizing the structure, composition, and complexity of [Scratch](https://scratch.mit.edu) programs.
## Getting Started
```bash
npm install scratch-analysis
```
```js
const analysis = require('scratch-analysis');
analysis(buffer, function (err, result) {
// handle any validation errors and ...
// do something interesting with the results!
});
```
## Analysis Modules
### General
The `scratch-analysis` module will return an object containing high-level summary information about the project:
| Key | Attributes |
| ----------------- | -------------------------------------------------------- |
| `scripts` | `count` |
| `blocks` | `count`, `unique`, `list`, `frequency` |
| `sprites` | `count` |
| `variables` | `count` |
| `lists` | `count` |
| `costumes` | `count`, `list`, `hash` |
| `sounds` | `count`, `list`, `hash` |
| `extensions` | `count`, `list` |
| `comments` | `count` |
### Concepts
**Coming Soon**
### Complexity
**Coming Soon**
### Classification
**Coming Soon**
## References
### New Frameworks for Studying and Assessing the Development of Computational Thinking
Author(s): Karen Brennan, Mitchel Resnick
PDF: [Download](https://web.media.mit.edu/~kbrennan/files/Brennan_Resnick_AERA2012_CT.pdf)

201
analyze.js Normal file
View file

@ -0,0 +1,201 @@
/**
* Returns an array of items matching the specified attribute.
* @param {object} project JSON representation of project
* @param {string} attribute Attribute to extract and flatten
* @return {Array} Array of items matching attribute
*/
const flatten = function (project, attribute) {
// Storage object
let result = [];
// If attribute exists at the top level of the project, append it
if (typeof project[attribute] !== 'undefined') {
result = project[attribute];
}
// Iterate over child elements and append to result array
for (var i in project.children) {
var child = project.children[i];
if (typeof child[attribute] !== 'undefined') {
result = result.concat(child[attribute]);
}
}
return result;
};
/**
* Extract summary information from a specific project attribute. Will attempt
* to concatinate all children when generating summary.
* @param {object} project JSON representation of project
* @param {string} attribute Attribute key
* @param {string} id "id" key
* @param {string} hash "hash" key
* @return {object} Summary
*/
const extract = function (project, attribute, id, hash) {
// Create storage objects and flatten project
var idList = null;
var hashList = null;
var elements = flatten(project, attribute);
// Extract ids if specified
if (typeof id !== 'undefined') {
idList = [];
for (var x in elements) {
idList.push(elements[x][id]);
}
}
// Extract hashes if specified
if (typeof hash !== 'undefined') {
hashList = [];
for (var y in elements) {
hashList.push(elements[y][hash]);
}
}
// Build result and return
var result = {
count: elements.length
};
if (idList !== null) result.id = idList;
if (hashList !== null) result.hash = hashList;
return result;
};
/**
* Extract number of sprites from a project object. Will attempt to ignore
* "children" which are not sprites.
* @param {object} input Project object
* @return {object} Sprite information
*/
const sprites = function (input) {
let result = 0;
for (var i in input.children) {
if (input.children[i].hasOwnProperty('spriteInfo')) result++;
}
return {count: result};
};
/**
* Tallys term frequency from an array of strings.
* @param {array} input Array of strings
* @return {object} Hash of unique strings with frequency values
*/
const frequency = function (input) {
var result = Object.create(null);
for (var i in input) {
var term = input[i];
if (typeof result[term] === 'undefined') result[term] = 0;
result[term]++;
}
return result;
};
/**
* Extract blocks and generate frequency count.
* @param {object} project JSON representation of project
* @return {object} All blocks in the projects with frequency counts
*/
const blocks = function (project) {
// Storage objects
var result = [];
/**
* Walk scripts array(s) and build block list.
* @param {array} stack Stack of blocks
* @return {void}
*/
var walk = function (stack) {
for (var i in stack) {
// Skip if item is not array
if (!Array.isArray(stack[i])) continue;
// Recurse if first item is not a string
if (typeof stack[i][0] !== 'string') {
walk(stack[i]);
continue;
}
// Add to block list
result.push(stack[i][0]);
// Don't pull in params from procedures
if (stack[i][0] === 'procDef') continue;
// Move to next item and walk
walk(stack[i].slice(1));
}
};
walk(flatten(project, 'scripts'));
// Generate frequency count
var freq = frequency(result);
// Build result and return
return {
count: result.length,
unique: Object.keys(freq).length,
id: result,
frequency: freq
};
};
/**
* Extract extensions list.
* @param {object} project JSON representation of project
* @return {object} List of used extensions
*/
const extensions = function (project) {
var result = {count: 0, id: []};
// Check to ensure project includes any extensions
if (typeof project.info.savedExtensions === 'undefined') return result;
// Iterate over extensions and build list
var ext = project.info.savedExtensions;
for (var i in ext) {
result.id.push(ext[i].extensionName);
}
// Count and return
result.count = result.id.length;
return result;
};
/**
* Analyzes project and appends metadata to the project object (_meta).
* @param {object} project JSON representation of project
* @param {Function} callback Error and project JSON with metadata attached.
* @return {void}
*/
module.exports = function (project, callback) {
// Create metadata object
var meta = {
scripts: extract(project, 'scripts'),
variables: extract(project, 'variables', 'name'),
lists: extract(project, 'lists', 'listName'),
comments: extract(project, 'scriptComments'),
sounds: extract(project, 'sounds', 'soundName', 'md5'),
costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5')
};
// Sprites
meta.sprites = sprites(project);
// Blocks
meta.blocks = blocks(project);
// Extensions
meta.extensions = extensions(project);
// Bind metadata to project and return
project._meta = meta;
callback(null, project);
};

25
lib/index.js Normal file
View file

@ -0,0 +1,25 @@
const parser = require('scratch-parser');
const sb2 = require('./sb2');
const sb3 = require('./sb3');
module.exports = function (buffer, callback) {
parser(buffer, false, (err, project) => {
if (err) return callback(err);
// Flatten array
project = project[0];
// Push project object to the appropriate analysis handler
switch (project.projectVersion) {
case 2:
sb2(project, callback);
break;
case 3:
sb3(project, callback);
break;
default:
throw new Error('Unsupported project version');
}
});
};

200
lib/sb2.js Normal file
View file

@ -0,0 +1,200 @@
/**
* Returns an array of items matching the specified attribute.
* @param {object} project Project object (SB2 format)
* @param {string} attribute Attribute to extract and flatten
* @return {array} Array of specified attribute
*/
const flatten = function (project, attribute) {
// Storage object
let result = [];
// If attribute exists at the top level of the project, append it
if (typeof project[attribute] !== 'undefined') {
result = project[attribute];
}
// Iterate over child elements and append to result array
for (let i in project.children) {
const child = project.children[i];
if (typeof child[attribute] !== 'undefined') {
result = result.concat(child[attribute]);
}
}
return result;
};
/**
* Extract summary information from a specific project attribute. Will attempt
* to concatinate all children when generating summary.
* @param {object} project Project object (SB2 format)
* @param {string} attribute Attribute key
* @param {string} id "id" key
* @param {string} hash "hash" key
* @return {object} Summary information
*/
const extract = function (project, attribute, id, hash) {
// Create storage objects and flatten project
let idList = null;
let hashList = null;
let elements = flatten(project, attribute);
// Extract ids if specified
if (typeof id !== 'undefined') {
idList = [];
for (var x in elements) {
idList.push(elements[x][id]);
}
}
// Extract hashes if specified
if (typeof hash !== 'undefined') {
hashList = [];
for (var y in elements) {
hashList.push(elements[y][hash]);
}
}
// Build result and return
var result = {
count: elements.length
};
if (idList !== null) result.id = idList;
if (hashList !== null) result.hash = hashList;
return result;
};
/**
* Extract number of sprites from a project object. Will attempt to ignore
* "children" which are not sprites.
* @param {object} input Project object (SB2 format)
* @return {object} Sprite information
*/
const sprites = function (input) {
let result = 0;
for (let i in input.children) {
if (input.children[i].hasOwnProperty('spriteInfo')) result++;
}
return {count: result};
};
/**
* Tallys term frequency from an array of strings.
* @param {array} input Array of strings
* @return {object} Frequency information
*/
const frequency = function (input) {
const result = Object.create(null);
for (let i in input) {
var term = input[i];
if (typeof result[term] === 'undefined') result[term] = 0;
result[term]++;
}
return result;
};
/**
* Extracts all blocks and generates a frequency count.
* @param {object} project Project object (SB2 format)
* @return {object} Block information
*/
const blocks = function (project) {
// Storage objects
const result = [];
/**
* Walk scripts array(s) and build block list.
* @param {array} stack Stack of blocks
* @return {void}
*/
const walk = function (stack) {
for (let i in stack) {
// Skip if item is not array
if (!Array.isArray(stack[i])) continue;
// Recurse if first item is not a string
if (typeof stack[i][0] !== 'string') {
walk(stack[i]);
continue;
}
// Add to block list
result.push(stack[i][0]);
// Don't pull in params from procedures
if (stack[i][0] === 'procDef') continue;
// Move to next item and walk
walk(stack[i].slice(1));
}
};
walk(flatten(project, 'scripts'));
// Generate frequency count
const freq = frequency(result);
// Build result and return
return {
count: result.length,
unique: Object.keys(freq).length,
id: result,
frequency: freq
};
};
/**
* Extracts extension information.
* @param {object} project Project object (SB2 format)
* @return {object} Extension information
*/
const extensions = function (project) {
const result = {count: 0, id: []};
// Check to ensure project includes any extensions
if (typeof project.info.savedExtensions === 'undefined') return result;
// Iterate over extensions and build list
var ext = project.info.savedExtensions;
for (let i in ext) {
result.id.push(ext[i].extensionName);
}
// Count and return
result.count = result.id.length;
return result;
};
/**
* Analyzes a project and returns summary information about the project.
* @param {object} project Project object (SB2 format)
* @param {Function} callback Callback function
* @return {void}
*/
module.exports = function (project, callback) {
// Create metadata object
const meta = {
scripts: extract(project, 'scripts'),
variables: extract(project, 'variables', 'name'),
lists: extract(project, 'lists', 'listName'),
comments: extract(project, 'scriptComments'),
sounds: extract(project, 'sounds', 'soundName', 'md5'),
costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5')
};
// Sprites
meta.sprites = sprites(project);
// Blocks
meta.blocks = blocks(project);
// Extensions
meta.extensions = extensions(project);
// Return all metadata
return callback(null, meta);
};

110
lib/sb3.js Normal file
View file

@ -0,0 +1,110 @@
const utility = require('./utility');
const scripts = function (targets) {
// Storage objects
let occurances = 0;
// Iterate over all blocks in each target, and look for "top level" blocks
for (let t in targets) {
for (let b in targets[t].blocks) {
if (targets[t].blocks[b].topLevel) occurances++;
}
};
return {
count: occurances
};
};
const variables = function (targets, attribute) {
// Storage objects
let occurances = 0;
let idList = [];
for (let t in targets) {
for (let a in targets[t][attribute]) {
const variable = targets[t][attribute][a];
occurances++;
idList.push(variable[0]);
}
};
return {
count: occurances,
id: idList
};
};
// Iterate over targets, extract attribute, and aggregate results
const extract = function (targets, attribute, id, hash) {
// Storage objects
let occurances = 0;
let idList = [];
let hashList = [];
for (let t in targets) {
for (let a in targets[t][attribute]) {
const asset = targets[t][attribute][a];
occurances++;
if (typeof id !== 'undefined') idList.push(asset[id]);
if (typeof hash !== 'undefined') hashList.push(asset[hash]);
}
};
const result = {count: occurances};
if (typeof id !== 'undefined') result.id = idList;
if (typeof hash !== 'undefined') result.hash = hashList;
return result;
};
const sprites = function (targets) {
return {
count: targets.length - 1
};
}
const blocks = function (targets) {
// Storage object
let result = [];
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);
}
}
// Calculate block frequency
const freq = utility.frequency(result);
// Return summary
return {
count: result.length,
unique: Object.keys(freq).length,
id: result,
frequency: freq
};
};
const extensions = function (extensions) {
return {
count: extensions.length,
id: extensions
};
};
module.exports = function (project, callback) {
const meta = {
scripts: scripts(project.targets),
variables: variables(project.targets, 'variables'),
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)
};
callback(null, meta);
};

23
lib/utility.js Normal file
View file

@ -0,0 +1,23 @@
/**
* Utility methods for costructing Scratch project summaries.
*/
class Utility {
/**
* Tallys term frequency from an array of strings.
* @param {array} input Array of strings
* @return {object} Frequency information
*/
static frequency (input) {
const result = Object.create(null);
for (let i in input) {
var term = input[i];
if (typeof result[term] === 'undefined') result[term] = 0;
result[term]++;
}
return result;
};
}
module.exports = Utility;

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "scratch-analysis",
"version": "1.0.0",
"description": "Analysis tool for summarizing the structure, composition, and complexity of Scratch programs.",
"main": "lib/index.js",
"directories": {
"bin": "bin",
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "npm run test:lint && npm run test:unit && npm run test:integration",
"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"
},
"author": "Scratch Foundation",
"license": "BSD-3-Clause",
"dependencies": {
"scratch-parser": "4.3.2",
"yargs": "12.0.5"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.10.0",
"eslint-config-scratch": "^5.0.0",
"tap": "^12.1.1"
}
}

42
test/analyze.js Normal file
View file

@ -0,0 +1,42 @@
var test = require('tap').test;
var data = require('../fixtures/data');
var analyze = require('../../lib/analyze');
test('spec', function (t) {
t.type(analyze, 'function');
t.end();
});
test('empty project', function (t) {
t.end();
});
test('example project', function (t) {
analyze(JSON.parse(data.example.json.toString()), function (err, res) {
t.equal(err, null);
t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res._meta.sprites, 'object');
t.type(res._meta.scripts, 'object');
t.type(res._meta.variables, 'object');
t.type(res._meta.lists, 'object');
t.type(res._meta.comments, 'object');
t.type(res._meta.sounds, 'object');
t.type(res._meta.costumes, 'object');
t.type(res._meta.blocks, 'object');
t.type(res._meta.extensions, 'object');
t.equal(res._meta.sprites.count, 2, 'expected number of sprites');
t.equal(res._meta.scripts.count, 5, 'expected number of scripts');
t.equal(res._meta.variables.count, 2, 'expected number of variables');
t.equal(res._meta.lists.count, 2, 'expected number of lists');
t.equal(res._meta.comments.count, 1, 'expected number of comments');
t.equal(res._meta.sounds.count, 4, 'expected number of sounds');
t.equal(res._meta.costumes.count, 16, 'expected number of costumes');
t.equal(res._meta.blocks.count, 16, 'expected number of blocks');
t.equal(res._meta.blocks.unique, 11, 'exepected number of blocks');
t.equal(res._meta.extensions.count, 1, 'expected number of extensions');
t.end();
});
});

BIN
test/fixtures/invalid/garbage.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

Binary file not shown.

71
test/fixtures/sb2/default.json vendored Normal file
View file

@ -0,0 +1,71 @@
{
"objName": "Stage",
"sounds": [{
"soundName": "pop",
"soundID": -1,
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
"sampleCount": 258,
"rate": 11025,
"format": ""
}],
"costumes": [{
"costumeName": "backdrop1",
"baseLayerID": -1,
"baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png",
"bitmapResolution": 1,
"rotationCenterX": 240,
"rotationCenterY": 180
}],
"currentCostumeIndex": 0,
"penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png",
"penLayerID": -1,
"tempoBPM": 60,
"videoAlpha": 0.5,
"children": [{
"objName": "Sprite1",
"sounds": [{
"soundName": "meow",
"soundID": -1,
"md5": "83c36d806dc92327b9e7049a565c6bff.wav",
"sampleCount": 18688,
"rate": 22050,
"format": ""
}],
"costumes": [{
"costumeName": "costume1",
"baseLayerID": -1,
"baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg",
"bitmapResolution": 1,
"rotationCenterX": 47,
"rotationCenterY": 55
},
{
"costumeName": "costume2",
"baseLayerID": -1,
"baseLayerMD5": "3696356a03a8d938318876a593572843.svg",
"bitmapResolution": 1,
"rotationCenterX": 47,
"rotationCenterY": 55
}],
"currentCostumeIndex": 0,
"scratchX": 0,
"scratchY": 0,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
"isDraggable": false,
"indexInLibrary": 1,
"visible": true,
"spriteInfo": {
}
}],
"info": {
"videoOn": false,
"userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/50.0.2661.102 Safari\/537.36",
"swfVersion": "v446",
"scriptCount": 0,
"spriteCount": 1,
"hasCloudData": false,
"flashVersion": "MAC 21,0,0,242"
}
}

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

Binary file not shown.

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

Binary file not shown.

103
test/fixtures/sb3/default.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": [],
"extensions": [],
"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"
}
}

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

Binary file not shown.

24
test/unit/error.js Normal file
View file

@ -0,0 +1,24 @@
const fs = require('fs');
const path = require('path');
const test = require('tap').test;
const analysis = require('../../lib/index');
const invalidBinary = fs.readFileSync(
path.resolve(__dirname, '../fixtures/invalid/garbage.jpg')
);
test('invalid object', t => {
analysis('{}', (err, result) => {
t.type(err, 'object');
t.type(result, 'undefined');
t.end();
});
});
test('invalid binary', t => {
analysis(invalidBinary, (err, result) => {
t.type(err, 'string');
t.type(result, 'undefined');
t.end();
});
});

256
test/unit/sb2.js Normal file
View file

@ -0,0 +1,256 @@
const fs = require('fs');
const path = require('path');
const test = require('tap').test;
const analysis = require('../../lib/index');
const defaultObject = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb2/default.json')
);
const defaultBinary = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb2/default.sb2')
);
const complexBinary = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb2/complex.sb2')
);
test('defalt (object)', t => {
analysis(defaultObject, (err, result) => {
t.true(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, 0);
t.deepEqual(result.variables.id, []);
t.type(result.lists, 'object');
t.equal(result.lists.count, 0);
t.deepEqual(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, [
'pop',
'meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(result.costumes.hash, [
'739b5e2a2435f6e1ec2993791b423146.png',
'09dc888b0b7df19f70d81588ae73420e.svg',
'3696356a03a8d938318876a593572843.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.deepEqual(result.blocks.id, []);
t.deepEqual(result.blocks.frequency, {});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 0);
t.deepEqual(result.extensions.id, []);
t.end();
});
});
test('defalt (binary)', t => {
analysis(defaultBinary, (err, result) => {
t.true(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, 0);
t.deepEqual(result.variables.id, []);
t.type(result.lists, 'object');
t.equal(result.lists.count, 0);
t.deepEqual(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, [
'pop',
'meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(result.costumes.hash, [
'739b5e2a2435f6e1ec2993791b423146.png',
'f9a1c175dbe2e5dee472858dd30d16bb.svg',
'6e8bd9ae68fdb02b7e1e3df656a75635.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.deepEqual(result.blocks.id, []);
t.deepEqual(result.blocks.frequency, {});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 0);
t.deepEqual(result.extensions.id, []);
t.end();
});
});
test('complex (binary)', t => {
analysis(complexBinary, (err, result) => {
t.true(typeof err === 'undefined' || err === null);
t.type(result, 'object');
t.type(result.scripts, 'object');
t.equal(result.scripts.count, 6);
t.type(result.variables, 'object');
t.equal(result.variables.count, 2);
t.deepEqual(result.variables.id, [
'global',
'local'
]);
t.type(result.lists, 'object');
t.equal(result.lists.count, 2);
t.deepEqual(result.lists.id, [
'globallist',
'locallist'
]);
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, [
'pop',
'meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(result.costumes.hash, [
'5b465b3b07d39019109d8dc6d6ee6593.svg',
'f9a1c175dbe2e5dee472858dd30d16bb.svg',
'6e8bd9ae68fdb02b7e1e3df656a75635.svg'
]);
t.type(result.sprites, 'object');
t.equal(result.sprites.count, 1);
t.type(result.blocks, 'object');
t.equal(result.blocks.count, 34);
t.equal(result.blocks.unique, 18);
t.deepEqual(result.blocks.id, [
"whenGreenFlag",
"doForever",
"changeGraphicEffect:by:",
"whenGreenFlag",
"deleteLine:ofList:",
"deleteLine:ofList:",
"doForever",
"forward:",
"turnRight:",
"randomFrom:to:",
"bounceOffEdge",
"whenGreenFlag",
"doForever",
"setGraphicEffect:to:",
"xpos",
"whenGreenFlag",
"doForever",
"call",
"randomFrom:to:",
"heading",
"randomFrom:to:",
"heading",
"procDef",
"setVar:to:",
"getParam",
"setVar:to:",
"getParam",
"append:toList:",
"getParam",
"append:toList:",
"getParam",
"LEGO WeDo 2.0\u001FwhenTilted",
"LEGO WeDo 2.0\u001FsetLED",
"randomFrom:to:"
]);
t.deepEqual(result.blocks.frequency, {
"LEGO WeDo 2.0\u001FsetLED": 1,
"LEGO WeDo 2.0\u001FwhenTilted": 1,
"bounceOffEdge": 1,
"call": 1,
"changeGraphicEffect:by:": 1,
"doForever": 4,
"deleteLine:ofList:": 2,
"forward:": 1,
"getParam": 4,
"heading": 2,
"procDef": 1,
"append:toList:": 2,
"randomFrom:to:": 4,
"setGraphicEffect:to:": 1,
"setVar:to:": 2,
"turnRight:": 1,
"whenGreenFlag": 4,
"xpos": 1
});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 1);
t.deepEqual(result.extensions.id, [
'LEGO WeDo 2.0'
]);
t.end();
});
});

260
test/unit/sb3.js Normal file
View file

@ -0,0 +1,260 @@
const fs = require('fs');
const path = require('path');
const test = require('tap').test;
const analysis = require('../../lib/index');
const defaultObject = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb3/default.json')
);
const defaultBinary = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb3/default.sb3')
);
const complexBinary = fs.readFileSync(
path.resolve(__dirname, '../fixtures/sb3/complex.sb3')
);
test('defalt (object)', t => {
analysis(defaultObject, (err, result) => {
t.true(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.deepEqual(result.variables.id, [
'my variable'
]);
t.type(result.lists, 'object');
t.equal(result.lists.count, 0);
t.deepEqual(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, [
'pop',
'Meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(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.deepEqual(result.blocks.id, []);
t.deepEqual(result.blocks.frequency, {});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 0);
t.deepEqual(result.extensions.id, []);
t.end();
});
});
test('defalt (binary)', t => {
analysis(defaultBinary, (err, result) => {
t.true(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.deepEqual(result.variables.id, [
'my variable'
]);
t.type(result.lists, 'object');
t.equal(result.lists.count, 0);
t.deepEqual(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, [
'pop',
'Meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(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.deepEqual(result.blocks.id, []);
t.deepEqual(result.blocks.frequency, {});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 0);
t.deepEqual(result.extensions.id, []);
t.end();
});
});
test('complex (binary)', t => {
analysis(complexBinary, (err, result) => {
t.true(typeof err === 'undefined' || err === null);
t.type(result, 'object');
t.type(result.scripts, 'object');
t.equal(result.scripts.count, 6);
t.type(result.variables, 'object');
t.equal(result.variables.count, 2);
t.deepEqual(result.variables.id, [
'global',
'local'
]);
t.type(result.lists, 'object');
t.equal(result.lists.count, 2);
t.deepEqual(result.lists.id, [
'globallist',
'locallist'
]);
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, [
'pop',
'meow'
]);
t.deepEqual(result.sounds.hash, [
'83a9787d4cb6f3b7632b4ddfebf74367.wav',
'83c36d806dc92327b9e7049a565c6bff.wav'
]);
t.type(result.costumes, 'object');
t.equal(result.costumes.count, 3);
t.deepEqual(result.costumes.id, [
'backdrop1',
'costume1',
'costume2'
]);
t.deepEqual(result.costumes.hash, [
'7633d36de03d1df75808f581bbccc742.svg',
'e6bcb4046c157f60c9f5c3bb5f299fce.svg',
'64208764c777be25d34d813dc0b743c7.svg'
]);
t.type(result.sprites, 'object');
t.equal(result.sprites.count, 1);
t.type(result.blocks, 'object');
t.equal(result.blocks.count, 34);
t.equal(result.blocks.unique, 18);
t.deepEqual(result.blocks.id, [
"event_whenflagclicked",
"control_forever",
"looks_changeeffectby",
"event_whenflagclicked",
"data_deleteoflist",
"data_deleteoflist",
"control_forever",
"motion_movesteps",
"motion_turnright",
"operator_random",
"motion_ifonedgebounce",
"event_whenflagclicked",
"control_forever",
"looks_seteffectto",
"motion_xposition",
"event_whenflagclicked",
"control_forever",
"procedures_call",
"operator_random",
"motion_direction",
"operator_random",
"motion_direction",
"procedures_definition",
"data_setvariableto",
"argument_reporter_string_number",
"data_setvariableto",
"argument_reporter_string_number",
"data_addtolist",
"argument_reporter_string_number",
"data_addtolist",
"argument_reporter_string_number",
"wedo2_whenTilted",
"wedo2_setLightHue",
"operator_random"
]);
t.deepEqual(result.blocks.frequency, {
"argument_reporter_string_number": 4,
"control_forever": 4,
"data_addtolist": 2,
"data_deleteoflist": 2,
"data_setvariableto": 2,
"event_whenflagclicked": 4,
"looks_changeeffectby": 1,
"looks_seteffectto": 1,
"motion_direction": 2,
"motion_ifonedgebounce": 1,
"motion_movesteps": 1,
"motion_turnright": 1,
"motion_xposition": 1,
"operator_random": 4,
"procedures_call": 1,
"procedures_definition": 1,
"wedo2_setLightHue": 1,
"wedo2_whenTilted": 1
});
t.type(result.extensions, 'object');
t.equal(result.extensions.count, 1);
t.deepEqual(result.extensions.id, [
'wedo2'
]);
t.end();
});
});

7
test/unit/spec.js Normal file
View file

@ -0,0 +1,7 @@
const test = require('tap').test;
const analysis = require('../../lib/index');
test('spec', t => {
t.type(analysis, 'function');
t.end();
});