diff --git a/js/summary.js b/js/summary.js index 6d2207588237..e7ba0d756c5d 100644 --- a/js/summary.js +++ b/js/summary.js @@ -400,6 +400,72 @@ function renderCheck(indent, check, formatter) { ); } +/** + * Renders checks into a formatted set of lines ready for display in the terminal. + * + * @param checks + * @param formatter + * @param options + * @returns {*[]} + */ +function renderChecks(checks, formatter, options = {}) { + // If no checks exist, return empty array + if (!checks || !checks.ordered_checks) { + return []; + } + + const { + indent = '', + showPassedChecks = true, + showFailedChecks = true, + } = options; + + // Process each check and filter based on options + const renderedChecks = checks.ordered_checks + .filter((check) => { + // Filter logic for passed/failed checks + if (check.fails === 0 && !showPassedChecks) return false; + if (check.fails > 0 && !showFailedChecks) return false; + return true; + }) + .map((check) => renderCheck(indent, check, formatter)); + + // Render metrics for checks if they exist + const checkMetrics = checks.metrics + ? renderMetrics({ metrics: checks.metrics }, formatter, { + ...options, + indent: indent + ' ', + sortByName: false, + }) + : []; + + // Combine metrics and checks + return [...checkMetrics, ...renderedChecks]; +} + +// function renderChecks(checks, formatter, options) { +// if (checks === undefined || checks === null) { +// return; +// } +// displayMetricsBlock(checks.metrics, { +// ...opts, +// indent: opts.indent + defaultIndent, +// sortByName: false, +// }); +// for (let i = 0; i < checks.ordered_checks.length; i++) { +// lines.push( +// renderCheck( +// metricGroupIndent + metricGroupIndent + opts.indent, +// checks.ordered_checks[i], +// formatter, +// ), +// ); +// } +// if (checks.ordered_checks.length > 0) { +// lines.push(''); +// } +// } + /** * @typedef {Object} summarizeMetricsOptions * @property {string} indent - The indentation string. @@ -809,6 +875,9 @@ const ANSIStyles = { reversed: '7', }; +/** + * ANSIFormatter provides methods for decorating text with ANSI color and style codes. + */ class ANSIFormatter { /** * Constructs an ANSIFormatter with configurable color and styling options @@ -871,255 +940,479 @@ class ANSIFormatter { /** * Generates a textual summary of test results, including checks, metrics, thresholds, groups, and scenarios. - * - * @param {Object} data - The data input for the summary (includes options, metrics, etc.). - * @param {Object} options - Additional options that override defaults. - * @param {Object} report - The report object containing thresholds, checks, metrics, groups, and scenarios. - * @returns {string} A formatted summary of the test results. */ -function generateTextSummary(data, options, report) { - const mergedOpts = Object.assign({}, defaultOptions, data.options, options); - const lines = []; - - // Create a formatter with default settings (colors enabled) - const formatter = new ANSIFormatter(); - - const defaultIndent = ' '; - const metricGroupIndent = ' '; - +class TestReportGenerator { /** - * Displays a metrics block name (section heading). + * Constructs a TestReportGenerator with a specified formatter * - * @param {string} sectionName - The section name (e.g., "checks", "http_req_duration"). - * @param {Partial} [opts] - Display options. + * @param {ANSIFormatter} formatter - The ANSI formatter to use for text decoration. + * // FIXME (@oleiade): needs JSDoc + * @param options */ - const displayMetricsBlockName = (sectionName, opts) => { - let bold = true; - if (opts && opts.bold === false) { - bold = false; - } - - let normalizedSectionName = sectionName.toUpperCase(); - - if (bold) { - normalizedSectionName = formatter.boldify(normalizedSectionName); - } - - let indent = ' '; - if (opts && opts.metricsBlockIndent) { - indent += opts.metricsBlockIndent; - } - lines.push(indent + normalizedSectionName); - }; + constructor(formatter, options = {}) { + this.formatter = formatter; + this.options = { + defaultIndent: ' ', + metricGroupIndent: ' ', + ...options, + }; + } + // FIXME (@oleiade): needs JSDoc /** - * Displays a block of metrics with the given options. + * Generates a textual summary of test results, including checks, metrics, thresholds, groups, and scenarios. * - * @param {Object[]} sectionMetrics - The metrics to display. - * @param {Partial} [opts] - Display options. + * @param data + * @param report + * @returns {*} */ - const displayMetricsBlock = (sectionMetrics, opts) => { - const summarizeOpts = Object.assign({}, mergedOpts, opts); - Array.prototype.push.apply( - lines, - renderMetrics( - { metrics: sectionMetrics }, - formatter, - summarizeOpts, - ), - ); - lines.push(''); - }; + generate(data, report) { + const reportBuilder = new ReportBuilder(this.formatter, this.options); + return reportBuilder + .addThresholds(report.thresholds) + .addTotalResults(report) + .addGroups(report.groups) + .addScenarios(report.scenarios) + .build(); + } +} + +/** + * Renders a section title with a specified formatter, indentation level, and options. + * + * For example, a bold section title at first indentation level with a block prefix and newline suffix: + * █ THRESHOLDS + * + * @param {string} title - The section title to render. + * @param {ANSIFormatter} formatter - The ANSI formatter to use for text decoration. + * @param {number} [indentationLevel = 1] - The indentation level for the section title. + * @param {Object} options - Additional options for rendering the section title. + * @param {string} [options.prefix=groupPrefix] - The prefix to use for the section title. + * @param {string} [options.suffix='\n'] - The suffix to use for the section title. + * @returns {string} - The formatted section title. + */ +function renderTitle( + title, + formatter, + indentationLevel = 1, + options = { prefix: groupPrefix, suffix: '\n' }, +) { + return ( + indent(indentationLevel) + + `${options.prefix} ${formatter.boldify(title)} ${options.suffix}` + ); +} +/** + * Exposes methods for generating a textual summary of test results. + */ +class ReportBuilder { /** - * Displays checks within a certain context (indentation, etc.). + * Creates a new ReportBuilder with a specified formatter and options. * - * @param {Object} checks - Checks data, containing `metrics` and `ordered_checks`. - * @param {Partial} [opts={indent: ''}] - Options including indentation. + * @param formatter + * @param options */ - const displayChecks = (checks, opts = { indent: '' }) => { - if (checks === undefined || checks === null) { - return; - } - displayMetricsBlock(checks.metrics, { - ...opts, - indent: opts.indent + defaultIndent, - sortByName: false, + constructor(formatter, options) { + this.formatter = formatter; + this.options = options; + this.sections = []; + } + + addThresholds(thresholds) { + if (!thresholds) return this; + + this.sections.push({ + title: renderTitle('THRESHOLDS', this.formatter), + content: this._renderThresholds(thresholds), // FIXME: this feels hacky, better way to handle this? }); - for (let i = 0; i < checks.ordered_checks.length; i++) { - lines.push( - renderCheck( - metricGroupIndent + metricGroupIndent + opts.indent, - checks.ordered_checks[i], - formatter, - ), - ); - } - if (checks.ordered_checks.length > 0) { - lines.push(''); - } - }; + return this; + } - /** - * Displays thresholds and their satisfaction status. - * - * @param {Record} thresholds - Threshold data. - */ - const displayThresholds = (thresholds) => { - if (thresholds === undefined || thresholds === null) { - return; - } + addTotalResults(report) { + this.sections.push({ + title: renderTitle('TOTAL RESULTS', this.formatter), + content: [ + ...this._renderChecks(report.checks), + ...this._renderMetrics(report.metrics), + ...'\n', + ], + }); + return this; + } - lines.push( - metricGroupIndent + - groupPrefix + - defaultIndent + - formatter.boldify('THRESHOLDS') + + addGroups(groups) { + if (!groups) return this; + + Object.entries(groups).forEach(([groupName, groupData]) => { + this.sections.push({ + title: `GROUP: ${groupName}`, + content: this._renderGroupContent(groupData), + }); + }); + return this; + } + + addScenarios(scenarios) { + if (!scenarios) return this; + + Object.entries(scenarios).forEach(([scenarioName, scenarioData]) => { + this.sections.push({ + title: `SCENARIO: ${scenarioName}`, + content: this._renderScenarioContent(scenarioData), + }); + }); + return this; + } + + build() { + return this.sections + .map((section) => [ + this.formatter.boldify(section.title), + ...section.content, '\n', - ); + ]) + .flat() + .join('\n'); + } - const mergedOpts = Object.assign( - {}, - defaultOptions, - data.options, - options, + _renderThresholds(thresholds) { + // Implement threshold rendering logic + return renderThresholds( + { metrics: this._processThresholds(thresholds) }, + this.formatter, + this.options, ); + } - let metrics = {}; - forEach(thresholds, (_, threshold) => { + // Private rendering methods + _processThresholds(thresholds) { + // Transform thresholds into a format suitable for rendering + const metrics = {}; + Object.values(thresholds).forEach((threshold) => { metrics[threshold.metric.name] = { ...threshold.metric, thresholds: threshold.thresholds, }; }); + return metrics; + } - Array.prototype.push.apply( - lines, - renderThresholds({ metrics }, formatter, { - ...mergedOpts, - indent: mergedOpts.indent + defaultIndent, - }), - ); - lines.push(''); - }; - - // THRESHOLDS - displayThresholds(report.thresholds); - - // TOTAL RESULTS - lines.push( - metricGroupIndent + - groupPrefix + - defaultIndent + - formatter.boldify('TOTAL RESULTS') + - '\n', - ); - - // CHECKS - displayChecks(report.checks); + _renderChecks(checks) { + return checks ? renderChecks(checks, this.formatter, this.options) : []; + } - // METRICS - forEach(report.metrics, (sectionName, sectionMetrics) => { - // If there are no metrics in this section, skip it - if (Object.keys(sectionMetrics).length === 0) { - return; - } + _renderMetrics(metrics) { + // Implement metrics rendering logic + return Object.entries(metrics) + .filter( + ([_, sectionMetrics]) => Object.keys(sectionMetrics).length > 0, + ) + .flatMap(([sectionName, sectionMetrics]) => [ + this.formatter.boldify(sectionName.toUpperCase()), + ...renderMetrics( + { metrics: sectionMetrics }, + this.formatter, + this.options, + ), + ]); + } - displayMetricsBlockName(sectionName); - displayMetricsBlock(sectionMetrics); - }); - // END OF TOTAL RESULTS - - // GROUPS - const summarize = (prefix, indent) => { - return (groupName, groupData) => { - lines.push( - metricGroupIndent + - indent + - prefix + - defaultIndent + - formatter.boldify(`GROUP: ${groupName}`) + - '\n', - ); - displayChecks(groupData.checks, { indent: indent }); - forEach(groupData.metrics, (sectionName, sectionMetrics) => { - // If there are no metrics in this section, skip it - if (Object.keys(sectionMetrics).length === 0) { - return; - } + _renderGroupContent(groupData) { + // Implement group content rendering + return [ + ...this._renderChecks(groupData.checks), + ...this._renderMetrics(groupData.metrics), + ...(groupData.groups + ? this._renderNestedGroups(groupData.groups) + : []), + ]; + } - displayMetricsBlockName(sectionName, { - metricsBlockIndent: indent, - }); - displayMetricsBlock(sectionMetrics, { - indent: indent + defaultIndent, - }); - }); - if (groupData.groups !== undefined) { - forEach( - groupData.groups, - summarize(detailsPrefix, indent + metricGroupIndent), - ); - } - }; - }; + _renderScenarioContent(scenarioData) { + // Similar to group content rendering + return [ + ...this._renderChecks(scenarioData.checks), + ...this._renderMetrics(scenarioData.metrics), + ...(scenarioData.groups + ? this._renderNestedGroups(scenarioData.groups) + : []), + ]; + } - const summarizeNestedGroups = (groupName, groupData) => { - lines.push( - metricGroupIndent + - groupPrefix + - ' ' + - formatter.boldify(`GROUP: ${groupName}`) + - '\n', - ); - forEach(groupData.metrics, (sectionName, sectionMetrics) => { - // If there are no metrics in this section, skip it - if (Object.keys(sectionMetrics).length === 0) { - return; - } + _renderNestedGroups(groups) { + // Render nested groups recursively + return Object.entries(groups).flatMap(([groupName, groupData]) => [ + this.formatter.boldify(`GROUP: ${groupName}`), + ...this._renderGroupContent(groupData), + ]); + } +} - displayMetricsBlockName(sectionName); - displayMetricsBlock(sectionMetrics); - }); - if (groupData.groups !== undefined) { - forEach(groupData.groups, summarizeNestedGroups); - } - }; +/** + * Indents text by a specified number of levels. + * + * @param level - The desired indentation level of the text. + * @returns {string} - The indented string. + */ +function indent(level = 1) { + return ' '.repeat(level * 2); +} - if (report.groups !== undefined) { - forEach(report.groups, summarize(groupPrefix, defaultIndent)); - } +/** + * Generates a textual summary of test results, including checks, metrics, thresholds, groups, and scenarios. + * + * @param {Object} data - The data input for the summary (includes options, metrics, etc.). + * @param {Object} options - Additional options that override defaults. + * @param {Object} report - The report object containing thresholds, checks, metrics, groups, and scenarios. + * @returns {string} A formatted summary of the test results. + */ +function generateTextSummary(data, options, report) { + const mergedOpts = Object.assign({}, defaultOptions, data.options, options); + const lines = []; - // SCENARIOS - if (report.scenarios !== undefined) { - forEach(report.scenarios, (scenarioName, scenarioData) => { - lines.push( - metricGroupIndent + - groupPrefix + - defaultIndent + - formatter.boldify(`SCENARIO: ${scenarioName}`) + - '\n', - ); - displayChecks(scenarioData.checks); - forEach(scenarioData.metrics, (sectionName, sectionMetrics) => { - // If there are no metrics in this section, skip it - if (Object.keys(sectionMetrics).length === 0) { - return; - } + // Create a formatter with default settings (colors enabled) + const formatter = new ANSIFormatter(); - displayMetricsBlockName(sectionName); - displayMetricsBlock(sectionMetrics); - }); - if (scenarioData.groups !== undefined) { - forEach( - scenarioData.groups, - summarize(detailsPrefix, metricGroupIndent), - ); - } - }); - } + const defaultIndent = ' '; + const metricGroupIndent = ' '; - return lines.join('\n'); + const reportGenerator = new TestReportGenerator(formatter, mergedOpts); + return reportGenerator.generate(data, report); + + // /** + // * Displays a metrics block name (section heading). + // * + // * @param {string} sectionName - The section name (e.g., "checks", "http_req_duration"). + // * @param {Partial} [opts] - Display options. + // */ + // const displayMetricsBlockName = (sectionName, opts) => { + // let bold = true; + // if (opts && opts.bold === false) { + // bold = false; + // } + // + // let normalizedSectionName = sectionName.toUpperCase(); + // + // if (bold) { + // normalizedSectionName = formatter.boldify(normalizedSectionName); + // } + // + // let indent = ' '; + // if (opts && opts.metricsBlockIndent) { + // indent += opts.metricsBlockIndent; + // } + // lines.push(indent + normalizedSectionName); + // }; + // + // /** + // * Displays a block of metrics with the given options. + // * + // * @param {Object[]} sectionMetrics - The metrics to display. + // * @param {Partial} [opts] - Display options. + // */ + // const displayMetricsBlock = (sectionMetrics, opts) => { + // const summarizeOpts = Object.assign({}, mergedOpts, opts); + // Array.prototype.push.apply( + // lines, + // renderMetrics( + // { metrics: sectionMetrics }, + // formatter, + // summarizeOpts, + // ), + // ); + // lines.push(''); + // }; + // + // /** + // * Displays checks within a certain context (indentation, etc.). + // * + // * @param {Object} checks - Checks data, containing `metrics` and `ordered_checks`. + // * @param {Partial} [opts={indent: ''}] - Options including indentation. + // */ + // const displayChecks = (checks, opts = { indent: '' }) => { + // if (checks === undefined || checks === null) { + // return; + // } + // displayMetricsBlock(checks.metrics, { + // ...opts, + // indent: opts.indent + defaultIndent, + // sortByName: false, + // }); + // for (let i = 0; i < checks.ordered_checks.length; i++) { + // lines.push( + // renderCheck( + // metricGroupIndent + metricGroupIndent + opts.indent, + // checks.ordered_checks[i], + // formatter, + // ), + // ); + // } + // if (checks.ordered_checks.length > 0) { + // lines.push(''); + // } + // }; + // + // /** + // * Displays thresholds and their satisfaction status. + // * + // * @param {Record} thresholds - Threshold data. + // */ + // const displayThresholds = (thresholds) => { + // if (thresholds === undefined || thresholds === null) { + // return; + // } + // + // lines.push( + // metricGroupIndent + + // groupPrefix + + // defaultIndent + + // formatter.boldify('THRESHOLDS') + + // '\n', + // ); + // + // const mergedOpts = Object.assign( + // {}, + // defaultOptions, + // data.options, + // options, + // ); + // + // let metrics = {}; + // forEach(thresholds, (_, threshold) => { + // metrics[threshold.metric.name] = { + // ...threshold.metric, + // thresholds: threshold.thresholds, + // }; + // }); + // + // Array.prototype.push.apply( + // lines, + // renderThresholds({ metrics }, formatter, { + // ...mergedOpts, + // indent: mergedOpts.indent + defaultIndent, + // }), + // ); + // lines.push(''); + // }; + + // // THRESHOLDS + // displayThresholds(report.thresholds); + // + // // TOTAL RESULTS + // lines.push( + // metricGroupIndent + + // groupPrefix + + // defaultIndent + + // formatter.boldify('TOTAL RESULTS') + + // '\n', + // ); + // + // // CHECKS + // displayChecks(report.checks); + // + // // METRICS + // forEach(report.metrics, (sectionName, sectionMetrics) => { + // // If there are no metrics in this section, skip it + // if (Object.keys(sectionMetrics).length === 0) { + // return; + // } + // + // displayMetricsBlockName(sectionName); + // displayMetricsBlock(sectionMetrics); + // }); + // // END OF TOTAL RESULTS + // + // // GROUPS + // const summarize = (prefix, indent) => { + // return (groupName, groupData) => { + // lines.push( + // metricGroupIndent + + // indent + + // prefix + + // defaultIndent + + // formatter.boldify(`GROUP: ${groupName}`) + + // '\n', + // ); + // displayChecks(groupData.checks, { indent: indent }); + // forEach(groupData.metrics, (sectionName, sectionMetrics) => { + // // If there are no metrics in this section, skip it + // if (Object.keys(sectionMetrics).length === 0) { + // return; + // } + // + // displayMetricsBlockName(sectionName, { + // metricsBlockIndent: indent, + // }); + // displayMetricsBlock(sectionMetrics, { + // indent: indent + defaultIndent, + // }); + // }); + // if (groupData.groups !== undefined) { + // forEach( + // groupData.groups, + // summarize(detailsPrefix, indent + metricGroupIndent), + // ); + // } + // }; + // }; + // + // const summarizeNestedGroups = (groupName, groupData) => { + // lines.push( + // metricGroupIndent + + // groupPrefix + + // ' ' + + // formatter.boldify(`GROUP: ${groupName}`) + + // '\n', + // ); + // forEach(groupData.metrics, (sectionName, sectionMetrics) => { + // // If there are no metrics in this section, skip it + // if (Object.keys(sectionMetrics).length === 0) { + // return; + // } + // + // displayMetricsBlockName(sectionName); + // displayMetricsBlock(sectionMetrics); + // }); + // if (groupData.groups !== undefined) { + // forEach(groupData.groups, summarizeNestedGroups); + // } + // }; + // + // if (report.groups !== undefined) { + // forEach(report.groups, summarize(groupPrefix, defaultIndent)); + // } + // + // // SCENARIOS + // if (report.scenarios !== undefined) { + // forEach(report.scenarios, (scenarioName, scenarioData) => { + // lines.push( + // metricGroupIndent + + // groupPrefix + + // defaultIndent + + // formatter.boldify(`SCENARIO: ${scenarioName}`) + + // '\n', + // ); + // displayChecks(scenarioData.checks); + // forEach(scenarioData.metrics, (sectionName, sectionMetrics) => { + // // If there are no metrics in this section, skip it + // if (Object.keys(sectionMetrics).length === 0) { + // return; + // } + // + // displayMetricsBlockName(sectionName); + // displayMetricsBlock(sectionMetrics); + // }); + // if (scenarioData.groups !== undefined) { + // forEach( + // scenarioData.groups, + // summarize(detailsPrefix, metricGroupIndent), + // ); + // } + // }); + // } + // + // return lines.join('\n'); } exports.humanizeValue = humanizeValue;