coverage_policy_tests.rs (11611B)
1 use std::{ 2 fs, 3 path::PathBuf, 4 time::{SystemTime, UNIX_EPOCH}, 5 }; 6 7 use super::{ 8 CoverageContract, LlvmCovMetric, enforce_metric, evaluate_report, metric_percent, path_matches, 9 validate_contract, validate_metric, 10 }; 11 12 const CONTRACT: &str = r#" 13 [policy] 14 enforce = true 15 require_regions = true 16 require_functions = true 17 require_lines = true 18 19 [toolchain] 20 rust = "1.92.0" 21 wasm_target = "wasm32-unknown-unknown" 22 23 [report] 24 output = "target/sdk-coverage/summary.json" 25 ignore_filename_regex = "generated" 26 27 [generated] 28 typescript = "generated TypeScript is checked elsewhere" 29 binding_crates = "generated binding crates are checked elsewhere" 30 wasm_glue = "wasm glue is checked through package validation" 31 32 [scopes.xtask_policy] 33 paths = ["tools/xtask/src/coverage_policy.rs"] 34 threshold = 100.0 35 36 [exclusions.generated] 37 paths = ["packages/*/src/generated/**"] 38 reason = "generated output is checked through reproducibility" 39 "#; 40 41 #[derive(Clone, Copy)] 42 struct Metrics { 43 lines: (u64, u64, f64), 44 functions: (u64, u64, f64), 45 regions: (u64, u64, f64), 46 } 47 48 fn covered() -> Metrics { 49 Metrics { 50 lines: (100, 100, 100.0), 51 functions: (50, 50, 100.0), 52 regions: (200, 200, 100.0), 53 } 54 } 55 56 fn contract(raw: &str) -> CoverageContract { 57 toml::from_str::<CoverageContract>(raw).expect("contract parses") 58 } 59 60 fn test_root(name: &str) -> PathBuf { 61 let stamp = SystemTime::now() 62 .duration_since(UNIX_EPOCH) 63 .expect("time") 64 .as_nanos(); 65 let root = std::env::temp_dir().join(format!( 66 "radroots_sdk_coverage_policy_{name}_{}_{}", 67 std::process::id(), 68 stamp 69 )); 70 let _ = fs::remove_dir_all(&root); 71 fs::create_dir_all(&root).expect("create root"); 72 root 73 } 74 75 fn metric_json(metric: (u64, u64, f64)) -> String { 76 format!( 77 r#"{{"count":{},"covered":{},"percent":{}}}"#, 78 metric.0, metric.1, metric.2 79 ) 80 } 81 82 fn summary_json(metrics: Metrics) -> String { 83 format!( 84 r#"{{"lines":{},"functions":{},"regions":{}}}"#, 85 metric_json(metrics.lines), 86 metric_json(metrics.functions), 87 metric_json(metrics.regions) 88 ) 89 } 90 91 fn report_json(filename: &str, file_metrics: Metrics, totals: Metrics) -> String { 92 format!( 93 r#"{{"data":[{{"files":[{{"filename":"{}","summary":{}}}],"totals":{}}}]}}"#, 94 filename, 95 summary_json(file_metrics), 96 summary_json(totals) 97 ) 98 } 99 100 fn write_report(root: &PathBuf, raw: &str) -> PathBuf { 101 let report_path = root.join("summary.json"); 102 fs::write(&report_path, raw).expect("write report"); 103 report_path 104 } 105 106 fn scope_file(root: &PathBuf) -> String { 107 root.join("tools/xtask/src/coverage_policy.rs") 108 .display() 109 .to_string() 110 } 111 112 #[test] 113 fn validates_contract_shape() { 114 validate_contract(&contract(CONTRACT)).expect("contract validates"); 115 } 116 117 #[test] 118 fn rejects_blank_contract_fields() { 119 let cases = [ 120 ("rust = \"1.92.0\"", "rust = \" \"", "toolchain.rust"), 121 ( 122 "wasm_target = \"wasm32-unknown-unknown\"", 123 "wasm_target = \" \"", 124 "toolchain.wasm_target", 125 ), 126 ( 127 "output = \"target/sdk-coverage/summary.json\"", 128 "output = \" \"", 129 "report.output", 130 ), 131 ( 132 "ignore_filename_regex = \"generated\"", 133 "ignore_filename_regex = \" \"", 134 "report.ignore_filename_regex", 135 ), 136 ( 137 "typescript = \"generated TypeScript is checked elsewhere\"", 138 "typescript = \" \"", 139 "generated.typescript", 140 ), 141 ( 142 "binding_crates = \"generated binding crates are checked elsewhere\"", 143 "binding_crates = \" \"", 144 "generated.binding_crates", 145 ), 146 ( 147 "wasm_glue = \"wasm glue is checked through package validation\"", 148 "wasm_glue = \" \"", 149 "generated.wasm_glue", 150 ), 151 ( 152 "paths = [\"tools/xtask/src/coverage_policy.rs\"]", 153 "paths = [\" \"]", 154 "scopes.xtask_policy.paths entry", 155 ), 156 ( 157 "reason = \"generated output is checked through reproducibility\"", 158 "reason = \" \"", 159 "exclusions.generated.reason", 160 ), 161 ( 162 "paths = [\"packages/*/src/generated/**\"]", 163 "paths = [\" \"]", 164 "exclusions.generated.paths entry", 165 ), 166 ]; 167 168 for (from, to, expected) in cases { 169 let raw = CONTRACT.replace(from, to); 170 let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); 171 assert!(error.contains(expected), "{error}"); 172 } 173 } 174 175 #[test] 176 fn rejects_contract_collection_errors() { 177 let mut no_scopes = contract(CONTRACT); 178 no_scopes.scopes.clear(); 179 assert_eq!( 180 validate_contract(&no_scopes).unwrap_err(), 181 "contracts/coverage.toml scopes must not be empty" 182 ); 183 184 let mut no_exclusions = contract(CONTRACT); 185 no_exclusions.exclusions.clear(); 186 assert_eq!( 187 validate_contract(&no_exclusions).unwrap_err(), 188 "contracts/coverage.toml exclusions must not be empty" 189 ); 190 191 let cases = [ 192 ( 193 CONTRACT.replace("[scopes.xtask_policy]", "[scopes.\"\"]"), 194 "scope name", 195 ), 196 ( 197 CONTRACT.replace("[exclusions.generated]", "[exclusions.\"\"]"), 198 "exclusion name", 199 ), 200 ( 201 CONTRACT.replace("threshold = 100.0", "threshold = 101.0"), 202 "scopes.xtask_policy.threshold", 203 ), 204 ( 205 CONTRACT.replace( 206 "paths = [\"tools/xtask/src/coverage_policy.rs\"]", 207 "paths = []", 208 ), 209 "scopes.xtask_policy.paths must not be empty", 210 ), 211 ( 212 CONTRACT.replace("paths = [\"packages/*/src/generated/**\"]", "paths = []"), 213 "exclusions.generated.paths must not be empty", 214 ), 215 ]; 216 217 for (raw, expected) in cases { 218 let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); 219 assert!(error.contains(expected), "{error}"); 220 } 221 } 222 223 #[test] 224 fn matches_recursive_scope_paths() { 225 assert!(path_matches( 226 "crates/sdk/src/**", 227 "crates/sdk/src/adapters/radrootsd.rs" 228 )); 229 assert!(path_matches("crates/sdk/src/**", "crates/sdk/src")); 230 assert!(path_matches( 231 "tools/xtask/src/coverage_policy.rs", 232 "tools/xtask/src/coverage_policy.rs" 233 )); 234 assert!(!path_matches( 235 "crates/sdk/src/**", 236 "crates/sql_wasm_runtime/src/lib.rs" 237 )); 238 } 239 240 #[test] 241 fn accepts_passing_reports_and_rejects_undercovered_scopes() { 242 let root = test_root("passing_and_undercovered"); 243 let filename = scope_file(&root); 244 let passing_report = report_json(&filename, covered(), covered()); 245 let report_path = write_report(&root, &passing_report); 246 evaluate_report(&root, &report_path, &contract(CONTRACT)).expect("passing report"); 247 248 let mut undercovered = covered(); 249 undercovered.lines = (100, 99, 99.0); 250 let failing_report = report_json(&filename, undercovered, covered()); 251 fs::write(&report_path, failing_report).expect("write failing report"); 252 let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) 253 .expect_err("undercovered report rejected"); 254 assert!(error.contains("coverage scope xtask_policy"), "{error}"); 255 fs::remove_dir_all(root).expect("cleanup"); 256 } 257 258 #[test] 259 fn disabled_enforcement_accepts_measured_undercoverage() { 260 let root = test_root("disabled"); 261 let filename = scope_file(&root); 262 let mut undercovered = covered(); 263 undercovered.lines = (100, 0, 0.0); 264 undercovered.functions = (50, 0, 0.0); 265 undercovered.regions = (200, 0, 0.0); 266 let report_path = write_report(&root, &report_json(&filename, undercovered, undercovered)); 267 let raw = CONTRACT.replace("enforce = true", "enforce = false"); 268 evaluate_report(&root, &report_path, &contract(&raw)).expect("disabled policy passes"); 269 fs::remove_dir_all(root).expect("cleanup"); 270 } 271 272 #[test] 273 fn rejects_unreadable_malformed_and_empty_reports() { 274 let root = test_root("bad_reports"); 275 let missing = root.join("missing.json"); 276 assert!(evaluate_report(&root, &missing, &contract(CONTRACT)).is_err()); 277 278 let malformed = write_report(&root, "{"); 279 assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); 280 281 fs::write(&malformed, r#"{"data":[]}"#).expect("write empty report"); 282 assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); 283 fs::remove_dir_all(root).expect("cleanup"); 284 } 285 286 #[test] 287 fn rejects_required_total_metric_failures() { 288 let root = test_root("total_metrics"); 289 let filename = scope_file(&root); 290 let mut totals = covered(); 291 let cases = [ 292 Metrics { 293 lines: (0, 0, 0.0), 294 ..totals 295 }, 296 { 297 totals = covered(); 298 totals.functions = (0, 0, 0.0); 299 totals 300 }, 301 { 302 totals = covered(); 303 totals.regions = (0, 0, 0.0); 304 totals 305 }, 306 { 307 totals = covered(); 308 totals.lines = (1, 2, 200.0); 309 totals 310 }, 311 ]; 312 313 let report_path = root.join("summary.json"); 314 for total_metrics in cases { 315 fs::write( 316 &report_path, 317 report_json(&filename, covered(), total_metrics), 318 ) 319 .expect("write report"); 320 assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); 321 } 322 fs::remove_dir_all(root).expect("cleanup"); 323 } 324 325 #[test] 326 fn rejects_scope_metric_validation_and_missing_scope_files() { 327 let root = test_root("scope_metrics"); 328 let filename = scope_file(&root); 329 let other_filename = root 330 .join("tools/xtask/src/coverage.rs") 331 .display() 332 .to_string(); 333 let report_path = root.join("summary.json"); 334 335 fs::write( 336 &report_path, 337 report_json(&other_filename, covered(), covered()), 338 ) 339 .expect("write unmatched report"); 340 let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) 341 .expect_err("unmatched scope rejected"); 342 assert!(error.contains("matched no report files"), "{error}"); 343 344 let mut invalid = covered(); 345 invalid.lines = (0, 0, 0.0); 346 fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); 347 assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); 348 349 invalid = covered(); 350 invalid.functions = (1, 2, 200.0); 351 fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); 352 assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); 353 fs::remove_dir_all(root).expect("cleanup"); 354 } 355 356 #[test] 357 fn metric_helpers_cover_edges() { 358 let valid = LlvmCovMetric { 359 count: 10, 360 covered: 10, 361 percent: 100.0, 362 }; 363 validate_metric("lines", &valid, true).expect("metric validates"); 364 enforce_metric("lines", &valid, 100.0).expect("metric passes"); 365 366 let missing = LlvmCovMetric { 367 count: 0, 368 covered: 0, 369 percent: 0.0, 370 }; 371 assert!(validate_metric("lines", &missing, true).is_err()); 372 assert!(enforce_metric("lines", &missing, 100.0).is_err()); 373 374 let invalid = LlvmCovMetric { 375 count: 1, 376 covered: 2, 377 percent: 200.0, 378 }; 379 assert!(validate_metric("lines", &invalid, true).is_err()); 380 assert_eq!(metric_percent(0, 0), 0.0); 381 assert_eq!(metric_percent(4, 2), 50.0); 382 }