use std::path::Path;
use std::str::FromStr;
use super::*;
const DAY: i64 = 86_400;
#[test]
fn suffix_units_resolve_to_seconds() {
assert_eq!(parse_window("90d").expect("90d"), 90 * DAY);
assert_eq!(parse_window("8w").expect("8w"), 8 * 7 * DAY);
assert_eq!(parse_window("1y").expect("1y"), SECONDS_PER_YEAR);
assert_eq!(parse_window("12mo").expect("12mo"), 12 * SECONDS_PER_MONTH);
}
#[test]
fn twelve_months_and_one_year_both_round_to_365_days() {
assert_eq!(secs_to_days(parse_window("12mo").expect("12mo")), 365);
assert_eq!(secs_to_days(parse_window("1y").expect("1y")), 365);
assert_eq!(secs_to_days(parse_window("90d").expect("90d")), 90);
}
#[test]
fn iso8601_durations_parse() {
assert_eq!(parse_window("P90D").expect("P90D"), 90 * DAY);
assert_eq!(parse_window("P8W").expect("P8W"), 8 * 7 * DAY);
assert_eq!(parse_window("P1Y").expect("P1Y"), SECONDS_PER_YEAR);
assert_eq!(parse_window("P12M").expect("P12M"), 12 * SECONDS_PER_MONTH);
assert_eq!(
parse_window("P1Y6M").expect("P1Y6M"),
SECONDS_PER_YEAR + 6 * SECONDS_PER_MONTH
);
}
#[test]
fn bad_windows_are_rejected() {
for bad in [
"", " ", "12", "10x", "-5d", "P", "PT5S", "12m", "abc", "0d", "P0D", "0w",
] {
assert!(
matches!(parse_window(bad), Err(Error::InvalidWindow(_))),
"expected {bad:?} to be rejected"
);
}
}
#[test]
fn window_errors_quote_full_input_and_hint() {
for bad in ["bogus", "12parsec", "10x", ""] {
let Err(Error::InvalidWindow(msg)) = parse_window(bad) else {
panic!("expected {bad:?} to be rejected as InvalidWindow");
};
assert!(
msg.contains(&format!("{bad:?}")),
"error for {bad:?} should quote the full input, got: {msg}"
);
assert!(
msg.contains("expected") && msg.contains("ISO 8601"),
"error for {bad:?} should carry the format hint, got: {msg}"
);
assert!(
bad.is_empty() || !msg.contains("\"\""),
"error for {bad:?} should not quote an empty magnitude, got: {msg}"
);
}
}
#[test]
fn defaults_match_the_issue_sample() {
let options = Options::default();
assert_eq!(options.long_window_days(), 365);
assert_eq!(options.recent_window_days(), 90);
assert_eq!(options.reference, "HEAD");
assert!(options.follow_renames);
assert!(options.exclude_bots);
assert!(!options.full_history);
assert!(!options.include_merges);
assert_eq!(options.risk_formula, RiskFormula::Weighted);
}
#[test]
fn default_bot_pattern_is_a_valid_regex() {
assert!(regex::Regex::new(DEFAULT_BOT_PATTERN).is_ok());
}
#[test]
fn risk_formula_parses_known_names() {
assert_eq!(
"weighted".parse::<RiskFormula>().expect("weighted"),
RiskFormula::Weighted
);
assert_eq!(
"percentile".parse::<RiskFormula>().expect("percentile"),
RiskFormula::Percentile
);
}
#[test]
fn risk_formula_rejects_unknown_name() {
assert!(
matches!("bogus".parse::<RiskFormula>(), Err(Error::InvalidFormula(name)) if name == "bogus")
);
}
#[test]
fn iso8601_unsupported_designator_is_rejected() {
assert!(matches!(parse_window("P5X"), Err(Error::InvalidWindow(_))));
}
#[test]
fn iso8601_trailing_magnitude_without_designator_is_rejected() {
assert!(matches!(parse_window("P5"), Err(Error::InvalidWindow(_))));
}
#[test]
fn overflowing_windows_are_rejected_not_wrapped() {
for bad in ["999999999999y", "P999999999999Y"] {
assert!(
matches!(parse_window(bad), Err(Error::InvalidWindow(msg)) if msg.contains("overflows")),
"expected {bad:?} to be rejected as overflow"
);
}
}
#[test]
fn secs_to_days_saturates_huge_and_floors_negative() {
let huge = (i64::from(u32::MAX) + 10) * SECONDS_PER_DAY;
assert_eq!(secs_to_days(huge), u32::MAX);
assert_eq!(secs_to_days(i64::MAX), u32::MAX);
assert_eq!(secs_to_days(-10 * SECONDS_PER_DAY), 0);
}
#[test]
fn defaults_leave_bus_factor_off_at_avelino_threshold() {
let options = Options::default();
assert!(!options.compute_bus_factor);
assert!((options.bus_factor_threshold - 0.5).abs() < f64::EPSILON);
assert!((DEFAULT_BUS_FACTOR_THRESHOLD - 0.5).abs() < f64::EPSILON);
}
#[test]
fn bus_factor_threshold_accepts_only_the_open_interval() {
for good in [0.5, 0.9, 0.01, 0.99] {
let got = validate_bus_factor_threshold(good).expect("valid threshold");
assert!((got - good).abs() < f64::EPSILON);
}
for bad in [0.0, 1.0, -0.1, 1.5, f64::NAN, f64::INFINITY] {
assert!(
validate_bus_factor_threshold(bad).is_err(),
"{bad} must be rejected"
);
}
}
#[test]
fn default_file_type_scope_is_metrics() {
assert_eq!(Options::default().file_types, FileTypeScope::Metrics);
assert_eq!(FileTypeScope::default(), FileTypeScope::Metrics);
}
#[test]
fn file_type_scope_parses_keywords() {
assert_eq!(
FileTypeScope::from_str("metrics").expect("metrics"),
FileTypeScope::Metrics
);
assert_eq!(
FileTypeScope::from_str("all").expect("all"),
FileTypeScope::All
);
assert_eq!(
FileTypeScope::from_str(" all ").expect("padded all"),
FileTypeScope::All
);
}
#[test]
fn file_type_scope_parses_custom_list_and_normalizes() {
let scope = FileTypeScope::from_str(" .RS, py , rs,, .Py ").expect("custom list");
assert_eq!(
scope,
FileTypeScope::Custom(vec!["rs".to_owned(), "py".to_owned()])
);
}
#[test]
fn file_type_scope_rejects_empty_and_blank_lists() {
for bad in ["", " ", ",", " , . , "] {
assert!(
matches!(
FileTypeScope::from_str(bad),
Err(Error::InvalidFileTypeScope(_))
),
"{bad:?} must be rejected as an empty scope"
);
}
}
#[test]
fn file_type_scope_rejects_multi_dot_suffixes() {
for bad in ["d.ts", "tar.gz", ".rs.bak", "rs,tar.gz", "a.b.c"] {
assert!(
matches!(
FileTypeScope::from_str(bad),
Err(Error::InvalidFileTypeScope(_))
),
"{bad:?} is a multi-dot suffix and must be rejected"
);
}
assert_eq!(
FileTypeScope::from_str(".rs").expect("leading dot is fine"),
FileTypeScope::Custom(vec!["rs".to_owned()])
);
}
#[test]
fn metrics_scope_includes_source_excludes_non_source() {
let metrics = FileTypeScope::Metrics;
assert!(metrics.includes(Path::new("src/lib.rs")));
assert!(metrics.includes(Path::new("app/main.py")));
assert!(!metrics.includes(Path::new("CHANGELOG.md")));
assert!(!metrics.includes(Path::new("Cargo.lock")));
assert!(!metrics.includes(Path::new("Cargo.toml")));
assert!(!metrics.includes(Path::new("Makefile")));
assert!(!metrics.includes(Path::new("LICENSE")));
}
#[test]
fn all_scope_includes_everything() {
let all = FileTypeScope::All;
assert!(all.includes(Path::new("src/lib.rs")));
assert!(all.includes(Path::new("CHANGELOG.md")));
assert!(all.includes(Path::new("Makefile")));
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn default_then_assign_is_the_supported_construction_path() {
let mut o = Options::default();
o.long_window_secs = 1;
o.compute_bus_factor = true;
o.file_types = FileTypeScope::All;
assert_eq!(o.long_window_secs, 1);
assert!(o.compute_bus_factor);
assert!(matches!(o.file_types, FileTypeScope::All));
}
#[test]
fn custom_scope_matches_only_listed_extensions_case_insensitively() {
let scope = FileTypeScope::from_str("rs,toml").expect("custom");
assert!(scope.includes(Path::new("src/lib.rs")));
assert!(scope.includes(Path::new("Cargo.toml")));
assert!(scope.includes(Path::new("BUILD.RS")));
assert!(!scope.includes(Path::new("app/main.py")));
assert!(!scope.includes(Path::new("README.md")));
assert!(!scope.includes(Path::new("Makefile")));
}