cbfl
Version:
library that can be used to automatically find points of failure in TypeScript Modules that are tested with Mocha
211 lines (210 loc) • 9.13 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFailureLocalizationHooks = exports.traverseHistory = void 0;
const childProcess = __importStar(require("child_process"));
const fs = __importStar(require("fs"));
const coverageConverter_1 = require("./coverageConverter");
const nodegit_1 = require("nodegit");
const https_1 = require("https");
const form_data_1 = __importDefault(require("form-data"));
const FaultLocalizations_1 = require("./FaultLocalizations");
console.log("hooks file loaded");
async function getAllOids(repo) {
const revwalk = nodegit_1.Revwalk.create(repo);
revwalk.reset();
revwalk.sorting(2 /* TIME */);
const commit = await repo.getHeadCommit();
revwalk.push(commit.id());
// step through all OIDs for the given reference
const allOids = [];
let hasNext = true;
while (hasNext) {
try {
const oid = await revwalk.next();
allOids.push(oid);
}
catch (err) {
hasNext = false;
}
}
return allOids;
}
async function getPreviousCommit(repo, commit) {
const revwalk = nodegit_1.Revwalk.create(repo);
revwalk.reset();
revwalk.sorting(2 /* TIME */);
revwalk.push(commit.id());
await revwalk.next();
return nodegit_1.Commit.lookup(repo, await revwalk.next());
}
const traverseHistory = async (mochaCommand, hooksFilePath) => {
const repo = await nodegit_1.Repository.open("./.git");
const allOids = await getAllOids(repo);
const processOptions = {
stdio: "inherit",
};
console.log("all Oids", allOids);
const hooksFile = fs.readFileSync(hooksFilePath);
for (const oid of allOids) {
//Todo: add error handling
try {
childProcess.execSync(`git checkout -f ${oid.tostrS()}`, processOptions);
childProcess.execSync(`npm install mocha@8.0.0`, processOptions);
childProcess.execSync(`npm install`, processOptions);
childProcess.execSync(`npm link cbfl`, processOptions);
fs.writeFileSync(hooksFilePath, hooksFile);
childProcess.execSync(`TARGET_COMMIT=${oid.tostrS()} ${mochaCommand}`, processOptions);
}
catch (err) {
console.log(err);
}
}
};
exports.traverseHistory = traverseHistory;
const createFailureLocalizationHooks = ({ mochaCommand, targetBranch = "master", gitlabApiToken, }) => {
const TEMP_COVERAGE_DIR = "./tempCoverageDir";
let commitID = "";
const changedLinesPerFile = new Map();
const faultLocalizations = new FaultLocalizations_1.FaultLocalizations();
const afterAllPromises = [];
return {
beforeAll: async () => {
console.log("hi from mocha before all hook");
const repo = await nodegit_1.Repository.open("./.git");
const target = process.env.TARGET_COMMIT
? await getPreviousCommit(repo, await nodegit_1.Commit.lookup(repo, process.env.TARGET_COMMIT))
: await repo.getReferenceCommit(targetBranch);
commitID = target.id().tostrS();
const targetTree = await target.getTree();
const diff = await nodegit_1.Diff.treeToIndex(repo, targetTree, undefined, {
contextLines: 0,
});
const patches = await diff.patches();
for (const patch of patches) {
const fileEnding = patch.newFile().path().split(".").pop();
if (fileEnding !== "ts") {
continue;
}
const hunks = await patch.hunks();
const lineNumbers = [];
for (const hunk of hunks) {
const startLine = hunk.newStart();
const numberOfLines = hunk.newLines();
lineNumbers.push(...Array.from(new Array(numberOfLines), (x, i) => i + startLine));
}
changedLinesPerFile.set(patch.newFile().path(), lineNumbers);
}
return Promise.resolve();
},
afterEach: async (currentTest) => {
if (currentTest.state === "failed") {
const fullTestTitle = getFullTestTitle(currentTest);
const currentTestPath = currentTest.file;
faultLocalizations.addFailedTest(currentTestPath, fullTestTitle);
console.log(`The test '${fullTestTitle} from the file ${currentTestPath} failed.`);
const coverageDir = TEMP_COVERAGE_DIR + "/" + fullTestTitle.replace(/\s/g, "");
const testCommand = `NODE_V8_COVERAGE=${coverageDir} ${mochaCommand} ${currentTestPath} --grep "^${fullTestTitle}$"`;
console.log("running the test again with the command: ", testCommand);
const promise = new Promise((resolve) => {
childProcess.exec(testCommand, async (error) => {
const coverageFile = coverageDir + "/" + fs.readdirSync(coverageDir)[0];
const loadedCoverage = await coverageConverter_1.loadCoverage(coverageFile);
fs.rmdirSync(coverageDir, { recursive: true });
if (!loadedCoverage) {
resolve();
return;
}
for (const [file, lines] of changedLinesPerFile) {
const changedLineCoverage = await coverageConverter_1.convertCoverage(loadedCoverage.coverage, loadedCoverage.sourceMap, file);
if (!changedLineCoverage) {
continue;
}
lines.forEach((line) => {
if (changedLineCoverage.s[line - 1]) {
console.log(`The test ${fullTestTitle} ran through line ${line} of the file ${file} which was recently changed!`);
faultLocalizations.addFailedLine(changedLineCoverage, changedLineCoverage.path, line, currentTestPath);
}
});
resolve();
}
});
});
afterAllPromises.push(promise);
return promise;
}
return Promise.resolve();
},
afterAll: async () => {
await Promise.all(afterAllPromises);
if (gitlabApiToken) {
addCommentsToFaultyFilesOnMergeRequest(faultLocalizations, gitlabApiToken);
}
else {
await faultLocalizations.saveToFile(commitID);
}
return Promise.resolve();
},
};
};
exports.createFailureLocalizationHooks = createFailureLocalizationHooks;
const addCommentsToFaultyFilesOnMergeRequest = (faultLocalizations, gitlabApiToken) => {
const comment = faultLocalizations.generateComment();
const form = new form_data_1.default();
form.append("body", comment);
const url = new URL(process.env.CI_API_V4_URL +
"/projects/" +
process.env.CI_PROJECT_ID +
"/merge_requests/" +
process.env.CI_MERGE_REQUEST_IID +
"/notes?private_token=" +
gitlabApiToken);
const options = {
method: "POST",
headers: form.getHeaders(),
};
const req = https_1.request(url, options, (res) => {
const chunks = [];
res.on("data", (chunk) => {
chunks.push(chunk);
});
res.on("end", (chunk) => {
const body = Buffer.concat(chunks);
console.log(body.toString());
});
res.on("error", (error) => {
console.error(error);
});
});
form.pipe(req);
};
function getFullTestTitle(currentTest) {
let fullTestTitle = currentTest.title;
let parent = currentTest;
while (parent.parent.title) {
parent = parent.parent;
fullTestTitle = parent.title + " " + fullTestTitle;
}
return fullTestTitle;
}