Refactor: extract lib.rs into focused sub-modules, add --output-dir CLI option

- Split monolithic src/lib.rs (~640 lines) into 7 sub-modules:
  types.rs, context.rs, user_data.rs, translate.rs, http.rs,
  csv_util.rs, test_utils.rs
- Add configurable --output-dir CLI option for CSV output files
- Implement atomic file writes (write-to-temp + rename) for all CSVs
- Change compute_car_counts() to return CarCounts directly (not Result)
- Add test-utils feature gate for shared test helpers
- Add HTTP mock tests with mockito (6 tests)
- Add proptest tests for compute_car_counts (3 property tests)
- Update integration tests to use shared test utilities
- Update documentation (API.md, ARCHITECTURE.md, DATA_STRUCTURES.md)
This commit is contained in:
Jan-Peter von Hunnius 2026-05-08 20:21:46 +02:00
parent d2c1418e5e
commit 67a79f190a
27 changed files with 1648 additions and 789 deletions

View file

@ -2,7 +2,8 @@
use proptest::prelude::*;
use top_drives::{compare_cars, Car};
use top_drives::{compare_cars, compute_car_counts, AnalysisContext, Car};
use top_drives::test_utils::{make_car, make_record};
/// Generate arbitrary Car instances for property testing.
fn car_strategy() -> impl Strategy<Value = Car> {
@ -166,4 +167,82 @@ proptest! {
assert!(escaped.starts_with('"'));
assert!(escaped.ends_with('"'));
}
// =======================================================================
// compute_car_counts: empty context always yields zero counts
// =======================================================================
#[test]
fn compute_car_counts_empty_ctx(
cars in proptest::collection::vec(car_strategy(), 0..20)
) {
let ctx = AnalysisContext::default();
let counts = compute_car_counts(&ctx, cars.clone());
// No best_cars entries → every car has 0 wins → by_rid is empty
assert_eq!(counts.by_rid.len(), 0);
// guid_to_rid is also empty since no car won any track
assert_eq!(counts.guid_to_rid.len(), 0);
// But car_map contains ALL cars from the list
assert_eq!(counts.car_map.len(), cars.len());
for car in &cars {
assert!(counts.car_map.contains_key(&car.guid));
}
}
// =======================================================================
// compute_car_counts: car with wins has correct count
// =======================================================================
#[test]
fn compute_car_counts_reflects_wins(
n_tracks in 1usize..30,
rid in "[a-zA-Z]{3,8}",
guid in "[a-zA-Z0-9_]{3,10}",
rq in 10u8..113u8,
) {
let ctx = AnalysisContext::default();
let rec = make_record(&rid, rq, 100.0, &guid);
// Insert the car as best on n_tracks tracks
for i in 0..n_tracks {
ctx.best_cars.insert(
(format!("track_{i}"), rq, "Dry".to_string()),
vec![rec.clone()],
);
}
let car_list = vec![make_car(&rid, rq, &guid)];
let counts = compute_car_counts(&ctx, car_list);
let entry = counts.by_rid.get(&rid);
assert!(entry.is_some(), "car should appear in by_rid");
let (_, count) = entry.unwrap();
assert_eq!(*count, n_tracks, "count should match number of tracks");
}
// =======================================================================
// compute_car_counts: ownership flag propagates correctly
// =======================================================================
#[test]
fn compute_car_counts_ownership(
rid in "[a-zA-Z]{3,8}",
guid in "[a-zA-Z0-9_]{3,10}",
rq in 10u8..113u8,
owned in any::<bool>(),
) {
let ctx = AnalysisContext::default();
let mut rec = make_record(&rid, rq, 100.0, &guid);
rec.owned = owned;
ctx.best_cars.insert(
("track_x".to_string(), rq, "Dry".to_string()),
vec![rec],
);
let car_list = vec![make_car(&rid, rq, &guid)];
let counts = compute_car_counts(&ctx, car_list);
let (record, _) = counts.by_rid.get(&rid).unwrap();
assert_eq!(record.owned, owned, "owned flag should propagate");
}
}