Address fourth review round: observability, docs accuracy, dep hygiene, tests

- Warn when no timing data was found at all (empty --times-dir previously
  produced empty CSVs without any hint) — best_cars empty + non-empty car
  list now logs a warning in parse_all_times
- Remove the inaccurate "known gap" doc from write_trash_csv: category 1
  (not best on any track) already covers non-final cars, so the
  documented gap never existed
- Gate env_logger behind the bin-logging feature (default-on) and mark
  the records binary as requiring it, so downstream lib consumers do not
  pull env_logger transitively; lib builds clean with --no-default-features
- Switch fetch_car progress output from println! to log::info! so
  RUST_LOG controls it
- atomic_write takes &Path instead of &PathBuf
- Add direct unit tests for is_final_build (sum-8 finals, maxed builds,
  sub-8 non-finals, malformed inputs) and a parsing test covering
  missing/null/non-numeric "t" fields (warn + skip, valid entries still
  parsed)
This commit is contained in:
Jan-Peter von Hunnius 2026-08-15 20:14:53 +02:00
parent 58b8d23e68
commit 1a844e7aa8
4 changed files with 107 additions and 10 deletions

View file

@ -336,6 +336,15 @@ pub async fn parse_all_times(
});
}
// Missing timing files are silent (expected for most cars), so a wrong or
// empty --times-dir would otherwise produce empty CSVs without any hint.
if ctx.best_cars.is_empty() && !car_list.is_empty() {
log::warn!(
"no timing data found in {times_dir} for {} car(s) — all output CSVs will be empty",
car_list.len()
);
}
Ok(car_list)
}
@ -394,7 +403,7 @@ async fn fetch_car(rid: &str, times_dir: &str) -> Result<(), AppError> {
let file_name = rid.to_owned() + ".json";
let url = format!("{CAR_TIMING_API_BASE}{rid}");
let local_file_path = format!("{times_dir}/{file_name}");
println!("Fetching {url}.");
log::info!("Fetching {url}.");
top_drives::get_file(&url, &local_file_path).await
}
@ -544,6 +553,55 @@ mod tests {
assert!(ctx.best_cars.contains_key(&("Monaco Streets".to_string(), 85, "Wet".to_string())));
}
#[test]
fn test_parse_car_times_skips_missing_or_non_numeric_time() {
let ctx = AnalysisContext::new();
let car = Car {
rid: "TestCar_2020".to_string(),
brand: "TestBrand".to_string(),
only_name: "TestModel".to_string(),
year: 2020,
rq: 85,
prize: false,
guid: "guid_test".to_string(),
};
let dir = tempfile::tempdir().unwrap();
let times_dir = dir.path().to_str().unwrap();
let file_path = dir.path().join("TestCar_2020.json");
// "t" missing entirely, null, and a string — all must be skipped
// without panicking or inserting a record.
let json_content = r#"{
"data": {
"111": {
"times": {
"laguna_a00": { "u": "Player1" },
"mnCity_a01": { "t": null, "u": "Player2" },
"testBowl_a02": { "t": "112.45", "u": "Player3" },
"butte_a01": { "t": 50.0, "u": "Player4" }
}
}
}
}"#;
fs::write(&file_path, json_content).unwrap();
let my_cars: HashSet<String> = HashSet::new();
parse_car_times(car, &my_cars, &ctx.best_cars, times_dir).unwrap();
// Only the valid butte entry survives.
assert!(!ctx.best_cars.contains_key(&("Laguna Seca".to_string(), 85, "Dry".to_string())));
assert!(!ctx.best_cars.contains_key(&(
"Monaco Streets".to_string(),
85,
"Wet".to_string()
)));
assert!(
ctx.best_cars.contains_key(&("Butte".to_string(), 85, "Wet".to_string())),
"valid time entry should still be parsed"
);
}
#[test]
fn test_parse_car_times_filters_downvoted() {
let ctx = AnalysisContext::new();