cbfl
Version:
library that can be used to automatically find points of failure in TypeScript Modules that are tested with Mocha
260 lines (231 loc) • 7.79 kB
text/typescript
import * as childProcess from "child_process";
import * as fs from "fs";
import { convertCoverage, loadCoverage } from "./coverageConverter";
import { Commit, Diff, Oid, Repository, Revwalk } from "nodegit";
import { IncomingMessage } from "http";
import { request, RequestOptions } from "https";
import FormData from "form-data";
import { FaultLocalizations } from "./FaultLocalizations";
import { ExecException, ExecSyncOptions } from "child_process";
console.log("hooks file loaded");
interface IFailureLocalizationOptions {
mochaCommand: string;
targetBranch: string;
gitlabApiToken: string;
}
async function getAllOids(repo: Repository) {
const revwalk = Revwalk.create(repo);
revwalk.reset();
revwalk.sorting(Revwalk.SORT.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: Repository,
commit: Commit
): Promise<Commit> {
const revwalk = Revwalk.create(repo);
revwalk.reset();
revwalk.sorting(Revwalk.SORT.TIME);
revwalk.push(commit.id());
await revwalk.next();
return Commit.lookup(repo, await revwalk.next());
}
export const traverseHistory = async (
mochaCommand: string,
hooksFilePath: string
) => {
const repo = await Repository.open("./.git");
const allOids: Oid[] = await getAllOids(repo);
const processOptions: ExecSyncOptions = {
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);
}
}
};
export const createFailureLocalizationHooks = ({
mochaCommand,
targetBranch = "master",
gitlabApiToken,
}: IFailureLocalizationOptions) => {
const TEMP_COVERAGE_DIR = "./tempCoverageDir";
let commitID = "";
const changedLinesPerFile = new Map<string, number[]>();
const faultLocalizations = new FaultLocalizations();
const afterAllPromises: Promise<void>[] = [];
return {
beforeAll: async () => {
console.log("hi from mocha before all hook");
const repo = await Repository.open("./.git");
const target = process.env.TARGET_COMMIT
? await getPreviousCommit(
repo,
await Commit.lookup(repo, process.env.TARGET_COMMIT)
)
: await repo.getReferenceCommit(targetBranch);
commitID = target.id().tostrS();
const targetTree = await target.getTree();
const diff = await 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: any) => {
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<void>((resolve) => {
childProcess.exec(
testCommand,
async (error: ExecException | null) => {
const coverageFile =
coverageDir + "/" + fs.readdirSync(coverageDir)[0];
const loadedCoverage = await loadCoverage(coverageFile);
fs.rmdirSync(coverageDir, { recursive: true });
if (!loadedCoverage) {
resolve();
return;
}
for (const [file, lines] of changedLinesPerFile) {
const changedLineCoverage = await convertCoverage(
loadedCoverage.coverage,
loadedCoverage.sourceMap,
file
);
if (!changedLineCoverage) {
continue;
}
lines.forEach((line: number) => {
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();
},
};
};
const addCommentsToFaultyFilesOnMergeRequest = (
faultLocalizations: FaultLocalizations,
gitlabApiToken: string
) => {
const comment = faultLocalizations.generateComment();
const form = new FormData();
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: RequestOptions = {
method: "POST",
headers: form.getHeaders(),
};
const req = request(url, options, (res: IncomingMessage) => {
const chunks: any = [];
res.on("data", (chunk) => {
chunks.push(chunk);
});
res.on("end", (chunk: any) => {
const body = Buffer.concat(chunks);
console.log(body.toString());
});
res.on("error", (error) => {
console.error(error);
});
});
form.pipe(req);
};
function getFullTestTitle(currentTest: any) {
let fullTestTitle = currentTest.title;
let parent = currentTest;
while (parent.parent.title) {
parent = parent.parent;
fullTestTitle = parent.title + " " + fullTestTitle;
}
return fullTestTitle;
}