doop-cli
Version:
Doop CLI helper to perform unit based operations on the current project
217 lines (198 loc) • 7.27 kB
JavaScript
var _ = require('lodash');
var async = require('async-chainable');
var asyncExec = require('async-chainable-exec');
var asyncFlush = require('async-chainable-flush');
var colors = require('chalk');
var doop = require('.');
var fs = require('fs');
var fspath = require('path');
var glob = require('glob');
var program = require('commander');
var temp = require('temp');
program
.version(require('./package.json').version)
.usage('[schms...]')
.description('Generate Mocha/Chai tests from unit schema files')
.option('-f, --force', 'Force overwrite existing test files without merging')
.option('-v, --verbose', 'Be verbose. Specify multiple times for increasing verbosity', function(i, v) { return v + 1 }, 0)
.option('--no-clobber', 'Dont attempt to update existing files')
.parse(process.argv);
async()
.use(asyncFlush)
.then(doop.chProjectRoot)
.then(doop.getUserSettings)
// Load project database {{{
.then('models', function(next) {
global.app = require(doop.settings.paths.project + '/units/core/app');
global.app.config = require(doop.settings.paths.project + '/config/index.conf');
require(doop.settings.paths.project + '/units/db/loader')(next);
})
// }}}
// Glob all *.schm.js files to determine schemas {{{
.then('schms', function(next) {
glob('**/*.schm.js', {cwd: fspath.join(doop.settings.paths.project, doop.settings.paths.units)}, function(err, files) {
if (err) return next(err);
if (!files.length) return next('No matching models found');
if (program.verbose >= 2) console.log('Found schm files:', files.map(f => colors.cyan(f)).join(', '));
if (program.args.length) { // Apply filters
files = files.filter(file => _.includes(program.args, fspath.basename(file, '.schm.js')));
}
next(null, files.map(function(file) {
return {
id: fspath.basename(file, '.schm.js'),
path: file,
};
}));
});
})
// }}}
// Determine owning units from found paths {{{
.forEach('schms', function(next, schm) {
schm.unit = doop.getUnitByResource(schm.path);
if (!schm.unit) return next('Unable to determine unit for model "' + schm.path + '"');
doop.getUnit(function(err, path) {
if (err) return next(err);
schm.unitPath = path[0];
next();
}, schm.unit);
})
// }}}
// Determine if the test file already exists {{{
.forEach('schms', function(next, schm) {
schm.testPath = fspath.join(schm.unitPath, schm.id + '.test.js');
fs.access(schm.testPath, function(err, stat) {
schm.testPathExisting = ! err;
next();
});
})
// }}}
// Final sanity checks before we run {{{
.then(function(next) {
if (!program.clobber && this.schms.some(schm => schm.testPathExisting)) {
return next('Refusing to overwrite existing test files: ' + this.schms.filter(schm => schm.testPathExisting).map(schm => schm.path).join(', '));
}
if (!this.schms.every(schm => this.models[schm.id])) {
return next('Unable to find matching MongoDB models: ' + this.schms.filter(schm => !this.models[schm.id]).map(schm => schm.id).join(', '));
}
next();
})
// }}}
// Generate testkit {{{
.forEach('schms', function(next, schm) {
schm.generated = {};
async()
.set('models', this.models)
.parallel({
get: function(next) {
var test = schm.generated.get = [
"\tit('GET /api/" + schm.unit + "', function(done) {",
"\t\tapp.test.agent.get(app.config.url + '/api/" + schm.unit + "')",
"\t\t\t.end(function(err, res) {",
"\t\t\t\tif (res.body.error) return done(res.body.error);",
"\t\t\t\texpect(err).to.not.be.ok;",
"\t\t\t\texpect(res.body).to.be.an.array",
"",
"\t\t\t\tres.body.forEach(function(i) {",
];
var sortedPaths = _(this.models[schm.id].$mongooseModel.schema.paths)
.map((v,k) => v)
.sortBy('path')
.value();
_.forEach(sortedPaths, function(path) {
var id = path.path;
if (id == '__v') return; // Weird item - ignore for now
var isDeep = /\./.test(id); // Is the path nested? (sub-docs, objects of objects)
if (id.startsWith('_') && id != '_id') { // Hidden value?
test.push("\t\t\t\t\texpect(i).to.not.have." + (isDeep ? 'deep.' : '') + "property('" + id + "');");
} else {
test.push("\t\t\t\t\texpect(i).to.have." + (isDeep ? 'deep.' : '') + "property('" + id + "');");
switch (path.instance.toLowerCase()) {
case 'string':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.a.string;");
if (path.enumValues && path.enumValues.length) {
test.push("\t\t\t\t\texpect(i." + id + ").to.be.oneOf([" + path.enumValues.map(i => "'" + i + "'").join(', ') + "]);");
}
break;
case 'number':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.a.number;");
break;
case 'date':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.a.date;");
break;
case 'boolean':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.a.boolean;");
break;
case 'array':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.an.array;");
break;
case 'object':
test.push("\t\t\t\t\texpect(i." + id + ").to.be.an.object;");
break;
case 'objectid':
// Do nothing - also don't report an error
break;
default:
if (program.verbose) console.log('Unknown Mongo data type:', colors.cyan(path.instance.toLowerCase()));
}
}
});
test.push('');
test.push('\t\t\t\t});');
test.push('');
test.push('\t\t\t\tdone();');
test.push('\t\t\t});');
test.push('\t});');
next();
},
})
.end(next);
})
// }}}
// Write main file / temporary file {{{
.forEach('schms', function(next, schm) {
if (!program.force && schm.testPathExisting) { // Write to temporary file
schm.testPathTemp = temp.path({suffix: '--' + schm.id + '.schm.js'});
if (program.verbose >= 2) console.log('Write', colors.cyan(schm.id), 'test to temporary file', colors.cyan(schm.testPathTemp));
var outStream = fs.createWriteStream(schm.testPathTemp);
} else { // Write to real file
if (program.verbose >= 2) console.log('Write', colors.cyan(schm.id), 'test to', colors.cyan(schm.testPath));
var outStream = fs.createWriteStream(schm.testPath);
}
outStream.on('finish', next);
outStream.write("var expect = require('chai').expect;\n\n");
outStream.write("describe('ReST interface /api/" + schm.id + "', function() {\n\n");
_.forEach(schm.generated, function(text, method) {
outStream.write(text.join('\n'));
});
outStream.write("\n\n});");
outStream.end();
})
// }}}
// Open merge session when needed {{{
.limit(1)
.forEach('schms', function(next, schm) {
if (program.force || !schm.testPathExisting) return next();
if (program.verbose) console.log('Merge', colors.cyan(schm.testPath), colors.cyan(schm.testPathTemp));
async()
.use(asyncExec)
.execDefaults({stdio: 'inherit'})
.exec([
'meld',
schm.testPath,
schm.testPathTemp,
])
.end(next);
})
// }}}
// End {{{
.flush()
.end(function(err) {
if (err) {
console.log(colors.red('Doop Error'), err.toString());
process.exit(1);
} else {
process.exit(0);
}
});
// }}}