Skip to content

Instantly share code, notes, and snippets.

@skovhus
Last active August 14, 2016 20:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skovhus/5a9e904eb39b3e4f74c1e11ac1f97b12 to your computer and use it in GitHub Desktop.
Save skovhus/5a9e904eb39b3e4f74c1e11ac1f97b12 to your computer and use it in GitHub Desktop.
Codemod for Tape to AVA
/**
* 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