mirror of
https://github.com/scratchfoundation/scratch-analysis.git
synced 2024-11-28 10:35:34 -05:00
commit
50d9f834b4
21 changed files with 1171 additions and 0 deletions
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
/node_modules
|
||||
/.nyc_output
|
||||
/coverage
|
3
.eslintrc
Normal file
3
.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["scratch", "scratch/node"]
|
||||
}
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal 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
45
README.md
Normal 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
23
lib/index.js
Normal 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
185
lib/sb2.js
Normal 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
110
lib/sb3.js
Normal 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
23
lib/utility.js
Normal 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
28
package.json
Normal 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
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
BIN
test/fixtures/sb2/complex.sb2
vendored
Normal file
Binary file not shown.
71
test/fixtures/sb2/default.json
vendored
Normal file
71
test/fixtures/sb2/default.json
vendored
Normal 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
BIN
test/fixtures/sb2/default.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb3/complex.sb3
vendored
Normal file
BIN
test/fixtures/sb3/complex.sb3
vendored
Normal file
Binary file not shown.
103
test/fixtures/sb3/default.json
vendored
Normal file
103
test/fixtures/sb3/default.json
vendored
Normal 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
BIN
test/fixtures/sb3/default.sb3
vendored
Normal file
Binary file not shown.
24
test/unit/error.js
Normal file
24
test/unit/error.js
Normal 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
256
test/unit/sb2.js
Normal 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
260
test/unit/sb3.js
Normal 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
7
test/unit/spec.js
Normal 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
19
test/unit/utility.js
Normal 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();
|
||||
});
|
Loading…
Reference in a new issue