lib/report/generateReport.ts (465 lines of code) (raw):
import * as fs from "fs";
import * as path from "path";
import * as Mustache from "mustache";
import {
OperationCoverageInfo,
OperationMeta,
TrafficValidationIssue,
TrafficValidationOptions,
} from "../swaggerValidator/trafficValidator";
import { LiveValidationIssue } from "../liveValidation/liveValidator";
import { FileLoader } from "../swagger/fileLoader";
import { OperationContext } from "../liveValidation/operationValidator";
export interface TrafficValidationIssueForRendering extends TrafficValidationIssue {
payloadFileLinkLabel?: string;
payloadFilePathWithPosition?: string;
errorsForRendering?: LiveValidationIssueForRendering[];
errorCodeLen: number;
}
export interface runtimeExceptionList {
payloadFilePath?: string;
code: string;
message: string;
specList: Array<{ specLabel: string; specLink: string }>;
}
export interface TrafficValidationIssueForRenderingInner {
generalErrorsInner: TrafficValidationIssueForRendering[];
errorCodeLen: number;
specFilePath?: string;
specFilePathWithPosition?: string;
operationInfo: OperationContext;
errorsForRendering: LiveValidationIssueForRendering[];
}
export interface LiveValidationIssueForRendering extends LiveValidationIssue {
friendlyName?: string;
link?: string;
payloadFilePath?: string | undefined;
payloadFilePathWithPosition?: string;
schemaPathWithPosition?: string;
payloadFileLinkLabel?: string | undefined;
}
export interface ErrorDefinitionDoc {
ErrorDefinitions: ErrorDefinition[];
}
export interface ErrorDefinition {
code: string;
friendlyName?: string;
link?: string;
}
export interface ValidationPassOperationsFormatInner extends OperationMeta {
readonly key: string;
}
export interface ValidationPassOperationsFormat {
readonly operationIdList: ValidationPassOperationsFormatInner[];
}
export interface OperationCoverageInfoForRendering extends OperationCoverageInfo {
specLinkLabel?: string;
validationPassOperations?: number;
validationPassOperationList: ValidationPassOperationsFormat[];
generalErrorsInnerList: TrafficValidationIssueForRenderingInner[];
}
export interface resultForRendering
extends OperationCoverageInfoForRendering,
TrafficValidationIssueForRendering {
index?: number;
}
export async function loadErrorDefinitions(): Promise<Map<string, ErrorDefinition>> {
const errorDefinitionDoc =
require("../../../documentation/error-definitions.json") as ErrorDefinitionDoc;
const errorsMap: Map<string, ErrorDefinition> = new Map();
errorDefinitionDoc.ErrorDefinitions.forEach((def) => {
errorsMap.set(def.code, def);
});
return errorsMap;
}
// used to pass data to the template rendering engine
export class CoverageView {
public package: string;
public language: string;
public apiVersion: string = "unknown";
public generatedDate: Date;
public markdownPath: string;
public markdown: string;
public undefinedOperationCount: number = 0;
public operationValidated: number = 0;
public operationFailed: number = 0;
public operationUnValidated: number = 0;
public generalErrorResults: Map<string, TrafficValidationIssue[]>;
public validationResultsForRendering: TrafficValidationIssueForRendering[] = [];
public coverageResultsForRendering: OperationCoverageInfoForRendering[] = [];
public resultsForRendering: resultForRendering[] = [];
private validationResults: TrafficValidationIssue[];
private sortedValidationResults: TrafficValidationIssue[];
private coverageResults: OperationCoverageInfo[];
private specLinkPrefix: string;
private payloadLinkPrefix: string;
private overrideLinkInReport: boolean;
private outputExceptionInReport: boolean;
public constructor(
validationResults: TrafficValidationIssue[],
coverageResults: OperationCoverageInfo[],
undefinedOperationCount: number = 0,
packageName: string = "",
language: string = "",
markdownPath: string = "",
overrideLinkInReport: boolean = false,
outputExceptionInReport: boolean = false,
specLinkPrefix: string = "",
payloadLinkPrefix: string = ""
) {
this.package = packageName;
this.markdownPath = markdownPath;
this.validationResults = validationResults;
this.coverageResults = coverageResults;
this.undefinedOperationCount = undefinedOperationCount;
this.generatedDate = new Date();
this.generalErrorResults = new Map();
this.language = language;
this.overrideLinkInReport = overrideLinkInReport;
this.outputExceptionInReport = outputExceptionInReport;
this.specLinkPrefix = specLinkPrefix;
this.payloadLinkPrefix = payloadLinkPrefix;
if (this.overrideLinkInReport === true) {
if (this.specLinkPrefix.endsWith("/")) {
this.specLinkPrefix = this.specLinkPrefix.substring(0, this.specLinkPrefix.length - 1);
}
if (this.payloadLinkPrefix.endsWith("/")) {
this.payloadLinkPrefix = this.payloadLinkPrefix.substring(
0,
this.payloadLinkPrefix.length - 1
);
}
}
this.setMetrics();
this.sortOperationIds();
}
public async prepareDataForRendering() {
try {
this.markdown = await this.readMarkdown();
const errorDefinitions = await loadErrorDefinitions();
let errorsForRendering: LiveValidationIssueForRendering[];
this.sortedValidationResults.forEach((element) => {
const payloadFile = element.payloadFilePath?.substring(
element.payloadFilePath.lastIndexOf("/") + 1
);
errorsForRendering = [];
element.errors?.forEach((error) => {
const errorDef = errorDefinitions.get(error.code);
errorsForRendering.push({
friendlyName: errorDef?.friendlyName ?? error.code,
link: errorDef?.link,
code: error.code,
message: error.message,
schemaPath: error.schemaPath,
schemaPathWithPosition: this.overrideLinkInReport
? `${this.specLinkPrefix}/${element.specFilePath?.substring(
element.specFilePath?.indexOf("specification")
)}#L${error.source.position.line}`
: `${element.specFilePath}#L${error.source.position.line}`,
pathsInPayload: error.pathsInPayload,
jsonPathsInPayload: error.jsonPathsInPayload,
severity: error.severity,
source: error.source,
params: error.params,
payloadFilePath: this.overrideLinkInReport
? `${this.payloadLinkPrefix}/${payloadFile}`
: element.payloadFilePath,
payloadFilePathWithPosition: this.overrideLinkInReport
? `${this.payloadLinkPrefix}/${payloadFile}#L${element.payloadFilePathPosition?.line}`
: `${element.payloadFilePath}#L${element.payloadFilePathPosition?.line}`,
payloadFileLinkLabel: payloadFile,
});
});
this.validationResultsForRendering.push({
payloadFilePath: this.overrideLinkInReport
? `${this.payloadLinkPrefix}/${payloadFile}`
: element.payloadFilePath,
payloadFileLinkLabel: payloadFile,
payloadFilePathWithPosition: this.overrideLinkInReport
? `${this.payloadLinkPrefix}/${payloadFile}#L${element.payloadFilePathPosition?.line}`
: `${element.payloadFilePath}#L${element.payloadFilePathPosition?.line}`,
errors: element.errors,
specFilePath: this.overrideLinkInReport
? `${this.specLinkPrefix}/${element.specFilePath?.substring(
element.specFilePath?.indexOf("specification")
)}`
: element.specFilePath,
errorsForRendering: errorsForRendering,
errorCodeLen: errorsForRendering.length,
operationInfo: element.operationInfo,
runtimeExceptions: element.runtimeExceptions,
});
});
const generalErrorsInnerOrigin = this.validationResultsForRendering.filter((x) => {
return x.errors && x.errors.length > 0;
});
this.coverageResults.forEach((element) => {
const specLink = this.overrideLinkInReport
? `${this.specLinkPrefix}/${element.spec?.substring(
element.spec?.indexOf("specification")
)}`
: `${element.spec}`;
let errorOperationIds = generalErrorsInnerOrigin.map(
(item) => item.operationInfo?.operationId
);
let passOperations: ValidationPassOperationsFormatInner[] = element.coveredOperationsList
.filter((item) => errorOperationIds.indexOf(item.operationId) === -1)
.map((item) => {
return {
key: item.operationId.split("_")[0],
operationId: item.operationId,
};
});
const passOperationsInnerList: ValidationPassOperationsFormatInner[][] = Object.values(
passOperations.reduce(
(res: { [key: string]: ValidationPassOperationsFormatInner[] }, item) => {
/* eslint-disable no-unused-expressions */
res[item.key] ? res[item.key].push(item) : (res[item.key] = [item]);
/* eslint-enable no-unused-expressions */
return res;
},
{}
)
);
const passOperationsListFormat: ValidationPassOperationsFormat[] = [];
passOperationsInnerList.forEach((element) => {
passOperationsListFormat.push({
operationIdList: element,
});
});
/**
* Sort untested operationId by bubble sort
* Controlling the results of localeCompare can set the sorting method
* X.localeCompare(Y) > 0 descending sort
* X.localeCompare(Y) < 0 ascending sort
*/
for (let i = 0; i < passOperationsListFormat.length - 1; i++) {
for (let j = 0; j < passOperationsListFormat.length - 1 - i; j++) {
if (
passOperationsListFormat[j].operationIdList[0].key.localeCompare(
passOperationsListFormat[j + 1].operationIdList[0].key
) > 0
) {
var temp = passOperationsListFormat[j];
passOperationsListFormat[j] = passOperationsListFormat[j + 1];
passOperationsListFormat[j + 1] = temp;
}
}
}
this.coverageResultsForRendering.push({
spec: specLink,
specLinkLabel: element.spec?.substring(element.spec?.lastIndexOf("/") + 1),
apiVersion: element.apiVersion,
coveredOperations: element.coveredOperations,
coveredOperationsList: element.coveredOperationsList,
validationPassOperations: element.coveredOperations - element.validationFailOperations,
validationPassOperationList: passOperationsListFormat,
validationFailOperations: element.validationFailOperations,
unCoveredOperations: element.unCoveredOperations,
unCoveredOperationsList: element.unCoveredOperationsList,
unCoveredOperationsListGen: element.unCoveredOperationsListGen,
totalOperations: element.totalOperations,
coverageRate: element.coverageRate,
generalErrorsInnerList: [],
});
});
this.resultsForRendering = this.coverageResultsForRendering.map((item) => {
const data = this.validationResultsForRendering.find(
(i) =>
i.specFilePath &&
item.spec.split(path.win32.sep).join(path.posix.sep).includes(i.specFilePath)
);
return {
...item,
...data,
} as any;
});
const generalErrorsInnerFormat: TrafficValidationIssueForRendering[][] = Object.values(
generalErrorsInnerOrigin.reduce(
(res: { [key: string]: TrafficValidationIssueForRendering[] }, item) => {
/* eslint-disable no-unused-expressions */
res[item!.operationInfo!.operationId + item!.specFilePath]
? res[item!.operationInfo!.operationId + item!.specFilePath].push(item)
: (res[item!.operationInfo!.operationId + item!.specFilePath] = [item]);
/* eslint-enable no-unused-expressions */
return res;
},
{}
)
);
const generalErrorsInnerList: TrafficValidationIssueForRenderingInner[] = [];
generalErrorsInnerFormat.forEach((element) => {
let errorCodeLen: number = 0;
element.forEach((item) => {
errorCodeLen = errorCodeLen + item.errorCodeLen;
});
let errorsForRendering: LiveValidationIssueForRendering[] = [];
element.forEach((item) => {
errorsForRendering = errorsForRendering.concat(item.errorsForRendering!);
});
generalErrorsInnerList.push({
generalErrorsInner: element,
errorCodeLen: errorCodeLen,
errorsForRendering: errorsForRendering,
operationInfo: element[0]!.operationInfo!,
specFilePath: this.overrideLinkInReport
? `${this.specLinkPrefix}/${element[0].specFilePath?.substring(
element[0].specFilePath?.indexOf("specification")
)}`
: element[0].specFilePath,
specFilePathWithPosition: this.overrideLinkInReport
? `${this.specLinkPrefix}/${element[0].specFilePath?.substring(
element[0].specFilePath?.indexOf("specification")
)}#L${element[0]!.operationInfo!.position!.line}`
: `${element[0].specFilePath}#L${element[0]!.operationInfo!.position!.line}`,
});
});
for (const [index, e] of this.resultsForRendering.entries()) {
e.index = index;
for (const i of generalErrorsInnerList) {
if (e.specFilePath === i.specFilePath && i) {
e.generalErrorsInnerList.push(i);
}
}
}
} catch (e) {
console.error(`Failed in prepareDataForRendering with err:${e?.stack};message:${e?.message}`);
}
}
private async readMarkdown() {
try {
const loader = new FileLoader({});
const res = await loader.load(this.markdownPath);
return res;
} catch (e) {
console.error(`Failed in read report.md file`);
return "";
}
}
private sortOperationIds() {
this.sortedValidationResults = this.validationResults.sort(function (op1, op2) {
const opId1 = op1.operationInfo!.operationId;
const opId2 = op2.operationInfo!.operationId;
if (opId1 < opId2) {
return -1;
}
if (opId1 > opId2) {
return 1;
}
return 0;
});
}
private setMetrics() {
if (this.coverageResults?.length > 0) {
this.apiVersion = this.coverageResults[0].apiVersion;
}
}
public formatGeneratedDate(): string {
const day = this.generatedDate.getDate();
const month = this.generatedDate.getMonth() + 1;
const year = this.generatedDate.getFullYear();
const hours = this.generatedDate.getHours();
const minutes = this.generatedDate.getMinutes();
return (
year +
"-" +
(month < 10 ? "0" + month : month) +
"-" +
(day < 10 ? "0" + day : day) +
" at " +
hours +
":" +
(minutes < 10 ? "0" + minutes : minutes) +
(hours < 13 ? "AM" : "PM")
);
}
public getTotalErrors(): number {
return this.validationResults.length;
}
public getGeneralErrors(): TrafficValidationIssueForRendering[] {
return this.validationResultsForRendering.filter((x) => {
return x.errors && x.errors.length > 0;
});
}
public getTotalGeneralErrors(): number {
return this.getGeneralErrors().length;
}
public getRunTimeErrors(): runtimeExceptionList[] {
if (this.outputExceptionInReport) {
const res = this.validationResults.filter((x) => {
return x.runtimeExceptions && x.runtimeExceptions.length > 0;
});
const resFormat: runtimeExceptionList[] = [];
res.forEach((element) => {
element.runtimeExceptions &&
element.runtimeExceptions.forEach((i) => {
const specList = i.spec;
const specListFormat: Array<{ specLabel: string; specLink: string }> = [];
specList &&
specList.forEach((k: string) => {
const specLink = this.overrideLinkInReport
? `${this.specLinkPrefix}/${k?.substring(k?.indexOf("specification"))}`
: k;
const specLabel = k?.substring(k?.lastIndexOf("/") + 1);
specListFormat.push({ specLabel, specLink });
});
resFormat.push({
code: i.code,
message: i.message,
payloadFilePath: element.payloadFilePath,
specList: specListFormat,
});
});
});
return resFormat;
} else {
return [];
}
}
public getTotalRunTimeErrors(): number {
return this.getRunTimeErrors().length;
}
}
export class ReportGenerator {
private sdkPackage: string;
private sdkLanguage: string;
private validationResults: TrafficValidationIssue[];
private coverageResults: OperationCoverageInfo[];
private undefinedOperationsCount: number;
private reportPath: string;
private overrideLinkInReport: boolean;
private outputExceptionInReport: boolean;
private specLinkPrefix: string;
private payloadLinkPrefix: string;
private markdownPath: string;
public constructor(
validationResults: TrafficValidationIssue[],
coverageResults: OperationCoverageInfo[],
undefinedOperationResults: number,
options: TrafficValidationOptions
) {
this.validationResults = validationResults;
this.coverageResults = coverageResults;
this.undefinedOperationsCount = undefinedOperationResults;
this.reportPath = path.resolve(process.cwd(), options.reportPath!);
this.sdkLanguage = options.sdkLanguage!;
this.sdkPackage = options.sdkPackage!;
this.markdownPath = options.markdownPath!;
this.overrideLinkInReport = options.overrideLinkInReport!;
this.outputExceptionInReport = options.outputExceptionInReport!;
this.specLinkPrefix = options.specLinkPrefix!;
this.payloadLinkPrefix = options.payloadLinkPrefix!;
}
public async generateHtmlReport() {
const templatePath = path.join(__dirname, "../templates/baseLayout.mustache");
const template = fs.readFileSync(templatePath, "utf-8");
const view = new CoverageView(
this.validationResults,
this.coverageResults,
this.undefinedOperationsCount,
this.sdkPackage,
this.sdkLanguage,
this.markdownPath,
this.overrideLinkInReport,
this.outputExceptionInReport,
this.specLinkPrefix,
this.payloadLinkPrefix
);
await view.prepareDataForRendering();
const general_errors = view.getGeneralErrors();
const runtime_errors = view.getRunTimeErrors();
console.log(JSON.stringify(general_errors, null, 2));
console.log(JSON.stringify(runtime_errors, null, 2));
const text = Mustache.render(template, view);
fs.writeFileSync(this.reportPath, text, "utf-8");
}
}