+9
-4
| { | ||
| "name": "ctrf-cli", | ||
| "version": "0.0.1", | ||
| "description": "Various CTRF utilities available by command line", | ||
| "version": "0.0.2", | ||
| "description": "Various CTRF utilities available from the command line", | ||
| "main": "dist/index.js", | ||
@@ -10,3 +10,4 @@ "bin": { | ||
| "scripts": { | ||
| "build": "tsc" | ||
| "build": "tsc", | ||
| "all": "npm run build" | ||
| }, | ||
@@ -19,4 +20,8 @@ "files": [ | ||
| "type": "git", | ||
| "url": "https://github.com/ctrf-io/ctrf-cli" | ||
| "url": "git+https://github.com/ctrf-io/ctrf-cli.git" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "provenance": true | ||
| }, | ||
| "homepage": "https://ctrf.io", | ||
@@ -23,0 +28,0 @@ "author": "Matthew Thomas", |
+5
-3
| # CTRF CLI | ||
| Various CTRF utilities available by command line | ||
| Various CTRF utilities available from the command line | ||
@@ -22,5 +22,7 @@ <div align="center"> | ||
| <p style="font-size: 14px; margin: 1rem 0;"> | ||
| Maintained by <a href="https://github.com/ma11hewthomas">Matthew Thomas</a><br/> | ||
| Contributions are very welcome! <br/> | ||
| Explore more <a href="https://www.ctrf.io/integrations">integrations</a> | ||
| Explore more <a href="https://www.ctrf.io/integrations">integrations</a> <br/> | ||
| <a href="https://app.formbricks.com/s/cmefs524mhlh1tl01gkpvefrb">Let us know your thoughts</a> | ||
| </p> | ||
@@ -27,0 +29,0 @@ </div> |
| #!/usr/bin/env node | ||
| export {}; |
-60
| #!/usr/bin/env node | ||
| "use strict"; | ||
| var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
| function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
| return new (P || (P = Promise))(function (resolve, reject) { | ||
| function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
| function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
| function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
| step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
| }); | ||
| }; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const yargs_1 = __importDefault(require("yargs/yargs")); | ||
| const helpers_1 = require("yargs/helpers"); | ||
| const merge_1 = require("./merge"); | ||
| const flaky_1 = require("./flaky"); | ||
| const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) | ||
| .command('merge <directory>', 'Merge CTRF reports into a single report', (yargs) => { | ||
| return yargs | ||
| .positional('directory', { | ||
| describe: 'Directory of the CTRF reports', | ||
| type: 'string', | ||
| demandOption: true, | ||
| }) | ||
| .option('output', { | ||
| alias: 'o', | ||
| describe: 'Output file name for merged report', | ||
| type: 'string', | ||
| default: 'ctrf-report.json', | ||
| }) | ||
| .option('output-dir', { | ||
| alias: 'd', | ||
| describe: 'Output directory for merged report', | ||
| type: 'string', | ||
| }) | ||
| .option('keep-reports', { | ||
| alias: 'k', | ||
| describe: 'Keep existing reports after merging', | ||
| type: 'boolean', | ||
| default: false, | ||
| }); | ||
| }, (argv) => __awaiter(void 0, void 0, void 0, function* () { | ||
| yield (0, merge_1.mergeReports)(argv.directory, argv.output, argv['output-dir'], argv['keep-reports']); | ||
| })) | ||
| .command('flaky <file>', 'Identify flaky tests from a CTRF report file', (yargs) => { | ||
| return yargs | ||
| .positional('file', { | ||
| describe: 'CTRF report file', | ||
| type: 'string', | ||
| demandOption: true, | ||
| }); | ||
| }, (argv) => __awaiter(void 0, void 0, void 0, function* () { | ||
| yield (0, flaky_1.identifyFlakyTests)(argv.file); | ||
| })) | ||
| .help() | ||
| .demandCommand(1, 'You need at least one command before moving on') | ||
| .argv; |
| export declare const CTRF_REPORT_FORMAT = "ctrf"; | ||
| export declare const CTRF_SPEC_VERSION = "0.0.0"; |
| export const CTRF_REPORT_FORMAT = 'ctrf'; | ||
| export const CTRF_SPEC_VERSION = '0.0.0'; |
| export declare function identifyFlakyTests(filePath: string): Promise<void>; |
| "use strict"; | ||
| var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
| function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
| return new (P || (P = Promise))(function (resolve, reject) { | ||
| function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
| function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
| function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
| step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
| }); | ||
| }; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.identifyFlakyTests = void 0; | ||
| const fs_1 = __importDefault(require("fs")); | ||
| const path_1 = __importDefault(require("path")); | ||
| function identifyFlakyTests(filePath) { | ||
| return __awaiter(this, void 0, void 0, function* () { | ||
| try { | ||
| const resolvedFilePath = path_1.default.resolve(filePath); | ||
| if (!fs_1.default.existsSync(resolvedFilePath)) { | ||
| console.error(`The file ${resolvedFilePath} does not exist.`); | ||
| return; | ||
| } | ||
| const fileContent = fs_1.default.readFileSync(resolvedFilePath, 'utf8'); | ||
| const report = JSON.parse(fileContent); | ||
| const flakyTests = report.results.tests.filter((test) => test.flaky === true); | ||
| if (flakyTests.length > 0) { | ||
| console.log(`Found ${flakyTests.length} flaky test(s):`); | ||
| flakyTests.forEach((test) => { | ||
| console.log(`- Test Name: ${test.name}, Retries: ${test.retries}`); | ||
| }); | ||
| } | ||
| else { | ||
| console.log(`No flaky tests found in ${resolvedFilePath}.`); | ||
| } | ||
| } | ||
| catch (error) { | ||
| console.error('Error identifying flaky tests:', error); | ||
| } | ||
| }); | ||
| } | ||
| exports.identifyFlakyTests = identifyFlakyTests; |
| export { mergeReports } from './methods/merge-reports'; | ||
| export { readReportsFromDirectory } from './methods/read-reports'; | ||
| export { readReportsFromGlobPattern } from './methods/read-reports'; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.readReportsFromGlobPattern = exports.readReportsFromDirectory = exports.mergeReports = void 0; | ||
| var merge_reports_1 = require("./methods/merge-reports"); | ||
| Object.defineProperty(exports, "mergeReports", { enumerable: true, get: function () { return merge_reports_1.mergeReports; } }); | ||
| var read_reports_1 = require("./methods/read-reports"); | ||
| Object.defineProperty(exports, "readReportsFromDirectory", { enumerable: true, get: function () { return read_reports_1.readReportsFromDirectory; } }); | ||
| var read_reports_2 = require("./methods/read-reports"); | ||
| Object.defineProperty(exports, "readReportsFromGlobPattern", { enumerable: true, get: function () { return read_reports_2.readReportsFromGlobPattern; } }); |
| export declare function mergeReports(directory: string, output: string, outputDir: string, keepReports: boolean): Promise<void>; |
| "use strict"; | ||
| var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
| function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
| return new (P || (P = Promise))(function (resolve, reject) { | ||
| function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
| function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
| function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
| step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
| }); | ||
| }; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.mergeReports = void 0; | ||
| const fs_1 = __importDefault(require("fs")); | ||
| const path_1 = __importDefault(require("path")); | ||
| function mergeReports(directory, output, outputDir, keepReports) { | ||
| return __awaiter(this, void 0, void 0, function* () { | ||
| try { | ||
| const directoryPath = path_1.default.resolve(directory); | ||
| const outputFileName = output; | ||
| const resolvedOutputDir = outputDir ? path_1.default.resolve(outputDir) : directoryPath; | ||
| const outputPath = path_1.default.join(resolvedOutputDir, outputFileName); | ||
| console.log("Merging CTRF reports..."); | ||
| const files = fs_1.default.readdirSync(directoryPath); | ||
| files.forEach((file) => { | ||
| console.log('Found file:', file); | ||
| }); | ||
| const ctrfReportFiles = files.filter((file) => { | ||
| try { | ||
| if (path_1.default.extname(file) !== '.json') { | ||
| console.log(`Skipping non-CTRF file: ${file}`); | ||
| return false; | ||
| } | ||
| const filePath = path_1.default.join(directoryPath, file); | ||
| const fileContent = fs_1.default.readFileSync(filePath, 'utf8'); | ||
| const jsonData = JSON.parse(fileContent); | ||
| if (!jsonData.hasOwnProperty('results')) { | ||
| console.log(`Skipping non-CTRF file: ${file}`); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| catch (error) { | ||
| console.error(`Error reading JSON file '${file}':`, error); | ||
| return false; | ||
| } | ||
| }); | ||
| if (ctrfReportFiles.length === 0) { | ||
| console.log('No CTRF reports found in the specified directory.'); | ||
| return; | ||
| } | ||
| if (!fs_1.default.existsSync(resolvedOutputDir)) { | ||
| fs_1.default.mkdirSync(resolvedOutputDir, { recursive: true }); | ||
| console.log(`Created output directory: ${resolvedOutputDir}`); | ||
| } | ||
| const mergedReport = ctrfReportFiles | ||
| .map((file) => { | ||
| console.log("Merging report:", file); | ||
| const filePath = path_1.default.join(directoryPath, file); | ||
| const fileContent = fs_1.default.readFileSync(filePath, 'utf8'); | ||
| return JSON.parse(fileContent); | ||
| }) | ||
| .reduce((acc, curr) => { | ||
| if (!acc.results) { | ||
| return curr; | ||
| } | ||
| acc.results.summary.tests += curr.results.summary.tests; | ||
| acc.results.summary.passed += curr.results.summary.passed; | ||
| acc.results.summary.failed += curr.results.summary.failed; | ||
| acc.results.summary.skipped += curr.results.summary.skipped; | ||
| acc.results.summary.pending += curr.results.summary.pending; | ||
| acc.results.summary.other += curr.results.summary.other; | ||
| acc.results.tests.push(...curr.results.tests); | ||
| acc.results.summary.start = Math.min(acc.results.summary.start, curr.results.summary.start); | ||
| acc.results.summary.stop = Math.max(acc.results.summary.stop, curr.results.summary.stop); | ||
| return acc; | ||
| }, { results: null }); | ||
| fs_1.default.writeFileSync(outputPath, JSON.stringify(mergedReport, null, 2)); | ||
| if (!keepReports) { | ||
| ctrfReportFiles.forEach((file) => { | ||
| const filePath = path_1.default.join(directoryPath, file); | ||
| if (file !== outputFileName) { | ||
| fs_1.default.unlinkSync(filePath); | ||
| } | ||
| }); | ||
| } | ||
| console.log('CTRF reports merged successfully.'); | ||
| console.log(`Merged report saved to: ${outputPath}`); | ||
| } | ||
| catch (error) { | ||
| console.error('Error merging CTRF reports:', error); | ||
| } | ||
| }); | ||
| } | ||
| exports.mergeReports = mergeReports; |
| import { CtrfReport } from "../../types/ctrf"; | ||
| /** | ||
| * Merges multiple CTRF reports into a single report. | ||
| * | ||
| * @param reports Array of CTRF report objects to be merged. | ||
| * @returns The merged CTRF report object. | ||
| */ | ||
| export declare function mergeReports(reports: CtrfReport[]): CtrfReport; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.mergeReports = void 0; | ||
| /** | ||
| * Merges multiple CTRF reports into a single report. | ||
| * | ||
| * @param reports Array of CTRF report objects to be merged. | ||
| * @returns The merged CTRF report object. | ||
| */ | ||
| function mergeReports(reports) { | ||
| if (!reports || reports.length === 0) { | ||
| throw new Error('No reports provided for merging.'); | ||
| } | ||
| const mergedReport = { | ||
| results: { | ||
| tool: reports[0].results.tool, | ||
| summary: initializeEmptySummary(), | ||
| tests: [], | ||
| }, | ||
| }; | ||
| reports.forEach((report) => { | ||
| const { summary, tests, environment, extra } = report.results; | ||
| mergedReport.results.summary.tests += summary.tests; | ||
| mergedReport.results.summary.passed += summary.passed; | ||
| mergedReport.results.summary.failed += summary.failed; | ||
| mergedReport.results.summary.skipped += summary.skipped; | ||
| mergedReport.results.summary.pending += summary.pending; | ||
| mergedReport.results.summary.other += summary.other; | ||
| if (summary.suites !== undefined) { | ||
| mergedReport.results.summary.suites = | ||
| (mergedReport.results.summary.suites || 0) + summary.suites; | ||
| } | ||
| mergedReport.results.summary.start = Math.min(mergedReport.results.summary.start, summary.start); | ||
| mergedReport.results.summary.stop = Math.max(mergedReport.results.summary.stop, summary.stop); | ||
| mergedReport.results.tests.push(...tests); | ||
| if (environment) { | ||
| mergedReport.results.environment = Object.assign(Object.assign({}, mergedReport.results.environment), environment); | ||
| } | ||
| if (extra) { | ||
| mergedReport.results.extra = Object.assign(Object.assign({}, mergedReport.results.extra), extra); | ||
| } | ||
| }); | ||
| return mergedReport; | ||
| } | ||
| exports.mergeReports = mergeReports; | ||
| /** | ||
| * Initializes an empty summary object. | ||
| * | ||
| * @returns An empty Summary object. | ||
| */ | ||
| function initializeEmptySummary() { | ||
| return { | ||
| tests: 0, | ||
| passed: 0, | ||
| failed: 0, | ||
| skipped: 0, | ||
| pending: 0, | ||
| other: 0, | ||
| start: Number.MAX_SAFE_INTEGER, | ||
| stop: 0, | ||
| }; | ||
| } |
| import { CtrfReport } from '../../types/ctrf'; | ||
| /** | ||
| * Reads a single CTRF report file from a specified path. | ||
| * | ||
| * @param filePath Path to the JSON file containing the CTRF report. | ||
| * @returns The parsed `CtrfReport` object. | ||
| * @throws If the file does not exist, is not a valid JSON, or does not conform to the `CtrfReport` structure. | ||
| */ | ||
| export declare function readSingleReport(filePath: string): CtrfReport; | ||
| /** | ||
| * Reads all CTRF report files from a given directory. | ||
| * | ||
| * @param directory Path to the directory containing JSON files. | ||
| * @returns An array of parsed `CtrfReport` objects. | ||
| * @throws If the directory does not exist or no valid CTRF reports are found. | ||
| */ | ||
| export declare function readReportsFromDirectory(directoryPath: string): CtrfReport[]; | ||
| /** | ||
| * Reads all CTRF report files matching a glob pattern. | ||
| * | ||
| * @param pattern The glob pattern to match files (e.g., ctrf/*.json). | ||
| * @returns An array of parsed `CtrfReport` objects. | ||
| * @throws If no valid CTRF reports are found. | ||
| */ | ||
| export declare function readReportsFromGlobPattern(pattern: string): CtrfReport[]; |
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.readReportsFromGlobPattern = exports.readReportsFromDirectory = exports.readSingleReport = void 0; | ||
| // TO BE REMOVED, WILL USE THE CTRF LIBRARY INSTEAD | ||
| const fs_1 = __importDefault(require("fs")); | ||
| const path_1 = __importDefault(require("path")); | ||
| const glob_1 = require("glob"); | ||
| /** | ||
| * Reads a single CTRF report file from a specified path. | ||
| * | ||
| * @param filePath Path to the JSON file containing the CTRF report. | ||
| * @returns The parsed `CtrfReport` object. | ||
| * @throws If the file does not exist, is not a valid JSON, or does not conform to the `CtrfReport` structure. | ||
| */ | ||
| function readSingleReport(filePath) { | ||
| if (!fs_1.default.existsSync(filePath)) { | ||
| throw new Error(`JSON file not found: ${filePath}`); | ||
| } | ||
| const resolvedPath = path_1.default.resolve(filePath); | ||
| if (!fs_1.default.existsSync(resolvedPath)) { | ||
| throw new Error(`The file '${resolvedPath}' does not exist.`); | ||
| } | ||
| try { | ||
| const content = fs_1.default.readFileSync(resolvedPath, 'utf8'); | ||
| const parsed = JSON.parse(content); | ||
| if (!isCtrfReport(parsed)) { | ||
| throw new Error(`The file '${resolvedPath}' is not a valid CTRF report.`); | ||
| } | ||
| return parsed; | ||
| } | ||
| catch (error) { | ||
| const errorMessage = error.message || 'Unknown error'; | ||
| throw new Error(`Failed to read or parse the file '${resolvedPath}': ${errorMessage}`); | ||
| } | ||
| } | ||
| exports.readSingleReport = readSingleReport; | ||
| /** | ||
| * Reads all CTRF report files from a given directory. | ||
| * | ||
| * @param directory Path to the directory containing JSON files. | ||
| * @returns An array of parsed `CtrfReport` objects. | ||
| * @throws If the directory does not exist or no valid CTRF reports are found. | ||
| */ | ||
| function readReportsFromDirectory(directoryPath) { | ||
| directoryPath = path_1.default.resolve(directoryPath); | ||
| if (!fs_1.default.existsSync(directoryPath)) { | ||
| throw new Error(`The directory '${directoryPath}' does not exist.`); | ||
| } | ||
| const files = fs_1.default.readdirSync(directoryPath); | ||
| const reports = files | ||
| .filter((file) => path_1.default.extname(file) === '.json') | ||
| .map((file) => { | ||
| const filePath = path_1.default.join(directoryPath, file); | ||
| try { | ||
| const content = fs_1.default.readFileSync(filePath, 'utf8'); | ||
| const parsed = JSON.parse(content); | ||
| if (!isCtrfReport(parsed)) { | ||
| console.warn(`Skipping invalid CTRF report file: ${file}`); | ||
| return null; | ||
| } | ||
| return parsed; | ||
| } | ||
| catch (error) { | ||
| console.warn(`Failed to read or parse file '${file}':`, error); | ||
| return null; | ||
| } | ||
| }) | ||
| .filter((report) => report !== null); | ||
| if (reports.length === 0) { | ||
| throw new Error(`No valid CTRF reports found in the directory '${directoryPath}'.`); | ||
| } | ||
| return reports; | ||
| } | ||
| exports.readReportsFromDirectory = readReportsFromDirectory; | ||
| /** | ||
| * Reads all CTRF report files matching a glob pattern. | ||
| * | ||
| * @param pattern The glob pattern to match files (e.g., ctrf/*.json). | ||
| * @returns An array of parsed `CtrfReport` objects. | ||
| * @throws If no valid CTRF reports are found. | ||
| */ | ||
| function readReportsFromGlobPattern(pattern) { | ||
| const files = glob_1.glob.sync(pattern); | ||
| if (files.length === 0) { | ||
| throw new Error(`No files found matching the pattern '${pattern}'.`); | ||
| } | ||
| const reports = files | ||
| .map((file) => { | ||
| try { | ||
| const content = fs_1.default.readFileSync(file, 'utf8'); | ||
| const parsed = JSON.parse(content); | ||
| if (!isCtrfReport(parsed)) { | ||
| console.warn(`Skipping invalid CTRF report file: ${file}`); | ||
| return null; | ||
| } | ||
| return parsed; | ||
| } | ||
| catch (error) { | ||
| console.warn(`Failed to read or parse file '${file}':`, error); | ||
| return null; | ||
| } | ||
| }) | ||
| .filter((report) => report !== null); | ||
| if (reports.length === 0) { | ||
| throw new Error(`No valid CTRF reports found matching the pattern '${pattern}'.`); | ||
| } | ||
| return reports; | ||
| } | ||
| exports.readReportsFromGlobPattern = readReportsFromGlobPattern; | ||
| /** | ||
| * Checks if an object conforms to the `CtrfReport` structure. | ||
| * | ||
| * @param obj The object to validate. | ||
| * @returns `true` if the object matches the `CtrfReport` type; otherwise, `false`. | ||
| */ | ||
| function isCtrfReport(obj) { | ||
| return (obj && | ||
| typeof obj === 'object' && | ||
| obj.results && | ||
| Array.isArray(obj.results.tests) && | ||
| typeof obj.results.summary === 'object' && | ||
| typeof obj.results.tool === 'object'); | ||
| } |
| import { InsightsMetric, Report, Test } from "../../types/ctrf.js"; | ||
| export interface SimplifiedTestData { | ||
| name: string; | ||
| suite?: string; | ||
| filePath?: string; | ||
| } | ||
| /** | ||
| * Utility function that determines if a test is flaky based on its retries and status. | ||
| * | ||
| * @param test - The CTRF test to evaluate. | ||
| * @returns `true` if the test is considered flaky, otherwise `false`. | ||
| */ | ||
| export declare function isTestFlaky(test: Test): boolean; | ||
| /** | ||
| * Utility function that formats a ratio (0-1) as a percentage string for display. | ||
| * | ||
| * @param ratio - The ratio to format (0-1) | ||
| * @param decimals - Number of decimal places (default: 2) | ||
| * @returns Formatted percentage string (e.g., "25.50%") | ||
| */ | ||
| export declare function formatAsPercentage(ratio: number, decimals?: number): string; | ||
| /** | ||
| * Utility function that formats an InsightsMetric as percentage strings for display. | ||
| * | ||
| * @param metric - The insights metric to format | ||
| * @param decimals - Number of decimal places (default: 2) | ||
| * @returns Object with formatted percentage strings | ||
| */ | ||
| export declare function formatInsightsMetricAsPercentage(metric: InsightsMetric, decimals?: number): { | ||
| current: string; | ||
| previous: string; | ||
| change: string; | ||
| }; | ||
| /** | ||
| * @param currentReport - The current CTRF report to enrich | ||
| * @param previousReports - Array of historical CTRF reports (ordered newest to oldest) | ||
| * @param baseline - Optional baseline specification: | ||
| * - undefined: Use most recent previous report (default) | ||
| * - number: Use report at this index in previousReports array (0 = most recent) | ||
| * - string: Use reportId | ||
| * @returns The current report fully enriched with run-level insights, test-level insights, and baseline comparisons | ||
| */ | ||
| export declare function enrichReportWithInsights(currentReport: Report, previousReports?: Report[], baseline?: number | string): Report; |
| // ======================================== | ||
| // UTILITY FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Utility function that determines if a test is flaky based on its retries and status. | ||
| * | ||
| * @param test - The CTRF test to evaluate. | ||
| * @returns `true` if the test is considered flaky, otherwise `false`. | ||
| */ | ||
| export function isTestFlaky(test) { | ||
| return (test.flaky || | ||
| (test.retries && test.retries > 0 && test.status === 'passed') || | ||
| false); | ||
| } | ||
| /** | ||
| * Utility function that formats a ratio (0-1) as a percentage string for display. | ||
| * | ||
| * @param ratio - The ratio to format (0-1) | ||
| * @param decimals - Number of decimal places (default: 2) | ||
| * @returns Formatted percentage string (e.g., "25.50%") | ||
| */ | ||
| export function formatAsPercentage(ratio, decimals = 2) { | ||
| return `${(ratio * 100).toFixed(decimals)}%`; | ||
| } | ||
| /** | ||
| * Utility function that formats an InsightsMetric as percentage strings for display. | ||
| * | ||
| * @param metric - The insights metric to format | ||
| * @param decimals - Number of decimal places (default: 2) | ||
| * @returns Object with formatted percentage strings | ||
| */ | ||
| export function formatInsightsMetricAsPercentage(metric, decimals = 2) { | ||
| return { | ||
| current: formatAsPercentage(metric.current, decimals), | ||
| previous: formatAsPercentage(metric.previous, decimals), | ||
| change: `${metric.change >= 0 ? '+' : ''}${formatAsPercentage(metric.change, decimals)}` | ||
| }; | ||
| } | ||
| /** | ||
| * Calculates the 95th percentile from an array of numbers. | ||
| * | ||
| * @param values - Array of numeric values | ||
| * @returns The 95th percentile value | ||
| */ | ||
| function calculateP95(values) { | ||
| if (values.length === 0) | ||
| return 0; | ||
| const sorted = [...values].sort((a, b) => a - b); | ||
| const index = Math.ceil(sorted.length * 0.95) - 1; | ||
| return Number(sorted[Math.max(0, index)].toFixed(2)); | ||
| } | ||
| /** | ||
| * Helper function to validate that reports have the necessary data for insights calculation. | ||
| */ | ||
| function validateReportForInsights(report) { | ||
| return !!(report?.results?.tests && Array.isArray(report.results.tests)); | ||
| } | ||
| /** | ||
| * Aggregates test metrics across multiple reports. | ||
| */ | ||
| function aggregateTestMetricsAcrossReports(reports) { | ||
| const metricsMap = new Map(); | ||
| for (let reportIndex = 0; reportIndex < reports.length; reportIndex++) { | ||
| const report = reports[reportIndex]; | ||
| if (!validateReportForInsights(report)) | ||
| continue; | ||
| for (const test of report.results.tests) { | ||
| const isPassed = test.status === 'passed'; | ||
| const isFailed = test.status === 'failed'; | ||
| const isSkipped = test.status === 'skipped'; | ||
| const isPending = test.status === 'pending'; | ||
| const isOther = test.status === 'other'; | ||
| const testName = test.name; | ||
| if (!metricsMap.has(testName)) { | ||
| metricsMap.set(testName, { | ||
| totalAttempts: 0, | ||
| totalAttemptsFailed: 0, | ||
| totalResults: 0, | ||
| totalResultsFailed: 0, | ||
| totalResultsPassed: 0, | ||
| totalResultsSkipped: 0, | ||
| totalResultsFlaky: 0, | ||
| totalAttemptsFlaky: 0, | ||
| totalResultsDuration: 0, | ||
| appearsInRuns: 0, | ||
| reportsAnalyzed: 0, | ||
| durations: [] | ||
| }); | ||
| } | ||
| const metrics = metricsMap.get(testName); | ||
| metrics.totalResults += 1; | ||
| metrics.totalAttempts += 1 + (test.retries || 0); | ||
| metrics.totalAttemptsFailed += test.retries || 0; | ||
| if (isFailed) { | ||
| metrics.totalResultsFailed += 1; | ||
| metrics.totalAttemptsFailed += 1 + (test.retries || 0); | ||
| } | ||
| else if (isPassed) { | ||
| metrics.totalResultsPassed += 1; | ||
| } | ||
| else if (isSkipped) { | ||
| metrics.totalResultsSkipped += 1; | ||
| } | ||
| else if (isPending) { | ||
| metrics.totalResultsSkipped += 1; | ||
| } | ||
| else if (isOther) { | ||
| metrics.totalResultsSkipped += 1; | ||
| } | ||
| if (isTestFlaky(test)) { | ||
| metrics.totalResultsFlaky += 1; | ||
| metrics.totalAttemptsFlaky += test.retries || 0; | ||
| } | ||
| metrics.totalResultsDuration += test.duration || 0; | ||
| metrics.durations.push(test.duration || 0); | ||
| } | ||
| const testsInThisReport = new Set(); | ||
| for (const test of report.results.tests) { | ||
| testsInThisReport.add(test.name); | ||
| } | ||
| for (const testName of testsInThisReport) { | ||
| const metrics = metricsMap.get(testName); | ||
| metrics.appearsInRuns += 1; | ||
| } | ||
| } | ||
| return metricsMap; | ||
| } | ||
| /** | ||
| * Consolidates all test-level metrics into overall run-level metrics. | ||
| */ | ||
| function consolidateTestMetricsToRunMetrics(metricsMap) { | ||
| let totalAttempts = 0; | ||
| let totalAttemptsFailed = 0; | ||
| let totalResults = 0; | ||
| let totalResultsFailed = 0; | ||
| let totalResultsPassed = 0; | ||
| let totalResultsSkipped = 0; | ||
| let totalResultsFlaky = 0; | ||
| let totalAttemptsFlaky = 0; | ||
| let totalResultsDuration = 0; | ||
| for (const metrics of metricsMap.values()) { | ||
| totalAttempts += metrics.totalAttempts; | ||
| totalAttemptsFailed += metrics.totalAttemptsFailed; | ||
| totalResults += metrics.totalResults; | ||
| totalResultsFailed += metrics.totalResultsFailed; | ||
| totalResultsPassed += metrics.totalResultsPassed; | ||
| totalResultsSkipped += metrics.totalResultsSkipped; | ||
| totalResultsFlaky += metrics.totalResultsFlaky; | ||
| totalAttemptsFlaky += metrics.totalAttemptsFlaky; | ||
| totalResultsDuration += metrics.totalResultsDuration; | ||
| } | ||
| return { | ||
| totalAttempts, | ||
| totalAttemptsFailed, | ||
| totalResults, | ||
| totalResultsFailed, | ||
| totalResultsPassed, | ||
| totalResultsSkipped, | ||
| totalResultsFlaky, | ||
| totalAttemptsFlaky, | ||
| totalResultsDuration, | ||
| reportsAnalyzed: metricsMap.size | ||
| }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Flaky Rate FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates overall flaky rate from consolidated run metrics. | ||
| * Flaky rate = (failed attempts from flaky tests) / (total attempts) as ratio 0-1 | ||
| */ | ||
| function calculateFlakyRateFromMetrics(runMetrics) { | ||
| if (runMetrics.totalAttempts === 0) { | ||
| return 0; | ||
| } | ||
| return Number((runMetrics.totalAttemptsFlaky / runMetrics.totalAttempts).toFixed(4)); | ||
| } | ||
| /** | ||
| * Internal helper function that calculates flaky rate insights across all reports (current + all previous). | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns InsightsMetric with current value calculated across all reports | ||
| */ | ||
| function calculateInsightFlakyRateCurrent(currentReport, previousReports) { | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const current = calculateFlakyRateFromMetrics(runMetrics); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Fail Rate FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates overall fail rate from consolidated run metrics. | ||
| * Fail rate = (totalResultsFailed / totalResults) as ratio 0-1 | ||
| */ | ||
| function calculateFailRateFromMetrics(runMetrics) { | ||
| if (runMetrics.totalResults === 0) { | ||
| return 0; | ||
| } | ||
| return Number((runMetrics.totalResultsFailed / runMetrics.totalResults).toFixed(4)); | ||
| } | ||
| /** | ||
| * Internal helper function that calculates fail rate insights across all reports (current + all previous). | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns InsightsMetric with current value calculated across all reports | ||
| */ | ||
| function calculateFailRateInsight(currentReport, previousReports) { | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const current = calculateFailRateFromMetrics(runMetrics); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Skipped Rate FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates overall skipped rate from consolidated run metrics. | ||
| * Skipped rate = (totalResultsSkipped / totalResults) as ratio 0-1 | ||
| */ | ||
| function calculateSkippedRateFromMetrics(runMetrics) { | ||
| if (runMetrics.totalResults === 0) { | ||
| return 0; | ||
| } | ||
| return Number((runMetrics.totalResultsSkipped / runMetrics.totalResults).toFixed(4)); | ||
| } | ||
| /** | ||
| * Internal helper function that calculates skipped rate insights across all reports (current + all previous). | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns InsightsMetric with current value calculated across all reports | ||
| */ | ||
| function calculateSkippedRateInsight(currentReport, previousReports) { | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const current = calculateSkippedRateFromMetrics(runMetrics); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Average Test Duration FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates average test duration from consolidated run metrics. | ||
| * Average test duration = (totalDuration / totalResults) | ||
| */ | ||
| function calculateAverageTestDurationFromMetrics(runMetrics) { | ||
| if (runMetrics.totalResults === 0) { | ||
| return 0; | ||
| } | ||
| return Number((runMetrics.totalResultsDuration / runMetrics.totalResults).toFixed(2)); | ||
| } | ||
| /** | ||
| * Internal helper function that calculates average test duration insights across all reports (current + all previous). | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns InsightsMetric with current value calculated across all reports | ||
| */ | ||
| function calculateAverageTestDurationInsight(currentReport, previousReports) { | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const current = calculateAverageTestDurationFromMetrics(runMetrics); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Average Run Duration FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates average run duration from consolidated run metrics. | ||
| * Average run duration = (totalDuration / reportsAnalyzed) | ||
| */ | ||
| function calculateAverageRunDurationFromMetrics(runMetrics) { | ||
| if (runMetrics.reportsAnalyzed === 0) { | ||
| return 0; | ||
| } | ||
| return Number((runMetrics.totalResultsDuration / runMetrics.reportsAnalyzed).toFixed(2)); | ||
| } | ||
| /** | ||
| * Internal helper function that calculates average run duration insights across all reports (current + all previous). | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns InsightsMetric with current value calculated across all reports | ||
| */ | ||
| function calculateAverageRunDurationInsight(currentReport, previousReports) { | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const current = calculateAverageRunDurationFromMetrics(runMetrics); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| // ======================================== | ||
| // INSIGHT Current FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Internal helper function that recursively calculates insights for each report based on all reports that came before it chronologically. | ||
| * Only sets the `current` field for each report - `previous` and `change` are calculated later. | ||
| * | ||
| * @param reports - Array of CTRF reports in reverse chronological order (newest first) | ||
| * @param index - Current index being processed (default: 0) | ||
| * @returns The reports array with insights populated for each report | ||
| */ | ||
| function calculateRunInsights(reports, index = 0) { | ||
| if (index >= reports.length) { | ||
| return reports; | ||
| } | ||
| const currentReport = reports[index]; | ||
| const previousReports = reports.slice(index + 1); // Reports that came before this one in time | ||
| const allReportsUpToThisPoint = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReportsUpToThisPoint); | ||
| const runMetrics = consolidateTestMetricsToRunMetrics(testMetrics); | ||
| const { reportsAnalyzed, ...relevantMetrics } = runMetrics; | ||
| currentReport.insights = { | ||
| flakyRate: { | ||
| current: calculateFlakyRateFromMetrics(runMetrics), | ||
| previous: 0, | ||
| change: 0 | ||
| }, | ||
| failRate: { | ||
| current: calculateFailRateFromMetrics(runMetrics), | ||
| previous: 0, | ||
| change: 0 | ||
| }, | ||
| skippedRate: { | ||
| current: calculateSkippedRateFromMetrics(runMetrics), | ||
| previous: 0, | ||
| change: 0 | ||
| }, | ||
| averageTestDuration: { | ||
| current: calculateAverageTestDurationFromMetrics(runMetrics), | ||
| previous: 0, | ||
| change: 0 | ||
| }, | ||
| averageRunDuration: { | ||
| current: calculateAverageRunDurationFromMetrics(runMetrics), | ||
| previous: 0, | ||
| change: 0 | ||
| }, | ||
| reportsAnalyzed: allReportsUpToThisPoint.length, | ||
| extra: relevantMetrics | ||
| }; | ||
| return calculateRunInsights(reports, index + 1); | ||
| } | ||
| // ======================================== | ||
| // TEST-LEVEL INSIGHTS FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates test-level flaky rate for a specific test. | ||
| */ | ||
| function calculateTestFlakyRate(testName, testMetrics) { | ||
| const current = testMetrics.totalAttempts === 0 ? 0 : | ||
| Number((testMetrics.totalAttemptsFlaky / testMetrics.totalAttempts).toFixed(4)); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| /** | ||
| * Calculates test-level fail rate for a specific test. | ||
| */ | ||
| function calculateTestFailRate(testName, testMetrics) { | ||
| const current = testMetrics.totalResults === 0 ? 0 : | ||
| Number((testMetrics.totalResultsFailed / testMetrics.totalResults).toFixed(4)); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| /** | ||
| * Calculates test-level skipped rate for a specific test. | ||
| */ | ||
| function calculateTestSkippedRate(testName, testMetrics) { | ||
| const current = testMetrics.totalResults === 0 ? 0 : | ||
| Number((testMetrics.totalResultsSkipped / testMetrics.totalResults).toFixed(4)); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| /** | ||
| * Calculates test-level average duration for a specific test. | ||
| */ | ||
| function calculateTestAverageDuration(testName, testMetrics) { | ||
| const current = testMetrics.totalResults === 0 ? 0 : | ||
| Number((testMetrics.totalResultsDuration / testMetrics.totalResults).toFixed(2)); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| /** | ||
| * Calculates test-level p95 duration for a specific test. | ||
| */ | ||
| function calculateTestP95Duration(testName, testMetrics) { | ||
| const current = calculateP95(testMetrics.durations); | ||
| return { current, previous: 0, change: 0 }; | ||
| } | ||
| /** | ||
| * Calculates test-level insights for a specific test. | ||
| */ | ||
| function calculateTestInsights(testName, testMetrics) { | ||
| const { appearsInRuns, reportsAnalyzed, ...relevantMetrics } = testMetrics; | ||
| return { | ||
| flakyRate: calculateTestFlakyRate(testName, testMetrics), | ||
| failRate: calculateTestFailRate(testName, testMetrics), | ||
| skippedRate: calculateTestSkippedRate(testName, testMetrics), | ||
| averageTestDuration: calculateTestAverageDuration(testName, testMetrics), | ||
| p95Duration: calculateTestP95Duration(testName, testMetrics), | ||
| appearsInRuns: testMetrics.appearsInRuns, | ||
| extra: relevantMetrics | ||
| }; | ||
| } | ||
| /** | ||
| * Internal helper function that adds test-level insights to all tests in the current report. | ||
| * | ||
| * @param currentReport - The current CTRF report to add insights to | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @returns The current report with test-level insights added to each test | ||
| */ | ||
| function addTestInsightsToCurrentReport(currentReport, previousReports) { | ||
| if (!validateReportForInsights(currentReport)) { | ||
| return currentReport; | ||
| } | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const testMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| const reportWithInsights = { | ||
| ...currentReport, | ||
| results: { | ||
| ...currentReport.results, | ||
| tests: currentReport.results.tests.map(test => { | ||
| const testName = test.name; | ||
| const metrics = testMetrics.get(testName); | ||
| if (metrics) { | ||
| const testInsights = calculateTestInsights(testName, metrics); | ||
| return { | ||
| ...test, | ||
| insights: testInsights | ||
| }; | ||
| } | ||
| return test; | ||
| }) | ||
| } | ||
| }; | ||
| return reportWithInsights; | ||
| } | ||
| // ======================================== | ||
| // BASELINE INSIGHTS FUNCTIONS | ||
| // ======================================== | ||
| /** | ||
| * Calculates test-level insights with baseline comparison for a specific test. | ||
| * | ||
| * @param testName - Name of the test | ||
| * @param currentTestMetrics - Current aggregated test metrics | ||
| * @param baselineTestMetrics - Baseline aggregated test metrics (optional) | ||
| * @returns TestInsights with current, previous, and change values | ||
| */ | ||
| function calculateTestInsightsWithBaseline(testName, currentTestMetrics, baselineTestMetrics) { | ||
| const currentFlakyRate = currentTestMetrics.totalAttempts === 0 ? 0 : | ||
| Number((currentTestMetrics.totalAttemptsFlaky / currentTestMetrics.totalAttempts).toFixed(4)); | ||
| const currentFailRate = currentTestMetrics.totalResults === 0 ? 0 : | ||
| Number((currentTestMetrics.totalResultsFailed / currentTestMetrics.totalResults).toFixed(4)); | ||
| const currentSkippedRate = currentTestMetrics.totalResults === 0 ? 0 : | ||
| Number((currentTestMetrics.totalResultsSkipped / currentTestMetrics.totalResults).toFixed(4)); | ||
| const currentAverageDuration = currentTestMetrics.totalResults === 0 ? 0 : | ||
| Number((currentTestMetrics.totalResultsDuration / currentTestMetrics.totalResults).toFixed(2)); | ||
| const currentP95Duration = calculateP95(currentTestMetrics.durations); | ||
| let baselineFlakyRate = 0; | ||
| let baselineFailRate = 0; | ||
| let baselineSkippedRate = 0; | ||
| let baselineAverageDuration = 0; | ||
| let baselineP95Duration = 0; | ||
| if (baselineTestMetrics) { | ||
| baselineFlakyRate = baselineTestMetrics.totalAttempts === 0 ? 0 : | ||
| Number((baselineTestMetrics.totalAttemptsFlaky / baselineTestMetrics.totalAttempts).toFixed(4)); | ||
| baselineFailRate = baselineTestMetrics.totalResults === 0 ? 0 : | ||
| Number((baselineTestMetrics.totalResultsFailed / baselineTestMetrics.totalResults).toFixed(4)); | ||
| baselineSkippedRate = baselineTestMetrics.totalResults === 0 ? 0 : | ||
| Number((baselineTestMetrics.totalResultsSkipped / baselineTestMetrics.totalResults).toFixed(4)); | ||
| baselineAverageDuration = baselineTestMetrics.totalResults === 0 ? 0 : | ||
| Number((baselineTestMetrics.totalResultsDuration / baselineTestMetrics.totalResults).toFixed(2)); | ||
| baselineP95Duration = calculateP95(baselineTestMetrics.durations); | ||
| } | ||
| const { appearsInRuns, reportsAnalyzed, ...relevantMetrics } = currentTestMetrics; | ||
| return { | ||
| flakyRate: { | ||
| current: currentFlakyRate, | ||
| previous: baselineFlakyRate, | ||
| change: Number((currentFlakyRate - baselineFlakyRate).toFixed(4)) | ||
| }, | ||
| failRate: { | ||
| current: currentFailRate, | ||
| previous: baselineFailRate, | ||
| change: Number((currentFailRate - baselineFailRate).toFixed(4)) | ||
| }, | ||
| skippedRate: { | ||
| current: currentSkippedRate, | ||
| previous: baselineSkippedRate, | ||
| change: Number((currentSkippedRate - baselineSkippedRate).toFixed(4)) | ||
| }, | ||
| averageTestDuration: { | ||
| current: currentAverageDuration, | ||
| previous: baselineAverageDuration, | ||
| change: Number((currentAverageDuration - baselineAverageDuration).toFixed(2)) | ||
| }, | ||
| p95Duration: { | ||
| current: currentP95Duration, | ||
| previous: baselineP95Duration, | ||
| change: Number((currentP95Duration - baselineP95Duration).toFixed(2)) | ||
| }, | ||
| appearsInRuns: currentTestMetrics.appearsInRuns, | ||
| extra: relevantMetrics | ||
| }; | ||
| } | ||
| /** | ||
| * Internal helper function that adds test-level insights with baseline comparison to all tests in the current report. | ||
| * | ||
| * @param currentReport - The current CTRF report to add insights to | ||
| * @param previousReports - Array of historical CTRF reports | ||
| * @param baselineReport - The baseline report to compare against (optional) | ||
| * @returns The current report with test-level insights (including baseline comparisons) added to each test | ||
| */ | ||
| function addTestInsightsWithBaselineToCurrentReport(currentReport, previousReports, baselineReport) { | ||
| if (!validateReportForInsights(currentReport)) { | ||
| return currentReport; | ||
| } | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const currentTestMetrics = aggregateTestMetricsAcrossReports(allReports); | ||
| let baselineTestMetrics; | ||
| if (baselineReport && validateReportForInsights(baselineReport)) { | ||
| const baselineIndex = previousReports.findIndex(report => report.results?.summary?.start === baselineReport.results?.summary?.start); | ||
| if (baselineIndex >= 0) { | ||
| const reportsUpToBaseline = previousReports.slice(baselineIndex); | ||
| baselineTestMetrics = aggregateTestMetricsAcrossReports(reportsUpToBaseline); | ||
| } | ||
| } | ||
| const reportWithInsights = { | ||
| ...currentReport, | ||
| results: { | ||
| ...currentReport.results, | ||
| tests: currentReport.results.tests.map(test => { | ||
| const testName = test.name; | ||
| const currentMetrics = currentTestMetrics.get(testName); | ||
| if (currentMetrics) { | ||
| const baselineMetrics = baselineTestMetrics?.get(testName); | ||
| const testInsights = calculateTestInsightsWithBaseline(testName, currentMetrics, baselineMetrics); | ||
| return { | ||
| ...test, | ||
| insights: testInsights | ||
| }; | ||
| } | ||
| return test; | ||
| }) | ||
| } | ||
| }; | ||
| return reportWithInsights; | ||
| } | ||
| /** | ||
| * Internal helper function that calculates baseline report-level insights using existing insights from current and previous reports. | ||
| * Both reports should already have their insights populated. | ||
| * | ||
| * @param currentReport - The current CTRF report with insights | ||
| * @param previousReport - The previous CTRF report with insights | ||
| * @returns Insights with current, previous, and change values calculated | ||
| */ | ||
| function calculateReportInsightsBaseline(currentReport, baslineReport) { | ||
| const currentInsights = currentReport.insights; | ||
| const previousInsights = baslineReport.insights; | ||
| if (!currentInsights || !previousInsights) { | ||
| console.log('Both reports must have insights populated'); | ||
| return currentReport.insights; | ||
| } | ||
| return { | ||
| flakyRate: { | ||
| current: currentInsights.flakyRate.current, | ||
| previous: previousInsights.flakyRate.current, | ||
| change: Number((currentInsights.flakyRate.current - previousInsights.flakyRate.current).toFixed(4)) | ||
| }, | ||
| failRate: { | ||
| current: currentInsights.failRate.current, | ||
| previous: previousInsights.failRate.current, | ||
| change: Number((currentInsights.failRate.current - previousInsights.failRate.current).toFixed(4)) | ||
| }, | ||
| skippedRate: { | ||
| current: currentInsights.skippedRate.current, | ||
| previous: previousInsights.skippedRate.current, | ||
| change: Number((currentInsights.skippedRate.current - previousInsights.skippedRate.current).toFixed(4)) | ||
| }, | ||
| averageTestDuration: { | ||
| current: currentInsights.averageTestDuration.current, | ||
| previous: previousInsights.averageTestDuration.current, | ||
| change: Number((currentInsights.averageTestDuration.current - previousInsights.averageTestDuration.current).toFixed(2)) | ||
| }, | ||
| averageRunDuration: { | ||
| current: currentInsights.averageRunDuration.current, | ||
| previous: previousInsights.averageRunDuration.current, | ||
| change: Number((currentInsights.averageRunDuration.current - previousInsights.averageRunDuration.current).toFixed(2)) | ||
| }, | ||
| reportsAnalyzed: currentInsights.reportsAnalyzed, | ||
| extra: currentInsights.extra | ||
| }; | ||
| } | ||
| /** | ||
| * Internal helper function that gets test details for tests that have been removed since the baseline report. | ||
| * A test is considered removed if it exists in the baseline report but not in the current report. | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param baselineReport - The baseline CTRF report to compare against | ||
| * @returns Array of CtrfTest objects that were removed since baseline | ||
| */ | ||
| function getTestsRemovedSinceBaseline(currentReport, baselineReport) { | ||
| if (!validateReportForInsights(currentReport) || !validateReportForInsights(baselineReport)) { | ||
| return []; | ||
| } | ||
| const currentTestNames = new Set(currentReport.results.tests.map(test => test.name)); | ||
| const removedTests = baselineReport.results.tests.filter(test => !currentTestNames.has(test.name)); | ||
| return removedTests.map(test => ({ | ||
| name: test.name, | ||
| suite: test.suite, | ||
| filePath: test.filePath | ||
| })); | ||
| } | ||
| /** | ||
| * Internal helper function that gets test details for tests that have been added since the baseline report. | ||
| * A test is considered added if it exists in the current report but not in the baseline report. | ||
| * | ||
| * @param currentReport - The current CTRF report | ||
| * @param baselineReport - The baseline CTRF report to compare against | ||
| * @returns Array of CtrfTest objects that were added since baseline | ||
| */ | ||
| function getTestsAddedSinceBaseline(currentReport, baselineReport) { | ||
| if (!validateReportForInsights(currentReport) || !validateReportForInsights(baselineReport)) { | ||
| return []; | ||
| } | ||
| const baselineTestNames = new Set(baselineReport.results.tests.map(test => test.name)); | ||
| const addedTests = currentReport.results.tests.filter(test => !baselineTestNames.has(test.name)); | ||
| return addedTests.map(test => ({ | ||
| name: test.name, | ||
| suite: test.suite, | ||
| filePath: test.filePath | ||
| })); | ||
| } | ||
| /** | ||
| * Internal helper function that sets the removed tests array to insights.extra.testsRemoved. | ||
| * Calculates which tests were removed since the baseline and adds them to the insights extra data. | ||
| * | ||
| * @param insights - The insights object to modify | ||
| * @param currentReport - The current CTRF report | ||
| * @param baselineReport - The baseline CTRF report to compare against | ||
| * @returns The insights object with testsRemoved added to extra | ||
| */ | ||
| function setTestsRemovedToInsights(insights, currentReport, baselineReport) { | ||
| const removedTests = getTestsRemovedSinceBaseline(currentReport, baselineReport); | ||
| return { | ||
| ...insights, | ||
| extra: { | ||
| ...insights.extra, | ||
| testsRemoved: removedTests | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Internal helper function that sets the added tests array to insights.extra.testsAdded. | ||
| * Calculates which tests were added since the baseline and adds them to the insights extra data. | ||
| * | ||
| * @param insights - The insights object to modify | ||
| * @param currentReport - The current CTRF report | ||
| * @param baselineReport - The baseline CTRF report to compare against | ||
| * @returns The insights object with testsAdded added to extra | ||
| */ | ||
| function setTestsAddedToInsights(insights, currentReport, baselineReport) { | ||
| const addedTests = getTestsAddedSinceBaseline(currentReport, baselineReport); | ||
| return { | ||
| ...insights, | ||
| extra: { | ||
| ...insights.extra, | ||
| testsAdded: addedTests | ||
| } | ||
| }; | ||
| } | ||
| // ======================================== | ||
| // MAIN CONSUMER API FUNCTION | ||
| // ======================================== | ||
| /** | ||
| * Helper function to find the baseline report based on the baseline parameter. | ||
| * | ||
| * @param reportsWithRunInsights - Array of reports with run insights (current first, then previous) | ||
| * @param baseline - Baseline specification (undefined, number, or string) | ||
| * @returns The baseline report to use for comparison, or null if not found | ||
| */ | ||
| function findBaselineReport(reportsWithRunInsights, baseline) { | ||
| if (baseline === undefined) { | ||
| return reportsWithRunInsights[1] || null; | ||
| } | ||
| if (typeof baseline === 'number') { | ||
| const targetIndex = baseline + 1; | ||
| if (targetIndex < reportsWithRunInsights.length) { | ||
| return reportsWithRunInsights[targetIndex]; | ||
| } | ||
| console.warn(`Baseline index ${baseline} is out of range. Available previous reports: ${reportsWithRunInsights.length - 1}`); | ||
| return null; | ||
| } | ||
| if (typeof baseline === 'string') { | ||
| const report = reportsWithRunInsights.find(report => report.results?.summary?.start?.toString() === baseline); | ||
| if (!report) { | ||
| console.warn(`No report found with start timestamp ID: ${baseline}`); | ||
| return null; | ||
| } | ||
| return report; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * @param currentReport - The current CTRF report to enrich | ||
| * @param previousReports - Array of historical CTRF reports (ordered newest to oldest) | ||
| * @param baseline - Optional baseline specification: | ||
| * - undefined: Use most recent previous report (default) | ||
| * - number: Use report at this index in previousReports array (0 = most recent) | ||
| * - string: Use reportId | ||
| * @returns The current report fully enriched with run-level insights, test-level insights, and baseline comparisons | ||
| */ | ||
| export function enrichReportWithInsights(currentReport, previousReports = [], baseline) { | ||
| if (!validateReportForInsights(currentReport)) { | ||
| console.warn('Current report is not valid for insights calculation'); | ||
| return currentReport; | ||
| } | ||
| const allReports = [currentReport, ...previousReports]; | ||
| const reportsWithRunInsights = calculateRunInsights([...allReports]); | ||
| const baselineReport = previousReports.length > 0 ? | ||
| findBaselineReport(reportsWithRunInsights, baseline) : null; | ||
| const currentReportWithRunInsights = reportsWithRunInsights[0]; // Current report is first | ||
| const currentReportWithTestInsights = addTestInsightsWithBaselineToCurrentReport(currentReportWithRunInsights, previousReports, baselineReport || undefined); | ||
| if (previousReports.length === 0) { | ||
| return currentReportWithTestInsights; | ||
| } | ||
| if (!baselineReport) { | ||
| return currentReportWithTestInsights; | ||
| } | ||
| let baselineInsights = calculateReportInsightsBaseline(currentReportWithTestInsights, baselineReport); | ||
| baselineInsights = setTestsAddedToInsights(baselineInsights, currentReportWithTestInsights, baselineReport); | ||
| baselineInsights = setTestsRemovedToInsights(baselineInsights, currentReportWithTestInsights, baselineReport); | ||
| return { | ||
| ...currentReportWithTestInsights, | ||
| insights: baselineInsights | ||
| }; | ||
| } |
| import { Report } from '../../types/ctrf.js'; | ||
| /** | ||
| * Stores previous results in the current report's previousResults array. | ||
| * Extracts key metrics from each previous report and adds them to the current report. | ||
| * | ||
| * @param currentReport The current CTRF report to enrich with previous results | ||
| * @param previousReports Array of previous CTRF reports to extract metrics from | ||
| * @returns The current report with previousResults populated | ||
| */ | ||
| export declare function storePreviousResults(currentReport: Report, previousReports: Report[]): Report; |
| /** | ||
| * Stores previous results in the current report's previousResults array. | ||
| * Extracts key metrics from each previous report and adds them to the current report. | ||
| * | ||
| * @param currentReport The current CTRF report to enrich with previous results | ||
| * @param previousReports Array of previous CTRF reports to extract metrics from | ||
| * @returns The current report with previousResults populated | ||
| */ | ||
| export function storePreviousResults(currentReport, previousReports) { | ||
| if (!currentReport || !Array.isArray(previousReports)) { | ||
| throw new Error('Invalid input: currentReport must be a valid CTRF report and previousReports must be an array'); | ||
| } | ||
| // Initialize previousResults if it doesn't exist | ||
| if (!currentReport.extra) { | ||
| currentReport.extra = {}; | ||
| } | ||
| // Extract metrics from each previous report | ||
| const previousResults = previousReports.map((report) => { | ||
| if (!report.results || !report.results.summary) { | ||
| throw new Error('Invalid previous report: missing results or summary'); | ||
| } | ||
| const summary = report.results.summary; | ||
| const tests = report.results.tests || []; | ||
| // Calculate flaky tests count | ||
| const flakyCount = tests.filter(test => test.flaky === true).length; | ||
| // Calculate duration (stop - start) | ||
| const duration = summary.stop - summary.start; | ||
| // Determine overall result status | ||
| let result = 'passed'; | ||
| if (summary.failed > 0) { | ||
| result = 'failed'; | ||
| } | ||
| else if ((summary.skipped > 0 || summary.pending > 0 || summary.other > 0) && summary.passed === 0) { | ||
| result = 'skipped'; | ||
| } | ||
| else if (summary.tests === 0) { | ||
| result = 'empty'; | ||
| } | ||
| return { | ||
| start: report.results.summary.start, | ||
| stop: report.results.summary.stop, | ||
| buildId: report.results.environment?.buildId, | ||
| buildName: report.results.environment?.buildName, | ||
| buildNumber: report.results.environment?.buildNumber, | ||
| buildUrl: report.results.environment?.buildUrl, | ||
| result, | ||
| tests: summary.skipped, | ||
| passed: summary.passed, | ||
| failed: summary.failed, | ||
| skipped: summary.skipped, | ||
| flaky: flakyCount, | ||
| other: summary.other, | ||
| duration, | ||
| environment: report.results.environment | ||
| }; | ||
| }); | ||
| previousResults.sort((a, b) => a.start - b.start); | ||
| currentReport.extra.previousResults = previousResults; | ||
| return currentReport; | ||
| } |
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
105
1.94%0
-100%5725
-90.76%3
-85.71%0
-100%2
Infinity%