refactor: modular architecture with unified error handling and DashMap concurrency
- Break monolithic records.rs into commands/, output/, parsing/ modules - Add lib.rs with shared types (AppError, Car, BestCarRecord, etc.) - Replace Arc<Mutex<HashMap>> with DashMap for concurrent access - Add unified AppError enum with thiserror - Add configurable CLI paths (--my-cars, --cars-db, --times-dir, --tracks-db) - Add serde deserialization for Car struct - Add CSV escaping for fields with commas/quotes/newlines - Fix track name splitting: split_once -> rsplit_once for _a handling - Add integration tests, proptest property-based tests, and unit tests - Add rustfmt.toml configuration - Update documentation (API, ARCHITECTURE, DATA_STRUCTURES, README)
This commit is contained in:
parent
9d57b18a4e
commit
a9a46ee551
24 changed files with 3541 additions and 1470 deletions
169
tests/proptest_tests.rs
Normal file
169
tests/proptest_tests.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! Property-based tests using proptest for sorting and comparison functions.
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use top_drives::{compare_cars, Car};
|
||||
|
||||
/// Generate arbitrary Car instances for property testing.
|
||||
fn car_strategy() -> impl Strategy<Value = Car> {
|
||||
(
|
||||
"[a-zA-Z]{2,10}", // rid
|
||||
"[a-zA-Z]{2,15}", // brand
|
||||
"[a-zA-Z0-9 ]{2,20}", // only_name
|
||||
1950u16..2030, // year
|
||||
10u8..113, // rq
|
||||
any::<bool>(), // prize
|
||||
"[a-zA-Z0-9_]{3,20}", // guid
|
||||
)
|
||||
.prop_map(|(rid, brand, only_name, year, rq, prize, guid)| Car {
|
||||
rid,
|
||||
brand,
|
||||
only_name,
|
||||
year,
|
||||
rq,
|
||||
prize,
|
||||
guid,
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
// =======================================================================
|
||||
// compare_cars: antisymmetry property
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn compare_cars_antisymmetric(
|
||||
a in car_strategy(),
|
||||
b in car_strategy(),
|
||||
) {
|
||||
// If compare_cars(a, b) == Less, then compare_cars(b, a) == Greater
|
||||
// If compare_cars(a, b) == Equal, then compare_cars(b, a) == Equal
|
||||
let ab = compare_cars(&a, &b);
|
||||
let ba = compare_cars(&b, &a);
|
||||
assert_eq!(ab.reverse(), ba);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// compare_cars: reflexivity (equal elements compare as Equal)
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn compare_cars_reflexive(a in car_strategy()) {
|
||||
assert_eq!(compare_cars(&a, &a), std::cmp::Ordering::Equal);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// compare_cars: transitivity
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn compare_cars_transitive(
|
||||
a in car_strategy(),
|
||||
b in car_strategy(),
|
||||
c in car_strategy(),
|
||||
) {
|
||||
let ab = compare_cars(&a, &b);
|
||||
let bc = compare_cars(&b, &c);
|
||||
let ac = compare_cars(&a, &c);
|
||||
|
||||
// If a <= b and b <= c, then a <= c
|
||||
if ab != std::cmp::Ordering::Greater && bc != std::cmp::Ordering::Greater {
|
||||
assert_ne!(ac, std::cmp::Ordering::Greater);
|
||||
}
|
||||
// If a >= b and b >= c, then a >= c
|
||||
if ab != std::cmp::Ordering::Less && bc != std::cmp::Ordering::Less {
|
||||
assert_ne!(ac, std::cmp::Ordering::Less);
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// compare_cars: RQ is the primary sort key
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn compare_cars_rq_primary(
|
||||
brand_a in "[a-zA-Z]{2,10}",
|
||||
brand_b in "[a-zA-Z]{2,10}",
|
||||
model_a in "[a-zA-Z]{2,10}",
|
||||
model_b in "[a-zA-Z]{2,10}",
|
||||
rq_a in 10u8..112u8,
|
||||
rq_offset in 1u8..=3u8,
|
||||
) {
|
||||
let rq_b = rq_a.saturating_add(rq_offset).min(113);
|
||||
let a = Car {
|
||||
rid: "A".to_string(),
|
||||
brand: brand_a,
|
||||
only_name: model_a,
|
||||
year: 2020,
|
||||
rq: rq_a,
|
||||
prize: false,
|
||||
guid: "guid_a".to_string(),
|
||||
};
|
||||
let b = Car {
|
||||
rid: "B".to_string(),
|
||||
brand: brand_b,
|
||||
only_name: model_b,
|
||||
year: 2021,
|
||||
rq: rq_b,
|
||||
prize: true,
|
||||
guid: "guid_b".to_string(),
|
||||
};
|
||||
|
||||
// Lower RQ always sorts first, regardless of other fields
|
||||
assert_eq!(compare_cars(&a, &b), std::cmp::Ordering::Less);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// compare_cars: brand is the secondary sort key when RQ is equal
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn compare_cars_brand_secondary(
|
||||
brand_a in "[a-zA-Z]{2,10}",
|
||||
model in "[a-zA-Z]{2,10}",
|
||||
rq in 10u8..113u8,
|
||||
) {
|
||||
let mut brand_b = brand_a.clone();
|
||||
brand_b.push('z'); // brand_b > brand_a lexicographically
|
||||
|
||||
let a = Car {
|
||||
rid: "A".to_string(),
|
||||
brand: brand_a,
|
||||
only_name: model,
|
||||
year: 2020,
|
||||
rq,
|
||||
prize: false,
|
||||
guid: "guid_a".to_string(),
|
||||
};
|
||||
let b = Car {
|
||||
rid: "B".to_string(),
|
||||
brand: brand_b,
|
||||
only_name: "Z".to_string(),
|
||||
year: 2021,
|
||||
rq,
|
||||
prize: true,
|
||||
guid: "guid_b".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(compare_cars(&a, &b), std::cmp::Ordering::Less);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// escape_csv_field: no double-escaping
|
||||
// =======================================================================
|
||||
|
||||
#[test]
|
||||
fn escape_csv_no_double_escape(field in "[a-zA-Z0-9 ]{0,50}") {
|
||||
let escaped = top_drives::escape_csv_field(&field);
|
||||
// Simple fields should not be modified
|
||||
assert_eq!(escaped, field);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_csv_with_comma(field in "[a-zA-Z]{1,10}") {
|
||||
let input = format!("{field},test");
|
||||
let escaped = top_drives::escape_csv_field(&input);
|
||||
assert!(escaped.starts_with('"'));
|
||||
assert!(escaped.ends_with('"'));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue