Fix multi-copy car handling: track all configs per guid and add copy count

- Change owned_configs from HashMap<String, String> to HashMap<String, Vec<String>>
  to capture all configs across copies (previously last-write-wins lost data)
- Add copy_count field to MyCarsData and CarCounts for wishlist copy-coverage logic
- Fix improve.csv to exclude already-owned final configs as targets (e.g., Fiat
  Campagnola 1974 now targets 332 instead of already-owned 323)
- Fix wishlist to check all owned configs for exact-match and final-build detection
- Fix trash to check all configs for final-build-not-best detection
- Update all tests for new Vec type and add multi-copy improve test
This commit is contained in:
Jan-Peter von Hunnius 2026-06-06 21:13:47 +02:00
parent b24b274d25
commit 62688926c3
7 changed files with 494 additions and 83 deletions

View file

@ -3,6 +3,7 @@
//! These tests exercise the full analysis pipeline with fixture data,
//! verifying end-to-end correctness from parsing through CSV output.
use std::collections::HashMap;
use std::fs;
use std::io::Write;
@ -20,7 +21,7 @@ use top_drives::{
fn test_full_pipeline_empty_input() {
let ctx = AnalysisContext::new();
let car_list: Vec<Car> = vec![];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let counts = compute_car_counts(&ctx, car_list, empty_my_data(), HashMap::new());
assert!(counts.by_rid.is_empty());
assert!(counts.guid_to_rid.is_empty());
@ -38,7 +39,7 @@ fn test_full_pipeline_single_car_multiple_tracks() {
}
let car = make_car("Ferrari_F40", 95, "guid_f40");
let counts = compute_car_counts(&ctx, vec![car], empty_my_data());
let counts = compute_car_counts(&ctx, vec![car], empty_my_data(), HashMap::new());
assert_eq!(counts.by_rid.len(), 1);
let (rec, count) = counts.by_rid.get(&("Ferrari_F40".to_string(), "332".to_string())).unwrap();
@ -82,7 +83,7 @@ fn test_full_pipeline_multiple_cars_different_rq() {
make_car("Car_C", 100, "guid_c"),
];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let counts = compute_car_counts(&ctx, car_list, empty_my_data(), HashMap::new());
assert_eq!(counts.by_rid.len(), 3);
@ -213,7 +214,7 @@ fn test_ownership_filtering_in_counts() {
let car_list =
vec![make_car("Owned_Car", 85, "guid_owned"), make_car("Unowned_Car", 90, "guid_unowned")];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let counts = compute_car_counts(&ctx, car_list, empty_my_data(), HashMap::new());
// Both should be in by_rid
assert_eq!(counts.by_rid.len(), 2);
@ -258,8 +259,8 @@ fn test_load_my_cars_data_with_fixture() {
// owned_configs only for locked cards with complete tune data
assert_eq!(data.owned_configs.len(), 2);
assert_eq!(data.owned_configs.get("guid_a"), Some(&"323".to_string()));
assert_eq!(data.owned_configs.get("guid_c"), Some(&"111".to_string()));
assert_eq!(data.owned_configs.get("guid_a"), Some(&vec!["323".to_string()]));
assert_eq!(data.owned_configs.get("guid_c"), Some(&vec!["111".to_string()]));
// Unlocked card should not be in owned_configs
assert!(!data.owned_configs.contains_key("guid_b"));
@ -290,7 +291,7 @@ fn test_load_my_cars_data_incomplete_tune_data() {
// Only the card with complete tune data has a config entry
assert_eq!(data.owned_configs.len(), 1);
assert_eq!(data.owned_configs.get("guid_full"), Some(&"233".to_string()));
assert_eq!(data.owned_configs.get("guid_full"), Some(&vec!["233".to_string()]));
assert!(!data.owned_configs.contains_key("guid_partial"));
assert!(!data.owned_configs.contains_key("guid_empty"));
}

View file

@ -1,5 +1,6 @@
//! Property-based tests using proptest for sorting and comparison functions.
use std::collections::HashMap;
use proptest::prelude::*;
use top_drives::test_utils::{make_car, make_record};
@ -177,7 +178,7 @@ proptest! {
cars in proptest::collection::vec(car_strategy(), 0..20)
) {
let ctx = AnalysisContext::default();
let counts = compute_car_counts(&ctx, cars.clone(), empty_my_data());
let counts = compute_car_counts(&ctx, cars.clone(), empty_my_data(), HashMap::new());
// 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
@ -212,7 +213,7 @@ proptest! {
}
let car_list = vec![make_car(&rid, rq, &guid)];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let counts = compute_car_counts(&ctx, car_list, empty_my_data(), HashMap::new());
let entry = counts.by_rid.get(&(rid.clone(), "332".to_string()));
assert!(entry.is_some(), "car should appear in by_rid");
@ -240,7 +241,7 @@ proptest! {
);
let car_list = vec![make_car(&rid, rq, &guid)];
let counts = compute_car_counts(&ctx, car_list, empty_my_data());
let counts = compute_car_counts(&ctx, car_list, empty_my_data(), HashMap::new());
let (record, _) = counts.by_rid.get(&(rid, "332".to_string())).unwrap();
assert_eq!(record.owned, owned, "owned flag should propagate");