Merge pull request #1 from thisandagain/develop

Initial version
This commit is contained in:
Andrew Sliwinski 2018-12-18 12:53:35 -05:00 committed by GitHub
commit 50d9f834b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1171 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)

23
lib/index.js Normal file
View file

@ -0,0 +1,23 @@
const parser = require('scratch-parser');
const sb2 = require('./sb2');
const sb3 = require('./sb3');
module.exports = function (buffer, callback) {
parser(buffer, false, (err, result) => {
if (err) return callback(err);
// Extract only the project object from the parser results
const project = result[0];
// Push project object to the appropriate analysis handler
switch (project.projectVersion) {
case 2:
sb2(project, callback);
break;
case 3:
sb3(project, callback);
break;
}
});
};

185
lib/sb2.js Normal file
View file

@ -0,0 +1,185 @@
const utility = require('./utility');
/**
* 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};
};
/**
* 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 = utility.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: []};
const ext = project.info.savedExtensions;
// Check to ensure project includes any extensions
if (typeof ext === 'undefined') return result;
// Iterate over extensions and build list
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 occurrences = 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) occurrences++;
}
}
return {
count: occurrences
};
};
const variables = function (targets, attribute) {
// Storage objects
let occurrences = 0;
let idList = [];
for (let t in targets) {
for (let a in targets[t][attribute]) {
const variable = targets[t][attribute][a];
occurrences++;
idList.push(variable[0]);
}
}
return {
count: occurrences,
id: idList
};
};
// Iterate over targets, extract attribute, and aggregate results
const extract = function (targets, attribute, id, hash) {
// Storage objects
let occurrences = 0;
let idList = [];
let hashList = [];
for (let t in targets) {
for (let a in targets[t][attribute]) {
const asset = targets[t][attribute][a];
occurrences++;
if (typeof id !== 'undefined') idList.push(asset[id]);
if (typeof hash !== 'undefined') hashList.push(asset[hash]);
}
}
const result = {count: occurrences};
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 (list) {
return {
count: list.length,
id: list
};
};
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 {
/**
* Tallies 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;

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"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": {
"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"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.10.0",
"eslint-config-scratch": "^5.0.0",
"tap": "^12.1.1"
}
}

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

19
test/unit/utility.js Normal file
View file

@ -0,0 +1,19 @@
const test = require('tap').test;
const utility = require('../../lib/utility');
test('spec', t => {
t.type(utility, 'function');
t.type(utility.frequency, 'function');
t.end();
});
test('frequency', t => {
const input = ['foo', 'foo', 'foo', 'bar', 'bar', 'baz'];
const result = utility.frequency(input);
t.deepEqual(result, {
foo: 3,
bar: 2,
baz: 1
});
t.end();
});