Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
+185
View File
@@ -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);
}
}
+11
View File
@@ -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;
+59
View File
@@ -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;
+131
View File
@@ -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;