pub const FORMULA_VERSION: &str = "v1";
const ALPHA: f64 = 1.0; const BETA: f64 = 1.5; const DELTA: f64 = 1.0;
const THRESHOLD_XS: f64 = 6.0;
const THRESHOLD_S: f64 = 10.0;
const THRESHOLD_M: f64 = 14.0;
const THRESHOLD_L: f64 = 18.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffortSize {
Xs,
S,
M,
L,
Xl,
}
impl EffortSize {
pub fn label(self) -> &'static str {
match self {
EffortSize::Xs => "XS",
EffortSize::S => "S",
EffortSize::M => "M",
EffortSize::L => "L",
EffortSize::Xl => "XL",
}
}
}
#[derive(Debug, Clone)]
pub struct EffortResult {
pub loc: u32,
pub files: u32,
pub test_loc: u32,
pub tests_factor: f64,
pub score: f64,
pub size: EffortSize,
}
impl EffortResult {
pub fn size_label(&self) -> &'static str {
self.size.label()
}
}
pub fn effort_tshirt_from_size(size: &str) -> i64 {
match size {
"XS" => 1,
"S" => 2,
"M" => 3,
"L" => 4,
"XL" => 5,
_ => 0,
}
}
pub fn effort_tshirt(size: EffortSize) -> i64 {
effort_tshirt_from_size(size.label())
}
pub fn size_for_score(score: f64) -> EffortSize {
if score <= THRESHOLD_XS {
EffortSize::Xs
} else if score <= THRESHOLD_S {
EffortSize::S
} else if score <= THRESHOLD_M {
EffortSize::M
} else if score <= THRESHOLD_L {
EffortSize::L
} else {
EffortSize::Xl
}
}
pub fn is_test_file(path: &str) -> bool {
let lower = path.to_lowercase();
if lower.contains("/tests/") || lower.starts_with("tests/") || lower == "tests" {
return true;
}
if lower.contains("/__tests__/") || lower.starts_with("__tests__/") {
return true;
}
let filename = lower.rsplit('/').next().unwrap_or(&lower);
if filename.ends_with("_test.rs") {
return true;
}
if filename.starts_with("test_") {
return true;
}
let parts: Vec<&str> = filename.split('.').collect();
if parts.len() >= 3 {
let discriminator = parts[parts.len() - 2];
if discriminator == "spec" || discriminator == "test" {
return true;
}
}
false
}
pub fn compute_effort<'a, I>(files: I) -> EffortResult
where
I: IntoIterator<Item = (&'a str, u32, u32)>,
{
let mut loc: u32 = 0;
let mut test_loc: u32 = 0;
let mut file_count: u32 = 0;
for (path, ins, del) in files {
let file_lines = ins.saturating_add(del);
loc = loc.saturating_add(file_lines);
file_count = file_count.saturating_add(1);
if is_test_file(path) {
test_loc = test_loc.saturating_add(file_lines);
}
}
let loc_f = loc as f64;
let test_loc_f = test_loc as f64;
let ratio = (test_loc_f / loc_f.max(1.0)).min(1.0);
let tests_factor = 1.0 - 0.3 * ratio;
let score = ALPHA * (loc_f + 1.0).log2()
+ BETA * (file_count as f64 + 1.0).log2()
+ DELTA * tests_factor;
let size = size_for_score(score);
EffortResult {
loc,
files: file_count,
test_loc,
tests_factor,
score,
size,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effort_tshirt_mapping() {
assert_eq!(effort_tshirt_from_size("XS"), 1);
assert_eq!(effort_tshirt_from_size("S"), 2);
assert_eq!(effort_tshirt_from_size("M"), 3);
assert_eq!(effort_tshirt_from_size("L"), 4);
assert_eq!(effort_tshirt_from_size("XL"), 5);
assert_eq!(
effort_tshirt_from_size("??"),
0,
"unknown label falls back to 0"
);
assert_eq!(effort_tshirt(EffortSize::M), 3);
}
#[test]
fn size_labels_match_spec() {
assert_eq!(EffortSize::Xs.label(), "XS");
assert_eq!(EffortSize::S.label(), "S");
assert_eq!(EffortSize::M.label(), "M");
assert_eq!(EffortSize::L.label(), "L");
assert_eq!(EffortSize::Xl.label(), "XL");
}
#[test]
fn thresholds_match_spec() {
assert_eq!(size_for_score(6.0), EffortSize::Xs);
assert_eq!(size_for_score(6.01), EffortSize::S);
assert_eq!(size_for_score(10.0), EffortSize::S);
assert_eq!(size_for_score(10.01), EffortSize::M);
assert_eq!(size_for_score(14.0), EffortSize::M);
assert_eq!(size_for_score(14.01), EffortSize::L);
assert_eq!(size_for_score(18.0), EffortSize::L);
assert_eq!(size_for_score(18.01), EffortSize::Xl);
assert_eq!(size_for_score(0.0), EffortSize::Xs);
assert_eq!(size_for_score(100.0), EffortSize::Xl);
}
#[test]
fn test_file_detection() {
assert!(is_test_file("src/tests/auth_tests.rs"));
assert!(is_test_file("tests/integration.rs"));
assert!(is_test_file("src/__tests__/foo.test.ts"));
assert!(is_test_file("__tests__/auth.js"));
assert!(is_test_file("src/auth_test.rs"));
assert!(is_test_file("src/test_auth.rs"));
assert!(is_test_file("src/auth.spec.ts"));
assert!(is_test_file("src/auth.spec.js"));
assert!(is_test_file("src/auth.test.ts"));
assert!(is_test_file("src/auth.test.js"));
assert!(!is_test_file("src/main.rs"));
assert!(!is_test_file("src/auth.rs"));
assert!(!is_test_file("Cargo.toml"));
assert!(!is_test_file("src/context.ts"));
assert!(!is_test_file("docs/testing.md"));
assert!(!is_test_file("src/attestation.rs"));
}
#[test]
fn formula_known_values() {
let result = compute_effort([("src/auth.rs", 25, 15), ("src/config.rs", 5, 5)]);
assert_eq!(result.loc, 50);
assert_eq!(result.files, 2);
assert_eq!(result.test_loc, 0);
assert!((result.tests_factor - 1.0).abs() < 1e-9);
let expected_score = 1.0 * 51_f64.log2() + 1.5 * 3_f64.log2() + 1.0 * 1.0;
assert!((result.score - expected_score).abs() < 1e-9);
assert_eq!(result.size, EffortSize::S);
}
#[test]
fn formula_all_test_code_reduces_score() {
let result = compute_effort([("src/auth_test.rs", 10, 10)]);
assert_eq!(result.loc, 20);
assert_eq!(result.test_loc, 20);
assert!((result.tests_factor - 0.7).abs() < 1e-9);
let expected = 1.0 * 21_f64.log2() + 1.5 * 2_f64.log2() + 0.7;
assert!((result.score - expected).abs() < 1e-9);
}
#[test]
fn formula_empty_commit() {
let result = compute_effort(std::iter::empty::<(&str, u32, u32)>());
assert_eq!(result.loc, 0);
assert_eq!(result.files, 0);
assert_eq!(result.test_loc, 0);
assert!((result.tests_factor - 1.0).abs() < 1e-9);
assert!((result.score - 1.0).abs() < 1e-9);
assert_eq!(result.size, EffortSize::Xs);
}
#[test]
fn formula_large_commit_does_not_overflow() {
let result = compute_effort([("src/generated.rs", 40_000, 32_000)]);
assert_eq!(result.loc, 72_000);
assert!(result.score.is_finite());
assert_eq!(result.size, EffortSize::Xl);
}
#[ignore]
#[test]
fn effort_cross_validate() {
}
}