UNPKG

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
"use strict"; 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; }