1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use crate::analysis::AnalysisCensus;
use crate::model::{AnalysisResult, DuplicationThreshold, Severity};
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::Path;
/// Measured execution phases and discovered input counts for one CLI run.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionMetrics {
/// Number of definition source files selected by discovery.
pub definition_files: usize,
/// Number of feature files selected by discovery.
pub feature_files: usize,
/// Total number of definition and feature files selected by discovery.
pub files_discovered: usize,
/// Time spent walking and classifying input files.
pub discovery_ms: f64,
/// Time spent parsing definition and feature sources.
pub parsing_ms: f64,
/// Time spent evaluating analysis rules.
pub analysis_ms: f64,
}
impl ExecutionMetrics {
/// Creates execution metrics and derives the combined discovered-file count.
pub fn new(
definition_files: usize,
feature_files: usize,
discovery_ms: f64,
parsing_ms: f64,
analysis_ms: f64,
) -> Self {
Self {
definition_files,
feature_files,
files_discovered: definition_files.saturating_add(feature_files),
discovery_ms,
parsing_ms,
analysis_ms,
}
}
}
/// Deterministic input and extraction counts for one CLI run.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CorpusCensus {
/// Number of definition source files selected by discovery.
pub(crate) definition_files: usize,
/// Number of selected definition files that yielded at least one definition.
pub(crate) definition_files_with_definitions: usize,
/// Number of step definitions extracted before analysis and mode filtering.
pub(crate) definitions_extracted: usize,
/// Number of feature files selected by discovery.
pub(crate) feature_files: usize,
/// Number of selected feature files parsed successfully.
pub(crate) feature_files_parsed: usize,
/// Number of converted Gherkin Markdown files that parsed but yielded no concrete steps.
pub(crate) feature_files_without_steps: usize,
/// Whether definitions or feature steps are known to be missing from the extracted corpus.
///
/// Set when a source imported a known registration through a module that could not be
/// resolved statically, or when Gherkin Markdown conversion yielded no steps. Those inputs
/// are absent from one or more rules, so the corpus — not just the comparison set — is a
/// subset of what a complete run would analyze.
pub(crate) incomplete: bool,
}
pub(crate) struct CliReportMetadata<'a> {
pub(crate) corpus: &'a CorpusCensus,
pub(crate) analysis: &'a AnalysisCensus,
pub(crate) execution_successful: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
/// Aggregate counts shared by every report format.
#[non_exhaustive]
pub struct Summary {
/// Number of extracted step definitions.
pub definitions_analyzed: usize,
/// Number of concrete feature steps.
pub feature_steps_analyzed: usize,
/// Number of active findings.
pub findings: usize,
/// Number of active error findings.
pub errors: usize,
/// Number of active warning findings.
pub warnings: usize,
/// Number of suppressed findings.
pub suppressed: usize,
/// Active findings grouped by stable rule name.
pub by_rule: BTreeMap<String, usize>,
/// Percentage-based duplication quality-gate result.
pub duplication: DuplicationThreshold,
}
impl Summary {
/// Computes report totals using the strict default duplication threshold.
pub fn from_result(result: &AnalysisResult) -> Self {
Self::from_result_with_threshold(result, 0.0)
}
/// Computes report totals using `threshold` as the allowed duplication percentage.
pub fn from_result_with_threshold(result: &AnalysisResult, threshold: f64) -> Self {
let active: Vec<_> = result
.findings
.iter()
.filter(|finding| finding.is_active())
.collect();
let mut by_rule = BTreeMap::new();
for finding in &active {
*by_rule.entry(finding.rule.to_string()).or_insert(0) += 1;
}
Self {
definitions_analyzed: result.definitions.len(),
feature_steps_analyzed: result.feature_steps.len(),
findings: active.len(),
errors: active
.iter()
.filter(|finding| finding.severity == Severity::Error)
.count(),
warnings: active
.iter()
.filter(|finding| finding.severity == Severity::Warning)
.count(),
suppressed: result
.findings
.iter()
.filter(|finding| finding.suppression.is_some())
.count(),
by_rule,
duplication: DuplicationThreshold::from_result(result, threshold),
}
}
}
/// Shared, precomputed input passed to every report renderer.
#[non_exhaustive]
pub struct ReportContext<'a> {
/// Complete analysis result, including suppressed findings.
pub result: &'a AnalysisResult,
/// Repository root used to relativize source locations.
pub root: &'a Path,
/// Configured duplication percentage threshold.
pub threshold: f64,
/// Aggregate counts shared across report formats.
pub summary: Summary,
/// Version of CukeDedup that produced the report.
pub tool_version: &'static str,
/// Optional CLI execution measurements.
pub metrics: Option<&'a ExecutionMetrics>,
}
impl<'a> ReportContext<'a> {
/// Creates a context and computes its shared summary once.
pub fn new(result: &'a AnalysisResult, root: &'a Path, threshold: f64) -> Self {
Self {
result,
root,
threshold,
summary: Summary::from_result_with_threshold(result, threshold),
tool_version: env!("CARGO_PKG_VERSION"),
metrics: None,
}
}
/// Creates a report context containing measured CLI execution phases.
pub fn with_metrics(
result: &'a AnalysisResult,
root: &'a Path,
threshold: f64,
metrics: &'a ExecutionMetrics,
) -> Self {
Self {
metrics: Some(metrics),
..Self::new(result, root, threshold)
}
}
}