Initial commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 <affected></affected>
|
||||
*
|
||||
* 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
|
||||
* <affected>
|
||||
* spec/foo.spec.js
|
||||
* </affected>
|
||||
*
|
||||
* 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
|
||||
* <affected>
|
||||
* spec/foo.spec.js
|
||||
* spec/baz.spec.js
|
||||
* </affected>
|
||||
*/
|
||||
|
||||
|
||||
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('<affected>');
|
||||
targetSpecs.forEach(function (spec) {
|
||||
console.log(spec);
|
||||
});
|
||||
console.log('</affected>');
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
function cleanArray (actual) {
|
||||
var newArray = [];
|
||||
for (var i = 0; i < actual.length; i++) {
|
||||
if (actual[i]) {
|
||||
newArray.push(actual[i]);
|
||||
}
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
module.exports = cleanArray;
|
||||
@@ -0,0 +1,7 @@
|
||||
var cfg = {
|
||||
requirePattern: "require\\('(\\..+)'\\)",
|
||||
defaultExt: '.js',
|
||||
validExtensions: ['.tpl', '.json', '.js', '.mustache']
|
||||
}
|
||||
|
||||
module.exports = cfg;
|
||||
@@ -0,0 +1,59 @@
|
||||
var fs = require('fs-extra');
|
||||
var path = require('path');
|
||||
var config = require('./fetchRequires.config.js');
|
||||
|
||||
function checkConfig () {
|
||||
if (!config.requirePattern) {
|
||||
throw new Error("requirePattern needed at fetchRequires.config.js - Use it to define the regular expression to use for finding require invocations. Example: require\\('(\\..+)'\\)");
|
||||
}
|
||||
if (!config.defaultExt) {
|
||||
throw new Error("defaultExt needed at fetchRequires.config.js - Use it to define the default extension to use for modules. Example: '.js'");
|
||||
}
|
||||
if (!config.validExtensions) {
|
||||
throw new Error("validExtensions needed at fetchRequires.config.js - Use it to laod modules as is. That's it, to define valid extensions that don't need appending the default extension. Example: ['.tpl', '.json']");
|
||||
}
|
||||
}
|
||||
|
||||
var fetchRequires = function (absoluteFilePath) {
|
||||
checkConfig();
|
||||
var requirePattern = config.requirePattern;
|
||||
var requireGlobalRegex = new RegExp(requirePattern, 'g');
|
||||
var requireRegex = new RegExp(requirePattern);
|
||||
var defaultExt = config.defaultExt;
|
||||
var validExtensions = config.validExtensions;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.readFile(absoluteFilePath, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
if (err.code && err.code === 'ENOENT') {
|
||||
var error = new Error("ENOENT: no such file '" + err.path + "'. Perhaps we're adding the default extension to a valid file. Check `validExtensions` in fetchRequires.config.js and add a new one if needed.");
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var requires = [];
|
||||
if (data) {
|
||||
var matchData = data.match(requireGlobalRegex);
|
||||
if (matchData) {
|
||||
matchData.forEach(function (require) {
|
||||
var exec = requireRegex.exec(require);
|
||||
if (exec) {
|
||||
var absolutePath = path.resolve(path.dirname(absoluteFilePath), exec[1]);
|
||||
var ext = path.extname(absolutePath);
|
||||
if (validExtensions.indexOf(ext) === -1) {
|
||||
absolutePath += defaultExt;
|
||||
}
|
||||
requires.push(absolutePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
resolve(requires);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = fetchRequires;
|
||||
@@ -0,0 +1,131 @@
|
||||
var path = require('path');
|
||||
var colors = require('colors');
|
||||
|
||||
var fetchRequires = require('./fetchRequires');
|
||||
var cleanArray = require('./cleanArray');
|
||||
|
||||
var FileTrie = function () {
|
||||
var trie = {
|
||||
root: {}
|
||||
};
|
||||
|
||||
var addFileRequires = function (filePath) {
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
if (!path.extname(filePath)) {
|
||||
reject('filePath must contain a valid file path.');
|
||||
return;
|
||||
}
|
||||
|
||||
var key = _buildKey(filePath, true);
|
||||
var fileName = path.basename(filePath);
|
||||
var lastNode = _writeKey(key);
|
||||
if (!lastNode[fileName]) {
|
||||
fetchRequires(filePath)
|
||||
.then(function (requires) {
|
||||
lastNode[fileName] = {};
|
||||
if (requires.length > 0) {
|
||||
lastNode[fileName].requires = requires;
|
||||
lastNode[fileName].fetched = false;
|
||||
var allRequirePromises = requires.reduce(function (acc, require) {
|
||||
acc.push(addFileRequires(require));
|
||||
return acc;
|
||||
}, []);
|
||||
Promise.all(allRequirePromises)
|
||||
.then(function () {
|
||||
lastNode[fileName].fetched = true;
|
||||
resolve();
|
||||
})
|
||||
.catch(function (reason) {
|
||||
reject(reason);
|
||||
});
|
||||
} else {
|
||||
lastNode[fileName].requires = [];
|
||||
lastNode[fileName].fetched = true;
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch(function (reason) {
|
||||
reject(reason);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
var markSubTree = function (entryFilePath, mark) {
|
||||
var filesToMark;
|
||||
var fileToMark;
|
||||
var node;
|
||||
|
||||
mark = mark || entryFilePath;
|
||||
filesToMark = [entryFilePath];
|
||||
while (filesToMark.length > 0) {
|
||||
fileToMark = filesToMark.pop();
|
||||
node = getNode(fileToMark);
|
||||
if (!node.marks) {
|
||||
node.marks = [];
|
||||
}
|
||||
if (node.marks.indexOf(mark) === -1) {
|
||||
node.marks.push(mark);
|
||||
if (node.requires && node.requires.length > 0) {
|
||||
filesToMark = filesToMark.concat(node.requires);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getNode = function (filePath) {
|
||||
var key = _buildKey(filePath);
|
||||
var node = trie.root;
|
||||
var validKey = true;
|
||||
for (var i = 0; i < key.length && validKey; i++) {
|
||||
if (!node[key[i]]) {
|
||||
validKey = false;
|
||||
} else {
|
||||
node = node[key[i]];
|
||||
}
|
||||
}
|
||||
|
||||
if (!validKey) {
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
var _buildKey = function (filePath, onlyDir) {
|
||||
var key = filePath;
|
||||
if (onlyDir) {
|
||||
key = path.dirname(filePath);
|
||||
}
|
||||
return cleanArray(key.split(path.sep));
|
||||
};
|
||||
|
||||
var _writeKey = function (key) {
|
||||
var node = trie.root;
|
||||
var subKey;
|
||||
|
||||
for (var i = 0; i < key.length; i++) {
|
||||
subKey = key[i];
|
||||
if (!node[subKey]) {
|
||||
node[subKey] = {};
|
||||
}
|
||||
node = node[subKey];
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
var print = function () {
|
||||
console.log(colors.yellow(JSON.stringify(trie)));
|
||||
};
|
||||
|
||||
return {
|
||||
addFileRequires: addFileRequires,
|
||||
getNode: getNode,
|
||||
markSubTree: markSubTree,
|
||||
print: print
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = FileTrie;
|
||||
Reference in New Issue
Block a user