Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

ctrf-cli

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ctrf-cli - npm Package Compare versions

Comparing version
0.0.1
to
0.0.2
+9
-4
package.json
{
"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",

# 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 {};
#!/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;
}