//! # Top Drives Records Analyzer //! //! A comprehensive Rust-based tool for analyzing car performance data in the Top Drives racing game. //! This module provides functionality to fetch, parse, and analyze track timing data from a //! community-maintained repository. //! //! ## Key Features //! //! - Fetches car and track data from GitHub repository //! - Parses JSON timing files for performance analysis //! - Identifies best performing cars across all tracks and conditions //! - Compares user collection against optimal car lists //! - Provides track-specific performance recommendations //! - Supports multiple track conditions (Dry, Wet, Dirt, Snow, etc.) //! //! ## Data Flow //! //! 1. **Fetch Phase**: Downloads car database and track definitions //! 2. **Parse Phase**: Processes JSON timing files for each car //! 3. **Analysis Phase**: Compares times across RQ ranges and conditions //! 4. **Output Phase**: Presents results in CSV format or console output //! //! ## Dependencies //! //! - `reqwest`: HTTP client for fetching data from GitHub //! - `serde_json`: JSON parsing for car and track data //! - `tokio`: Asynchronous runtime for concurrent processing //! - `futures`: Future utilities for handling async operations use std::collections::{HashMap, HashSet}; use std::fs; use std::sync::Arc; use clap::{Parser, Subcommand}; use tokio::sync::Semaphore; use tokio::task; use futures::future::join_all; use serde_json::Value; use top_drives::{ AnalysisContext, BestCarRecord, Car, TripletKey, compare_cars, get_file, load_my_cars_data, translate_track_name, translate_track_type, }; /// Maximum RQ level in the Top Drives database. /// Used as the upper bound when iterating RQ ranges and as a fallback for parse errors. const MAX_RQ: u8 = 113; /// Command-line interface for the Top Drives Records Analyzer. #[derive(Parser)] #[command(name = "records", about = "Top Drives Records Analyzer", version)] struct Cli { #[command(subcommand)] command: Commands, } /// Available subcommands for car performance analysis. #[derive(Subcommand)] enum Commands { /// Download latest car and track data from repository Fetch, /// Show best performing cars in your collection Best, /// Show best cars not owned by you (prefix + if not on wishlist) Missing, /// Show wishlist cars that are no longer among the best Wishlist, /// Identify cars that can be safely removed from collection Trash, /// Show detailed timing data for all tracks Details, /// Show best car for a specific track/condition/RQ combination Track { /// Track name (e.g. "Laguna Seca") track: String, /// Track condition (e.g. "Dry", "Wet") condition: String, /// RQ limit (e.g. 85) rq: u8, }, } /// Main entry point for the Top Drives Records Analyzer /// /// Parses command line arguments using `clap` and executes the requested analysis operation. /// Supports multiple subcommands for different types of car performance analysis. /// /// # Example Usage /// /// ```bash /// cargo run --bin records -- best /// cargo run --bin records -- track "Laguna Seca" "Dry" "85" /// ``` #[tokio::main] async fn main() { let cli = Cli::parse(); match cli.command { Commands::Fetch => { // Download phase: Get fresh data from all sources // This is the foundation for all other operations get_cars_file().await; get_tracks_file().await; fetch_times().await; } Commands::Best => { // Standard best cars analysis // Shows cars you own that perform well across tracks let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); let _car_list = parse_all_times(&my_data.owned, &ctx).await; print_best_cars(&ctx, false, &my_data.wishlist); // false = include owned cars } Commands::Missing => { // Missing cars analysis // Shows high-performing cars you don't own // Cars not yet on the wishlist are prefixed with "+" let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); let _car_list = parse_all_times(&my_data.owned, &ctx).await; print_best_cars(&ctx, true, &my_data.wishlist); // true = filter out owned cars } Commands::Wishlist => { // Wishlist review // Shows cars on the wishlist that are no longer among the best let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); let car_list = parse_all_times(&my_data.owned, &ctx).await; print_wishlist_stale(&ctx, &my_data.wishlist, &car_list); } Commands::Trash => { // Collection optimization // Identifies cars that can be safely removed let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); let car_list = parse_all_times(&my_data.owned, &ctx).await; print_trash(&ctx, &car_list, &my_data.owned); } Commands::Details => { // Comprehensive data export // Outputs all timing data for external analysis let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); parse_all_times(&my_data.owned, &ctx).await; print_best_times(&ctx); } Commands::Track { track, condition, rq } => { // Track-specific analysis let my_data = load_my_cars_data(); let ctx = AnalysisContext::new(); parse_all_times(&my_data.owned, &ctx).await; print_best_car(&ctx, &track, &condition, rq); } } } /// Identifies and prints cars that can be safely removed from the user's collection /// /// Compares the user's owned cars against the list of best performing cars across all tracks. /// /// # Arguments /// /// * `ctx` - Analysis context containing the best cars data /// * `car_list` - Pre-loaded list of all cars, to avoid re-reading the data file /// * `owned` - Set of GUIDs the user owns /// /// Only considers cars that are not among the best performers and are below RQ 80 (to avoid /// removing potentially useful higher-end cars). /// /// # Output Format /// ```csv /// RQ,Brand,Model,Year /// 45,Toyota,Corolla,1995 /// 52,Honda,Civic,1992 /// ``` /// /// # Note /// Prize cars are excluded from trash recommendations as they may have collection value /// beyond their performance characteristics. fn print_trash(ctx: &AnalysisContext, car_list: &[Car], owned: &HashSet) { // Extract GUIDs of all best performing cars across all tracks and conditions // Lock is scoped to avoid holding it during file I/O below let best_cars: HashSet<_> = { let guard = ctx.best_cars.lock().unwrap(); guard .values() .flat_map(|records| records.iter().map(|record| record.guid.clone())) .collect() }; // Lock dropped here // Build a lookup map for car details by GUID (reusing pre-loaded car_list) let car_map: HashMap<_, _> = car_list .iter() .map(|c| (c.guid.clone(), c)) .collect(); // Check each car in the user's collection for my_car in owned { if !best_cars.contains(my_car) { if let Some(bad_car) = car_map.get(my_car) { if !bad_car.prize && bad_car.rq < 80 { println!( "{},{},{},{}", bad_car.rq, bad_car.brand, bad_car.only_name, bad_car.year ); } } } } } /// Prints cars on the wishlist that are no longer among the best performers /// /// Compares the user's wishlist (from `my_cars.json`) against the current best-car /// analysis results. Cars that appear on the wishlist but are no longer best for any /// track/condition/RQ combination are reported with a `-` prefix, suggesting the /// wishlist may need updating. /// /// Cars that are best but not yet on the wishlist are shown by the `missing` command /// instead, so they are intentionally excluded here to avoid duplication. /// /// # Output Format /// /// ```csv /// -RQ,Brand,Model,Year (on wishlist but no longer best) /// ``` /// /// # Arguments /// /// * `ctx` - Analysis context containing the best cars data /// * `wishlist` - Set of car GUIDs loaded from the wishlists section of `my_cars.json` /// * `car_list` - Pre-loaded list of all cars, to avoid re-reading the data file fn print_wishlist_stale(ctx: &AnalysisContext, wishlist: &HashSet, car_list: &[Car]) { // Collect GUIDs of all cars that are currently best for at least one combination // Lock is scoped to avoid holding it during file I/O below let current_best_guids: HashSet = { let best_cars = ctx.best_cars.lock().unwrap(); best_cars .values() .flat_map(|records| records.iter().map(|r| r.guid.clone())) .collect() }; // Lock dropped here // Build a lookup map for car details by GUID (reusing pre-loaded car_list) let car_map: HashMap<_, _> = car_list .iter() .map(|c| (c.guid.clone(), c)) .collect(); // Cars on wishlist that are no longer best — prefix with "-" let mut stale: Vec<_> = wishlist .difference(¤t_best_guids) .filter_map(|guid| car_map.get(guid)) .collect(); stale.sort_by(|a, b| compare_cars(*a, *b)); for car in &stale { println!("-{},{},{},{}", car.rq, car.brand, car.only_name, car.year); } } /// Prints detailed timing information for all tracks, conditions, and RQ levels /// /// Outputs comprehensive performance data showing the best car for every combination /// of track, surface condition, and RQ level. Results are sorted by RQ then track ID /// for consistent output ordering. /// /// # Output Format /// ```csv /// RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time /// 85,laguna,Dry,85,McLaren,F1,1993,233,112.45 /// 85,monaco,Dry,85,Pagani,Zonda,2002,111,45.23 /// 82,laguna,Dry,82,Ferrari,250 GTO,1962,111,115.67 /// ``` /// /// # Note /// This generates a large amount of output data and is primarily used for /// external analysis or when detailed performance data is required. fn print_best_times(ctx: &AnalysisContext) { // Scope the lock to only the data extraction phase let sorted_data: Vec<(TripletKey, Vec)> = { let best_cars = ctx.best_cars.lock().unwrap(); // Collect keys and sort them by RQ and Track ID for consistent output let mut sorted_keys: Vec<_> = best_cars.keys().collect(); sorted_keys.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0))); // Sort by RQ, then Track ID sorted_keys .into_iter() .filter_map(|key| best_cars.get(key).map(|records| (key.clone(), records.clone()))) .collect() }; // Lock dropped here for (key, records) in sorted_data { for best_car in records { println!( "{},{},{},{},{},{},{},{},{}", key.1, // RQ level key.0, // Track ID key.2, // Surface condition best_car.rq, // Car's RQ best_car.brand, // Car brand best_car.only_name, // Car model best_car.year, // Car year best_car.config, // Tune configuration best_car.time // Track time ); } } } /// Prints a ranked list of the best performing cars across all tracks and conditions /// /// Aggregates performance data to show which cars appear most frequently as top performers. /// Cars are ranked by how often they achieve the best time on different tracks, providing /// a measure of overall car quality and versatility. /// /// # Arguments /// /// * `ctx` - Analysis context containing the best cars data /// * `filter_out_owned` - If true, excludes cars already owned by the user from the output /// * `wishlist` - Set of car GUIDs on the user's wishlist /// /// # Output Format /// ```csv /// RQ,Brand,Model,Year,Count /// 85,McLaren,F1,1993,15 /// 82,Ferrari,250 GTO,1962,12 /// 80,Lamborghini,Countach,1974,8 /// ``` /// /// # Note /// The count represents how many times this car achieved the best time across different /// track/RQ/condition combinations, indicating overall performance quality. fn print_best_cars(ctx: &AnalysisContext, filter_out_owned: bool, wishlist: &HashSet) { // Scope the lock to only the data extraction phase let car_count: HashMap = { let best_cars = ctx.best_cars.lock().unwrap(); let mut counts: HashMap = HashMap::new(); // Count occurrences of each car across all tracks and conditions for (_key, best_list) in best_cars.iter() { for best_car in best_list { let entry = counts .entry(best_car.car_rid.clone()) .or_insert((best_car.clone(), 0)); entry.1 += 1; // Increment occurrence count } } counts }; // Lock dropped here // Convert to vector and sort by RQ first, then by car RID for consistent ordering let mut car_count_vec: Vec<_> = car_count.iter().collect(); car_count_vec.sort_by(|a, b| a.1 .0.rq.cmp(&b.1 .0.rq).then(a.0.cmp(&b.0))); for (_car_rid, (car_record, count)) in car_count_vec { // Apply ownership filter if requested if !(filter_out_owned && car_record.owned) { // Check if this car is already on the wishlist (by GUID) // In "missing" mode: prefix "+" if not on wishlist (suggested addition) // In "best" mode: prefix "+" if not on wishlist (missing from wishlist) let on_wishlist = wishlist.contains(&car_record.guid); let prefix = if !on_wishlist { "+" } else { "" }; println!( "{}{},{},{},{},{}", prefix, car_record.rq, // Car's RQ level car_record.brand, // Car brand car_record.only_name, // Car model car_record.year, // Car year count // Number of times this car was best ); } } } /// Finds and displays the best performing car for a specific track, condition, and RQ level /// /// Provides targeted recommendations for specific racing scenarios. Useful for determining /// the optimal car choice when you need to win a particular race or meet specific requirements. /// /// # Arguments /// /// * `ctx` - Analysis context containing the best cars data /// * `track` - The track identifier (e.g., "laguna", "monaco") /// * `condition` - Surface condition (e.g., "Dry", "Wet", "Dirt") /// * `rq` - RQ level limit (cars with this RQ or lower will be considered) /// /// # Output Example /// ``` /// Best car for Track: laguna, Condition: Dry, RQ: 85 -> McLaren F1 (1993) with time 112.45 /// ``` /// /// # Note /// If no car is found for the specified combination, it may indicate that no data /// is available for that specific track/condition/RQ combination. fn print_best_car(ctx: &AnalysisContext, track: &str, condition: &str, rq: u8) { let best_cars = ctx.best_cars.lock().unwrap(); let key = ( track.to_string(), rq, condition.to_string(), ); if let Some(best_list) = best_cars.get(&key) { for best_car in best_list { println!( "Best car for Track: {}, Condition: {}, RQ: {} -> {} {} ({}) with time {:.2}", track, condition, rq, best_car.brand, best_car.only_name, best_car.year, best_car.time ); } } else { println!( "No best car found for Track: {}, Condition: {}, RQ: {}", track, condition, rq ); } } /// Parses timing data for a single car from its JSON file /// /// This is the core function responsible for processing individual car performance data. /// It reads the car's JSON file, extracts timing information for each track and condition, /// filters out invalid or downvoted times, and updates the analysis context. /// /// # Arguments /// /// * `car` - The car data structure containing metadata about the car to process /// * `my_cars` - Set of GUIDs the user owns (used to mark ownership) /// * `ctx` - Analysis context containing the shared best cars map /// /// # Data Processing /// /// 1. **File Reading**: Loads the car's JSON timing file from the times/ directory /// 2. **Configuration Parsing**: Processes each tune configuration (111, 233, 323, etc.) /// 3. **Track Processing**: Extracts timing data for each track and surface condition /// 4. **Validation**: Filters out invalid times and downvoted entries /// 5. **Time Conversion**: Handles special track calculations (e.g., Test Bowl) /// 6. **Database Update**: Updates the analysis context with new records /// /// # Special Cases /// /// - **Test Bowl Track**: Time calculation is inverted (2000.0 / time) due to unique track mechanics /// - **Downvoted Times**: Entries with downvotes are excluded to maintain data quality /// - **Minimum Time Threshold**: Times below 1.2 seconds are considered invalid /// /// # Performance /// /// This function is designed to be called concurrently for multiple cars to improve /// overall processing speed when analyzing large car databases. async fn parse_car_times(car: Car, my_cars: &HashSet, ctx: &AnalysisContext) { // Construct file path for this car's timing data let file_name = car.rid.to_owned() + ".json"; let local_file_path = "times/".to_owned() + file_name.as_str(); // Read file content, emitting a warning on failure let file_content = match fs::read_to_string(&local_file_path) { Ok(content) => content, Err(e) => { eprintln!("Warning: could not read {local_file_path}: {e}"); String::new() } }; // Parse JSON data, emitting a warning on failure let json_data: Value = match serde_json::from_str(&file_content) { Ok(v) => v, Err(e) => { if !file_content.is_empty() { eprintln!("Warning: could not parse {local_file_path}: {e}"); } Value::Null } }; // Process each configuration variant (111, 233, 323, etc.) if let Some(configurations) = json_data["data"].as_object() { for (config, entry) in configurations { // Process timing data for each track if let Some(times) = entry["times"].as_object() { for (track_id, details) in times { // Parse track ID to separate base track name from surface condition // Example: "laguna_a00" -> ("laguna", "00") if let Some((cleaned_track_id, track_type)) = track_id.split_once("_a") { // Extract the raw time value from JSON let mut time = details["t"].as_f64().unwrap_or(0.0); // Check for community downvotes (quality control) let down_votes = details["down"].as_array(); let no_down_votes = match down_votes { None => true, // No downvotes = valid Some(votes) => votes.is_empty(), // Empty array = no downvotes }; // Special handling for Test Bowl track // The track measures distance over time, so we invert the calculation if track_id.starts_with("testBowl") { time = 2000.0 / time; // Convert to standard time format } // Determine if user owns this car for output marking let owned = my_cars.contains(&car.guid); let guid = car.guid.clone(); // Apply quality filters before processing if time > 1.2 && no_down_votes { update_best_car( &ctx.best_cars, &translate_track_name(cleaned_track_id), car.rq, translate_track_type(track_type), BestCarRecord { car_rid: car.rid.clone(), brand: car.brand.clone(), only_name: car.only_name.clone(), year: car.year, rq: car.rq, config: config.clone(), time, owned, guid, }, ); } } } } } } } /// Updates the best cars database with a new performance record /// /// Implements the core logic for determining the best performing cars across different /// RQ ranges. For each RQ level from the car's base RQ up to MAX_RQ, this /// function determines whether the new record represents an improvement or tie. /// /// # Arguments /// /// * `best_cars` - Shared mutable reference to the best cars map /// * `track_id` - The track identifier /// * `car_rq` - The base RQ level of the car /// * `surface` - The track surface condition (Dry, Wet, etc.) /// * `new_record` - The new performance record to evaluate /// /// # Algorithm /// /// For each RQ level from car's RQ to MAX_RQ: /// 1. **Better Time**: If new record is faster than current best, replace the list /// 2. **Tie Time**: If new record matches current best time, add to list (if not duplicate) /// 3. **Worse Time**: Ignore the new record /// /// # Design Rationale /// /// The RQ range iteration allows cars to compete at higher RQ levels than their base /// rating, simulating how cars might perform when upgraded or in different matchups. /// This provides more comprehensive analysis across the entire performance spectrum. fn update_best_car( best_cars: &Arc>>>, track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord, ) { /// Tolerance for considering two times as equal (5ms) const TIME_TOLERANCE: f64 = 0.005; let mut best_cars = best_cars.lock().unwrap(); // Check performance across RQ range from car's base RQ up to MAX_RQ for rq in car_rq..=MAX_RQ { let key = (track_id.to_string(), rq, surface.to_string()); match best_cars.get_mut(&key) { Some(existing_list) => { let best_time = existing_list.first().map(|r| r.time).unwrap_or(f64::MAX); if new_record.time < best_time { // New record is faster than current best // Replace entire list - only keep the fastest car *existing_list = vec![new_record.clone()]; } else if (new_record.time - best_time).abs() < TIME_TOLERANCE { // Times are essentially equal (within tolerance) // This is a tie - add the car only if not already present // Prevents duplicate entries for the same car if !existing_list .iter() .any(|r| r.car_rid == new_record.car_rid) { existing_list.push(new_record.clone()); } } // Note: If new_record.time > best_time, we ignore it // The existing best car remains in the list } None => { // First time we've seen this track/RQ/surface combination // Initialize the list with this car as the first (and currently best) entry best_cars.insert(key, vec![new_record.clone()]); } } } } /// Orchestrates concurrent parsing of all car timing files /// /// Spawns a separate async task for each car in the database, allowing for /// parallel processing of timing data. This significantly improves performance /// when processing large numbers of cars. /// /// # Performance /// /// Uses Tokio's spawn system with a semaphore to create one task per car, /// enabling concurrent file I/O and JSON parsing operations while limiting /// concurrency to 20 parallel tasks. The function waits for all tasks to /// complete before returning. /// /// # Note /// /// This function is called by most analysis commands as it's the first step /// in processing the timing data before generating reports. /// /// # Returns /// /// Returns the full car list so callers can reuse it without re-reading the file. async fn parse_all_times(my_cars: &HashSet, ctx: &AnalysisContext) -> Vec { // Use Arc for efficient O(1) lookups across concurrent tasks let my_cars = Arc::new(my_cars.clone()); let car_list = get_car_list(); let mut handles = vec![]; // Limit concurrent parsing to 20 to avoid excessive resource usage let semaphore = Arc::new(Semaphore::new(20)); // Create one async task per car for concurrent processing for car in car_list.iter() { let my_cars = Arc::clone(&my_cars); let best_cars = Arc::clone(&ctx.best_cars); let car = car.clone(); let sem = Arc::clone(&semaphore); let handle = task::spawn(async move { let _permit = sem.acquire().await.unwrap(); parse_car_times(car, &my_cars, &AnalysisContext { best_cars }).await; }); handles.push(handle); } // Wait for all parsing tasks to complete and report any failures let results = join_all(handles).await; for result in results { if let Err(e) = result { eprintln!("Warning: parsing task failed: {e}"); } } car_list } /// Fetches timing data files for all cars using controlled concurrency /// /// Downloads JSON timing files for every car in the database while limiting /// concurrent connections to avoid overwhelming the server. Uses a semaphore /// to restrict simultaneous downloads to 20 concurrent requests. /// /// # Rate Limiting /// /// Uses a semaphore with a limit of 20 concurrent requests to be respectful /// to the server and avoid being rate-limited or blocked. /// /// # Performance /// /// Balances download speed with server load by processing cars in parallel /// but with controlled concurrency limits. async fn fetch_times() { // Limit concurrent downloads to 20 to avoid overwhelming the server let semaphore = Arc::new(Semaphore::new(20)); let mut handles = vec![]; for car in get_car_list() { let permit = semaphore.clone().acquire_owned().await.unwrap(); handles.push(tokio::spawn(async move { fetch_car(car.rid).await; drop(permit); // Release the semaphore permit })); } // Wait for all downloads to complete join_all(handles).await; } /// Downloads timing data for a single car from the remote API /// /// Fetches the JSON timing file for a specific car and saves it locally. /// Called by `fetch_times()` for each car in the database. /// /// # Arguments /// /// * `rid` - The unique car identifier used in the API URL /// /// # File Storage /// /// Files are saved in the `times/` directory with the format `{rid}.json` /// where `rid` is the car's unique identifier. /// /// # Logging /// /// Prints progress information showing which car's data is being fetched. async fn fetch_car(rid: String) { let file_name = rid.to_owned() + ".json"; let url = "https://api.topdrivesrecords.com/car/".to_owned() + rid.as_str(); let local_file_path = "times/".to_owned() + file_name.as_str(); println!("Fetching {}.", url); get_file(url.as_str(), local_file_path.as_str()).await; } /// Loads the complete car database from the local repository file /// /// Reads and parses the `cars_repo.json` file to build a comprehensive /// list of all cars available in the Top Drives database. This includes /// all metadata needed for performance analysis. /// /// # Returns /// /// Returns a `Vec` containing all cars from the database with their /// complete metadata including RID, brand, model, year, RQ, and prize status. /// /// # File Source /// /// * **File**: `cars_repo.json` /// * **Format**: JSON array of car objects /// * **Fields**: rid, brand, onlyName, year, rq, prize, guid /// /// # Error Handling /// /// Uses `unwrap()` for simplicity - in a production environment, proper /// error handling should be implemented for missing or malformed data. fn get_car_list() -> Vec { let mut car_list: Vec = Vec::new(); // Read the cars database file let cars_file = "cars_repo.json"; let content = match fs::read_to_string(cars_file) { Ok(c) => c, Err(e) => { eprintln!("Warning: could not read {cars_file}: {e}"); return car_list; } }; let cars_data: Vec = match serde_json::from_str(&content) { Ok(v) => v, Err(e) => { eprintln!("Warning: could not parse {cars_file}: {e}"); return car_list; } }; // Parse each car entry and build Car structures for car in cars_data { let new_car = Car { rid: String::from(car["rid"].as_str().unwrap_or("No RID")), brand: String::from(car["brand"].as_str().unwrap_or("No Brand")), only_name: String::from(car["onlyName"].as_str().unwrap_or("No Name")), year: car["year"].as_u64().unwrap_or(1900) as u16, rq: car["rq"].as_u64().unwrap_or(200) as u8, prize: car["prize"].as_bool().unwrap_or(false), guid: String::from(car["guid"].as_str().unwrap_or("No GUID")), }; car_list.push(new_car); } car_list } /// Downloads the tracks database file from the remote repository /// /// Fetches the latest track definitions including track names, conditions, /// and identifiers. This data is essential for translating internal track /// codes to human-readable names in the analysis output. /// /// # Remote Source /// /// * **URL**: GitHub repository tracks database /// * **Purpose**: Provides track name mappings and condition definitions /// * **Update Frequency**: Should be fetched periodically for latest tracks async fn get_tracks_file() { println!("Fetching tracks file..."); get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/tracks_repo.json","tracks_repo.json").await; } /// Downloads the cars database file from the remote repository /// /// Fetches the complete car database containing all car metadata including /// identifiers, names, years, RQ levels, and prize status. This serves as /// the foundation for all car-related analysis operations. /// /// # Remote Source /// /// * **URL**: GitHub repository cars database /// * **Purpose**: Provides comprehensive car metadata for analysis /// * **Update Frequency**: Should be fetched for latest car additions async fn get_cars_file() { println!("Fetching cars file..."); get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/cars_final.json", "cars_repo.json").await; } #[cfg(test)] mod tests { use super::*; #[test] fn test_split_once() { // Test normal case let result = "laguna_a00".split_once("_a"); assert_eq!(result, Some(("laguna", "00"))); // Test no delimiter let result = "laguna".split_once("_a"); assert_eq!(result, None); // Test multiple delimiters (should split at first occurrence) let result = "test_a_data_a_more".split_once("_a"); assert_eq!(result, Some(("test", "_data_a_more"))); // Test empty strings let result = "".split_once("_a"); assert_eq!(result, None); } #[test] fn test_translate_track_name() { // Test known tracks assert_eq!(translate_track_name("laguna"), "Laguna Seca"); assert_eq!(translate_track_name("mnCity"), "Monaco Streets"); assert_eq!(translate_track_name("testBowl"), "Test Bowl"); // Test unknown track (should return as-is) assert_eq!(translate_track_name("unknown_track"), "unknown_track"); // Test drag strips assert_eq!(translate_track_name("drag100"), "0-100mph"); assert_eq!(translate_track_name("mile4"), "1/4 Mile Drag"); } #[test] fn test_translate_track_type() { // Test surface conditions assert_eq!(translate_track_type("00"), "Dry"); assert_eq!(translate_track_type("01"), "Wet"); assert_eq!(translate_track_type("10"), "Dirt"); assert_eq!(translate_track_type("11"), "Dirt Wet"); assert_eq!(translate_track_type("30"), "Ice"); assert_eq!(translate_track_type("60"), "Snow"); // Test unknown condition (should return as-is) assert_eq!(translate_track_type("99"), "99"); } #[test] fn test_update_best_car_logic() { // Verify test data is constructed correctly let test_record = BestCarRecord { car_rid: "Test_Car".to_string(), brand: "TestBrand".to_string(), only_name: "TestCar".to_string(), year: 2020, rq: 85, config: "111".to_string(), time: 100.0, owned: false, guid: "test_guid".to_string(), }; assert_eq!(test_record.time, 100.0); assert_eq!(test_record.rq, 85); assert_eq!(test_record.config, "111"); // Test demonstrates expected behavior: // - Better times should replace existing records // - Tie times should be added to the list // - Worse times should be ignored } #[test] fn test_rq_range_analysis() { let ctx = AnalysisContext::new(); let test_record = BestCarRecord { car_rid: "Test_Car".to_string(), brand: "TestBrand".to_string(), only_name: "TestCar".to_string(), year: 2020, rq: 85, config: "111".to_string(), time: 100.0, owned: false, guid: "test_guid".to_string(), }; // Test that car appears in RQ range from its base RQ to MAX_RQ update_best_car(&ctx.best_cars, "laguna", 85, "Dry", test_record); let best_cars = ctx.best_cars.lock().unwrap(); // Should appear at RQ 85, 90, 95, ..., MAX_RQ for rq in 85..=MAX_RQ { let key = ("laguna".to_string(), rq, "Dry".to_string()); assert!( best_cars.contains_key(&key), "Car should appear at RQ level {}", rq ); } } #[test] fn test_time_validation() { // Test demonstrates expected time validation behavior: // - Times below 1.2 seconds should be filtered out // - Valid times should be processed normally // - This logic is tested indirectly through the parsing functions let valid_time = 112.45; // Valid time let invalid_time = 1.0; // Invalid time (too low) assert!(valid_time > 1.2, "Valid time should be above threshold"); assert!(invalid_time < 1.2, "Invalid time should be below threshold"); } #[test] fn test_track_name_completeness() { // Test that we have a reasonable number of track mappings let test_tracks = vec![ "laguna", "mnCity", "testBowl", "drag100", "mile4", "airplaneHangars", "dealsGap", "northloop", ]; for track in test_tracks { let name = translate_track_name(track); assert_ne!( name, track, "Track {} should have a proper name mapping", track ); assert!(!name.is_empty(), "Track name should not be empty"); } } #[test] fn test_condition_completeness() { // Test that we have mappings for all common condition codes let test_conditions = vec!["00", "01", "10", "11", "20", "30", "40", "41", "50", "60"]; for condition in test_conditions { let name = translate_track_type(condition); assert_ne!( name, condition, "Condition {} should have a proper name mapping", condition ); assert!(!name.is_empty(), "Condition name should not be empty"); } } #[test] fn test_time_tolerance_constant() { // Verify the time tolerance is 5ms as required // We test this indirectly by verifying tie behavior let ctx = AnalysisContext::new(); let record1 = BestCarRecord { car_rid: "Car_A".to_string(), brand: "Brand".to_string(), only_name: "Model".to_string(), year: 2020, rq: 85, config: "111".to_string(), time: 100.0, owned: false, guid: "guid_a".to_string(), }; let record2 = BestCarRecord { car_rid: "Car_B".to_string(), brand: "Brand".to_string(), only_name: "Model".to_string(), year: 2020, rq: 85, config: "111".to_string(), time: 100.003, // Within 0.005 tolerance owned: false, guid: "guid_b".to_string(), }; let record3 = BestCarRecord { car_rid: "Car_C".to_string(), brand: "Brand".to_string(), only_name: "Model".to_string(), year: 2020, rq: 85, config: "111".to_string(), time: 100.006, // Outside 0.005 tolerance owned: false, guid: "guid_c".to_string(), }; update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record1); update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record2); update_best_car(&ctx.best_cars, "laguna", 85, "Dry", record3); let best_cars = ctx.best_cars.lock().unwrap(); let key = ("laguna".to_string(), 85, "Dry".to_string()); let records = best_cars.get(&key).unwrap(); // record1 and record2 should both be present (within tolerance) // record3 should not be added (outside tolerance, and worse time) assert_eq!(records.len(), 2, "Should have 2 tied records within 0.005 tolerance"); assert!(records.iter().any(|r| r.car_rid == "Car_A")); assert!(records.iter().any(|r| r.car_rid == "Car_B")); assert!(!records.iter().any(|r| r.car_rid == "Car_C")); } }