diff --git a/ChangeLog.md b/ChangeLog.md index 15c6daba..babb8dfd 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # Change Log - oav +## 09/12/2024 3.5.0 + +- Add names of `Passed Operations` in the HTML and JSON reports. + ## 07/12/2024 3.4.0 - During example generation, fields `password`, `adminPassword`, and `pwd` are all generated with a single value of "" instead of random characters. diff --git a/lib/apiScenario/coverageCalculator.ts b/lib/apiScenario/coverageCalculator.ts index f9b2e454..d9c8f0f2 100644 --- a/lib/apiScenario/coverageCalculator.ts +++ b/lib/apiScenario/coverageCalculator.ts @@ -10,6 +10,7 @@ export interface OperationCoverageResult { coveredOperationNumber: number; totalOperationNumber: number; coverage: number; + coveredOperationIds: string[]; uncoveredOperationIds: string[]; } @@ -22,6 +23,7 @@ export class CoverageCalculator { coveredOperationNumber: 0, totalOperationNumber: 0, coverage: 0, + coveredOperationIds: [], uncoveredOperationIds: [], }; @@ -55,6 +57,7 @@ export class CoverageCalculator { allOperationIds.size === 0 ? 0 : coverageOperationIds.size / allOperationIds.size; ret.coveredOperationNumber = coverageOperationIds.size; ret.totalOperationNumber = allOperationIds.size; + ret.coveredOperationIds = [...coverageOperationIds]; const difference = [...allOperationIds].filter((x) => !coverageOperationIds.has(x)); ret.uncoveredOperationIds = difference; return ret; @@ -70,6 +73,7 @@ export class CoverageCalculator { coveredOperationNumber: 0, totalOperationNumber: 0, coverage: 0, + coveredOperationIds: [], uncoveredOperationIds: [], }; @@ -102,6 +106,7 @@ export class CoverageCalculator { allOperationIds.size === 0 ? 0 : coverageOperationIds.size / allOperationIds.size; result.coveredOperationNumber = coverageOperationIds.size; result.totalOperationNumber = allOperationIds.size; + result.coveredOperationIds = [...coverageOperationIds]; const difference = [...allOperationIds].filter((x) => !coverageOperationIds.has(x)); result.uncoveredOperationIds = difference; diff --git a/lib/apiScenario/postmanCollectionGenerator.ts b/lib/apiScenario/postmanCollectionGenerator.ts index ca8a7170..ad2154f6 100644 --- a/lib/apiScenario/postmanCollectionGenerator.ts +++ b/lib/apiScenario/postmanCollectionGenerator.ts @@ -376,6 +376,9 @@ class PostmanCollectionRunner { apiVersion: getApiVersionFromFilePath(specPath), unCoveredOperations: result.uncoveredOperationIds.length, coveredOperations: result.totalOperationNumber - result.uncoveredOperationIds.length, + coveredOperationsList: result.coveredOperationIds.map((id) => { + return { operationId: id }; + }), validationFailOperations: new Set( trafficValidationResult .filter((it) => key.indexOf(it.specFilePath!) !== -1 && it.errors!.length > 0) diff --git a/lib/report/generateReport.ts b/lib/report/generateReport.ts index cf5af99c..81560e62 100644 --- a/lib/report/generateReport.ts +++ b/lib/report/generateReport.ts @@ -3,6 +3,7 @@ import * as path from "path"; import * as Mustache from "mustache"; import { OperationCoverageInfo, + OperationMeta, TrafficValidationIssue, TrafficValidationOptions, } from "../swaggerValidator/trafficValidator"; @@ -52,9 +53,18 @@ export interface ErrorDefinition { 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[]; } @@ -201,18 +211,77 @@ export class CoverageView { 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, @@ -235,9 +304,6 @@ export class CoverageView { } as any; }); - const generalErrorsInnerOrigin = this.validationResultsForRendering.filter((x) => { - return x.errors && x.errors.length > 0; - }); const generalErrorsInnerFormat: TrafficValidationIssueForRendering[][] = Object.values( generalErrorsInnerOrigin.reduce( (res: { [key: string]: TrafficValidationIssueForRendering[] }, item) => { diff --git a/lib/swaggerValidator/trafficValidator.ts b/lib/swaggerValidator/trafficValidator.ts index c92887df..93c68d58 100644 --- a/lib/swaggerValidator/trafficValidator.ts +++ b/lib/swaggerValidator/trafficValidator.ts @@ -53,6 +53,7 @@ export interface OperationCoverageInfo { readonly spec: string; readonly apiVersion: string; readonly coveredOperations: number; + readonly coveredOperationsList: OperationMeta[]; readonly validationFailOperations: number; readonly unCoveredOperations: number; readonly unCoveredOperationsList: OperationMeta[]; @@ -290,11 +291,13 @@ export class TrafficValidator { let coveredOperations: number; let coverageRate: number; let validationFailOperations: number; + let coveredOperationsList: OperationMeta[]; let unCoveredOperationsList: unCoveredOperationsFormatInner[]; this.operationSpecMapper.forEach((value: string[], key: string) => { // identify the spec has been match traffic file let isMatch: boolean = true; const unCoveredOperationsListFormat: unCoveredOperationsFormat[] = []; + coveredOperationsList = []; unCoveredOperationsList = []; if (this.trafficOperation.get(key) === undefined) { coveredOperations = 0; @@ -308,6 +311,7 @@ export class TrafficValidator { this.coverageData.set(key, coverageRate); const unValidatedOperations = [...value]; validatedOperations!.forEach((element) => { + coveredOperationsList.push({ operationId: element }); unValidatedOperations.splice(unValidatedOperations.indexOf(element), 1); }); unValidatedOperations.forEach((element) => { @@ -356,6 +360,18 @@ export class TrafficValidator { return 0; }); + const sortedCoveredOperationsList = coveredOperationsList.sort(function (op1, op2) { + const opId1 = op1.operationId; + const opId2 = op2.operationId; + if (opId1 < opId2) { + return -1; + } + if (opId1 > opId2) { + return 1; + } + return 0; + }); + /** * Sort untested operationId by bubble sort * Controlling the results of localeCompare can set the sorting method @@ -385,6 +401,7 @@ export class TrafficValidator { unCoveredOperations: value.length - coveredOperations, totalOperations: value.length, validationFailOperations: validationFailOperations, + coveredOperationsList: sortedCoveredOperationsList, unCoveredOperationsList: sortedUnCoveredOperationsList, unCoveredOperationsListGen: unCoveredOperationsListFormat, }); diff --git a/lib/templates/baseLayout.mustache b/lib/templates/baseLayout.mustache index 4d2adb62..38d660e7 100644 --- a/lib/templates/baseLayout.mustache +++ b/lib/templates/baseLayout.mustache @@ -280,7 +280,52 @@ white-space: nowrap; } + .testPassed { + margin-bottom: 40px; + } + + .testPassed table { + border: none; + } + + .testPassed .table-body { + border: none; + } + + .testPassed .table-body>table>tbody>tr>td { + border: none; + } + + .testPassed .table-body table tbody tr td { + border-bottom: 1px solid var(--border-color); + } + + .testPassed table tbody tr td:nth-child(-n + 2) { + text-align: center; + word-break: break-all; + } + + .testPassed .operationTag { + text-align: left; + } + + .testPassed .operationTag .tag { + color: var(--report-base-colo); + background-color: var(--report-bg-blue); + border: 1px solid var(--report-bg-blue); + display: inline-block; + height: 32px; + padding: 0 10px; + margin: 4px 2px; + line-height: 30px; + font-size: 14px; + border-radius: 4px; + box-sizing: border-box; + white-space: nowrap; + } + .notTested table, + .testPassed table, .testFailed table, .runtimeExceptions table, .innerTable table { @@ -556,7 +601,11 @@ ( {{validationFailOperations}} ) - Pass ( {{validationPassOperations}} ) + + Pass + ( {{validationPassOperations}} ) + +
{{specLinkLabel}} @@ -667,6 +716,33 @@ +
+

Passed Operations ( {{validationPassOperations}} )

+
+
+ + + + + + + {{#validationPassOperationList}} + + + + {{/validationPassOperationList}} + +
+
+ {{#operationIdList}} + {{operationId}} + {{/operationIdList}} +
+
+
+
+
+
{{/resultsForRendering}} diff --git a/lib/validate.ts b/lib/validate.ts index e89af612..7a37db7c 100644 --- a/lib/validate.ts +++ b/lib/validate.ts @@ -235,6 +235,18 @@ export async function validateTraffic( operationIds: item.unCoveredOperationsList.map((opeartion) => opeartion.operationId), }; }), + passedOperationsList: validator.operationCoverageResult.map((item) => { + return { + spec: item.spec, + operationIds: item.coveredOperationsList + .map((opeartion) => opeartion.operationId) + .filter((id) => { + return !trafficValidationResult.some( + (error) => error.operationInfo?.operationId === id + ); + }), + }; + }), failedOperations: validator.operationCoverageResult .map((item) => item.validationFailOperations) .reduce((a, b) => a + b, 0), diff --git a/package-lock.json b/package-lock.json index 1f3ef3ce..2b3563e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oav", - "version": "3.4.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oav", - "version": "3.4.0", + "version": "3.5.0", "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", diff --git a/package.json b/package.json index 6cb3f8f4..69bcbd1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oav", - "version": "3.4.0", + "version": "3.5.0", "author": { "name": "Microsoft Corporation", "email": "azsdkteam@microsoft.com", diff --git a/test/__snapshots__/trafficValidatorTests.ts.snap b/test/__snapshots__/trafficValidatorTests.ts.snap index 6f5c85d9..d6df1e7e 100644 --- a/test/__snapshots__/trafficValidatorTests.ts.snap +++ b/test/__snapshots__/trafficValidatorTests.ts.snap @@ -7,6 +7,14 @@ Object { "apiVersion": "2019-02-02", "coverageRate": 0.14285714285714285, "coveredOperations": 2, + "coveredOperationsList": Array [ + Object { + "operationId": "Table_Delete", + }, + Object { + "operationId": "Table_Query", + }, + ], "totalOperations": 14, "unCoveredOperations": 12, "unCoveredOperationsList": Array [