/** * affectedFiles * * This program outputs a list of files affected by changes in other files that are in the former ones dependency tree. * The problem that inspired this program is to know what test files can be broken because of modifications on * another files in the code base. This way, we'll know the exact test files that we must run to check that * nothing breaks. * * It needs a config file called `tree.config.json` with the next properties: * - testsFolder: the folder to build the dependency tree from. In our case, the specs folder. * ex: "testsFolder": "lib/assets/test/spec/builder/" * - filesRegex: the regular expression for knowing what files must be taken into account for building the dependency tree. * ex: "filesRegex": "spec\\.js$" * * Input: the program needs a list of files to check against. See example below for an explanation. * Output: it outputs the list of affected files between the tags * * Example: * Say we have two spec files in folder specs/ with its own dependencies. * * spec/ * + * |-- foo.spec.js - require('lib/component'), require('lib/calendar'), require('lib/tools/dropdown') * | * |-- baz.spec.js - require('lib/whatever'), require('lib/calendar') * * Run 1: What spec files are affected by a change in files 'lib/tools/dropdown.js' and 'lib/common/utils.js'? (It's only required in foo.spec.js dependency tree) * > affectedFiles lib/tools/drowndown.js lib/common/utils.js * output * * spec/foo.spec.js * * * Run 2: What spec files are affected by a change in file 'lib/calendar.js'? (It's required in both specs) * > affectedFiles lib/tools/calendar.js * output * * spec/foo.spec.js * spec/baz.spec.js * */ var fs = require('fs-extra'); var colors = require('colors'); var recursive = require('recursive-readdir'); var minimist = require('minimist'); var _ = require('underscore'); var FileTrie = require('./fileTrie'); var configFile = './tree.config.json'; var start = Date.now(); var trie = new FileTrie(); var error = false; var config; var addTrigger = function (triggers, currentTrigger, affectedSpecs) { if (!triggers[currentTrigger]) { triggers[currentTrigger] = []; } triggers[currentTrigger] = _.uniq(triggers[currentTrigger].concat(affectedSpecs)); return triggers; }; var logTriggers = function (triggers) { var keys = Object.keys(triggers); keys.forEach(function (key) { console.log(''); console.log(colors.yellow(key)); console.log(colors.yellow(new Array(key.length + 1).join('-'))); triggers[key].forEach(function (trigger) { console.log(trigger); }); }); }; var main = function (testsFolder, modifiedFiles, filesRegex) { filesRegex = filesRegex || 'spec\\.js$'; var onlyTheseFiles = function (file, stats) { var theRegex = new RegExp(filesRegex); return !stats.isDirectory() && !theRegex.test(file); }; function promiseMap (xs, f) { const reducer = (ysAcc$, x) => ysAcc$.then(ysAcc => f(x).then(y => ysAcc.push(y) && ysAcc)); return xs.reduce(reducer, Promise.resolve([])); } function readFiles (folder) { return recursive(folder, [onlyTheseFiles]); } function getAffectedFilesFrom (files) { if (!files || files.length === 0) { console.error('Spec files not found.'); process.exit(1); } console.log('Found ' + files.length + ' spec files.'); var allFilePromises = files.reduce(function (acc, file) { acc.push(trie.addFileRequires(file)); return acc; }, []); Promise.all(allFilePromises) .then(function () { console.log('Dependency tree created.'); console.log(colors.magenta('Took ' + (Date.now() - start))); console.log('Getting reverse spec dependencies...'); var markStart = Date.now(); files.forEach(function (file) { trie.markSubTree(file); }); console.log(colors.magenta('Took ' + (Date.now() - markStart))); var specsInfo = _.chain(modifiedFiles) .reduce(function (acc, modifiedFile) { console.log(colors.magenta(acc.affectedSpecs.length)); var node = trie.getNode(modifiedFile); if (node && node.marks && node.marks.length > 0) { acc.affectedSpecs = acc.affectedSpecs.concat(node.marks); acc.triggers = addTrigger(acc.triggers, modifiedFile, node.marks); return acc; } return acc; }, { affectedSpecs: [], triggers: {} }) .value(); var targetSpecs = _.uniq(specsInfo.affectedSpecs); logTriggers(specsInfo.triggers); console.log(''); console.log(''); targetSpecs.forEach(function (spec) { console.log(spec); }); console.log(''); }) .catch(function (reason) { console.error(colors.red(reason)); process.exit(-1); }); } promiseMap(testsFolder, readFiles) .then(function (files) { const flattenFiles = [].concat.apply([], files); getAffectedFilesFrom(flattenFiles); }) .catch(function (error) { console.error(error); process.exit(1); }); }; // Read configuration & run program try { if (fs.statSync(configFile)) { config = fs.readJsonSync(configFile); if (!config.testsFolder) { console.error('`testsFolder` not found in config file.'); error = true; } else { var modifiedFiles = minimist(process.argv.slice(2))._; main(config.testsFolder, modifiedFiles, config.filesRegex); } if (error) { process.exit(1); } } } catch (err) { if (err.code && err.code === 'ENOENT') { console.error('Config file `tree.config.json` not found!'); process.exit(1); } }