proto-coverage-reporter
Version:
Jest custome reporter for gRPC server E2E testing
160 lines (157 loc) • 6.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const github_1 = require("@actions/github");
const const_1 = require("../const");
const logs_1 = require("../logs");
const proto_1 = require("./proto");
const Table = require('cli-table'); // eslint-disable-line @typescript-eslint/no-var-requires
const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires
class ProtoCoverageReporter {
globalConfig;
options;
protoSpec = {};
constructor(globalConfig, options) {
this.globalConfig = globalConfig;
this.options = options;
const { coverageFrom } = options;
// proto解析
if (!coverageFrom || !coverageFrom.length) {
throw new Error('[proto-coverage-reporter]: coverageFrom option is required');
}
for (const data of coverageFrom) {
const { packageName, serviceProtoPath } = data;
if (!packageName || !serviceProtoPath) {
throw new Error('[proto-coverage-reporter]: packageName and serviceProtoPath are required');
}
const serviceProtoAbsolutePath = this.getServiceProtoAbsolutePath(serviceProtoPath);
const methodSpec = (0, proto_1.parseMethodSpec)(serviceProtoAbsolutePath, packageName);
this.protoSpec[packageName] = methodSpec;
}
fs.mkdirSync(const_1.logsDirPath, { recursive: true });
}
async onRunComplete(testContexts, originalResults) {
const logsMap = await (0, logs_1.readLogsMap)();
const parsed = this.parseResult(logsMap);
this.stdoutCoverage(parsed);
this.createPRComment(parsed);
this.removeLogsDir();
}
parseResult(logsMap) {
const parsed = Object.entries(this.protoSpec).reduce((acc, [packageName, methods]) => {
acc[packageName] = Object.entries(methods).reduce((acc, [methodName, spec]) => {
const desiredStatusCodes = spec.status_codes;
const statusCodes = logsMap[packageName]?.[methodName]?.status_codes || {};
const uncheckedStatusCodes = desiredStatusCodes.filter(statusCode => !statusCodes[const_1.Status[statusCode]]);
const uncehckedPercentage = uncheckedStatusCodes.length / desiredStatusCodes.length;
const coverage = Math.round(100 - uncehckedPercentage * 100);
acc[methodName] = {
status_codes: {
expected: desiredStatusCodes,
unchecked: uncheckedStatusCodes,
coverage,
},
};
return acc;
}, {});
return acc;
}, {});
return parsed;
}
stdoutCoverage(result) {
const table = new Table({
style: { head: ['white'] },
head: ['Package', 'Method', 'Coverage', 'Unchecked Status'],
});
for (const [packageName, methods] of Object.entries(result)) {
for (const [methodName, { status_codes }] of Object.entries(methods)) {
const { coverage, unchecked } = status_codes;
table.push([
packageName,
coverage === 100 ? chalk.green(methodName) : chalk.red(methodName),
coverage === 100 ? chalk.green(`${coverage}%`) : chalk.red(`${coverage}%`),
chalk.red(unchecked.join(', ')),
]);
}
}
console.log(table.toString());
}
async createPRComment(result) {
try {
if (process.env.CI !== 'true' || typeof process.env.GITHUB_TOKEN !== 'string' || !process.env.GITHUB_TOKEN)
return;
const { eventName, repo: { owner, repo }, sha } = github_1.context;
console.log('eventName', eventName);
if (!eventName || !['push'].includes(eventName))
return;
const octokit = (0, github_1.getOctokit)(process.env.GITHUB_TOKEN);
const { data: prs } = await octokit.rest.pulls.list({
owner,
repo,
head: sha,
state: 'open',
});
if (!prs || !prs.length)
return;
const targetPr = prs.find(pr => pr.head.sha === sha);
if (!targetPr)
return;
const { number: issue_number } = targetPr;
// remove existing comment
const { data: comments } = await octokit.rest.issues.listComments({
owner,
repo,
issue_number
});
const targetComment = comments.find(comment => comment.body?.includes('__Proto_Coverage_Result__'));
if (targetComment) {
await octokit.rest.issues.deleteComment({
owner,
repo,
comment_id: targetComment.id
});
}
const coverages = [];
const formattedLogs = [];
for (const [packageName, methods] of Object.entries(result)) {
for (const [methodName, { status_codes }] of Object.entries(methods)) {
const { coverage, unchecked } = status_codes;
coverages.push(coverage);
formattedLogs.push(`| ${packageName} | ${methodName} | ${coverage}% | ${unchecked.join(', ')} |`);
}
}
const totalCoverage = Math.round(coverages.reduce((acc, cur) => acc + cur, 0) / coverages.length);
await octokit.rest.issues.createComment({
owner,
repo,
issue_number,
body: `

<details open>
<summary>Coverage Report</summary>
| Package | Method | Coverage | Unchecked Status |
| --- | --- | --- | --- |
${formattedLogs.join('\n')}
</details>
`.trim()
});
}
catch (e) {
console.error(e);
console.error('Failed to create PR comment');
}
}
getServiceProtoAbsolutePath(serviceProtoPath) {
if (serviceProtoPath.startsWith('<rootDir>')) {
return path.join(this.globalConfig.rootDir, serviceProtoPath.slice('<rootDir>'.length));
}
else {
return serviceProtoPath;
}
}
removeLogsDir() {
fs.rmSync(const_1.logsDirPath, { recursive: true });
}
}
exports.default = ProtoCoverageReporter;