use super::super::HealthSort;
use super::super::component_rollup::{
ComponentTemplateUnit, ComponentTemplateUnitScope, append_component_rollup_findings,
collect_component_template_units,
};
use super::super::filters::{
filter_complexity_findings_by_diff, filter_hotspots_by_diff, filter_large_functions_by_diff,
};
use super::super::findings::{
CollectFindingsInput, CrapFindingMergeInput, collect_findings, collect_findings_with_resolver,
merge_crap_findings,
};
use super::super::findings_pipeline::annotate_outstanding_dimensions;
use super::super::ignore::build_ignore_set;
use super::super::large_functions::{LargeFunctionInput, collect_large_functions};
use super::super::runtime_filter::{RuntimeCoverageFilterContext, apply_runtime_coverage_filters};
use super::super::scoring;
use super::super::sort_findings;
use super::super::threshold_overrides::{
ComplexityFunctionContext, CrapFunctionContext, GlobalHealthThresholds,
MeasuredThresholdMetrics, ThresholdOverrideResolver, ThresholdOverrideStateTracker,
};
use crate::baseline::HealthBaselineData;
use crate::source::ModuleInfo;
use fallow_output::{ComplexityViolation, ExceededThreshold, FindingSeverity};
use fallow_types::discover::FileId;
use fallow_types::extract::FunctionComplexity;
use rustc_hash::{FxHashMap, FxHashSet};
use std::path::{Path, PathBuf};
fn make_module(file_id: FileId, complexity: Vec<FunctionComplexity>) -> ModuleInfo {
ModuleInfo {
line_offsets: vec![0],
complexity,
..ModuleInfo::empty(file_id)
}
}
fn make_fc(name: &str, cyclomatic: u16, cognitive: u16, line_count: u32) -> FunctionComplexity {
FunctionComplexity {
name: name.to_string(),
is_private_member: false,
line: 1,
col: 0,
cyclomatic,
cognitive,
line_count,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
}
}
fn make_fc_with_contributions(name: &str, cyclomatic: u16, cognitive: u16) -> FunctionComplexity {
use fallow_types::extract::{
ComplexityContribution, ComplexityContributionKind, ComplexityMetric,
};
let mut fc = make_fc(name, cyclomatic, cognitive, 50);
fc.contributions = vec![ComplexityContribution {
line: 2,
col: 4,
metric: ComplexityMetric::Cyclomatic,
kind: ComplexityContributionKind::If,
weight: 1,
nesting: 0,
}];
fc
}
#[test]
fn collect_findings_omits_contributions_without_breakdown_flag() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc_with_contributions("complexFn", 25, 5)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
assert!(
findings[0].contributions.is_empty(),
"contributions must be omitted without the breakdown flag"
);
}
#[test]
fn collect_findings_includes_contributions_with_breakdown_flag() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc_with_contributions("complexFn", 25, 5)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
true,
);
assert_eq!(findings.len(), 1);
assert_eq!(
findings[0].contributions.len(),
1,
"contributions must flow through when the breakdown flag is set"
);
}
fn threshold_resolver(
overrides: &[fallow_config::HealthThresholdOverride],
) -> ThresholdOverrideResolver {
ThresholdOverrideResolver::new(
overrides,
GlobalHealthThresholds {
cyclomatic: 20,
cognitive: 15,
crap: 30.0,
unit_size: 60,
},
)
}
#[test]
fn collect_findings_uses_threshold_override_as_local_ceiling() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("complexFn", 25, 20, 50)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/a.ts".to_string()],
functions: vec!["complexFn".to_string()],
max_cyclomatic: Some(30),
max_cognitive: Some(25),
max_crap: None,
max_unit_size: None,
reason: Some("approved assembly".to_string()),
}]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut input = CollectFindingsInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
complexity_breakdown: false,
};
let (findings, _, _) = collect_findings_with_resolver(&mut input);
assert!(findings.is_empty());
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
));
}
#[test]
fn collect_findings_reports_when_local_ceiling_is_exceeded() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("complexFn", 31, 20, 50)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/a.ts".to_string()],
functions: vec!["complexFn".to_string()],
max_cyclomatic: Some(30),
max_cognitive: Some(25),
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut input = CollectFindingsInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
complexity_breakdown: false,
};
let (findings, _, _) = collect_findings_with_resolver(&mut input);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].effective_thresholds.unwrap().max_cyclomatic, 30);
assert!(matches!(
findings[0].threshold_source,
Some(fallow_output::ThresholdSource::Override)
));
}
#[test]
fn collect_findings_reports_stale_override_when_under_global_thresholds() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("complexFn", 10, 8, 20)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/a.ts".to_string()],
functions: vec!["complexFn".to_string()],
max_cyclomatic: Some(30),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut input = CollectFindingsInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
complexity_breakdown: false,
};
let (findings, _, _) = collect_findings_with_resolver(&mut input);
assert!(findings.is_empty());
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Stale
));
}
#[test]
fn threshold_override_tracker_reports_no_match_only_when_requested() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/missing.ts".to_string()],
functions: vec!["missingFn".to_string()],
max_cyclomatic: Some(30),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_no_match_entries(&resolver, false);
assert!(tracker.into_states().is_empty());
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_no_match_entries(&resolver, true);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::NoMatch
));
}
#[test]
fn an_attached_crap_value_always_carries_the_crap_bit_in_exceeded() {
let positioned = |name: &str, line: u32, col: u32, cyclomatic: u16| FunctionComplexity {
line,
col,
..make_fc(name, cyclomatic, 0, 20)
};
let path = PathBuf::from("/project/src/mixed.ts");
let functions = vec![
positioned("cyclomaticOnly", 1, 0, 25),
positioned("crapOnly", 20, 0, 5),
positioned("both", 40, 0, 25),
positioned("clean", 60, 0, 2),
];
let crap_by_line = [(1u32, 5.0), (20, 90.0), (40, 90.0), (60, 1.0)];
let modules = vec![make_module(FileId(0), functions)];
let mut file_paths: FxHashMap<FileId, &PathBuf> = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let resolver = threshold_resolver(&[]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut collect_input = CollectFindingsInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
complexity_breakdown: false,
};
let (mut findings, _, _) = collect_findings_with_resolver(&mut collect_input);
let mut per_function_crap: FxHashMap<PathBuf, Vec<scoring::PerFunctionCrap>> =
FxHashMap::default();
per_function_crap.insert(
path.clone(),
crap_by_line
.iter()
.map(|&(line, crap)| scoring::PerFunctionCrap {
line,
col: 0,
crap,
coverage_pct: None,
coverage_tier: fallow_output::CoverageTier::None,
coverage_source: fallow_output::CoverageSource::Estimated,
})
.collect(),
);
let mut merge_input = CrapFindingMergeInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
per_function_crap: &per_function_crap,
template_inherit_provenance: &FxHashMap::default(),
complexity_breakdown: false,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
};
merge_crap_findings(&mut findings, &mut merge_input);
let mut names: Vec<&str> = findings.iter().map(|f| f.name.as_str()).collect();
names.sort_unstable();
assert_eq!(
names,
vec!["both", "crapOnly", "cyclomaticOnly"],
"the fixture must cover a CRAP-only, a complexity-only and a combined finding"
);
for finding in &findings {
assert_eq!(
finding.crap.is_some(),
finding.exceeded.includes_crap(),
"{}: an attached crap value and the CRAP bit must agree: {finding:#?}",
finding.name
);
}
}
#[test]
fn no_match_rows_name_the_dimensions_the_entry_configures() {
let entry = |max_cyclomatic, max_crap, max_unit_size| fallow_config::HealthThresholdOverride {
files: vec!["src/missing.ts".to_string()],
functions: Vec::new(),
max_cyclomatic,
max_cognitive: None,
max_crap,
max_unit_size,
reason: None,
};
let dimensions = |overrides: &[fallow_config::HealthThresholdOverride]| {
let resolver = threshold_resolver(overrides);
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_no_match_entries(&resolver, true);
tracker
.into_states()
.iter()
.map(|state| state.dimension)
.collect::<Vec<_>>()
};
assert_eq!(
dimensions(&[entry(None, Some(500.0), None)]),
vec![fallow_output::ThresholdOverrideDimension::Crap]
);
assert_eq!(
dimensions(&[entry(Some(500), Some(500.0), None)]),
vec![
fallow_output::ThresholdOverrideDimension::Complexity,
fallow_output::ThresholdOverrideDimension::Crap
]
);
assert_eq!(
dimensions(&[entry(None, None, Some(500))]),
vec![fallow_output::ThresholdOverrideDimension::Complexity]
);
}
#[test]
fn build_ignore_set_empty_patterns() {
let set = build_ignore_set(&[]);
assert!(set.is_empty());
}
#[test]
fn build_ignore_set_matches_glob() {
let patterns = vec!["src/generated/**".to_string()];
let set = build_ignore_set(&patterns);
assert!(set.is_match(Path::new("src/generated/types.ts")));
assert!(!set.is_match(Path::new("src/utils.ts")));
}
#[test]
fn build_ignore_set_multiple_patterns() {
let patterns = vec!["*.test.ts".to_string(), "dist/**".to_string()];
let set = build_ignore_set(&patterns);
assert!(set.is_match(Path::new("foo.test.ts")));
assert!(set.is_match(Path::new("dist/index.js")));
assert!(!set.is_match(Path::new("src/index.ts")));
}
#[test]
#[should_panic(expected = "validated at config load time")]
fn build_ignore_set_panics_on_unvalidated_invalid_pattern() {
let patterns = vec!["[invalid".to_string(), "*.js".to_string()];
let _ = build_ignore_set(&patterns);
}
fn make_finding(name: &str, exceeded: ExceededThreshold) -> ComplexityViolation {
ComplexityViolation {
path: PathBuf::from("/project/src/a.ts"),
name: name.to_string(),
line: 1,
col: 0,
cyclomatic: match exceeded {
ExceededThreshold::Cyclomatic
| ExceededThreshold::Both
| ExceededThreshold::CyclomaticCrap
| ExceededThreshold::All => 25,
_ => 8,
},
cognitive: match exceeded {
ExceededThreshold::Cognitive
| ExceededThreshold::Both
| ExceededThreshold::CognitiveCrap
| ExceededThreshold::All => 20,
_ => 5,
},
line_count: 10,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded,
severity: FindingSeverity::Moderate,
crap: exceeded.includes_crap().then_some(30.0),
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}
}
#[test]
fn sort_findings_by_severity_surfaces_crap_before_single_metric_findings() {
let mut findings = vec![
make_finding("cyclomatic", ExceededThreshold::Cyclomatic),
make_finding("cognitive", ExceededThreshold::Cognitive),
make_finding("both", ExceededThreshold::Both),
make_finding("crap", ExceededThreshold::Crap),
make_finding("cyclomatic_crap", ExceededThreshold::CyclomaticCrap),
make_finding("all", ExceededThreshold::All),
];
sort_findings(&mut findings, HealthSort::Severity);
let names = findings
.iter()
.map(|finding| finding.name.as_str())
.collect::<Vec<_>>();
assert_eq!(
names,
[
"all",
"cyclomatic_crap",
"crap",
"both",
"cyclomatic",
"cognitive",
]
);
}
#[test]
fn sort_findings_breaks_metric_ties_by_source_location() {
let expected = ["worst", "alpha", "beta", "column", "line", "file"];
for root in ["/checkout/first", "/other/checkout with spaces"] {
for sort in [
HealthSort::Severity,
HealthSort::Cyclomatic,
HealthSort::Cognitive,
HealthSort::Lines,
] {
let mut findings = [
("file", "src/b.ts", 1, 1),
("line", "src/a.ts", 20, 1),
("column", "src/a.ts", 10, 3),
("beta", "src/a.ts", 10, 2),
("alpha", "src/a.ts", 10, 2),
("worst", "src/z.ts", 1, 1),
]
.map(|(name, path, line, col)| {
let mut finding = make_finding(name, ExceededThreshold::Cyclomatic);
finding.path = Path::new(root).join(path);
finding.line = line;
finding.col = col;
if name == "worst" {
finding.severity = FindingSeverity::Critical;
finding.cyclomatic += 1;
finding.cognitive += 1;
finding.line_count += 1;
}
finding
});
for shift in 0..findings.len() {
findings.rotate_left(shift);
findings.reverse();
sort_findings(&mut findings, sort);
assert_eq!(
findings.each_ref().map(|finding| finding.name.as_str()),
expected,
"sort {sort:?} with root {root} and input rotation {shift}"
);
}
}
}
}
#[test]
fn collect_findings_empty_modules() {
let (findings, files, functions) = collect_findings(
&[],
&FxHashMap::default(),
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert!(findings.is_empty());
assert_eq!(files, 0);
assert_eq!(functions, 0);
}
#[test]
fn collect_findings_below_threshold() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(FileId(0), vec![make_fc("doStuff", 5, 3, 10)])];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, files, functions) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert!(findings.is_empty());
assert_eq!(files, 1);
assert_eq!(functions, 1);
}
#[test]
fn collect_findings_exceeds_cyclomatic_only() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("complexFn", 25, 5, 50)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].cyclomatic, 25);
assert!(matches!(
findings[0].exceeded,
ExceededThreshold::Cyclomatic
));
}
#[test]
fn collect_findings_exceeds_cognitive_only() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(FileId(0), vec![make_fc("nestedFn", 5, 20, 30)])];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
assert!(matches!(findings[0].exceeded, ExceededThreshold::Cognitive));
}
#[test]
fn collect_findings_exceeds_both() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("terribleFn", 25, 20, 100)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
assert!(matches!(findings[0].exceeded, ExceededThreshold::Both));
}
#[test]
fn collect_findings_multiple_functions_per_file() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![
make_fc("ok", 5, 3, 10),
make_fc("bad", 25, 20, 50),
make_fc("also_bad", 21, 5, 30),
],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, files, functions) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 2);
assert_eq!(files, 1);
assert_eq!(functions, 3);
}
#[test]
fn collect_findings_ignores_matching_files() {
let path = PathBuf::from("/project/src/generated/types.ts");
let modules = vec![make_module(FileId(0), vec![make_fc("genFn", 25, 20, 50)])];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let ignore_set = build_ignore_set(&["src/generated/**".to_string()]);
let (findings, files, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&ignore_set,
None,
None,
20,
15,
false,
);
assert!(findings.is_empty());
assert_eq!(files, 0);
}
#[test]
fn collect_findings_filters_by_changed_files() {
let path_a = PathBuf::from("/project/src/a.ts");
let path_b = PathBuf::from("/project/src/b.ts");
let modules = vec![
make_module(FileId(0), vec![make_fc("fnA", 25, 20, 50)]),
make_module(FileId(1), vec![make_fc("fnB", 25, 20, 50)]),
];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path_a);
file_paths.insert(FileId(1), &path_b);
let mut changed = FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
let (findings, files, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
Some(&changed),
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].name, "fnA");
assert_eq!(files, 1);
}
fn build_diff(text: &str) -> fallow_output::DiffIndex {
fallow_output::DiffIndex::from_unified_diff(text)
}
#[test]
fn filter_complexity_findings_by_diff_keeps_hotspot_overlapping_diff_line() {
let mut findings = vec![ComplexityViolation {
path: PathBuf::from("/project/src/big.ts"),
name: "wide_fn".into(),
line: 10,
col: 0,
cyclomatic: 30,
cognitive: 30,
line_count: 110,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::Both,
severity: FindingSeverity::High,
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}];
let diff = build_diff(
"diff --git a/src/big.ts b/src/big.ts\n\
--- a/src/big.ts\n\
+++ b/src/big.ts\n\
@@ -114,1 +114,2 @@\n\
ctx\n\
+touched\n",
);
filter_complexity_findings_by_diff(&mut findings, &diff, Path::new("/project"));
assert_eq!(findings.len(), 1);
}
#[test]
fn filter_complexity_findings_by_diff_drops_finding_outside_diff() {
let mut findings = vec![ComplexityViolation {
path: PathBuf::from("/project/src/elsewhere.ts"),
name: "outside".into(),
line: 10,
col: 0,
cyclomatic: 30,
cognitive: 30,
line_count: 5,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::Both,
severity: FindingSeverity::High,
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}];
let diff = build_diff(
"diff --git a/src/big.ts b/src/big.ts\n\
--- a/src/big.ts\n\
+++ b/src/big.ts\n\
@@ -114,1 +114,2 @@\n\
ctx\n\
+touched\n",
);
filter_complexity_findings_by_diff(&mut findings, &diff, Path::new("/project"));
assert!(findings.is_empty());
}
#[test]
fn filter_complexity_findings_by_diff_handles_zero_line_count() {
let mut findings = vec![ComplexityViolation {
path: PathBuf::from("/project/src/a.ts"),
name: "zero_extent".into(),
line: 5,
col: 0,
cyclomatic: 30,
cognitive: 30,
line_count: 0,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::Both,
severity: FindingSeverity::High,
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}];
let diff = build_diff(
"diff --git a/src/a.ts b/src/a.ts\n\
--- a/src/a.ts\n\
+++ b/src/a.ts\n\
@@ -4,1 +4,2 @@\n\
ctx\n\
+touched\n",
);
filter_complexity_findings_by_diff(&mut findings, &diff, Path::new("/project"));
assert_eq!(findings.len(), 1);
}
#[test]
fn filter_hotspots_by_diff_uses_file_level_membership() {
use fallow_output::HotspotEntry;
let mut hotspots = vec![
HotspotEntry {
path: PathBuf::from("/project/src/touched.ts"),
score: 90.0,
commits: 50,
weighted_commits: 25.0,
lines_added: 1000,
lines_deleted: 500,
complexity_density: 0.4,
fan_in: 5,
trend: crate::churn::ChurnTrend::Stable,
ownership: None,
is_test_path: false,
},
HotspotEntry {
path: PathBuf::from("/project/src/untouched.ts"),
score: 90.0,
commits: 50,
weighted_commits: 25.0,
lines_added: 1000,
lines_deleted: 500,
complexity_density: 0.4,
fan_in: 5,
trend: crate::churn::ChurnTrend::Stable,
ownership: None,
is_test_path: false,
},
];
let diff = build_diff(
"diff --git a/src/touched.ts b/src/touched.ts\n\
--- a/src/touched.ts\n\
+++ b/src/touched.ts\n\
@@ -0,0 +1,1 @@\n\
+new\n",
);
filter_hotspots_by_diff(&mut hotspots, &diff, Path::new("/project"));
assert_eq!(hotspots.len(), 1);
assert_eq!(hotspots[0].path, PathBuf::from("/project/src/touched.ts"));
}
#[test]
fn filter_large_functions_by_diff_uses_range_overlap() {
use fallow_output::LargeFunctionEntry;
let mut entries = vec![
LargeFunctionEntry {
path: PathBuf::from("/project/src/a.ts"),
name: "kept".into(),
line: 10,
line_count: 100,
},
LargeFunctionEntry {
path: PathBuf::from("/project/src/a.ts"),
name: "dropped".into(),
line: 500,
line_count: 100,
},
];
let diff = build_diff(
"diff --git a/src/a.ts b/src/a.ts\n\
--- a/src/a.ts\n\
+++ b/src/a.ts\n\
@@ -49,1 +49,2 @@\n\
ctx\n\
+touched\n",
);
filter_large_functions_by_diff(&mut entries, &diff, Path::new("/project"));
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "kept");
}
fn dominant_unit_size_vital_signs() -> fallow_output::VitalSigns {
fallow_output::VitalSigns {
unit_size_profile: Some(fallow_output::RiskProfile {
low_risk: 0.0,
medium_risk: 0.0,
high_risk: 0.0,
very_high_risk: 100.0,
}),
..Default::default()
}
}
#[test]
fn collect_large_functions_respects_max_unit_size_override() {
let test_path = PathBuf::from("/project/src/math.test.ts");
let src_path = PathBuf::from("/project/src/math.ts");
let modules = vec![
make_module(FileId(0), vec![make_fc("<arrow>", 1, 1, 218)]),
make_module(FileId(1), vec![make_fc("bigHelper", 1, 1, 218)]),
];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &test_path);
file_paths.insert(FileId(1), &src_path);
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["**/*.test.*".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: None,
max_unit_size: Some(500),
reason: None,
}]);
let vital_signs = dominant_unit_size_vital_signs();
let input = LargeFunctionInput {
vital_signs: &vital_signs,
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
thresholds: &resolver,
};
let entries = collect_large_functions(&input);
let listed: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(listed, vec!["bigHelper"]);
assert!(entries.iter().all(|entry| entry.path == src_path));
}
#[test]
fn collect_large_functions_default_lists_every_oversized_function() {
let test_path = PathBuf::from("/project/src/math.test.ts");
let src_path = PathBuf::from("/project/src/math.ts");
let modules = vec![
make_module(FileId(0), vec![make_fc("<arrow>", 1, 1, 218)]),
make_module(FileId(1), vec![make_fc("bigHelper", 1, 1, 218)]),
];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &test_path);
file_paths.insert(FileId(1), &src_path);
let resolver = threshold_resolver(&[]);
let vital_signs = dominant_unit_size_vital_signs();
let input = LargeFunctionInput {
vital_signs: &vital_signs,
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
thresholds: &resolver,
};
let entries = collect_large_functions(&input);
assert_eq!(entries.len(), 2);
}
#[test]
fn collect_findings_skips_module_without_path() {
let modules = vec![make_module(FileId(99), vec![make_fc("orphan", 25, 20, 50)])];
let file_paths = FxHashMap::default();
let (findings, files, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert!(findings.is_empty());
assert_eq!(files, 0);
}
#[test]
fn collect_findings_at_exact_threshold_not_reported() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![make_fc("borderline", 20, 15, 20)],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert!(findings.is_empty());
}
#[test]
fn collect_findings_preserves_function_metadata() {
let path = PathBuf::from("/project/src/a.ts");
let modules = vec![make_module(
FileId(0),
vec![FunctionComplexity {
name: "processData".to_string(),
is_private_member: false,
line: 42,
col: 8,
cyclomatic: 25,
cognitive: 18,
line_count: 75,
param_count: 2,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
}],
)];
let mut file_paths = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let (findings, _, _) = collect_findings(
&modules,
&file_paths,
Path::new("/project"),
&globset::GlobSet::empty(),
None,
None,
20,
15,
false,
);
assert_eq!(findings.len(), 1);
let f = &findings[0];
assert_eq!(f.name, "processData");
assert_eq!(f.line, 42);
assert_eq!(f.col, 8);
assert_eq!(f.cyclomatic, 25);
assert_eq!(f.cognitive, 18);
assert_eq!(f.line_count, 75);
assert_eq!(f.path, PathBuf::from("/project/src/a.ts"));
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "test fixture; linear setup/assert, length is not a maintainability concern"
)]
fn merge_crap_findings_disambiguates_same_line_functions() {
let path = PathBuf::from("/project/src/curried.ts");
let outer = FunctionComplexity {
name: "handler".to_string(),
is_private_member: false,
line: 1,
col: 23,
cyclomatic: 1,
cognitive: 0,
line_count: 11,
param_count: 1,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
};
let inner = FunctionComplexity {
name: "<arrow>".to_string(),
is_private_member: false,
line: 1,
col: 43,
cyclomatic: 7,
cognitive: 0,
line_count: 10,
param_count: 1,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
};
let modules = vec![make_module(FileId(0), vec![inner.clone(), outer.clone()])];
let mut file_paths: FxHashMap<FileId, &PathBuf> = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let mut findings: Vec<ComplexityViolation> = Vec::new();
let mut per_function_crap: FxHashMap<PathBuf, Vec<scoring::PerFunctionCrap>> =
FxHashMap::default();
per_function_crap.insert(
path.clone(),
vec![
scoring::PerFunctionCrap {
line: inner.line,
col: inner.col,
crap: 56.0,
coverage_pct: None,
coverage_tier: fallow_output::CoverageTier::None,
coverage_source: fallow_output::CoverageSource::Estimated,
},
scoring::PerFunctionCrap {
line: outer.line,
col: outer.col,
crap: 2.0,
coverage_pct: None,
coverage_tier: fallow_output::CoverageTier::None,
coverage_source: fallow_output::CoverageSource::Estimated,
},
],
);
let resolver = threshold_resolver(&[]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut input = CrapFindingMergeInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
per_function_crap: &per_function_crap,
template_inherit_provenance: &FxHashMap::default(),
complexity_breakdown: false,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
};
merge_crap_findings(&mut findings, &mut input);
assert_eq!(
findings.len(),
1,
"expected one CRAP finding for inner arrow"
);
let f = &findings[0];
assert_eq!(f.name, "<arrow>", "name must come from inner arrow");
assert_eq!(f.line, 1);
assert_eq!(f.col, 43, "col must disambiguate same-line arrows");
assert_eq!(f.cyclomatic, 7, "cyclomatic must come from inner arrow");
assert_eq!(f.cognitive, 0);
assert_eq!(
f.crap,
Some(56.0),
"CRAP must match the function it's reported against"
);
let cc = f64::from(f.cyclomatic);
#[expect(
clippy::suboptimal_flops,
reason = "cc * cc + cc matches the CRAP formula specification"
)]
let expected_crap = cc * cc + cc;
assert!(
(f.crap.unwrap() - expected_crap).abs() < 0.01,
"CRAP must be consistent with reported CC: cc={cc}, crap={:?}, expected={expected_crap}",
f.crap,
);
}
#[test]
fn merge_crap_findings_picks_outer_when_outer_exceeds() {
let path = PathBuf::from("/project/src/curried_outer.ts");
let outer = FunctionComplexity {
name: "complex".to_string(),
is_private_member: false,
line: 5,
col: 10,
cyclomatic: 8,
cognitive: 0,
line_count: 20,
param_count: 1,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
};
let inner = FunctionComplexity {
name: "<arrow>".to_string(),
is_private_member: false,
line: 5,
col: 30,
cyclomatic: 1,
cognitive: 0,
line_count: 1,
param_count: 1,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
source_hash: None,
contributions: Vec::new(),
};
let modules = vec![make_module(FileId(0), vec![inner.clone(), outer.clone()])];
let mut file_paths: FxHashMap<FileId, &PathBuf> = FxHashMap::default();
file_paths.insert(FileId(0), &path);
let mut findings: Vec<ComplexityViolation> = Vec::new();
let mut per_function_crap: FxHashMap<PathBuf, Vec<scoring::PerFunctionCrap>> =
FxHashMap::default();
per_function_crap.insert(
path.clone(),
vec![
scoring::PerFunctionCrap {
line: inner.line,
col: inner.col,
crap: 2.0,
coverage_pct: None,
coverage_tier: fallow_output::CoverageTier::None,
coverage_source: fallow_output::CoverageSource::Estimated,
},
scoring::PerFunctionCrap {
line: outer.line,
col: outer.col,
crap: 72.0,
coverage_pct: None,
coverage_tier: fallow_output::CoverageTier::None,
coverage_source: fallow_output::CoverageSource::Estimated,
},
],
);
let resolver = threshold_resolver(&[]);
let mut tracker = ThresholdOverrideStateTracker::default();
let mut input = CrapFindingMergeInput {
modules: &modules,
file_paths: &file_paths,
config_root: Path::new("/project"),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
per_function_crap: &per_function_crap,
template_inherit_provenance: &FxHashMap::default(),
complexity_breakdown: false,
threshold_resolver: &resolver,
threshold_state_tracker: &mut tracker,
};
merge_crap_findings(&mut findings, &mut input);
assert_eq!(findings.len(), 1);
let f = &findings[0];
assert_eq!(f.name, "complex");
assert_eq!(f.col, 10);
assert_eq!(f.cyclomatic, 8);
assert_eq!(f.crap, Some(72.0));
}
fn fx_summary(
tracked: usize,
hit: usize,
unhit: usize,
untracked: usize,
) -> fallow_output::RuntimeCoverageSummary {
#[expect(
clippy::cast_precision_loss,
reason = "test fixture totals are tiny, f64 precision is fine"
)]
let coverage_percent = if tracked == 0 {
0.0
} else {
(hit as f64 / tracked as f64) * 100.0
};
fallow_output::RuntimeCoverageSummary {
data_source: fallow_output::RuntimeCoverageDataSource::Local,
last_received_at: None,
functions_tracked: tracked,
functions_hit: hit,
functions_unhit: unhit,
functions_untracked: untracked,
coverage_percent,
trace_count: 512,
period_days: 7,
deployments_seen: 2,
capture_quality: None,
}
}
fn fx_evidence(
static_status: &str,
test_coverage: &str,
v8_tracking: &str,
) -> fallow_output::RuntimeCoverageEvidence {
fallow_output::RuntimeCoverageEvidence {
static_status: static_status.to_owned(),
test_coverage: test_coverage.to_owned(),
test_only_reference: None,
v8_tracking: v8_tracking.to_owned(),
untracked_reason: None,
observation_days: 7,
deployments_observed: 2,
}
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "test fixture; linear setup/assert, length is not a maintainability concern"
)]
fn runtime_coverage_top_applies_after_baseline_filtering() {
let root = Path::new("/project");
let baseline = HealthBaselineData {
findings: vec![],
finding_counts: std::collections::BTreeMap::new(),
identity_finding_counts: std::collections::BTreeMap::new(),
runtime_coverage_findings: vec![
"fallow:prod:aaaaaaaa".to_owned(),
"fallow:prod:bbbbbbbb".to_owned(),
],
runtime_coverage_source_hashes: vec![],
target_keys: vec![],
};
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected,
signals: Vec::new(),
summary: fx_summary(3, 0, 2, 1),
findings: vec![
fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:aaaaaaaa".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/a.ts"),
function: "alpha".to_owned(),
line: 10,
verdict: fallow_output::RuntimeCoverageVerdict::ReviewRequired,
invocations: Some(0),
confidence: fallow_output::RuntimeCoverageConfidence::Medium,
evidence: fx_evidence("used", "not_covered", "tracked"),
actions: vec![],
source_hash: None,
discriminators: None,
},
fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:bbbbbbbb".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/b.ts"),
function: "beta".to_owned(),
line: 20,
verdict: fallow_output::RuntimeCoverageVerdict::CoverageUnavailable,
invocations: None,
confidence: fallow_output::RuntimeCoverageConfidence::None,
evidence: fx_evidence("used", "not_covered", "untracked"),
actions: vec![],
source_hash: None,
discriminators: None,
},
fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:cccccccc".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/c.ts"),
function: "gamma".to_owned(),
line: 30,
verdict: fallow_output::RuntimeCoverageVerdict::ReviewRequired,
invocations: Some(0),
confidence: fallow_output::RuntimeCoverageConfidence::Medium,
evidence: fx_evidence("used", "not_covered", "tracked"),
actions: vec![],
source_hash: None,
discriminators: None,
},
],
hot_paths: vec![
fallow_output::RuntimeCoverageHotPath {
id: "fallow:hot:11111111".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/hot-a.ts"),
function: "hotAlpha".to_owned(),
line: 1,
end_line: 5,
invocations: 500,
percentile: 99,
actions: vec![],
},
fallow_output::RuntimeCoverageHotPath {
id: "fallow:hot:22222222".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/hot-b.ts"),
function: "hotBeta".to_owned(),
line: 2,
end_line: 8,
invocations: 250,
percentile: 50,
actions: vec![],
},
],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root)
.with_baseline(Some(&baseline))
.with_top(Some(1)),
);
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].function, "gamma");
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected
);
assert_eq!(report.summary.functions_tracked, 3);
assert_eq!(report.summary.functions_hit, 0);
assert_eq!(report.summary.functions_unhit, 2);
assert_eq!(report.summary.functions_untracked, 1);
assert!((report.summary.coverage_percent - 0.0).abs() < 0.05);
assert_eq!(report.hot_paths.len(), 1);
assert_eq!(report.hot_paths[0].function, "hotAlpha");
}
#[test]
fn runtime_coverage_baseline_refreshes_to_clean_when_only_baselined_findings_remain() {
let root = Path::new("/project");
let baseline = HealthBaselineData {
findings: vec![],
finding_counts: std::collections::BTreeMap::new(),
identity_finding_counts: std::collections::BTreeMap::new(),
runtime_coverage_findings: vec!["fallow:prod:aaaaaaaa".to_owned()],
runtime_coverage_source_hashes: vec![],
target_keys: vec![],
};
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected,
signals: Vec::new(),
summary: fx_summary(2, 1, 1, 0),
findings: vec![fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:aaaaaaaa".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/a.ts"),
function: "alpha".to_owned(),
line: 10,
verdict: fallow_output::RuntimeCoverageVerdict::ReviewRequired,
invocations: Some(0),
confidence: fallow_output::RuntimeCoverageConfidence::Medium,
evidence: fx_evidence("used", "not_covered", "tracked"),
actions: vec![],
source_hash: None,
discriminators: None,
}],
hot_paths: vec![],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_baseline(Some(&baseline)),
);
assert!(report.findings.is_empty());
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::Clean
);
assert_eq!(report.summary.functions_tracked, 2);
assert_eq!(report.summary.functions_hit, 1);
assert_eq!(report.summary.functions_unhit, 1);
assert_eq!(report.summary.functions_untracked, 0);
assert!((report.summary.coverage_percent - 50.0).abs() < 0.05);
}
#[test]
fn runtime_coverage_changed_review_uses_hot_path_verdict() {
let root = Path::new("/project");
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::Clean,
signals: Vec::new(),
summary: fx_summary(2, 2, 0, 0),
findings: vec![],
hot_paths: vec![fallow_output::RuntimeCoverageHotPath {
id: "fallow:hot:33333333".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/hot.ts"),
function: "renderHotPath".to_owned(),
line: 7,
end_line: 24,
invocations: 9_500,
percentile: 99,
actions: vec![],
}],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_changed_files(Some(&changed_files)),
);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::HotPathTouched
);
}
#[test]
fn runtime_coverage_changed_review_ignores_unmodified_hot_paths() {
let root = Path::new("/project");
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/other.ts"));
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::Clean,
signals: Vec::new(),
summary: fx_summary(2, 2, 0, 0),
findings: vec![],
hot_paths: vec![fallow_output::RuntimeCoverageHotPath {
id: "fallow:hot:44444444".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/hot.ts"),
function: "renderHotPath".to_owned(),
line: 7,
end_line: 24,
invocations: 9_500,
percentile: 90,
actions: vec![],
}],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_changed_files(Some(&changed_files)),
);
assert!(report.hot_paths.is_empty());
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::Clean
);
}
fn fx_runtime_coverage_report_with_hot_paths(
hot_paths: Vec<fallow_output::RuntimeCoverageHotPath>,
) -> fallow_output::RuntimeCoverageReport {
fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::Clean,
signals: Vec::new(),
summary: fx_summary(2, 2, 0, 0),
findings: vec![],
hot_paths,
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
}
}
fn fx_hot_path(
id: &str,
path: &str,
line: u32,
end_line: u32,
) -> fallow_output::RuntimeCoverageHotPath {
fallow_output::RuntimeCoverageHotPath {
id: id.to_owned(),
stable_id: None,
path: PathBuf::from(path),
function: "renderHotPath".to_owned(),
line,
end_line,
invocations: 9_500,
percentile: 99,
actions: vec![],
}
}
#[test]
fn runtime_coverage_diff_index_keeps_hot_paths_with_added_line_in_range() {
let root = Path::new("/project");
let diff = "diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -10,1 +10,2 @@\n\
+ // touch the body\n\
line 11\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:01010101",
"src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_diff_index(Some(&diff_index)),
);
assert_eq!(report.hot_paths.len(), 1);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::HotPathTouched
);
}
#[test]
fn runtime_coverage_diff_index_drops_hot_paths_when_added_line_outside_range() {
let root = Path::new("/project");
let diff = "diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -50,1 +50,2 @@\n\
+ // unrelated change far below the hot function\n\
line 51\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:02020202",
"src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_diff_index(Some(&diff_index)),
);
assert!(report.hot_paths.is_empty());
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::Clean
);
}
#[test]
fn runtime_coverage_diff_index_falls_back_to_single_line_when_end_line_zero() {
let root = Path::new("/project");
let diff = "diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -7,1 +7,2 @@\n\
+ // exactly the function's start line\n\
line 8\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:03030303",
"src/hot.ts",
7,
0,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_diff_index(Some(&diff_index)),
);
assert_eq!(report.hot_paths.len(), 1);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::HotPathTouched
);
}
#[test]
fn runtime_coverage_diff_index_resolves_absolute_hot_path_against_root() {
let root = Path::new("/project");
let diff = "diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -10,1 +10,2 @@\n\
+ // touched\n\
line 11\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:04040404",
"/project/src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_diff_index(Some(&diff_index)),
);
assert_eq!(report.hot_paths.len(), 1);
}
#[test]
fn runtime_coverage_diff_index_authoritative_for_files_in_diff() {
let root = Path::new("/project");
let diff = "diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -50,1 +50,2 @@\n\
+ // outside the hot function\n\
line 51\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:05050505",
"src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root)
.with_changed_files(Some(&changed_files))
.with_diff_index(Some(&diff_index)),
);
assert!(report.hot_paths.is_empty());
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::Clean
);
}
#[test]
fn runtime_coverage_per_file_fallback_to_changed_files_when_diff_omits_file() {
let root = Path::new("/project");
let diff = "diff --git a/src/other.ts b/src/other.ts\n\
--- a/src/other.ts\n\
+++ b/src/other.ts\n\
@@ -1,1 +1,2 @@\n\
+ // unrelated\n\
line 2\n";
let diff_index = fallow_output::DiffIndex::from_unified_diff(diff);
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:0a0a0a0a",
"src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root)
.with_changed_files(Some(&changed_files))
.with_diff_index(Some(&diff_index)),
);
assert_eq!(report.hot_paths.len(), 1);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::HotPathTouched
);
}
#[test]
fn runtime_coverage_pr_context_promotes_hot_path_touched_above_cold_code() {
let root = Path::new("/project");
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected,
signals: Vec::new(),
summary: fx_summary(2, 1, 1, 0),
findings: vec![fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:cold0001".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/cold.ts"),
function: "coldFn".to_owned(),
line: 4,
verdict: fallow_output::RuntimeCoverageVerdict::SafeToDelete,
invocations: Some(0),
confidence: fallow_output::RuntimeCoverageConfidence::High,
evidence: fx_evidence("unused", "not_covered", "tracked"),
actions: vec![],
source_hash: None,
discriminators: None,
}],
hot_paths: vec![fx_hot_path("fallow:hot:0b0b0b0b", "src/hot.ts", 7, 24)],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_changed_files(Some(&changed_files)),
);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::HotPathTouched
);
assert_eq!(
report.signals,
vec![
fallow_output::RuntimeCoverageSignal::ColdCodeDetected,
fallow_output::RuntimeCoverageSignal::HotPathTouched,
]
);
}
#[test]
fn runtime_coverage_standalone_keeps_cold_code_primary_above_unchanged_hot_paths() {
let root = Path::new("/project");
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::Clean,
signals: Vec::new(),
summary: fx_summary(2, 1, 1, 0),
findings: vec![fallow_output::RuntimeCoverageFinding {
id: "fallow:prod:cold0002".to_owned(),
stable_id: None,
path: PathBuf::from("/project/src/cold.ts"),
function: "coldFn".to_owned(),
line: 4,
verdict: fallow_output::RuntimeCoverageVerdict::SafeToDelete,
invocations: Some(0),
confidence: fallow_output::RuntimeCoverageConfidence::High,
evidence: fx_evidence("unused", "not_covered", "tracked"),
actions: vec![],
source_hash: None,
discriminators: None,
}],
hot_paths: vec![fx_hot_path("fallow:hot:0c0c0c0c", "src/hot.ts", 7, 24)],
blast_radius: vec![],
importance: vec![],
watermark: None,
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(&mut report, &RuntimeCoverageFilterContext::new(root));
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected
);
assert_eq!(
report.signals,
vec![fallow_output::RuntimeCoverageSignal::ColdCodeDetected]
);
assert_eq!(report.hot_paths.len(), 1);
}
#[test]
fn runtime_coverage_license_grace_outranks_pr_context_signals() {
let root = Path::new("/project");
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fallow_output::RuntimeCoverageReport {
schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
verdict: fallow_output::RuntimeCoverageReportVerdict::LicenseExpiredGrace,
signals: Vec::new(),
summary: fx_summary(2, 1, 1, 0),
findings: vec![],
hot_paths: vec![fx_hot_path("fallow:hot:0d0d0d0d", "src/hot.ts", 7, 24)],
blast_radius: vec![],
importance: vec![],
watermark: Some(fallow_output::RuntimeCoverageWatermark::LicenseExpiredGrace),
warnings: vec![],
actionable: true,
actionability_reason: None,
actionability_verdict: None,
provenance: fallow_output::RuntimeCoverageProvenance::default(),
};
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_changed_files(Some(&changed_files)),
);
assert_eq!(
report.verdict,
fallow_output::RuntimeCoverageReportVerdict::LicenseExpiredGrace
);
assert!(
report
.signals
.contains(&fallow_output::RuntimeCoverageSignal::LicenseExpiredGrace)
);
assert!(
report
.signals
.contains(&fallow_output::RuntimeCoverageSignal::HotPathTouched)
);
}
#[test]
fn retain_hot_paths_drops_when_diff_touches_file_but_no_added_lines() {
let root = Path::new("/project");
let diff = fallow_output::DiffIndex::from_unified_diff(
"diff --git a/src/hot.ts b/src/hot.ts\n\
--- a/src/hot.ts\n\
+++ b/src/hot.ts\n\
@@ -10,3 +10,1 @@\n\
-one\n\
-two\n\
-three\n\
ctx\n",
);
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:deletiononly",
"src/hot.ts",
10,
12,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root)
.with_diff_index(Some(&diff))
.with_changed_files(Some(&changed_files)),
);
assert!(
report.hot_paths.is_empty(),
"diff touched the file with no added lines: must drop, not fall through to changed_files"
);
}
#[test]
fn runtime_coverage_changed_files_matches_relative_hot_path_against_absolute_set() {
let root = Path::new("/project");
let mut changed_files = FxHashSet::default();
changed_files.insert(PathBuf::from("/project/src/hot.ts"));
let mut report = fx_runtime_coverage_report_with_hot_paths(vec![fx_hot_path(
"fallow:hot:06060606",
"src/hot.ts",
7,
24,
)]);
apply_runtime_coverage_filters(
&mut report,
&RuntimeCoverageFilterContext::new(root).with_changed_files(Some(&changed_files)),
);
assert_eq!(report.hot_paths.len(), 1);
}
const ROLLUP_ROOT: &str = "/proj";
fn rollup_resolver(
overrides: &[fallow_config::HealthThresholdOverride],
cyclomatic: u16,
cognitive: u16,
) -> ThresholdOverrideResolver {
ThresholdOverrideResolver::new(
overrides,
GlobalHealthThresholds {
cyclomatic,
cognitive,
crap: 30.0,
unit_size: 60,
},
)
}
fn template_units_for(
owner: &str,
template_path: &str,
cyclomatic: u16,
cognitive: u16,
) -> FxHashMap<PathBuf, Vec<ComponentTemplateUnit>> {
let mut map = FxHashMap::default();
map.insert(
PathBuf::from(owner),
vec![ComponentTemplateUnit {
path: PathBuf::from(template_path),
cyclomatic,
cognitive,
line_count: 30,
}],
);
map
}
fn no_template_units() -> FxHashMap<PathBuf, Vec<ComponentTemplateUnit>> {
FxHashMap::default()
}
fn make_class_finding(
path: &str,
name: &str,
line: u32,
cyclomatic: u16,
cognitive: u16,
) -> ComplexityViolation {
ComplexityViolation {
path: PathBuf::from(path),
name: name.to_string(),
line,
col: 0,
cyclomatic,
cognitive,
line_count: 20,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::Both,
severity: FindingSeverity::Moderate,
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}
}
fn make_template_finding(
path: &str,
line: u32,
cyclomatic: u16,
cognitive: u16,
) -> ComplexityViolation {
ComplexityViolation {
path: PathBuf::from(path),
name: "<template>".to_string(),
line,
col: 0,
cyclomatic,
cognitive,
line_count: 30,
param_count: 0,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
exceeded: ExceededThreshold::Both,
severity: FindingSeverity::Moderate,
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
component_rollup: None,
contributions: Vec::new(),
effective_thresholds: None,
threshold_source: None,
}
}
#[test]
fn rollup_external_template_via_provenance_lookup() {
let component_ts = PathBuf::from("/proj/src/host-game.component.ts");
let template_html = PathBuf::from("/proj/src/host-game.component.html");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "handleClick", 42, 3, 4),
make_template_finding(template_html.to_str().unwrap(), 1, 6, 10),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
template_html.to_str().unwrap(),
6,
10,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 8, 8),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
assert_eq!(findings.len(), 3, "rollup is strictly additive");
let rollup = findings
.iter()
.find(|f| f.name == "<component>")
.expect("rollup must be present");
assert_eq!(rollup.path, component_ts);
assert_eq!(rollup.cyclomatic, 9, "9 = worst class 3 + template 6");
assert_eq!(rollup.cognitive, 14, "14 = worst class 4 + template 10");
assert_eq!(rollup.line, 42, "anchored at worst class function line");
let breakdown = rollup.component_rollup.as_ref().expect("breakdown present");
assert_eq!(
breakdown.component, "host-game.component",
"component identifier is the .ts owner's file stem"
);
assert_eq!(breakdown.class_worst_function, "handleClick");
assert_eq!(breakdown.class_cyclomatic, 3);
assert_eq!(breakdown.template_cyclomatic, 6);
assert_eq!(breakdown.template_path, template_html);
}
#[test]
fn rollup_inline_template_owner_is_same_ts_file() {
let component_ts = PathBuf::from("/proj/src/inline.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "ngOnInit", 25, 5, 8),
make_template_finding(component_ts.to_str().unwrap(), 10, 4, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
4,
6,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 8, 8),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
let rollup = findings
.iter()
.find(|f| f.name == "<component>")
.expect("rollup must be present for inline-template case without provenance lookup");
assert_eq!(rollup.cyclomatic, 9);
assert_eq!(rollup.cognitive, 14);
let breakdown = rollup.component_rollup.as_ref().unwrap();
assert_eq!(breakdown.template_path, component_ts);
assert_eq!(breakdown.component, "inline.component");
}
#[test]
fn rollup_picks_worst_class_function_by_cyclomatic() {
let component_ts = PathBuf::from("/proj/src/multi.component.ts");
let template = PathBuf::from("/proj/src/multi.component.html");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "first", 10, 3, 4),
make_class_finding(component_ts.to_str().unwrap(), "worst", 20, 8, 9),
make_class_finding(component_ts.to_str().unwrap(), "middle", 30, 5, 6),
make_template_finding(template.to_str().unwrap(), 1, 4, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
template.to_str().unwrap(),
4,
6,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 8, 8),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
let rollup = findings.iter().find(|f| f.name == "<component>").unwrap();
assert_eq!(rollup.cyclomatic, 12, "8 (worst.cyc) + 4 (template.cyc)");
let breakdown = rollup.component_rollup.as_ref().unwrap();
assert_eq!(breakdown.class_worst_function, "worst");
assert_eq!(breakdown.class_cyclomatic, 8);
}
#[test]
fn rollup_skipped_when_no_template_unit() {
let component_ts = "/proj/src/only-class.component.ts";
let mut findings = vec![make_class_finding(component_ts, "Foo.method", 10, 5, 7)];
let before = findings.len();
append_component_rollup_findings(
&mut findings,
&no_template_units(),
&rollup_resolver(&[], 30, 25),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
assert_eq!(findings.len(), before, "no template means no rollup");
}
#[test]
fn rollup_survives_a_template_below_its_own_thresholds() {
let component_ts = PathBuf::from("/proj/src/quiet-template.component.ts");
let template_html = PathBuf::from("/proj/src/quiet-template.component.html");
let mut findings = vec![make_class_finding(
component_ts.to_str().unwrap(),
"classify",
29,
22,
18,
)];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
template_html.to_str().unwrap(),
11,
9,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 20, 25),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
let rollup = findings
.iter()
.find(|finding| finding.name == "<component>")
.expect("a below-threshold template still contributes its complexity");
assert_eq!(rollup.cyclomatic, 33, "22 (worst class) + 11 (template)");
assert_eq!(rollup.cognitive, 27, "18 (worst class) + 9 (template)");
let breakdown = rollup.component_rollup.as_ref().expect("breakdown");
assert_eq!(breakdown.template_path, template_html);
}
#[test]
fn rollup_skipped_when_no_class_findings() {
let template_html = PathBuf::from("/proj/src/orphan.component.html");
let component_ts = PathBuf::from("/proj/src/orphan.component.ts");
let mut findings = vec![make_template_finding(
template_html.to_str().unwrap(),
1,
6,
10,
)];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
template_html.to_str().unwrap(),
6,
10,
);
let before = findings.len();
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 8, 8),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
assert_eq!(
findings.len(),
before,
"no class methods above threshold means no rollup"
);
}
#[test]
fn rollup_skipped_when_multiple_templates_on_one_owner() {
let component_ts = PathBuf::from("/proj/src/twin.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "TwinA.fn", 10, 5, 7),
make_template_finding(component_ts.to_str().unwrap(), 5, 3, 4),
make_template_finding(component_ts.to_str().unwrap(), 50, 4, 5),
];
let mut template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
3,
4,
);
template_units
.get_mut(component_ts.as_path())
.expect("owner entry")
.push(ComponentTemplateUnit {
path: component_ts.clone(),
cyclomatic: 4,
cognitive: 5,
line_count: 30,
});
let before = findings.len();
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&[], 30, 25),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
assert_eq!(
findings.len(),
before,
"two templates on one owner is defensively skipped"
);
}
#[test]
fn collect_component_template_units_attributes_owners_and_respects_suppression() {
let html_id = FileId(1);
let inline_id = FileId(2);
let svelte_id = FileId(3);
let html_path = PathBuf::from("/proj/src/host.component.html");
let inline_path = PathBuf::from("/proj/src/inline.component.ts");
let svelte_path = PathBuf::from("/proj/src/Widget.svelte");
let owner_path = PathBuf::from("/proj/src/host.component.ts");
let modules = vec![
make_module(html_id, vec![make_fc("<template>", 6, 10, 30)]),
make_module(inline_id, vec![make_fc("<template>", 4, 6, 30)]),
make_module(svelte_id, vec![make_fc("<template>", 9, 21, 30)]),
];
let mut file_paths = FxHashMap::default();
file_paths.insert(html_id, &html_path);
file_paths.insert(inline_id, &inline_path);
file_paths.insert(svelte_id, &svelte_path);
let scope = ComponentTemplateUnitScope {
config_root: Path::new(ROLLUP_ROOT),
ignore_set: &globset::GlobSet::empty(),
changed_files: None,
ws_roots: None,
};
let without_lookup = collect_component_template_units(&modules, &file_paths, None, &scope);
assert!(
!without_lookup.contains_key(&owner_path),
"an unattributable .html template must not roll up: {without_lookup:#?}"
);
assert!(
without_lookup.contains_key(&inline_path),
"inline templates own themselves: {without_lookup:#?}"
);
assert!(
!without_lookup.contains_key(&svelte_path),
"SFC dialects have no component rollup: {without_lookup:#?}"
);
let mut lookup = FxHashMap::default();
lookup.insert(html_path.clone(), owner_path.clone());
let with_lookup =
collect_component_template_units(&modules, &file_paths, Some(&lookup), &scope);
let units = with_lookup
.get(&owner_path)
.expect("the provenance lookup attributes the .html template");
assert_eq!(units.len(), 1);
assert_eq!(units[0].path, html_path);
assert_eq!(units[0].cyclomatic, 6);
let mut suppressed_module = make_module(html_id, vec![make_fc("<template>", 6, 10, 30)]);
suppressed_module
.suppressions
.push(crate::suppress::Suppression::issue(
1,
0,
crate::suppress::IssueKind::Complexity,
));
let suppressed_modules = vec![suppressed_module];
let suppressed =
collect_component_template_units(&suppressed_modules, &file_paths, Some(&lookup), &scope);
assert!(
suppressed.is_empty(),
"a suppressed template contributes nothing: {suppressed:#?}"
);
}
fn issue_2163_globals() -> GlobalHealthThresholds {
GlobalHealthThresholds {
cyclomatic: 20,
cognitive: 15,
crap: 30.0,
unit_size: 60,
}
}
fn svelte_template_finding(exceeded: ExceededThreshold) -> ComplexityViolation {
let mut finding = make_finding("<template>", exceeded);
finding.path = PathBuf::from("/project/src/Widget.svelte");
finding.cyclomatic = 9;
finding.cognitive = 21;
finding.crap = Some(90.0);
finding
}
#[test]
fn override_row_names_crap_as_the_outstanding_dimension() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
cyclomatic: 9,
cognitive: 21,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let findings = vec![svelte_template_finding(ExceededThreshold::Crap)];
annotate_outstanding_dimensions(&mut tracker, &findings);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert_eq!(states[0].override_index, 0);
assert_eq!(
states[0].dimension,
fallow_output::ThresholdOverrideDimension::Complexity
);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
));
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Crap]
);
}
#[test]
fn crap_only_override_row_names_complexity_as_outstanding() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: Some(500.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 25,
cognitive: 21,
crap: 90.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let mut finding = svelte_template_finding(ExceededThreshold::Cyclomatic);
finding.cyclomatic = 25;
annotate_outstanding_dimensions(&mut tracker, &[finding]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert_eq!(
states[0].dimension,
fallow_output::ThresholdOverrideDimension::Crap
);
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity]
);
}
#[test]
fn override_configuring_every_ceiling_reports_nothing_outstanding() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: Some(500.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
cyclomatic: 9,
cognitive: 21,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 9,
cognitive: 21,
crap: 90.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
annotate_outstanding_dimensions(&mut tracker, &[]);
let states = tracker.into_states();
assert_eq!(states.len(), 2);
assert_eq!(
states[0].dimension,
fallow_output::ThresholdOverrideDimension::Complexity
);
assert_eq!(
states[1].dimension,
fallow_output::ThresholdOverrideDimension::Crap
);
assert!(states.iter().all(|state| state.outstanding.is_empty()));
assert!(
states
.iter()
.all(|state| matches!(state.status, fallow_output::ThresholdOverrideStatus::Active))
);
}
#[test]
fn a_configured_ceiling_that_is_still_breached_reports_insufficient_and_outstanding() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: Some(40.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 9,
cognitive: 21,
crap: 90.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let findings = vec![svelte_template_finding(ExceededThreshold::Crap)];
annotate_outstanding_dimensions(&mut tracker, &findings);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Insufficient
));
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Crap]
);
}
#[test]
fn a_partly_configured_complexity_override_still_reports_the_surviving_dimension() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: None,
max_crap: Some(5000.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
cyclomatic: 25,
cognitive: 21,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 25,
cognitive: 21,
crap: 90.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let findings = vec![svelte_template_finding(ExceededThreshold::Cognitive)];
annotate_outstanding_dimensions(&mut tracker, &findings);
let states = tracker.into_states();
assert_eq!(states.len(), 2);
assert!(
states.iter().all(|state| state.outstanding
== vec![fallow_output::ThresholdOverrideDimension::Complexity])
);
}
#[test]
fn a_superseded_override_row_reports_the_resolved_ceiling_not_its_own() {
let resolver = threshold_resolver(&[
fallow_config::HealthThresholdOverride {
files: vec!["src/**".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(25),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
},
fallow_config::HealthThresholdOverride {
files: vec!["src/hot.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(100),
max_cognitive: Some(100),
max_crap: None,
max_unit_size: None,
reason: None,
},
]);
let relative = Path::new("src/hot.ts");
let (applied, matches) = resolver.resolve(relative, "hot");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: Path::new("/project/src/hot.ts"),
function: "hot",
line: 1,
col: 0,
cyclomatic: 30,
cognitive: 29,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
annotate_outstanding_dimensions(&mut tracker, &[]);
let states = tracker.into_states();
assert_eq!(states.len(), 2);
assert!(
states
.iter()
.all(|state| matches!(state.status, fallow_output::ThresholdOverrideStatus::Active)),
"{:?}",
states.iter().map(|state| state.status).collect::<Vec<_>>()
);
assert!(
states
.iter()
.all(|state| state.effective_thresholds.max_cyclomatic == 100)
);
}
#[test]
fn a_suppressed_unit_reports_a_stale_crap_row_and_still_counts_as_matched() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/supp.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: Some(100.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/supp.ts");
let (applied, matches) = resolver.resolve(relative, "legacyParse");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_crap(
CrapFunctionContext {
path: Path::new("/project/src/supp.ts"),
function: "legacyParse",
line: 2,
col: 0,
suppressed: true,
},
MeasuredThresholdMetrics {
cyclomatic: 30,
cognitive: 29,
crap: 930.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_no_match_entries(&resolver, true);
annotate_outstanding_dimensions(&mut tracker, &[]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Stale
));
assert!(states[0].outstanding.is_empty());
}
#[test]
fn a_suppressed_unit_reports_a_stale_complexity_row_and_still_counts_as_matched() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/supp.ts".to_string()],
functions: vec!["gnarly".to_string()],
max_cyclomatic: Some(40),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let (applied, matches) = resolver.resolve(Path::new("src/supp.ts"), "gnarly");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: Path::new("/project/src/supp.ts"),
function: "gnarly",
line: 2,
col: 0,
cyclomatic: 30,
cognitive: 12,
line_count: Some(40),
suppressed: true,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_no_match_entries(&resolver, true);
annotate_outstanding_dimensions(&mut tracker, &[]);
let states = tracker.into_states();
assert_eq!(states.len(), 1, "one stale row, and not a `no_match` one");
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Stale
),
"the suppression, not the raised ceiling, keeps the unit quiet: {:#?}",
states[0]
);
assert!(states[0].outstanding.is_empty());
}
#[test]
fn a_max_unit_size_override_still_scores_a_suppressed_unit() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/supp.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: None,
max_unit_size: Some(500),
reason: None,
}]);
let (applied, matches) = resolver.resolve(Path::new("src/supp.ts"), "gnarly");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: Path::new("/project/src/supp.ts"),
function: "gnarly",
line: 2,
col: 0,
cyclomatic: 5,
cognitive: 4,
line_count: Some(100),
suppressed: true,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
),
"100 lines exceed the global 60 and clear the raised 500: {:#?}",
states[0]
);
}
#[test]
fn threshold_override_metrics_carry_line_count_on_complexity_rows_only() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: Some(500.0),
max_unit_size: None,
reason: None,
}]);
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(Path::new("src/Widget.svelte"), "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
cyclomatic: 9,
cognitive: 21,
line_count: Some(100),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "<template>",
line: 1,
col: 0,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 9,
cognitive: 21,
crap: 90.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let states = tracker.into_states();
assert_eq!(states.len(), 2);
let complexity_row = states
.iter()
.find(|state| state.dimension == fallow_output::ThresholdOverrideDimension::Complexity)
.expect("complexity row");
assert_eq!(
complexity_row.metrics.expect("metrics").line_count,
Some(100)
);
let crap_row = states
.iter()
.find(|state| state.dimension == fallow_output::ThresholdOverrideDimension::Crap)
.expect("crap row");
assert!(crap_row.metrics.expect("metrics").line_count.is_none());
}
#[test]
fn same_named_units_do_not_steal_each_others_outstanding_dimensions() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/handlers.js".to_string()],
functions: vec!["handler".to_string()],
max_cyclomatic: Some(25),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/handlers.js");
let absolute = Path::new("/project/src/handlers.js");
let (applied, matches) = resolver.resolve(relative, "handler");
let mut tracker = ThresholdOverrideStateTracker::default();
for (line, cyclomatic, cognitive) in [(2u32, 30u16, 29u16), (37, 12, 11)] {
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "handler",
line,
col: 2,
cyclomatic,
cognitive,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
}
let mut breaching = make_finding("handler", ExceededThreshold::All);
breaching.path = absolute.to_path_buf();
breaching.line = 2;
breaching.col = 2;
let mut crap_only = make_finding("handler", ExceededThreshold::Crap);
crap_only.path = absolute.to_path_buf();
crap_only.line = 37;
crap_only.col = 2;
annotate_outstanding_dimensions(&mut tracker, &[breaching, crap_only]);
let states = tracker.into_states();
assert_eq!(states.len(), 2, "one row per matched unit");
let by_line: Vec<_> = states
.iter()
.map(|state| (state.line, state.outstanding.clone()))
.collect();
assert_eq!(
by_line,
vec![
(
Some(2),
vec![
fallow_output::ThresholdOverrideDimension::Complexity,
fallow_output::ThresholdOverrideDimension::Crap
]
),
(
Some(37),
vec![fallow_output::ThresholdOverrideDimension::Crap]
),
]
);
}
#[test]
fn a_component_rollup_does_not_steal_its_anchor_methods_outstanding_dimension() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: Some(40.0),
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/widget.component.ts");
let absolute = Path::new("/proj/src/widget.component.ts");
let (applied, matches) = resolver.resolve(relative, "classify");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_crap(
CrapFunctionContext {
path: absolute,
function: "classify",
line: 29,
col: 10,
suppressed: false,
},
MeasuredThresholdMetrics {
cyclomatic: 9,
cognitive: 1,
crap: 110.0,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let mut method = make_finding("classify", ExceededThreshold::Crap);
method.path = absolute.to_path_buf();
method.line = 29;
method.col = 10;
let mut rollup = make_finding("<component>", ExceededThreshold::Cyclomatic);
rollup.path = absolute.to_path_buf();
rollup.line = 29;
rollup.col = 10;
annotate_outstanding_dimensions(&mut tracker, &[method, rollup]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Crap],
"the row describes `classify`, not the rollup anchored on top of it"
);
}
#[test]
fn a_component_rollup_is_measured_against_the_override_ceilings() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: None,
max_unit_size: None,
reason: None,
}];
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&overrides, 3, 3),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
assert!(
findings.iter().all(|finding| finding.name != "<component>"),
"the raised ceilings cover the rollup as well"
);
}
#[test]
fn a_surviving_component_rollup_publishes_its_override_ceilings() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(20),
max_cognitive: Some(20),
max_crap: None,
max_unit_size: None,
reason: None,
}];
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
append_component_rollup_findings(
&mut findings,
&template_units,
&rollup_resolver(&overrides, 3, 3),
Path::new(ROLLUP_ROOT),
&mut ThresholdOverrideStateTracker::default(),
);
let rollup = findings
.iter()
.find(|finding| finding.name == "<component>")
.expect("23 cyclomatic still exceeds the raised ceiling of 20");
assert_eq!(
rollup
.effective_thresholds
.expect("override ceilings are published")
.max_cyclomatic,
20
);
assert!(matches!(
rollup.threshold_source,
Some(fallow_output::ThresholdSource::Override)
));
assert!(
rollup.exceeded.includes_cyclomatic() && !rollup.exceeded.includes_cognitive(),
"18 cognitive is under the raised ceiling, 23 cyclomatic is not"
);
}
#[test]
fn a_component_scoped_entry_that_silences_the_rollup_is_not_reported_no_match() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: vec!["<component>".to_string()],
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: None,
max_unit_size: None,
reason: None,
}];
let resolver = rollup_resolver(&overrides, 3, 3);
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
let mut tracker = ThresholdOverrideStateTracker::default();
append_component_rollup_findings(
&mut findings,
&template_units,
&resolver,
Path::new(ROLLUP_ROOT),
&mut tracker,
);
tracker.record_no_match_entries(&resolver, true);
assert!(
findings.iter().all(|finding| finding.name != "<component>"),
"the entry reaches the rollup and lifts it under the ceilings"
);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
),
"the entry did its job, so the row reads `active`, never `no_match`"
);
assert_eq!(states[0].function.as_deref(), Some("<component>"));
assert_eq!(states[0].path.as_deref(), Some(component_ts.as_path()));
assert_eq!(
states[0].line,
Some(29),
"the row carries the rollup's anchor position, like every other row"
);
}
#[test]
fn a_crap_only_component_entry_that_emits_no_row_is_still_reported_no_match() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: vec!["<component>".to_string()],
max_cyclomatic: None,
max_cognitive: None,
max_crap: Some(500.0),
max_unit_size: None,
reason: None,
}];
let resolver = rollup_resolver(&overrides, 3, 3);
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
let mut tracker = ThresholdOverrideStateTracker::default();
append_component_rollup_findings(
&mut findings,
&template_units,
&resolver,
Path::new(ROLLUP_ROOT),
&mut tracker,
);
tracker.record_no_match_entries(&resolver, true);
let states = tracker.into_states();
assert_eq!(
states.len(),
1,
"the entry produced no dimension row, so it must surface as no_match"
);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::NoMatch
),
"an entry that changes nothing must say so, not disappear: {:?}",
states[0].status
);
}
#[test]
fn a_surviving_component_rollup_row_names_its_outstanding_dimension() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: vec!["<component>".to_string()],
max_cyclomatic: Some(20),
max_cognitive: Some(20),
max_crap: None,
max_unit_size: None,
reason: None,
}];
let resolver = rollup_resolver(&overrides, 3, 3);
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
let mut tracker = ThresholdOverrideStateTracker::default();
append_component_rollup_findings(
&mut findings,
&template_units,
&resolver,
Path::new(ROLLUP_ROOT),
&mut tracker,
);
annotate_outstanding_dimensions(&mut tracker, &findings);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Insufficient
));
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity],
"23 cyclomatic still exceeds the raised ceiling of 20"
);
}
#[test]
fn a_component_rollup_row_does_not_score_a_unit_size_term() {
let overrides = [fallow_config::HealthThresholdOverride {
files: vec!["src/widget.component.ts".to_string()],
functions: vec!["<component>".to_string()],
max_cyclomatic: Some(500),
max_cognitive: Some(500),
max_crap: None,
max_unit_size: Some(40),
reason: None,
}];
let resolver = rollup_resolver(&overrides, 3, 3);
let component_ts = PathBuf::from("/proj/src/widget.component.ts");
let mut findings = vec![
make_class_finding(component_ts.to_str().unwrap(), "classify", 29, 15, 12),
make_template_finding(component_ts.to_str().unwrap(), 3, 8, 6),
];
let template_units = template_units_for(
component_ts.to_str().unwrap(),
component_ts.to_str().unwrap(),
8,
6,
);
let mut tracker = ThresholdOverrideStateTracker::default();
append_component_rollup_findings(
&mut findings,
&template_units,
&resolver,
Path::new(ROLLUP_ROOT),
&mut tracker,
);
annotate_outstanding_dimensions(&mut tracker, &findings);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
),
"the raised ceilings cleared the rollup, and its 50-line combined span \
must not be scored against the raised 40: {:#?}",
states[0]
);
assert!(states[0].outstanding.is_empty(), "{:#?}", states[0]);
assert!(
states[0].metrics.expect("metrics").line_count.is_none(),
"the rollup's synthetic span must not surface as a measured unit size"
);
}
#[test]
fn a_unit_size_only_override_reports_a_complexity_row_when_it_matches() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: None,
max_unit_size: Some(500),
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: Path::new("/project/src/Widget.svelte"),
function: "<template>",
line: 7,
col: 0,
cyclomatic: 9,
cognitive: 10,
line_count: Some(320),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
tracker.record_no_match_entries(&resolver, true);
let states = tracker.into_states();
assert_eq!(states.len(), 1, "one row, and not a `no_match` one");
assert_eq!(
states[0].dimension,
fallow_output::ThresholdOverrideDimension::Complexity
);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Active
),
"320 lines exceeded the global 60 and clears the raised 500"
);
assert_eq!(states[0].effective_thresholds.max_unit_size, 500);
}
#[test]
fn a_unit_size_only_override_that_is_still_breached_reports_insufficient() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: None,
max_unit_size: Some(100),
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: Path::new("/project/src/Widget.svelte"),
function: "<template>",
line: 7,
col: 0,
cyclomatic: 9,
cognitive: 10,
line_count: Some(320),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Insufficient
));
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity],
"320 lines still exceed the raised 100, and unit size is a complexity term"
);
}
#[test]
fn a_unit_size_breach_and_a_complexity_finding_name_one_outstanding_dimension() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(25),
max_cognitive: None,
max_crap: None,
max_unit_size: Some(100),
reason: None,
}]);
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(Path::new("src/Widget.svelte"), "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 7,
col: 0,
cyclomatic: 30,
cognitive: 10,
line_count: Some(320),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let mut finding = make_finding("<template>", ExceededThreshold::Cyclomatic);
finding.path = absolute.to_path_buf();
finding.line = 7;
finding.col = 0;
annotate_outstanding_dimensions(&mut tracker, &[finding]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity]
);
}
#[test]
fn a_cyclomatic_only_override_is_insufficient_when_cognitive_still_breaches() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: Some(500),
max_cognitive: None,
max_crap: None,
max_unit_size: None,
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 7,
col: 0,
cyclomatic: 25,
cognitive: 22,
line_count: Some(30),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let mut finding = svelte_template_finding(ExceededThreshold::Cognitive);
finding.line = 7;
finding.col = 0;
annotate_outstanding_dimensions(&mut tracker, &[finding]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Insufficient
),
"cognitive is still over the un-raised global ceiling"
);
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity],
"status and outstanding must agree on the same dimension"
);
}
#[test]
fn a_unit_size_only_override_is_not_stale_while_complexity_still_fires() {
let resolver = threshold_resolver(&[fallow_config::HealthThresholdOverride {
files: vec!["src/Widget.svelte".to_string()],
functions: Vec::new(),
max_cyclomatic: None,
max_cognitive: None,
max_crap: None,
max_unit_size: Some(500),
reason: None,
}]);
let relative = Path::new("src/Widget.svelte");
let absolute = Path::new("/project/src/Widget.svelte");
let (applied, matches) = resolver.resolve(relative, "<template>");
let mut tracker = ThresholdOverrideStateTracker::default();
tracker.record_complexity(
ComplexityFunctionContext {
path: absolute,
function: "<template>",
line: 7,
col: 0,
cyclomatic: 25,
cognitive: 21,
line_count: Some(32),
suppressed: false,
},
&matches,
issue_2163_globals(),
applied.effective,
);
let mut finding = svelte_template_finding(ExceededThreshold::Cyclomatic);
finding.line = 7;
finding.col = 0;
annotate_outstanding_dimensions(&mut tracker, &[finding]);
let states = tracker.into_states();
assert_eq!(states.len(), 1);
assert!(
matches!(
states[0].status,
fallow_output::ThresholdOverrideStatus::Insufficient
),
"32 lines clear the raised unit size, but the dimension still breaches"
);
assert_eq!(
states[0].outstanding,
vec![fallow_output::ThresholdOverrideDimension::Complexity]
);
}