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:
parent
b24b274d25
commit
62688926c3
7 changed files with 494 additions and 83 deletions
|
|
@ -57,7 +57,6 @@ pub async fn run_analysis(
|
|||
let ctx = AnalysisContext::new();
|
||||
let owned = Arc::new(my_data.owned.clone());
|
||||
let car_list = parse_all_times(owned, &ctx, cars_db_path, times_dir).await?;
|
||||
// Move only owned_configs; construct a new MyCarsData to transfer ownership.
|
||||
let counts = compute_car_counts(&ctx, car_list, my_data.owned_configs.clone());
|
||||
let counts = compute_car_counts(&ctx, car_list, my_data.owned_configs.clone(), my_data.copy_count.clone());
|
||||
Ok((ctx, counts))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,13 @@ pub struct CarCounts {
|
|||
pub car_map: HashMap<String, Car>,
|
||||
/// Full car list from the database (kept for any callers that need iteration).
|
||||
pub car_list: Vec<Car>,
|
||||
/// Actual tune config for each owned car (guid → config string from playerDeck).
|
||||
/// Actual tune configs for each owned car (guid → list of configs from all copies).
|
||||
/// Used by the improve command to determine which cars are not yet at a final build.
|
||||
pub owned_configs: HashMap<String, String>,
|
||||
pub owned_configs: HashMap<String, Vec<String>>,
|
||||
/// Number of copies owned for each car GUID.
|
||||
/// Used by wishlist to determine if the player has enough copies
|
||||
/// to cover all qualifying configs.
|
||||
pub copy_count: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
/// Computes per-car win counts from the analysis context.
|
||||
|
|
@ -69,7 +73,8 @@ pub struct CarCounts {
|
|||
pub fn compute_car_counts(
|
||||
ctx: &AnalysisContext,
|
||||
car_list: Vec<Car>,
|
||||
owned_configs: HashMap<String, String>,
|
||||
owned_configs: HashMap<String, Vec<String>>,
|
||||
copy_count: HashMap<String, usize>,
|
||||
) -> CarCounts {
|
||||
let mut by_rid: HashMap<(String, String), (BestCarRecord, usize)> = HashMap::new();
|
||||
let mut guid_to_rid: HashMap<String, HashSet<(String, String)>> = HashMap::new();
|
||||
|
|
@ -92,14 +97,15 @@ pub fn compute_car_counts(
|
|||
let car_map: HashMap<String, Car> =
|
||||
car_list.iter().map(|c| (c.guid.clone(), c.clone())).collect();
|
||||
|
||||
CarCounts { by_rid, guid_to_rid, car_map, car_list, owned_configs }
|
||||
CarCounts { by_rid, guid_to_rid, car_map, car_list, owned_configs, copy_count }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn empty_owned_configs() -> HashMap<String, String> {
|
||||
fn empty_owned_configs() -> HashMap<String, Vec<String>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +143,7 @@ mod tests {
|
|||
}];
|
||||
|
||||
let my_data = empty_owned_configs();
|
||||
let counts = compute_car_counts(&ctx, car_list, my_data);
|
||||
let counts = compute_car_counts(&ctx, car_list, my_data, HashMap::new());
|
||||
|
||||
// Should have exactly 1 entry in by_rid
|
||||
assert_eq!(counts.by_rid.len(), 1);
|
||||
|
|
@ -190,7 +196,7 @@ mod tests {
|
|||
ctx.best_cars.insert(("Track2".to_string(), 85, "Dry".to_string()), vec![record_slow]);
|
||||
|
||||
let my_data = empty_owned_configs();
|
||||
let counts = compute_car_counts(&ctx, vec![], my_data);
|
||||
let counts = compute_car_counts(&ctx, vec![], my_data, HashMap::new());
|
||||
// Different normalized configs → separate entries
|
||||
assert_eq!(counts.by_rid.len(), 2);
|
||||
let (rec_fast, count_fast) =
|
||||
|
|
|
|||
|
|
@ -188,14 +188,15 @@ pub fn write_trash_csv(
|
|||
let Some(best_configs) = counts.guid_to_rid.get(*guid) else {
|
||||
return true;
|
||||
};
|
||||
if let Some(actual_config) = counts.owned_configs.get(*guid) {
|
||||
if is_final_build(actual_config) {
|
||||
let is_best = best_configs.iter().any(|(_, nc)| nc == actual_config);
|
||||
if !is_best {
|
||||
if let Some(configs) = counts.owned_configs.get(*guid) {
|
||||
// Any copy with a final build not matching any best config → trash
|
||||
if configs.iter().any(|actual_config| {
|
||||
is_final_build(actual_config)
|
||||
&& !best_configs.iter().any(|(_, nc)| nc == actual_config)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.filter_map(|guid| {
|
||||
|
|
@ -203,7 +204,8 @@ pub fn write_trash_csv(
|
|||
if car.prize || car.rq >= TRASH_MIN_RQ {
|
||||
return None;
|
||||
}
|
||||
let config = counts.owned_configs.get(guid).cloned().unwrap_or_default();
|
||||
let configs = counts.owned_configs.get(guid).cloned().unwrap_or_default();
|
||||
let config = configs.join("/");
|
||||
Some(TrashEntry { car, config })
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -279,11 +281,18 @@ pub fn write_details_csv(output_dir: &str, ctx: &AnalysisContext) -> Result<(),
|
|||
// Wishlist logic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Computes the ideal wishlist: very_best unowned cars, capped at max_wishlist, sorted by RQ.
|
||||
/// Computes the ideal wishlist: cars whose best configs reach `min_count` tracks,
|
||||
/// capped at `max_wishlist`, sorted by RQ.
|
||||
///
|
||||
/// A car qualifies only when **one config** reaches `min_count` tracks — counts are
|
||||
/// NOT summed across different configs. If multiple configs for the same car qualify,
|
||||
/// the one with the highest count is kept as the representative entry.
|
||||
///
|
||||
/// **Owned cars** are included only if the player has a **final build** that differs
|
||||
/// from the qualifying config. This allows wishlisting a second copy for a different
|
||||
/// tune (e.g., the player owns config 323 but needs config 332).
|
||||
///
|
||||
/// Non-final builds are excluded — they belong in `improve.csv` instead.
|
||||
pub fn compute_ideal_wishlist(
|
||||
counts: &CarCounts,
|
||||
owned: &HashSet<String>,
|
||||
|
|
@ -294,13 +303,39 @@ pub fn compute_ideal_wishlist(
|
|||
let mut guid_best_entry: HashMap<String, (BestCarRecord, usize)> = HashMap::new();
|
||||
|
||||
for (record, count) in counts.by_rid.values() {
|
||||
if owned.contains(&record.guid) {
|
||||
continue;
|
||||
}
|
||||
// Filter by per-config count — a single config must reach min_count
|
||||
if *count < min_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
if owned.contains(&record.guid) {
|
||||
let copies = counts.copy_count.get(&record.guid).copied().unwrap_or(1);
|
||||
let qualifying_configs = counts
|
||||
.guid_to_rid
|
||||
.get(&record.guid)
|
||||
.map(|s| s.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if copies >= qualifying_configs && qualifying_configs > 0 {
|
||||
// Player has enough copies to cover all qualifying configs → skip
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check ALL owned configs for this guid
|
||||
if let Some(configs) = counts.owned_configs.get(&record.guid) {
|
||||
// If ANY copy already has this exact config → skip this config
|
||||
if configs.iter().any(|c| c == &record.normalized_config) {
|
||||
continue;
|
||||
}
|
||||
// If no copy is a final build → skip (belongs in improve.csv)
|
||||
if !configs.iter().any(|c| is_final_build(c)) {
|
||||
continue;
|
||||
}
|
||||
// At least one copy is final but none has this exact config → include
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the entry with the highest per-config count (or first on tie).
|
||||
let entry = guid_best_entry
|
||||
.entry(record.guid.clone())
|
||||
|
|
@ -316,6 +351,7 @@ pub fn compute_ideal_wishlist(
|
|||
candidates
|
||||
}
|
||||
|
||||
|
||||
/// Writes wishlist.csv, add.csv, and remove.csv from a single computed ideal wishlist.
|
||||
///
|
||||
/// Computes the ideal wishlist once and derives all three output files from it,
|
||||
|
|
@ -471,18 +507,34 @@ pub fn write_improve_csv(
|
|||
track_count: usize,
|
||||
}
|
||||
|
||||
// Pre-compute configs already owned as final builds, grouped by car_rid.
|
||||
// This lets us exclude targets the player already has on another copy.
|
||||
let owned_final_configs: HashSet<(String, String)> = counts
|
||||
.owned_configs
|
||||
.iter()
|
||||
.filter_map(|(guid, configs)| {
|
||||
let car = counts.car_map.get(guid)?;
|
||||
let finals: Vec<(String, String)> = configs
|
||||
.iter()
|
||||
.filter(|c| is_final_build(c))
|
||||
.map(|c| (car.rid.clone(), c.clone()))
|
||||
.collect();
|
||||
if finals.is_empty() { None } else { Some(finals) }
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
// Iterate over owned cars from the player's actual config data.
|
||||
// For each owned car, check if its actual config is NOT a final build.
|
||||
let mut entries: Vec<ImproveEntry> = Vec::new();
|
||||
|
||||
for (guid, actual_config) in &counts.owned_configs {
|
||||
// Check if the player's actual config is already a final build (sum == 8) or maxed (sum > 8).
|
||||
// Only unfinished cars need to be in improve.csv.
|
||||
if is_final_build(actual_config) {
|
||||
continue; // Already a final build or maxed — skip
|
||||
for (guid, configs) in &counts.owned_configs {
|
||||
// Skip this car if ALL copies are already at final builds
|
||||
let has_unfinished = configs.iter().any(|c| !is_final_build(c));
|
||||
if !has_unfinished {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up the car metadata
|
||||
let Some(car) = counts.car_map.get(guid) else {
|
||||
eprintln!(
|
||||
"Warning: owned car {guid} not found in cars database — skipped for improve"
|
||||
|
|
@ -490,24 +542,31 @@ pub fn write_improve_csv(
|
|||
continue;
|
||||
};
|
||||
|
||||
// Emit one row per normalized config that this car is best on.
|
||||
// Each row shows the ideal target config and its respective track count,
|
||||
// so the user can see all viable upgrade paths.
|
||||
// Collect target configs, excluding any already owned on another copy of this car.
|
||||
// Keep only the best remaining target (highest track count).
|
||||
if let Some(keys) = counts.guid_to_rid.get(guid) {
|
||||
for key in keys {
|
||||
if let Some((record, track_count)) = counts.by_rid.get(key) {
|
||||
let best_target = keys.iter()
|
||||
.filter_map(|key| {
|
||||
let (record, track_count) = counts.by_rid.get(key)?;
|
||||
if owned_final_configs.contains(&(car.rid.clone(), record.normalized_config.clone())) {
|
||||
return None;
|
||||
}
|
||||
Some((record.normalized_config.clone(), *track_count))
|
||||
})
|
||||
.max_by_key(|(_, count)| *count);
|
||||
|
||||
if let Some((target, track_count)) = best_target {
|
||||
entries.push(ImproveEntry {
|
||||
brand: car.brand.clone(),
|
||||
only_name: car.only_name.clone(),
|
||||
year: car.year,
|
||||
rq: car.rq,
|
||||
target: record.normalized_config.clone(),
|
||||
track_count: *track_count,
|
||||
target,
|
||||
track_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let count = entries.len();
|
||||
|
||||
|
|
@ -613,7 +672,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
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 owned: HashSet<String> = HashSet::new();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
|
@ -636,7 +695,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
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 owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
|
@ -645,6 +704,69 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_ideal_wishlist_includes_owned_car_different_config() {
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 100.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
for i in 0..15 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 105.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
for i in 15..25 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
||||
assert_eq!(wishlist.len(), 1, "Owned car with different config should be wishlisted");
|
||||
assert_eq!(wishlist[0].0.normalized_config, "332", "Should wishlist the config player doesn't have");
|
||||
assert!(wishlist[0].1 >= 10, "Track count should meet minimum");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_ideal_wishlist_excludes_owned_same_config() {
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 100.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
for i in 0..15 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["332".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
||||
assert_eq!(wishlist.len(), 0, "Car with same config should be excluded from wishlist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_ideal_wishlist_respects_max() {
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
|
|
@ -665,7 +787,7 @@ mod tests {
|
|||
|
||||
let car_list: Vec<Car> =
|
||||
(0..5u8).map(|j| make_car(&format!("Car_{j}"), 80 + j, &format!("guid_{j}"))).collect();
|
||||
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 owned: HashSet<String> = HashSet::new();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 3);
|
||||
|
|
@ -758,7 +880,7 @@ mod tests {
|
|||
car_d, // not best, prize car — not trash
|
||||
];
|
||||
|
||||
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 owned: HashSet<String> = vec![
|
||||
"guid_a".to_string(),
|
||||
|
|
@ -802,10 +924,10 @@ mod tests {
|
|||
];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), "233".to_string());
|
||||
owned_configs.insert("guid_b".to_string(), "111".to_string());
|
||||
owned_configs.insert("guid_a".to_string(), vec!["233".to_string()]);
|
||||
owned_configs.insert("guid_b".to_string(), vec!["111".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs);
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string(), "guid_b".to_string()]
|
||||
.into_iter()
|
||||
|
|
@ -887,7 +1009,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_empty_analysis_context() {
|
||||
let ctx = AnalysisContext::new();
|
||||
let counts = compute_car_counts(&ctx, vec![], empty_my_data());
|
||||
let counts = compute_car_counts(&ctx, vec![], empty_my_data(), HashMap::new());
|
||||
assert!(counts.by_rid.is_empty());
|
||||
assert!(counts.guid_to_rid.is_empty());
|
||||
assert!(counts.car_map.is_empty());
|
||||
|
|
@ -902,7 +1024,7 @@ mod tests {
|
|||
ctx.best_cars.insert(("Track1".to_string(), 90, "Dry".to_string()), vec![rec]);
|
||||
|
||||
let car = make_car("Only_Car", 90, "guid_only");
|
||||
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(&("Only_Car".to_string(), "332".to_string())).unwrap();
|
||||
|
|
@ -920,7 +1042,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let car = make_car("Car_A", 85, "guid_a");
|
||||
let counts = compute_car_counts(&ctx, vec![car], empty_my_data());
|
||||
let counts = compute_car_counts(&ctx, vec![car], empty_my_data(), HashMap::new());
|
||||
|
||||
let all_owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &all_owned, 10, 300);
|
||||
|
|
@ -937,7 +1059,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let car = make_car("Car_A", 85, "guid_a");
|
||||
let counts = compute_car_counts(&ctx, vec![car], empty_my_data());
|
||||
let counts = compute_car_counts(&ctx, vec![car], empty_my_data(), HashMap::new());
|
||||
|
||||
let no_owned: HashSet<String> = HashSet::new();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &no_owned, 10, 300);
|
||||
|
|
@ -975,30 +1097,27 @@ mod tests {
|
|||
|
||||
// Player owns the car with an unfinished config (sum = 3)
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), "111".to_string());
|
||||
owned_configs.insert("guid_a".to_string(), vec!["111".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, vec![car], owned_configs);
|
||||
let counts = compute_car_counts(&ctx, vec![car], owned_configs, HashMap::new());
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
||||
let count = write_improve_csv(output_dir, &counts).unwrap();
|
||||
|
||||
// Should produce 2 rows: one for 332 (count 5), one for 323 (count 3)
|
||||
assert_eq!(count, 2, "Unfinished car should have one row per best config");
|
||||
// Should produce 1 row: the best target config (332, count 5)
|
||||
assert_eq!(count, 1, "Unfinished car should show only the best target config");
|
||||
|
||||
let path = dir.path().join("improve.csv");
|
||||
let content = fs::read_to_string(path).unwrap();
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
|
||||
// Header + 2 data rows
|
||||
assert_eq!(lines.len(), 3);
|
||||
// Header + 1 data row
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0], "RQ,Brand,Model,Year,Target,Count");
|
||||
|
||||
// First row should be the config with more tracks (332, count 5)
|
||||
assert!(lines[1].contains("332,5"), "First row should be 332 with count 5");
|
||||
// Second row should be 323 with count 3
|
||||
assert!(lines[2].contains("323,3"), "Second row should be 323 with count 3");
|
||||
assert!(lines[1].contains("332,5"), "Row should be 332 with count 5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1018,9 +1137,9 @@ mod tests {
|
|||
|
||||
// Player owns the car already at a final build (323, sum = 8)
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), "323".to_string());
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, vec![car], owned_configs);
|
||||
let counts = compute_car_counts(&ctx, vec![car], owned_configs, HashMap::new());
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
|
@ -1030,7 +1149,144 @@ mod tests {
|
|||
assert_eq!(count, 0, "Car already at final build should not appear in improve.csv");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_improve_csv_filters_already_owned_config() {
|
||||
// Player has one copy at config 323 (final) — already covers 323.
|
||||
// Car is best with 332 (5 tracks) and 323 (3 tracks).
|
||||
// Since the only copy is already final (323), no unfinished copies exist.
|
||||
// Improve list should be empty.
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 90.0, "guid_a1");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 95.0, "guid_a1");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
// 5 tracks with config 332
|
||||
for i in 0..5 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
// 3 tracks with config 323
|
||||
for i in 5..8 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a1")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a1".to_string(), vec!["323".to_string()]); // copy 1: final
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
||||
let count = write_improve_csv(output_dir, &counts).unwrap();
|
||||
|
||||
// Copy 1 is already final (323) — excluded from improve.
|
||||
// No unfinished copies exist -> empty improve list.
|
||||
assert_eq!(count, 0, "No unfinished copies — improve list should be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_improve_csv_deduplicates_targets() {
|
||||
// Player has one unfinished copy at config 221.
|
||||
// Car is best with 332 (5 tracks) and 323 (3 tracks).
|
||||
// Improve should show ONE row: the best target (332, count 5).
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 90.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 95.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
for i in 0..5 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
for i in 5..8 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["221".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
||||
let count = write_improve_csv(output_dir, &counts).unwrap();
|
||||
|
||||
assert_eq!(count, 1, "Should show one row (best target only)");
|
||||
|
||||
let content = fs::read_to_string(dir.path().join("improve.csv")).unwrap();
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(lines.len(), 2); // header + 1 data row
|
||||
assert!(lines[1].contains("332,5"), "Should show best target 332 with count 5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_improve_csv_multi_copy_excludes_owned_final_config() {
|
||||
// Player has 2 copies: copy 1 at "323" (final), copy 2 at "222" (not final).
|
||||
// Car is best with 323 (60 tracks) and 332 (16 tracks).
|
||||
// Improve should show target 332 (not 323 which is already owned on copy 1).
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 90.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 95.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
for i in 0..60 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
for i in 60..76 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string(), "222".to_string()]);
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, HashMap::new());
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
||||
let count = write_improve_csv(output_dir, &counts).unwrap();
|
||||
|
||||
assert_eq!(count, 1, "Should show one target for the unfinished copy");
|
||||
|
||||
let content = fs::read_to_string(dir.path().join("improve.csv")).unwrap();
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(lines.len(), 2); // header + 1 data row
|
||||
assert!(
|
||||
lines[1].contains("332,16"),
|
||||
"Should show 332 as target (323 already owned), got: {}",
|
||||
lines[1]
|
||||
);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// remove.csv tests
|
||||
// remove.csv tests for cars not in best cars data
|
||||
// =======================================================================
|
||||
|
||||
|
|
@ -1052,7 +1308,7 @@ mod tests {
|
|||
make_car("Car_B", 90, "guid_b"), // NOT best on any track
|
||||
make_car("Car_C", 75, "guid_c"), // NOT best on any track
|
||||
];
|
||||
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 dir = tempfile::tempdir().unwrap();
|
||||
let output_dir = dir.path().to_str().unwrap();
|
||||
|
|
@ -1080,4 +1336,145 @@ mod tests {
|
|||
assert!(lines[1].starts_with("75,"), "First car should be RQ 75, got: {}", lines[1]);
|
||||
assert!(lines[2].starts_with("90,"), "Second car should be RQ 90, got: {}", lines[2]);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Copy count tests
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn test_wishlist_copy_count_enough_copies() {
|
||||
// Player has 2 copies of a car, car has 2 qualifying configs.
|
||||
// Player should be able to cover both → not wishlisted.
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 90.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 95.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
for i in 0..15 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
for i in 15..25 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string()]);
|
||||
|
||||
let mut copy_count = HashMap::new();
|
||||
copy_count.insert("guid_a".to_string(), 2); // 2 copies, 2 qualifying configs
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, copy_count);
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
||||
assert_eq!(wishlist.len(), 0, "2 copies / 2 qualifying → should NOT be wishlisted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wishlist_copy_count_not_enough_copies() {
|
||||
// Player has 1 copy of a car, car has 3 qualifying configs.
|
||||
// Player can't cover all → should be wishlisted for the missing configs.
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 90.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 95.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
let mut rec_233 = make_record("Car_A", 85, 100.0, "guid_a");
|
||||
rec_233.config = "233".to_string();
|
||||
rec_233.normalized_config = "233".to_string();
|
||||
|
||||
for i in 0..15 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
for i in 15..25 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
for i in 25..35 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_233.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string()]);
|
||||
|
||||
let mut copy_count = HashMap::new();
|
||||
copy_count.insert("guid_a".to_string(), 1); // 1 copy, 3 qualifying configs
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, copy_count);
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
||||
assert_eq!(wishlist.len(), 1, "1 copy / 3 qualifying → should be wishlisted");
|
||||
assert_ne!(
|
||||
wishlist[0].0.normalized_config, "323",
|
||||
"Should wishlist a config player doesn't have"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wishlist_copy_count_three_copies_three_configs() {
|
||||
// Player has 3 copies, car has 3 qualifying configs.
|
||||
// Player can cover all → not wishlisted.
|
||||
let ctx = AnalysisContext::new();
|
||||
|
||||
let mut rec_332 = make_record("Car_A", 85, 90.0, "guid_a");
|
||||
rec_332.config = "332".to_string();
|
||||
rec_332.normalized_config = "332".to_string();
|
||||
|
||||
let mut rec_323 = make_record("Car_A", 85, 95.0, "guid_a");
|
||||
rec_323.config = "323".to_string();
|
||||
rec_323.normalized_config = "323".to_string();
|
||||
|
||||
let mut rec_233 = make_record("Car_A", 85, 100.0, "guid_a");
|
||||
rec_233.config = "233".to_string();
|
||||
rec_233.normalized_config = "233".to_string();
|
||||
|
||||
for i in 0..15 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_332.clone()]);
|
||||
}
|
||||
for i in 15..25 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_323.clone()]);
|
||||
}
|
||||
for i in 25..35 {
|
||||
ctx.best_cars
|
||||
.insert((format!("Track{i}"), 85, "Dry".to_string()), vec![rec_233.clone()]);
|
||||
}
|
||||
|
||||
let car_list = vec![make_car("Car_A", 85, "guid_a")];
|
||||
|
||||
let mut owned_configs = HashMap::new();
|
||||
owned_configs.insert("guid_a".to_string(), vec!["323".to_string()]);
|
||||
|
||||
let mut copy_count = HashMap::new();
|
||||
copy_count.insert("guid_a".to_string(), 3); // 3 copies, 3 qualifying configs
|
||||
|
||||
let counts = compute_car_counts(&ctx, car_list, owned_configs, copy_count);
|
||||
|
||||
let owned: HashSet<String> = vec!["guid_a".to_string()].into_iter().collect();
|
||||
let wishlist = compute_ideal_wishlist(&counts, &owned, 10, 300);
|
||||
|
||||
assert_eq!(wishlist.len(), 0, "3 copies / 3 qualifying → should NOT be wishlisted");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,6 @@ pub fn make_car(rid: &str, rq: u8, guid: &str) -> Car {
|
|||
}
|
||||
|
||||
/// Creates an empty owned configs map for tests that don't need ownership data.
|
||||
pub fn empty_my_data() -> HashMap<String, String> {
|
||||
pub fn empty_my_data() -> HashMap<String, Vec<String>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,15 @@ pub struct MyCarsData {
|
|||
pub owned: HashSet<String>,
|
||||
/// GUIDs of wishlisted cars (from `wishlists[0].cards`)
|
||||
pub wishlist: HashSet<String>,
|
||||
/// Actual tune config for each owned car (guid → "{engineMajor}{weightMajor}{chassisMajor}").
|
||||
/// Only populated for locked cards. Used by the improve command to determine
|
||||
/// which cars are not yet at a final build.
|
||||
pub owned_configs: HashMap<String, String>,
|
||||
/// Actual tune configs for each owned car (guid → list of configs from all copies).
|
||||
/// Multiple copies of the same car model share the same guid, so this Vec
|
||||
/// captures all their individual configs. Previously a single String lost
|
||||
/// data from all but the last-processed copy.
|
||||
pub owned_configs: HashMap<String, Vec<String>>,
|
||||
/// Number of copies owned for each car GUID.
|
||||
/// Multiple copies of the same car model share the same guid.
|
||||
/// `owned` HashSet deduplicates, so this counts duplicates.
|
||||
pub copy_count: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
/// Loads and parses `my_cars.json` once, extracting both owned and wishlist data.
|
||||
|
|
@ -55,13 +60,15 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
|||
|
||||
// Extract owned car GUIDs and their actual configs from playerDeck
|
||||
let mut owned: HashSet<String> = HashSet::new();
|
||||
let mut owned_configs: HashMap<String, String> = HashMap::new();
|
||||
let mut owned_configs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut copy_count: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
if let Some(deck) = json.get("playerDeck").and_then(|d| d.as_array()) {
|
||||
for card in deck {
|
||||
if card.get("locked").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
if let Some(guid) = card.get("cardId").and_then(|v| v.as_str()) {
|
||||
owned.insert(guid.to_string());
|
||||
*copy_count.entry(guid.to_string()).or_insert(0) += 1;
|
||||
// Extract actual tune config: engineMajor + weightMajor + chassisMajor.
|
||||
// Skip cards with incomplete tune data to avoid false "000" entries
|
||||
// that would incorrectly appear as needing upgrade.
|
||||
|
|
@ -70,7 +77,7 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
|||
let chassis = card.get("chassisMajor").and_then(|v| v.as_u64());
|
||||
if let (Some(e), Some(w), Some(c)) = (engine, weight, chassis) {
|
||||
let config = format!("{e}{w}{c}");
|
||||
owned_configs.insert(guid.to_string(), config);
|
||||
owned_configs.entry(guid.to_string()).or_default().push(config);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Warning: locked card {guid} has incomplete tune data — skipped"
|
||||
|
|
@ -90,7 +97,7 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
|||
.map(|cards| cards.iter().filter_map(|c| c.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(MyCarsData { owned, wishlist, owned_configs })
|
||||
Ok(MyCarsData { owned, wishlist, owned_configs, copy_count })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue