Pop quiz, hot shot! You got some unit-tests. You want to know whether it's written in QUnit or Jasmine. What do you do? What do you do?
Get a Javascript parser... Uglify! Install it for node
npm install uglify-js
Read in the file and parse it to get an AST
var parser = require('uglify-js').parser
, fs = require('fs')
, filename = process.argv[0]
var ast = parser.parse(String(fs.readFileSync(filename)))
Print out the AST
console.log(JSON.stringify(ast))
Look at the AST for an example QUnit test file
[
"toplevel",
[
[
"stat",
[
"call",
[
"name",
"test"
],
[
[
"string",
"validate phone #"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"ok"
],
[
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
"770 123 4567"
]
]
],
[
"string",
"basic"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"test"
],
[
[
"string",
"no non-numbers"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"equal"
],
[
[
"name",
"false"
],
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
"a"
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"equal"
],
[
[
"name",
"false"
],
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
"?"
]
]
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"test"
],
[
[
"string",
"must be in right grouping"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"equal"
],
[
[
"name",
"false"
],
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
" 3 9 3"
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"equal"
],
[
[
"name",
"false"
],
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
"12 23 2345 3 thuno"
]
]
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"test"
],
[
[
"string",
"dashes are ok"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"ok"
],
[
[
"call",
[
"name",
"validatePhone"
],
[
[
"string",
"770-123-4567e"
]
]
]
]
]
]
]
]
]
]
]
]
]
Do the same for an example Jasmine test file
[
"toplevel",
[
[
"stat",
[
"call",
[
"name",
"describe"
],
[
[
"string",
"hello"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should say hello"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"dot",
[
"call",
[
"name",
"expect"
],
[
[
"call",
[
"name",
"hello"
],
[]
]
]
],
"toBe"
],
[
[
"string",
"hello world"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should not say not hello"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"dot",
[
"call",
[
"name",
"expect"
],
[
[
"call",
[
"name",
"hello"
],
[]
]
]
],
"toNotBe"
],
[
[
"string",
"not hello world"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should throw exception"
],
[
"function",
null,
[],
[
[
"throw",
[
"new",
[
"name",
"Error"
],
[
[
"string",
"Crap"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should throw another exception"
],
[
"function",
null,
[],
[
[
"throw",
[
"new",
[
"name",
"Error"
],
[
[
"string",
"Blah"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should throw even more exceptions"
],
[
"function",
null,
[],
[
[
"throw",
[
"new",
[
"name",
"Error"
],
[
[
"string",
"Foobar"
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should be firefox"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"dot",
[
"call",
[
"name",
"expect"
],
[
[
"dot",
[
"name",
"navigator"
],
"userAgent"
]
]
],
"toMatch"
],
[
[
"regexp",
"Firefox",
""
]
]
]
]
]
]
]
]
],
[
"stat",
[
"call",
[
"name",
"it"
],
[
[
"string",
"should do async"
],
[
"function",
null,
[],
[
[
"stat",
[
"call",
[
"name",
"waits"
],
[
[
"num",
1000
]
]
]
]
]
]
]
]
]
]
]
]
]
]
]
]
List some common patterns for the AST for the QUnit example
"call",
[
"name",
"test"
],
[
[
"string",
,
"call",
[
"name",
"ok"
],
and
"call",
[
"name",
"equal"
],
List some common patterns for the AST for the Jasmine example
"call",
[
"name",
"describe"
],
[
[
"string",
,
"call",
[
"name",
"it"
],
[
[
"string",
and
"call",
[
"name",
"expect"
],
Traverse the ASTs and match against these patterns and count the occurances of all patterns, grouping by framework.
So, first a function to traverse the AST
function traverse(node, visit){
visit(node)
if (node instanceof Array){
for (var i = 0, len = node.length; i < len; i++)
traverse(node[i], visit)
}
}
Write the pattern detectors as functions
var patterns = {
jasmine: [
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'describe' &&
node[2][0][0] === 'string'
},
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'it' &&
node[2][0][0] === 'string'
},
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'expect'
}
],
qunit: [
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'test' &&
node[2][0][0] === 'string'
},
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'ok'
},
function(node){
return node[0] === 'call' &&
node[1][0] === 'name' &&
node[1][1] === 'equal'
}
]
}
Keep a tally
var tally = {
jasmine: 0,
qunit: 0
}
Now actually traverse the AST, try to match the patterns and count the number of hits
function patternMatches(node, test){
try{
return test(node)
}catch(e){
return false
}
}
function detectPatterns(node){
for (var framework in patterns){
patterns[framework].forEach(function(pat, idx){
if (patternMatches(node, pat)){
tally[framework]++
}
})
}
}
traverse(ast, detectPatterns)
How many hits did we get for QUnit vs Jasmine?
for (var key in tally){
console.log(tally[key] + ' points for ' + key + '.')
}
Is that your final answer?
console.log('Tests were written in ' + (tally.qunit > tally.jasmine ?
'QUnit': 'Jasmine') + '.')
Sample output
airportyh$ node detect.js tests.js
0 points for jasmine.
10 points for qunit.
Tests were written in QUnit.
airportyh$ node detect.js specs.js
11 points for jasmine.
0 points for qunit.
Tests were written in Jasmine.
Full script at this gist.