Last active
August 14, 2016 20:36
-
-
Save skovhus/5a9e904eb39b3e4f74c1e11ac1f97b12 to your computer and use it in GitHub Desktop.
Codemod for Tape to AVA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Codemod for transforming Tape tests into AVA. | |
* | |
* jscodeshift -t tape-to-ava-codemod.js my-folder | |
* | |
* TODO: | |
* - [ ] Figure out when to keep `t.end` and when to remove it | |
* - [ ] rename first param in test callback function, if it is not t | |
* (and replace usage of identifier in block) | |
* - [ ] write test and submit to https://github.com/avajs/ava-codemods/issues/5 | |
*/ | |
const tapeToAvaAsserts = { | |
// 1:1 mapping are kept for completeness. | |
'fail': 'fail', | |
'pass': 'pass', | |
'ok': 'truthy', | |
'true': 'truthy', | |
'assert': 'truthy', | |
'notOk': 'falsy', | |
'false': 'falsy', | |
'notok': 'falsy', | |
'error': 'ifError', | |
'ifErr': 'ifError', | |
'iferror': 'ifError', | |
'equal': 'is', | |
'equals': 'is', | |
'isEqual': 'is', | |
'strictEqual': 'is', | |
'strictEquals': 'is', | |
'notEqual': 'not', | |
'notStrictEqual': 'not', | |
'notStrictEquals': 'not', | |
'isNotEqual': 'not', | |
'doesNotEqual': 'not', | |
'isInequal': 'not', | |
'deepEqual': 'deepEqual', | |
'isEquivalent': 'deepEqual', | |
'same': 'deepEqual', | |
'notDeepEqual': 'notDeepEqual', | |
'notEquivalent': 'notDeepEqual', | |
'notDeeply': 'notDeepEqual', | |
'notSame': 'notDeepEqual', | |
'isNotDeepEqual': 'notDeepEqual', | |
'isNotEquivalent': 'notDeepEqual', | |
'isInequivalent': 'notDeepEqual', | |
'skip': 'skip', | |
'throws': 'throws', | |
'doesNotThrow': 'notThrows', | |
}; | |
const unsupportedTestFunctions = new Set([ | |
// No equivalent in AVA: | |
'timeoutAfter', | |
'deepLooseEqual', | |
'looseEqual', | |
'looseEquals', | |
'notDeepLooseEqual', | |
'notLooseEqual', | |
'notLooseEquals', | |
]); | |
/** | |
* Updates CommonJS and import statements from Tape to AVA | |
* @return string with test function name if transformations were made | |
*/ | |
function updateTapeRequireAndImport(j, ast) { | |
let testFunctionName = null; | |
ast.find(j.CallExpression, { | |
callee: { name: 'require' }, | |
arguments: arg => arg[0].value === 'tape', | |
}) | |
.filter(p => p.value.arguments.length === 1) | |
.forEach(p => { | |
p.node.arguments[0].value = 'ava'; | |
testFunctionName = p.parentPath.value.id.name; | |
}); | |
ast.find(j.ImportDeclaration, { | |
source: { | |
type: 'Literal', | |
value: 'tape', | |
}, | |
}) | |
.forEach(p => { | |
p.node.source.value = 'ava'; | |
testFunctionName = p.value.specifiers[0].local.name; | |
}); | |
return testFunctionName; | |
} | |
export default function tapeToAva(fileInfo, api, options) { | |
const j = api.jscodeshift; | |
const ast = j(fileInfo.source); | |
const testFunctionName = updateTapeRequireAndImport(j, ast); | |
if (testFunctionName) { | |
const transforms = [ | |
function updateAssertions() { | |
function renameAssertion(name, newName) { | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: 't' }, | |
property: { name: name }, | |
}, | |
}) | |
.forEach(p => { | |
p.get('callee').get('property').replace(j.identifier(newName)); | |
}); | |
} | |
Object.keys(tapeToAvaAsserts).forEach(function(k) { | |
renameAssertion(k, tapeToAvaAsserts[k]); | |
}); | |
}, | |
function testOptionArgument() { | |
// Convert Tape option parameters, test([name], [opts], cb) | |
ast.find(j.CallExpression, { | |
callee: { name: testFunctionName }, | |
}).forEach(p => { | |
p.value.arguments.forEach(a => { | |
if (a.type === 'ObjectExpression') { | |
a.properties.forEach(tapeOption => { | |
const tapeOptionKey = tapeOption.key.name; | |
const tapeOptionValue = tapeOption.value.value; | |
if (tapeOptionKey === 'skip' && tapeOptionValue === true) { | |
p.value.callee.name += '.skip'; | |
} | |
if (tapeOptionKey === 'timeout') { | |
throw new Error('Codemod transformation of "timeout" option is not supported'); | |
} | |
}); | |
p.value.arguments = p.value.arguments.filter(pa => pa.type !== 'ObjectExpression'); | |
} | |
}); | |
}); | |
}, | |
function updateTapeComments() { | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: 't' }, | |
property: { name: 'comment' }, | |
}, | |
}) | |
.forEach(p => { | |
p.node.callee = 'console.log'; | |
}); | |
}, | |
function updateThrows() { | |
// The semantics of t.throws(fn, expected, msg) is different from tape to ava | |
// tape: if expected is a string, it is set to msg | |
// ava: if expected is a string it is transformed to a function. | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: 't' }, | |
property: { name: 'throws' }, | |
}, | |
arguments: arg => arg.length === 2 && arg[1].type === 'Literal' && typeof arg[1].value === 'string', | |
}) | |
.forEach(p => { | |
const [fn, msg] = p.node.arguments; | |
p.node.arguments = [fn, j.literal(null), msg]; | |
}); | |
}, | |
function updateTapeOnFinish() { | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: testFunctionName }, | |
property: { name: 'onFinish' }, | |
}, | |
}) | |
.forEach(p => { | |
p.node.callee.property.name = 'after.always'; | |
}); | |
}, | |
function detectUnsupportedFeatures() { | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: 't' }, | |
property: ({ name }) => unsupportedTestFunctions.has(name), | |
}, | |
}) | |
.forEach(p => { | |
throw new Error(`Codemod transformation of "${p.value.callee.property.name}" is not supported`); | |
}); | |
ast.find(j.CallExpression, { | |
callee: { | |
object: { name: testFunctionName }, | |
property: { name: 'createStream' }, | |
}, | |
}) | |
.forEach(p => { | |
throw new Error('Codemod transformation of "createStream" is not supported'); | |
}); | |
}, | |
function rewriteTestCallExpression() { | |
// To be on the safe side we rewrite the test(...) function to either | |
// test.cb.serial(...) or test.serial(...) | |
// | |
// - .serial as Tape runs all tests serially | |
// - .cb for tests containing t.end, as we cannot detect if the test have any asynchronicity | |
// | |
// cb.serail can be sometimes removed to get a performance boost. | |
ast.find(j.CallExpression, { | |
callee: { name: 'test' }, | |
}).forEach(p => { | |
// TODO: if t.end is in the scope of the test function we could | |
// remove it and not use cb style. | |
const containsEndFunction = j(p).find(j.CallExpression, { | |
callee: { | |
object: { name: 't' }, | |
property: { name: 'end' }, | |
}, | |
}).size() > 0; | |
const newTestFunction = containsEndFunction ? 'cb.serial' : 'serial'; | |
p.node.callee = j.memberExpression( | |
j.identifier('test'), | |
j.identifier(newTestFunction) | |
); | |
}); | |
}, | |
]; | |
transforms.forEach(t => t()); | |
} | |
return ast.toSource({ quote: 'single' }); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment