lib/apiScenario/markdownReport.ts (200 lines of code) (raw):
import { readFileSync } from "fs";
import path from "path";
import Handlebars from "handlebars";
import * as hd from "humanize-duration";
import moment from "moment";
import {
LiveValidationIssue,
RequestResponseLiveValidationResult,
} from "../liveValidation/liveValidator";
import { RuntimeError, StepResult, ApiScenarioTestResult } from "./newmanReportValidator";
const spaceReg = /(\n|\t|\r)/gi;
const getErrorCodeDocLink = (code: string): string => {
return `https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/api-scenario/references/ErrorCodeReference.md#${code}`;
};
function getOavErrorCodeDocLink(code: string) {
return `https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/Semantic-and-Model-Violations-Reference.md#${code}`;
}
const commonHelper = (opts: HelperOpts) => ({
renderPlain: (s: string) => s,
renderWhitespace: (n: number) => " ".repeat(n),
renderUri: (s: string) => `${path.join(opts.swaggerRootDir, s)}`,
renderSymbol: (result: ResultState) => `${resultStateSymbol[result]}`,
renderScenarioTitle: (ts: ApiScenarioMarkdownResult) => {
let s = `${ts.testScenarioName}`;
if (ts.failedStepsCount <= 0 && ts.fatalStepsCount <= 0) {
return s;
}
s = `${s}: `;
if (ts.fatalStepsCount > 0) {
s = `${s}${ts.fatalStepsCount} Fatal Step(s)`;
}
if (ts.fatalStepsCount > 0 && ts.failedStepsCount > 0) {
s = `${s}, `;
}
if (ts.failedStepsCount > 0) {
s = `${s} ${ts.failedStepsCount} Failed Step(s)`;
}
return s;
},
renderStepTitle: (ts: ApiScenarioMarkdownStepResult) => {
let s = `${ts.stepName}`;
if (ts.failedErrorsCount <= 0 && ts.fatalErrorsCount <= 0) {
return s;
}
s = `${s}: `;
if (ts.fatalErrorsCount > 0) {
s = `${s}${ts.fatalErrorsCount} Fatal Error(s)`;
}
if (ts.fatalErrorsCount > 0 && ts.failedErrorsCount > 0) {
s = `${s}, `;
}
if (ts.failedErrorsCount > 0) {
s = `${s} ${ts.failedErrorsCount} Validation Error(s)`;
}
return s;
},
renderDuration: (start: Date, end: Date) =>
`${hd.default(moment.duration(moment(end).diff(moment(start))).asMilliseconds())}`,
renderResponseTime: (responseTime: number) => `${hd.default(responseTime)}`,
shouldReportError: (sr: ApiScenarioMarkdownStepResult) =>
sr.failedErrorsCount + sr.fatalErrorsCount > 0,
renderFatalErrorCode: (e: RuntimeError) => `[${e.code}](${getErrorCodeDocLink(e.code)})`,
renderFatalErrorDetail: (e: RuntimeError) => `${e.message.replace(spaceReg, " ")}`,
renderLiveValidationErrorCode: (e: LiveValidationIssue) =>
`[${e.code}](${getOavErrorCodeDocLink(e.code)})`,
renderLiveValidationErrorDetail: (e: LiveValidationIssue) =>
`${e.message.replace(spaceReg, " ")}`,
shouldReportPayload: (e: string) => e !== undefined && e !== "",
});
type ResultState = keyof typeof ResultStateStrings;
const ResultStateStrings = {
fatal: `Fatal`,
failed: `Failed`,
succeeded: `Succeeded`,
warning: `Warning`,
};
export const resultStateSymbol: { [key in ResultState]: string } = {
fatal: "❌",
failed: "❌",
succeeded: "️✔️",
warning: "⚠️",
};
interface ApiScenarioMarkdownStepResult {
stepName: string;
result: ResultState;
exampleFilePath?: string;
payloadPath?: string;
correlationId?: string;
operationId: string;
responseTime?: number;
statusCode?: number;
fatalErrorsCount: number;
failedErrorsCount: number;
warningErrorsCount: number;
runtimeError?: RuntimeError[];
liveValidationResult?: RequestResponseLiveValidationResult;
}
interface ApiScenarioMarkdownResult {
testScenarioName: string;
result: ResultState;
swaggerFilePaths: string[];
startTime: Date;
endTime: Date;
runId: string;
fatalStepsCount: number;
failedStepsCount: number;
warningStepsCount: number;
steps: ApiScenarioMarkdownStepResult[];
}
interface HelperOpts {
swaggerRootDir: "root";
}
export const compileHandlebarsTemplate = <T>(fileName: string, opts: HelperOpts) => {
const generationViewTemplate = readFileSync(
path.join(__dirname, "templates", fileName)
).toString();
const templateDelegate = Handlebars.compile<T>(generationViewTemplate, { noEscape: true });
const helpers = commonHelper(opts);
return (data: T) => templateDelegate(data, { helpers });
};
const generateMarkdownReportView = compileHandlebarsTemplate<ApiScenarioMarkdownResult>(
"markdownReport.handlebars",
{
swaggerRootDir: "root",
}
);
const generateJUnitCaseReportView = compileHandlebarsTemplate<ApiScenarioMarkdownStepResult>(
"junitCaseReport.handlebars",
{
swaggerRootDir: "root",
}
);
const stepIsFatal = (sr: StepResult) => sr.runtimeError && sr.runtimeError.length > 0;
const stepIsFailed = (sr: StepResult) =>
(sr.liveValidationResult && sr.liveValidationResult.requestValidationResult.errors.length > 0) ||
(sr.liveValidationResult && sr.liveValidationResult.responseValidationResult.errors.length > 0) ||
(sr.liveValidationForLroFinalGetResult &&
sr.liveValidationForLroFinalGetResult.responseValidationResult.errors.length > 0) ||
(sr.roundtripValidationResult && sr.roundtripValidationResult.errors.length > 0);
const asMarkdownStepResult = (sr: StepResult): ApiScenarioMarkdownStepResult => {
let result: ResultState = "succeeded";
if (stepIsFatal(sr)) {
result = "fatal";
} else if (stepIsFailed(sr)) {
result = "failed";
}
const failedErrorsCount =
(sr.liveValidationResult ? sr.liveValidationResult.requestValidationResult.errors.length : 0) +
(sr.liveValidationResult ? sr.liveValidationResult.responseValidationResult.errors.length : 0) +
(sr.liveValidationForLroFinalGetResult
? sr.liveValidationForLroFinalGetResult.responseValidationResult.errors.length
: 0) +
(sr.roundtripValidationResult ? sr.roundtripValidationResult.errors.length : 0);
const r: ApiScenarioMarkdownStepResult = {
result,
fatalErrorsCount: sr.runtimeError ? sr.runtimeError.length : 0,
failedErrorsCount: failedErrorsCount,
warningErrorsCount: 0,
...sr,
};
return r;
};
const asMarkdownResult = (tsr: ApiScenarioTestResult): ApiScenarioMarkdownResult => {
const fatalCount = tsr.stepResult.filter(
(sr) => sr.runtimeError && sr.runtimeError.length > 0
).length;
const errorCount = tsr.stepResult.filter((sr) => stepIsFailed(sr)).length;
let resultState: ResultState = "succeeded";
if (fatalCount > 0) {
resultState = "fatal";
} else if (errorCount > 0) {
resultState = "failed";
} else {
resultState = "succeeded";
}
const r: ApiScenarioMarkdownResult = {
testScenarioName: tsr.apiScenarioName!,
result: resultState,
swaggerFilePaths: tsr.swaggerFilePaths,
startTime: new Date(tsr.startTime!),
endTime: new Date(tsr.endTime!),
runId: tsr.runId!,
fatalStepsCount: fatalCount,
failedStepsCount: errorCount,
warningStepsCount: 0,
steps: tsr.stepResult.map(asMarkdownStepResult),
};
return r;
};
export const generateMarkdownReportHeader = (): string => "<h3>Azure API Test Report</h3>";
export const generateMarkdownReport = (testScenarioResult: ApiScenarioTestResult): string => {
const result = asMarkdownResult(testScenarioResult);
const body = generateMarkdownReportView(result);
return body;
};
export const generateJUnitCaseReport = (sr: StepResult): string => {
const result = asMarkdownStepResult(sr);
const body = generateJUnitCaseReportView(result);
return body;
};