Enforce feature isolation and expand copy_count test coverage
- Gate env_logger init behind #[cfg(feature = "bin-logging")] in
records.rs main() so the binary respects the feature flag
- Set default-features = false on the dev-dependency self-reference
in Cargo.toml; previously default features were silently re-enabled
during tests, making feature isolation accidental rather than real
- Bind guid.to_string() once per card in user_data.rs deck loop and
reuse it via clone, replacing three allocations per locked card
- Add per-field help text to the Track subcommand args clarifying
human-readable names ("Laguna Seca", "Dry") vs raw codes
- Document write_details_csv DashMap snapshot memory tradeoff
(full clone, roughly doubled peak memory during the write)
- Extend my_cars fixture test: duplicate guids count as 2 copies
with per-copy configs, and incomplete-tune cards still count in
copy_count
This commit is contained in:
parent
1a844e7aa8
commit
ec8be9d138
5 changed files with 35 additions and 9 deletions
|
|
@ -32,7 +32,7 @@ bin-logging = ["dep:env_logger"]
|
||||||
test-utils = []
|
test-utils = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
top_drives = { path = ".", features = ["test-utils"] }
|
top_drives = { path = ".", features = ["test-utils"], default-features = false }
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
mockito = "1"
|
mockito = "1"
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,13 @@ pub fn write_trash_csv(
|
||||||
/// of track, surface condition, and RQ level. Results are sorted by RQ then track ID
|
/// of track, surface condition, and RQ level. Results are sorted by RQ then track ID
|
||||||
/// for consistent output ordering.
|
/// for consistent output ordering.
|
||||||
///
|
///
|
||||||
|
/// # Memory
|
||||||
|
///
|
||||||
|
/// Takes a full snapshot of the `best_cars` DashMap (cloning every key and record
|
||||||
|
/// list) so it can be sorted and written without holding DashMap shard locks.
|
||||||
|
/// Peak memory roughly doubles for the duration of the write — accepted here
|
||||||
|
/// because this runs once at the end of analysis, not in a hot loop.
|
||||||
|
///
|
||||||
/// # Output Format
|
/// # Output Format
|
||||||
/// ```csv
|
/// ```csv
|
||||||
/// RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
|
/// RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,14 @@ enum Commands {
|
||||||
/// Generate details.csv with all timing data
|
/// Generate details.csv with all timing data
|
||||||
Details,
|
Details,
|
||||||
/// Show best car for a specific track/condition/RQ combination
|
/// Show best car for a specific track/condition/RQ combination
|
||||||
Track { track: String, condition: String, rq: u8 },
|
Track {
|
||||||
|
/// Human-readable track name (e.g. "Laguna Seca", not the raw code "laguna")
|
||||||
|
track: String,
|
||||||
|
/// Human-readable surface condition (e.g. "Dry", "Wet"; not the raw code "00")
|
||||||
|
condition: String,
|
||||||
|
/// RQ level (car's RQ up to 113)
|
||||||
|
rq: u8,
|
||||||
|
},
|
||||||
/// Generate improve.csv (owned cars not in final config, sorted by best tracks)
|
/// Generate improve.csv (owned cars not in final config, sorted by best tracks)
|
||||||
Improve,
|
Improve,
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +159,7 @@ enum Commands {
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Default to warn level so library/bin warnings stay visible without RUST_LOG;
|
// Default to warn level so library/bin warnings stay visible without RUST_LOG;
|
||||||
// RUST_LOG can still silence or raise verbosity.
|
// RUST_LOG can still silence or raise verbosity.
|
||||||
|
#[cfg(feature = "bin-logging")]
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||||
if let Err(e) = run().await {
|
if let Err(e) = run().await {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,9 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
||||||
for card in deck {
|
for card in deck {
|
||||||
if card.get("locked").and_then(|v| v.as_bool()).unwrap_or(false) {
|
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()) {
|
if let Some(guid) = card.get("cardId").and_then(|v| v.as_str()) {
|
||||||
owned.insert(guid.to_string());
|
let guid = guid.to_string();
|
||||||
*copy_count.entry(guid.to_string()).or_insert(0) += 1;
|
owned.insert(guid.clone());
|
||||||
|
*copy_count.entry(guid.clone()).or_insert(0) += 1;
|
||||||
// Extract actual tune config: engineMajor + weightMajor + chassisMajor.
|
// Extract actual tune config: engineMajor + weightMajor + chassisMajor.
|
||||||
// Skip cards with incomplete tune data to avoid false "000" entries
|
// Skip cards with incomplete tune data to avoid false "000" entries
|
||||||
// that would incorrectly appear as needing upgrade.
|
// that would incorrectly appear as needing upgrade.
|
||||||
|
|
@ -77,7 +78,7 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
||||||
let chassis = card.get("chassisMajor").and_then(|v| v.as_u64());
|
let chassis = card.get("chassisMajor").and_then(|v| v.as_u64());
|
||||||
if let (Some(e), Some(w), Some(c)) = (engine, weight, chassis) {
|
if let (Some(e), Some(w), Some(c)) = (engine, weight, chassis) {
|
||||||
let config = format!("{e}{w}{c}");
|
let config = format!("{e}{w}{c}");
|
||||||
owned_configs.entry(guid.to_string()).or_default().push(config);
|
owned_configs.entry(guid).or_default().push(config);
|
||||||
} else {
|
} else {
|
||||||
log::warn!("locked card {guid} has incomplete tune data — skipped");
|
log::warn!("locked card {guid} has incomplete tune data — skipped");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,7 @@ fn test_load_my_cars_data_with_fixture() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
"playerDeck": [
|
"playerDeck": [
|
||||||
{ "cardId": "guid_a", "locked": true, "engineMajor": 3, "weightMajor": 2, "chassisMajor": 3 },
|
{ "cardId": "guid_a", "locked": true, "engineMajor": 3, "weightMajor": 2, "chassisMajor": 3 },
|
||||||
|
{ "cardId": "guid_a", "locked": true, "engineMajor": 2, "weightMajor": 3, "chassisMajor": 3 },
|
||||||
{ "cardId": "guid_b", "locked": false, "engineMajor": 2, "weightMajor": 3, "chassisMajor": 3 },
|
{ "cardId": "guid_b", "locked": false, "engineMajor": 2, "weightMajor": 3, "chassisMajor": 3 },
|
||||||
{ "cardId": "guid_c", "locked": true, "engineMajor": 1, "weightMajor": 1, "chassisMajor": 1 }
|
{ "cardId": "guid_c", "locked": true, "engineMajor": 1, "weightMajor": 1, "chassisMajor": 1 }
|
||||||
],
|
],
|
||||||
|
|
@ -251,15 +252,19 @@ fn test_load_my_cars_data_with_fixture() {
|
||||||
|
|
||||||
let data = load_my_cars_data(path.to_str().unwrap()).unwrap();
|
let data = load_my_cars_data(path.to_str().unwrap()).unwrap();
|
||||||
|
|
||||||
// Only locked cards are owned
|
// Only locked cards are owned (set deduplicates duplicate guids)
|
||||||
assert_eq!(data.owned.len(), 2);
|
assert_eq!(data.owned.len(), 2);
|
||||||
assert!(data.owned.contains("guid_a"));
|
assert!(data.owned.contains("guid_a"));
|
||||||
assert!(data.owned.contains("guid_c"));
|
assert!(data.owned.contains("guid_c"));
|
||||||
assert!(!data.owned.contains("guid_b"));
|
assert!(!data.owned.contains("guid_b"));
|
||||||
|
|
||||||
// owned_configs only for locked cards with complete tune data
|
// copy_count tracks duplicates: guid_a has two copies, guid_c one
|
||||||
assert_eq!(data.owned_configs.len(), 2);
|
assert_eq!(data.copy_count.get("guid_a"), Some(&2));
|
||||||
assert_eq!(data.owned_configs.get("guid_a"), Some(&vec!["323".to_string()]));
|
assert_eq!(data.copy_count.get("guid_c"), Some(&1));
|
||||||
|
assert!(!data.copy_count.contains_key("guid_b"));
|
||||||
|
|
||||||
|
// owned_configs collects one entry per copy with complete tune data
|
||||||
|
assert_eq!(data.owned_configs.get("guid_a"), Some(&vec!["323".to_string(), "233".to_string()]));
|
||||||
assert_eq!(data.owned_configs.get("guid_c"), Some(&vec!["111".to_string()]));
|
assert_eq!(data.owned_configs.get("guid_c"), Some(&vec!["111".to_string()]));
|
||||||
// Unlocked card should not be in owned_configs
|
// Unlocked card should not be in owned_configs
|
||||||
assert!(!data.owned_configs.contains_key("guid_b"));
|
assert!(!data.owned_configs.contains_key("guid_b"));
|
||||||
|
|
@ -289,6 +294,11 @@ fn test_load_my_cars_data_incomplete_tune_data() {
|
||||||
// All locked cards are owned
|
// All locked cards are owned
|
||||||
assert_eq!(data.owned.len(), 3);
|
assert_eq!(data.owned.len(), 3);
|
||||||
|
|
||||||
|
// copy_count counts every locked card, even without complete tune data
|
||||||
|
assert_eq!(data.copy_count.get("guid_full"), Some(&1));
|
||||||
|
assert_eq!(data.copy_count.get("guid_partial"), Some(&1));
|
||||||
|
assert_eq!(data.copy_count.get("guid_empty"), Some(&1));
|
||||||
|
|
||||||
// Only the card with complete tune data has a config entry
|
// Only the card with complete tune data has a config entry
|
||||||
assert_eq!(data.owned_configs.len(), 1);
|
assert_eq!(data.owned_configs.len(), 1);
|
||||||
assert_eq!(data.owned_configs.get("guid_full"), Some(&vec!["233".to_string()]));
|
assert_eq!(data.owned_configs.get("guid_full"), Some(&vec!["233".to_string()]));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue