diff --git a/Cargo.lock b/Cargo.lock index 20fd306..b0d484a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -189,6 +189,7 @@ dependencies = [ "env_logger", "epaint", "font-loader", + "hotwatch", "human-sort", "kuchiki", "log", @@ -197,6 +198,7 @@ dependencies = [ "pollster", "raw-window-handle", "rfd", + "tempfile", "thiserror", "toml_edit", "unicode-segmentation", @@ -247,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" dependencies = [ "lazy-bytes-cast", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -533,7 +535,7 @@ checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c" dependencies = [ "bitflags", "libloading 0.7.1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -621,7 +623,7 @@ checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -785,6 +787,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + [[package]] name = "fnv" version = "1.0.7" @@ -801,7 +815,7 @@ dependencies = [ "core-text", "libc", "servo-fontconfig", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -830,6 +844,41 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "futf" version = "0.1.4" @@ -911,7 +960,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1046,6 +1095,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hotwatch" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39301670a6f5798b75f36a1b149a379a50df5aa7c71be50f4b41ec6eab445cb8" +dependencies = [ + "log", + "notify", +] + [[package]] name = "html5ever" version = "0.25.1" @@ -1088,6 +1147,26 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inplace_it" version = "0.3.3" @@ -1106,6 +1185,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.10.1" @@ -1136,6 +1224,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "khronos-egl" version = "4.1.0" @@ -1148,9 +1246,9 @@ dependencies = [ [[package]] name = "kstring" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8d7e992938cc9078c8db5fd5bdc400e7f9da6efa384c280902a8922b676221" +checksum = "8b310ccceade8121d7d77fee406160e457c2f4e7c7982d589da3499bc7ea4526" dependencies = [ "serde", ] @@ -1179,6 +1277,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.106" @@ -1192,7 +1296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ "cfg-if 1.0.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1202,7 +1306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" dependencies = [ "cfg-if 1.0.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1307,9 +1411,28 @@ dependencies = [ [[package]] name = "minimal-lexical" -version = "0.1.4" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c64630dcdd71f1a64c435f54885086a0de5d6a12d104d69b165fb7d5286d677" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] [[package]] name = "mio" @@ -1319,9 +1442,21 @@ checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "miow", + "miow 0.3.7", "ntapi", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", ] [[package]] @@ -1333,7 +1468,19 @@ dependencies = [ "crossbeam", "crossbeam-queue", "log", - "mio", + "mio 0.7.14", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", ] [[package]] @@ -1342,7 +1489,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1408,6 +1555,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -1465,22 +1623,40 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "nom" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" dependencies = [ "memchr", "minimal-lexical", "version_check", ] +[[package]] +name = "notify" +version = "4.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio 0.6.23", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + [[package]] name = "ntapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1621,7 +1797,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1676,7 +1852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ "phf_shared", - "rand", + "rand 0.7.3", ] [[package]] @@ -1792,12 +1968,24 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", "rand_pcg", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -1805,7 +1993,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", ] [[package]] @@ -1817,13 +2015,31 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", ] [[package]] @@ -1832,7 +2048,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" dependencies = [ - "rand_core", + "rand_core 0.5.1", ] [[package]] @@ -1869,6 +2085,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "renderdoc-sys" version = "0.7.1" @@ -1895,7 +2120,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2019,6 +2244,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + [[package]] name = "slotmap" version = "1.0.6" @@ -2176,6 +2407,20 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "tendril" version = "0.4.2" @@ -2324,7 +2569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi", + "winapi 0.3.9", "winapi-util", ] @@ -2570,7 +2815,7 @@ checksum = "ecad156490d6b620308ed411cfee90d280b3cbd13e189ea0d3fada8acc89158a" dependencies = [ "web-sys", "widestring", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2650,7 +2895,7 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2668,6 +2913,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -2678,6 +2929,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2690,7 +2947,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2715,7 +2972,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio", + "mio 0.7.14", "mio-misc", "ndk", "ndk-glue", @@ -2727,7 +2984,7 @@ dependencies = [ "scopeguard", "smithay-client-toolkit 0.12.3", "wayland-client 0.28.6", - "winapi", + "winapi 0.3.9", "x11-dl", ] @@ -2746,7 +3003,17 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 49d2ec6..467c8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ env_logger = { version = "0.9", default-features = false, features = ["atty", "h epaint = { version = "0.15", default-features = false, features = ["single_threaded"] } font-loader = "0.11" human-sort = "0.2" +hotwatch = "0.4" kuchiki = "0.8" log = "0.4" ordered-multimap = "0.4" @@ -43,6 +44,9 @@ winit_input_helper = "0.10" [target.'cfg(windows)'.build-dependencies] embed-resource = "1.6" +[dev-dependencies] +tempfile = "3.2" + [profile.release] codegen-units = 1 lto = true diff --git a/src/config.rs b/src/config.rs index cba4d72..2fd4c83 100644 --- a/src/config.rs +++ b/src/config.rs @@ -193,14 +193,14 @@ impl Config { /// Get window configuration if it's valid. pub(crate) fn get_window(&self) -> Option { - let window = &self.doc["window"]; + let window = &self.doc.get("window")?; - let x = window["x"].as_integer()?; - let y = window["y"].as_integer()?; + let x = window.get("x").and_then(|t| t.as_integer())?; + let y = window.get("y").and_then(|t| t.as_integer())?; let position = PhysicalPosition::new(x as i32, y as i32); - let width = window["width"].as_integer()?; - let height = window["height"].as_integer()?; + let width = window.get("width").and_then(|t| t.as_integer())?; + let height = window.get("height").and_then(|t| t.as_integer())?; let size = PhysicalSize::new( (width as u32).max(self.min_size.width), (height as u32).max(self.min_size.height), @@ -221,7 +221,10 @@ impl Config { /// Update the setup exports path. pub(crate) fn update_setups_path>(&mut self, setups_path: P) { - self.setups_path = setups_path.as_ref().to_path_buf(); + self.setups_path = setups_path + .as_ref() + .canonicalize() + .unwrap_or_else(|_| setups_path.as_ref().to_path_buf()); // Note that to_string_lossy() is destructive when the path contains invalid UTF-8 sequences. // If this is a problem in practice, we _could_ write unencodable paths as an array of diff --git a/src/framework.rs b/src/framework.rs index 4fc07f7..8ba8add 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -35,7 +35,7 @@ pub(crate) enum Error { } /// User event handling is performed with this type. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug)] pub(crate) enum UserEvent { /// Configuration error handling events ConfigHandler(ConfigHandler), @@ -46,6 +46,9 @@ pub(crate) enum UserEvent { /// Change the path for setup export files. SetupPath(Option), + /// File system event for the setup export path. + FsChange(hotwatch::Event), + /// Change the theme preference. Theme(UserTheme), } @@ -103,6 +106,11 @@ impl Framework { self.egui_state.on_event(&self.egui_ctx, event); } + /// Handle file system change events. + pub(crate) fn handle_fs_change(&mut self, event: hotwatch::Event) { + self.gui.handle_fs_change(event); + } + /// Resize egui. pub(crate) fn resize(&mut self, size: PhysicalSize) { self.screen_descriptor.physical_width = size.width; diff --git a/src/gui.rs b/src/gui.rs index 7eb9d93..592002d 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -4,13 +4,15 @@ use self::grid::SetupGrid; use crate::config::{Config, UserTheme}; use crate::framework::UserEvent; use crate::setup::{Setup, Setups}; -use crate::str_ext::Ellipsis; +use crate::str_ext::{Ellipsis, HumanCompare}; use copypasta::{ClipboardContext, ClipboardProvider}; use egui::widgets::color_picker::{color_edit_button_srgba, Alpha}; use egui::{CtxRef, Widget}; +use hotwatch::Hotwatch; use std::collections::{HashMap, VecDeque}; use std::path::Path; use std::time::{Duration, Instant}; +use thiserror::Error; use winit::event_loop::EventLoopProxy; mod grid; @@ -23,6 +25,9 @@ pub(crate) struct Gui { /// A tree of `Setups` containing all known setup exports. setups: Setups, + /// Filesystem watcher for changes to any setup exports. + hotwatch: Hotwatch, + /// Selected track name. selected_track_name: Option, @@ -85,6 +90,12 @@ pub(crate) struct ShowWarning { context: String, } +#[derive(Debug, Error)] +pub(crate) enum Error { + #[error("File system watch error: {0}")] + Notify(#[from] hotwatch::Error), +} + impl Gui { /// Create a GUI. pub(crate) fn new( @@ -93,10 +104,16 @@ impl Gui { event_loop_proxy: EventLoopProxy, show_errors: VecDeque, show_warnings: VecDeque, - ) -> Self { - Self { + ) -> Result { + let mut hotwatch = Hotwatch::new()?; + let watcher = Self::watch_setups_path(event_loop_proxy.clone()); + + hotwatch.watch(config.get_setups_path(), watcher)?; + + Ok(Self { config, setups, + hotwatch, selected_track_name: None, selected_car_name: None, selected_setups: Vec::new(), @@ -107,7 +124,7 @@ impl Gui { show_errors, show_warnings, show_tooltips: HashMap::new(), - } + }) } /// Draw the UI using egui. @@ -208,11 +225,104 @@ impl Gui { } } + /// Create a file system watcher. + fn watch_setups_path(event_loop_proxy: EventLoopProxy) -> impl Fn(hotwatch::Event) { + move |event| { + event_loop_proxy + .send_event(UserEvent::FsChange(event)) + .expect("Event loop must exist"); + } + } + + /// Handle file system change events. + /// + /// Called by the closure from `Self::watch_setups_path`. + pub(crate) fn handle_fs_change(&mut self, event: hotwatch::Event) { + use crate::setup::UpdateKind::*; + + // Update the setups tree. + let updates = self.setups.update(&event, &self.config); + for update in updates { + match update { + AddedSetup(track_name, car_name, index) => { + if self.selected_track_name.as_ref() == Some(&track_name) + && self.selected_car_name.as_ref() == Some(&car_name) + { + // Update selected setups when a new one is added + for i in self.selected_setups.iter_mut() { + if *i >= index { + *i += 1; + } + } + } + } + RemovedSetup(track_name, car_name, index) => { + if self.selected_track_name.as_ref() == Some(&track_name) + && self.selected_car_name.as_ref() == Some(&car_name) + { + // Update selected setups when an old one is removed + self.selected_setups.retain(|i| *i != index); + for i in self.selected_setups.iter_mut() { + if *i >= index { + *i -= 1; + } + } + } + } + RemovedCar(track_name, car_name) => { + if self.selected_track_name.as_ref() == Some(&track_name) + && self.selected_car_name.as_ref() == Some(&car_name) + { + self.selected_car_name = None; + self.selected_setups.clear(); + } + } + RemovedTrack(track_name) => { + if self.selected_track_name.as_ref() == Some(&track_name) { + self.selected_track_name = None; + self.selected_car_name = None; + self.selected_setups.clear(); + } + } + } + } + + // Show warning window if necessary. + if let hotwatch::Event::Error(error, path) = event { + let msg = path.map_or("Error while watching file system".to_string(), |path| { + format!("Error while watching path: `{:?}`", path) + }); + + self.show_warnings.push_front(ShowWarning::new(error, msg)); + } + } + /// Update setups export path. pub(crate) fn update_setups_path>(&mut self, setups_path: P) { + if let Err(error) = self.hotwatch.unwatch(self.config.get_setups_path()) { + self.show_warnings.push_front(ShowWarning::new( + error, + format!( + "Unable to stop watching setup exports path for changes: `{:?}`", + self.config.get_setups_path() + ), + )); + } + self.config.update_setups_path(setups_path); self.setups = Setups::new(&mut self.show_warnings, &self.config); self.clear_filters(); + + let watcher = Self::watch_setups_path(self.event_loop_proxy.clone()); + if let Err(error) = self.hotwatch.watch(self.config.get_setups_path(), watcher) { + self.show_warnings.push_front(ShowWarning::new( + error, + format!( + "Unable to watch setup exports path for changes: `{:?}`", + self.config.get_setups_path() + ), + )); + } } /// Clear track, car, and setup filters. @@ -233,7 +343,7 @@ impl Gui { }; track_selection.show_ui(ui, |ui| { let mut track_names: Vec<_> = self.setups.tracks().keys().collect(); - track_names.sort_unstable(); + track_names.sort_unstable_by(|a, b| a.human_compare(b)); for track_name in track_names { let checked = self.selected_track_name.as_ref() == Some(track_name); @@ -282,7 +392,7 @@ impl Gui { .expect("Invalid track name") .keys() .collect(); - car_names.sort_unstable(); + car_names.sort_unstable_by(|a, b| a.human_compare(b)); for car_name in car_names { let checked = self.selected_car_name.as_ref() == Some(car_name); @@ -317,16 +427,13 @@ impl Gui { output_track_name = track_name.as_str(); output_car_name = car_name.as_str(); - let mut setups: Vec<_> = tracks + let setups = tracks .get(track_name) .expect("Invalid track name") .get(car_name) - .expect("Invalid car name") - .iter() - .collect(); - setups.sort_unstable_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap()); + .expect("Invalid car name"); - for (i, (name, _)) in setups.iter().enumerate() { + for (i, info) in setups.iter().enumerate() { let position = selected_setups.iter().position(|&v| v == i); let mut checked = position.is_some(); let color = position @@ -335,7 +442,7 @@ impl Gui { .cloned() .unwrap_or_else(|| ui.visuals().text_color()); - let checkbox = egui::Checkbox::new(&mut checked, name) + let checkbox = egui::Checkbox::new(&mut checked, info.name()) .text_color(color) .ui(ui); if checkbox.clicked() { @@ -348,7 +455,7 @@ impl Gui { } for i in selected_setups { - output.push(&setups[*i].1); + output.push(setups[*i].setup()); } } } diff --git a/src/gui/grid.rs b/src/gui/grid.rs index 96a0b80..178eba9 100644 --- a/src/gui/grid.rs +++ b/src/gui/grid.rs @@ -1,4 +1,5 @@ use crate::setup::Setup; +use crate::str_ext::HumanCompare; use epaint::Galley; use std::cmp::Ordering; use std::sync::Arc; @@ -116,7 +117,7 @@ impl<'setup> SetupGrid<'setup> { // Compute diff between `value` and first column let color = colors.next().unwrap_or_else(|| ui.visuals().text_color()); let (color, background) = if let Some(first_value) = first_value.as_ref() { - match string_compare(&value, first_value) { + match value.human_compare(first_value) { Ordering::Less => (ui.visuals().text_color(), Some(diff_colors.0)), Ordering::Greater => (ui.visuals().text_color(), Some(diff_colors.1)), Ordering::Equal => (color, None), @@ -202,15 +203,6 @@ fn intersect_keys<'a>(mut all_keys: impl Iterator>) -> Vec<& output } -fn string_compare(a: &str, b: &str) -> Ordering { - if a.starts_with('-') && b.starts_with('-') { - // Reverse parameter order when comparing negative numbers - human_sort::compare(b, a) - } else { - human_sort::compare(a, b) - } -} - #[cfg(test)] mod tests { use super::*; @@ -301,35 +293,4 @@ mod tests { let keys = intersect_keys(list.into_iter()); assert!(keys.is_empty()); } - - #[test] - fn test_string_compare_text() { - assert_eq!(string_compare("a", "b"), Ordering::Less); - assert_eq!(string_compare("ab", "abc"), Ordering::Less); - assert_eq!(string_compare("abc", "abc"), Ordering::Equal); - } - - #[test] - fn test_string_compare_numbers() { - assert_eq!(string_compare("1", "1"), Ordering::Equal); - assert_eq!(string_compare("10", "10"), Ordering::Equal); - assert_eq!(string_compare("1", "10"), Ordering::Less); - assert_eq!(string_compare("10", "1"), Ordering::Greater); - - assert_eq!(string_compare("1", "2"), Ordering::Less); - assert_eq!(string_compare("10", "2"), Ordering::Greater); - assert_eq!(string_compare("1", "-2"), Ordering::Greater); - assert_eq!(string_compare("10", "-2"), Ordering::Greater); - assert_eq!(string_compare("-1", "2"), Ordering::Less); - assert_eq!(string_compare("-10", "2"), Ordering::Less); - assert_eq!(string_compare("-1", "-2"), Ordering::Greater); - assert_eq!(string_compare("-10", "-2"), Ordering::Less); - } - - #[test] - #[ignore = "Fractions are not yet supported"] - fn test_string_compare_fractions() { - assert_eq!(string_compare("3/8", "1/2"), Ordering::Less); - assert_eq!(string_compare("5/8", "1/2"), Ordering::Greater); - } } diff --git a/src/main.rs b/src/main.rs index 94a4718..a39d9fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use crate::framework::{ConfigHandler, Framework, UserEvent}; use crate::gpu::{Error as GpuError, Gpu}; -use crate::gui::Gui; +use crate::gui::{Error as GuiError, Gui}; use crate::setup::Setups; use log::error; use std::collections::VecDeque; @@ -43,6 +43,9 @@ enum Error { #[error("Window creation error: {0}")] Winit(#[from] winit::error::OsError), + #[error("GUI Error: {0}")] + Gui(#[from] GuiError), + #[error("GPU Error: {0}")] Gpu(#[from] GpuError), } @@ -64,7 +67,7 @@ fn create_window() -> Result<(EventLoop, winit::window::Window, Gpu, }; let window_builder = { - #[cfg(windows)] + #[cfg(target_os = "windows")] { // Magic number from cartunes.rc const ICON_RESOURCE_ID: u16 = 2; @@ -74,7 +77,7 @@ fn create_window() -> Result<(EventLoop, winit::window::Window, Gpu, )) } - #[cfg(not(windows))] + #[cfg(not(target_os = "windows"))] window_builder }; @@ -93,7 +96,7 @@ fn create_window() -> Result<(EventLoop, winit::window::Window, Gpu, let config = Framework::unwrap_config(&mut errors, event_loop.create_proxy(), config); let setups = Setups::new(&mut warnings, &config); let theme = config.theme().as_winit_theme(&window); - let gui = Gui::new(config, setups, event_loop.create_proxy(), errors, warnings); + let gui = Gui::new(config, setups, event_loop.create_proxy(), errors, warnings)?; let gpu = Gpu::new(&window, window_size)?; let framework = Framework::new(window_size, scale_factor, theme, gui, &gpu); @@ -143,6 +146,9 @@ fn main() -> Result<(), Error> { UserEvent::SetupPath(Some(setups_path)) => { framework.update_setups_path(setups_path); } + UserEvent::FsChange(event) => { + framework.handle_fs_change(event); + } UserEvent::Theme(theme) => { let theme = theme.as_winit_theme(&window); framework.change_theme(theme, true); diff --git a/src/setup.rs b/src/setup.rs index aac82df..f77e863 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -2,14 +2,17 @@ use crate::config::Config; use crate::gui::ShowWarning; -use crate::str_ext::Capitalize; +use crate::str_ext::{Capitalize, HumanCompare}; use kuchiki::traits::TendrilSink; use ordered_multimap::ListOrderedMultimap; use std::collections::{HashMap, VecDeque}; use std::fs; use std::path::{Path, PathBuf}; use thiserror::Error; -use walkdir::{DirEntry, WalkDir}; +use walkdir::WalkDir; + +#[cfg(test)] +mod tests; // Parsing setup exports can fail. #[derive(Debug, Error)] @@ -42,6 +45,21 @@ impl Error { } } +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum UpdateKind { + /// A setup has been added; the track name, car name, and index are provided. + AddedSetup(String, String, usize), + + /// A setup has been removed; the track name, car name, and index are provided. + RemovedSetup(String, String, usize), + + /// A car has been removed; the track name and car name are provided. + RemovedCar(String, String), + + /// A track has been removed; the track name is provided. + RemovedTrack(String), +} + /// Internal representation of a setup export. /// /// The structure is a tree that can be described with this ASCII pictograph. @@ -50,15 +68,15 @@ impl Error { /// [Setups] /// ├── "Concord Speedway" /// │ ├── "VW Beetle" -/// │ │ └── ([FileName], [Setup]) +/// │ │ └── SetupInfo /// │ └── "Skip Barber Formula 2000" -/// │ ├── ([FileName], [Setup]) -/// │ ├── ([FileName], [Setup]) -/// │ └── ([FileName], [Setup]) +/// │ ├── SetupInfo +/// │ ├── SetupInfo +/// │ └── SetupInfo /// └── "Okayama International Raceway" /// └── "VW Beetle" -/// ├── ([FileName], [Setup]) -/// └── ([FileName], [Setup]) +/// ├── SetupInfo +/// └── SetupInfo /// ``` /// /// The first layer of depth contains track names (human-readable), meaning that setups are sorted @@ -67,8 +85,9 @@ impl Error { /// At the second layer of depth are the car names (human readable). Setups are also sorted by the /// cars there were exported for. /// -/// Finally at the third level, each car has a list of `Setup` trees along with the file name that -/// it was loaded from (without the extension). Each car can have as many setups as needed. +/// Finally at the third level, each car has a list of [`SetupInfo`] headers, which contains a +/// `Setup` tree along with the file name that it was loaded from (without the extension) and the +/// full file path. Each car can have as many setups as needed. /// /// The `Setup` type is similarly an alias for a deeply nested `HashMap` representing a single /// instance of a car setup. @@ -113,28 +132,29 @@ pub(crate) struct Setups { tracks: Tracks, } +/// Information about a setup +pub(crate) struct SetupInfo { + /// The setup data. + setup: Setup, + /// Name of the setup (the filename without extension). + name: String, + /// Full file path for setup. + path: PathBuf, +} + type Tracks = HashMap; -type Cars = HashMap>; +type Cars = HashMap>; pub(crate) type Setup = ListOrderedMultimap; type Props = ListOrderedMultimap; impl Setups { /// Recursively load all HTML files from the config setup exports path into a `Setups` tree. pub(crate) fn new(warnings: &mut VecDeque, config: &Config) -> Self { - // Check if a directory entry is an HTML file. - fn is_html(entry: &DirEntry) -> bool { - entry - .file_name() - .to_str() - .map(|s| s.ends_with(".htm") || s.ends_with(".html")) - .unwrap_or(false) - } - let mut setups = Self::default(); let path = config.get_setups_path(); - let walker = WalkDir::new(path) - .into_iter() - .filter_entry(|entry| entry.file_type().is_dir() || is_html(entry)); + let walker = WalkDir::new(path).into_iter().filter_entry(|entry| { + entry.file_type().is_dir() || is_html(entry.file_name().to_str()) + }); for entry in walker { match entry { @@ -160,9 +180,140 @@ impl Setups { } } + // Sort `SetupInfo`s by name. + for track in setups.tracks.values_mut() { + for setups in track.values_mut() { + setups.sort_by(|a, b| a.name().human_compare(b.name())); + } + } + setups } + /// Update setups when the file system changes. + pub(crate) fn update(&mut self, event: &hotwatch::Event, config: &Config) -> Vec { + use hotwatch::Event::*; + + let mut result = Vec::new(); + + match event { + Create(path) | Write(path) => { + if path.is_file() && is_html(path.as_path().to_str()) { + self.add(&mut result, path, None, config); + } + } + Remove(path) => { + if is_html(path.as_path().to_str()) { + self.remove(&mut result, path); + } + } + Rename(from, to) => { + let old_name_is_html = is_html(from.as_path().to_str()); + let new_name_is_html = to.is_file() && is_html(to.as_path().to_str()); + + if old_name_is_html && !new_name_is_html { + self.remove(&mut result, from); + } else if new_name_is_html { + self.add(&mut result, to, Some(from), config); + } + } + _ => (), + } + + result + } + + /// Add a path to the setup tree or replace an existing entry. + fn add( + &mut self, + result: &mut Vec, + path: &Path, + old_path: Option<&Path>, + config: &Config, + ) { + if let Ok((track_name, car_name, setup)) = setup_from_html(path, config) { + let file_name = path + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| car_name.clone()); + let cars = self.tracks.entry(track_name.clone()).or_default(); + let setups = cars.entry(car_name.clone()).or_default(); + + // Find an existing SetupInfo by path + let index = setups.iter().enumerate().find_map(|(i, setup_info)| { + Some(i).filter(|_| { + setup_info.path == path || Some(setup_info.path.as_path()) == old_path + }) + }); + + if let Some(index) = index { + // Special handling for replacements + setups[index] = SetupInfo::new(setup, file_name, path); + } else { + // Find the index where the setup should be inserted + let index = setups.partition_point(|setup_info| setup_info.name < file_name); + setups.insert(index, SetupInfo::new(setup, file_name, path)); + + // Only emit `AddedSetups` when adding a new entry + result.push(UpdateKind::AddedSetup(track_name, car_name, index)); + } + } + } + + /// Remove a path from the setup tree. + fn remove(&mut self, result: &mut Vec, path: &Path) { + let mut remove_track = None; + + for (track_name, track) in self.tracks.iter_mut() { + let mut remove_car = None; + + for (car_name, setups) in track.iter_mut() { + // Find the SetupInfo by path + let index = setups + .iter() + .enumerate() + .find_map(|(i, setup_info)| Some(i).filter(|_| setup_info.path == path)); + + if let Some(index) = index { + // Remove the `SetupInfo` for the removed path + setups.remove(index); + + // Record the removed setup + result.push(UpdateKind::RemovedSetup( + track_name.to_string(), + car_name.to_string(), + index, + )); + + if setups.is_empty() { + // Record the removed car + result.push(UpdateKind::RemovedCar( + track_name.to_string(), + car_name.to_string(), + )); + + remove_car = Some(car_name.to_string()); + } + } + } + + if let Some(car_name) = remove_car { + track.remove(&car_name); + } + + if track.is_empty() { + // Record the removed track + result.push(UpdateKind::RemovedTrack(track_name.to_string())); + + remove_track = Some(track_name.to_string()); + } + } + + if let Some(track_name) = remove_track { + self.tracks.remove(&track_name); + } + } + /// Get a reference to the tracks tree. pub(crate) fn tracks(&self) -> &Tracks { &self.tracks @@ -179,12 +330,38 @@ impl Setups { .unwrap_or_else(|| car_name.clone()); let cars = self.tracks.entry(track_name).or_default(); let setups = cars.entry(car_name).or_default(); - setups.push((file_name, setup)); + setups.push(SetupInfo::new(setup, file_name, path)); Ok(()) } } +impl SetupInfo { + /// Create a new `SetupInfo` descriptor. + pub(crate) fn new>(setup: Setup, name: String, path: P) -> Self { + let path = path.as_ref().to_path_buf(); + + Self { setup, name, path } + } + + /// Get a reference to the inner [`Setup`]. + pub(crate) fn setup(&self) -> &Setup { + &self.setup + } + + /// Get a reference to the name. + pub(crate) fn name(&self) -> &str { + &self.name + } +} + +// Check if a directory entry is an HTML file. +fn is_html(file_name: Option<&str>) -> bool { + file_name + .map(|s| s.ends_with(".htm") || s.ends_with(".html")) + .unwrap_or(false) +} + /// Parse an HTML file into a `Setup`. fn setup_from_html>( path: P, @@ -336,723 +513,3 @@ fn get_properties(mut node_ref: Option) -> Props { map } - -#[cfg(test)] -mod tests { - use super::*; - use winit::dpi::PhysicalSize; - - fn create_ordered_multimap(list: &[(&str, &str)]) -> ListOrderedMultimap { - list.iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() - } - - #[test] - fn test_load_dir() { - let mut config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); - config.update_setups_path("./fixtures"); - let mut warnings = VecDeque::new(); - let setups = Setups::new(&mut warnings, &config); - - assert!(warnings.is_empty()); - - let cars = setups - .tracks() - .get("Centripetal Circuit") - .unwrap() - .get("Skip Barber Formula 2000") - .unwrap(); - assert_eq!(cars.len(), 1); - let (file_name, skip_barber) = &cars[0]; - assert_eq!(file_name, "skip_barber_centripetal"); - assert_eq!(skip_barber.keys().len(), 6); - - let cars = setups - .tracks() - .get("Charlotte Motor Speedway - Legends Oval") - .unwrap() - .get("Global Mazda MX-5 Cup") - .unwrap(); - assert_eq!(cars.len(), 1); - let (file_name, mx5) = &cars[0]; - assert_eq!(file_name, "mx5_charlotte_legends_oval"); - assert_eq!(mx5.keys().len(), 6); - - let cars = setups - .tracks() - .get("Circuit des 24 Heures du Mans - 24 Heures du Mans") - .unwrap() - .get("Dallara P217") - .unwrap(); - assert_eq!(cars.len(), 1); - let (file_name, dallara) = &cars[0]; - assert_eq!(file_name, "iracing_lemans_default"); - assert_eq!(dallara.keys().len(), 18); - - let cars = setups - .tracks() - .get("Nürburgring Combined") - .unwrap() - .get("Porsche 911 GT3 R") - .unwrap(); - assert_eq!(cars.len(), 1); - let (file_name, porche911) = &cars[0]; - assert_eq!(file_name, "baseline"); - assert_eq!(porche911.keys().len(), 12); - - assert_eq!(setups.tracks().len(), 4); - } - - #[test] - fn test_setup_skip_barber() { - let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); - let (track_name, car_name, setup) = - setup_from_html("./fixtures/skip_barber_centripetal.htm", &config).unwrap(); - - assert_eq!(track_name, "Centripetal Circuit".to_string()); - assert_eq!(car_name, "Skip Barber Formula 2000".to_string()); - assert_eq!(setup.keys().len(), 6); - - // Front - let expected = create_ordered_multimap(&[("Brake bias", "54%")]); - let front = setup.get("Front").unwrap(); - assert_eq!(front, &expected); - - // Left Front - let expected = create_ordered_multimap(&[ - ("Cold pressure", "25.0 psi"), - ("Last hot pressure", "25.0 psi"), - ("Last temps O M I", "119F"), - ("Last temps O M I", "119F"), - ("Last temps O M I", "119F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "301 lbs"), - ("Ride height", "1.95 in"), - ("Spring perch offset", "5 x 1/16 in."), - ("Camber", "-1.6 deg"), - ("Caster", "+12.2 deg"), - ]); - let left_front = setup.get("Left Front").unwrap(); - assert_eq!(left_front, &expected); - - // Left Rear - let expected = create_ordered_multimap(&[ - ("Cold pressure", "25.0 psi"), - ("Last hot pressure", "25.0 psi"), - ("Last temps O M I", "119F"), - ("Last temps O M I", "119F"), - ("Last temps O M I", "119F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "438 lbs"), - ("Ride height", "3.20 in"), - ("Camber", "-2.1 deg"), - ]); - let left_rear = setup.get("Left Rear").unwrap(); - assert_eq!(left_rear, &expected); - - // Right Front - let expected = create_ordered_multimap(&[ - ("Cold pressure", "25.0 psi"), - ("Last hot pressure", "25.0 psi"), - ("Last temps I M O", "119F"), - ("Last temps I M O", "119F"), - ("Last temps I M O", "119F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "301 lbs"), - ("Ride height", "1.95 in"), - ("Spring perch offset", "5 x 1/16 in."), - ("Camber", "-1.6 deg"), - ("Caster", "+12.2 deg"), - ]); - let right_front = setup.get("Right Front").unwrap(); - assert_eq!(right_front, &expected); - - // Right Rear - let expected = create_ordered_multimap(&[ - ("Cold pressure", "25.0 psi"), - ("Last hot pressure", "25.0 psi"), - ("Last temps I M O", "119F"), - ("Last temps I M O", "119F"), - ("Last temps I M O", "119F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "438 lbs"), - ("Ride height", "3.20 in"), - ("Camber", "-2.1 deg"), - ]); - let right_rear = setup.get("Right Rear").unwrap(); - assert_eq!(right_rear, &expected); - - // Rear - let expected = - create_ordered_multimap(&[("Fuel level", "4.2 gal"), ("Anti-roll bar", "6")]); - let rear = setup.get("Rear").unwrap(); - assert_eq!(rear, &expected); - } - - #[test] - fn test_setup_mx5() { - let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); - let (track_name, car_name, setup) = - setup_from_html("./fixtures/mx5_charlotte_legends_oval.htm", &config).unwrap(); - - assert_eq!( - track_name, - "Charlotte Motor Speedway - Legends Oval".to_string() - ); - assert_eq!(car_name, "Global Mazda MX-5 Cup".to_string()); - assert_eq!(setup.keys().len(), 6); - - // Front - let expected = create_ordered_multimap(&[ - ("Toe-in", r#"-0/16""#), - ("Cross weight", "50.0%"), - ("Anti-roll bar", "Firm"), - ]); - let front = setup.get("Front").unwrap(); - assert_eq!(front, &expected); - - // Left Front - let expected = create_ordered_multimap(&[ - ("Cold pressure", "30.0 psi"), - ("Last hot pressure", "30.0 psi"), - ("Last temps O M I", "103F"), - ("Last temps O M I", "103F"), - ("Last temps O M I", "103F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "605 lbs"), - ("Ride height", "4.83 in"), - ("Spring perch offset", r#"2.563""#), - ("Bump stiffness", "+10 clicks"), - ("Rebound stiffness", "+8 clicks"), - ("Camber", "-2.7 deg"), - ]); - let left_front = setup.get("Left Front").unwrap(); - assert_eq!(left_front, &expected); - - // Left Rear - let expected = create_ordered_multimap(&[ - ("Cold pressure", "30.0 psi"), - ("Last hot pressure", "30.0 psi"), - ("Last temps O M I", "103F"), - ("Last temps O M I", "103F"), - ("Last temps O M I", "103F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "540 lbs"), - ("Ride height", "4.86 in"), - ("Spring perch offset", r#"1.625""#), - ("Bump stiffness", "+8 clicks"), - ("Rebound stiffness", "+10 clicks"), - ("Camber", "-2.7 deg"), - ]); - let left_rear = setup.get("Left Rear").unwrap(); - assert_eq!(left_rear, &expected); - - // Right Front - let expected = create_ordered_multimap(&[ - ("Cold pressure", "30.0 psi"), - ("Last hot pressure", "30.0 psi"), - ("Last temps I M O", "103F"), - ("Last temps I M O", "103F"), - ("Last temps I M O", "103F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "552 lbs"), - ("Ride height", "4.84 in"), - ("Spring perch offset", r#"2.781""#), - ("Bump stiffness", "+10 clicks"), - ("Rebound stiffness", "+8 clicks"), - ("Camber", "-2.7 deg"), - ]); - let right_front = setup.get("Right Front").unwrap(); - assert_eq!(right_front, &expected); - - // Right Rear - let expected = create_ordered_multimap(&[ - ("Cold pressure", "30.0 psi"), - ("Last hot pressure", "30.0 psi"), - ("Last temps I M O", "103F"), - ("Last temps I M O", "103F"), - ("Last temps I M O", "103F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Corner weight", "488 lbs"), - ("Ride height", "4.87 in"), - ("Spring perch offset", r#"1.844""#), - ("Bump stiffness", "+8 clicks"), - ("Rebound stiffness", "+10 clicks"), - ("Camber", "-2.7 deg"), - ]); - let right_rear = setup.get("Right Rear").unwrap(); - assert_eq!(right_rear, &expected); - - // Rear - let expected = create_ordered_multimap(&[ - ("Fuel level", "5.3 gal"), - ("Toe-in", r#"+2/16""#), - ("Anti-roll bar", "Unhooked"), - ]); - let rear = setup.get("Rear").unwrap(); - assert_eq!(rear, &expected); - } - - #[test] - fn test_setup_dallara_p217() { - let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); - let (track_name, car_name, setup) = - setup_from_html("./fixtures/iracing_lemans_default.htm", &config).unwrap(); - - assert_eq!( - track_name, - "Circuit des 24 Heures du Mans - 24 Heures du Mans".to_string() - ); - assert_eq!(car_name, "Dallara P217".to_string()); - assert_eq!(setup.keys().len(), 18); - - // Left Front Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.0 psi"), - ("Last hot pressure", "22.0 psi"), - ("Last temps O M I", "178F"), - ("Last temps O M I", "182F"), - ("Last temps O M I", "187F"), - ("Tread remaining", "99%"), - ("Tread remaining", "98%"), - ("Tread remaining", "98%"), - ]); - let left_front_tire = setup.get("Left Front Tire").unwrap(); - assert_eq!(left_front_tire, &expected); - - // Left Rear Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.0 psi"), - ("Last hot pressure", "22.3 psi"), - ("Last temps O M I", "186F"), - ("Last temps O M I", "196F"), - ("Last temps O M I", "200F"), - ("Tread remaining", "98%"), - ("Tread remaining", "97%"), - ("Tread remaining", "97%"), - ]); - let left_rear_tire = setup.get("Left Rear Tire").unwrap(); - assert_eq!(left_rear_tire, &expected); - - // Right Front Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.0 psi"), - ("Last hot pressure", "21.8 psi"), - ("Last temps I M O", "183F"), - ("Last temps I M O", "179F"), - ("Last temps I M O", "173F"), - ("Tread remaining", "98%"), - ("Tread remaining", "98%"), - ("Tread remaining", "99%"), - ]); - let right_front_tire = setup.get("Right Front Tire").unwrap(); - assert_eq!(right_front_tire, &expected); - - // Right Rear Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.0 psi"), - ("Last hot pressure", "22.1 psi"), - ("Last temps I M O", "199F"), - ("Last temps I M O", "195F"), - ("Last temps I M O", "182F"), - ("Tread remaining", "97%"), - ("Tread remaining", "97%"), - ("Tread remaining", "98%"), - ]); - let right_rear_tire = setup.get("Right Rear Tire").unwrap(); - assert_eq!(right_rear_tire, &expected); - - // Aero Settings - let expected = create_ordered_multimap(&[ - ("Downforce trim", "Low"), - ("Rear wing angle", "12 deg"), - ("# of dive planes", "1"), - ("Wing gurney setting", "Off"), - ("Deck gurney setting", "On"), - ]); - let aero_settings = setup.get("Aero Settings").unwrap(); - assert_eq!(aero_settings, &expected); - - // Aero Calculator - let expected = create_ordered_multimap(&[ - ("Front RH at speed", r#"1.575""#), - ("Rear RH at speed", r#"1.181""#), - ("Downforce balance", "40.48%"), - ("L/D", "4.981"), - ]); - let aero_calculator = setup.get("Aero Calculator").unwrap(); - assert_eq!(aero_calculator, &expected); - - // Front - let expected = create_ordered_multimap(&[ - ("Third spring", "571 lbs/in"), - ("Third perch offset", r#"1.791""#), - ("Third spring defl", "0.292 in"), - ("Third spring defl", "of"), - ("Third spring defl", "3.090 in"), - ("Third slider defl", "1.988 in"), - ("Third slider defl", "of"), - ("Third slider defl", "3.937 in"), - ("ARB size", "Medium"), - ("ARB blades", "P2"), - ("Toe-in", r#"-1/32""#), - ("Third pin length", r#"7.913""#), - ("Front pushrod length", r#"7.520""#), - ("Power steering assist", "3"), - ("Steering ratio", "11.0"), - ("Display page", "Race1"), - ]); - let front = setup.get("Front").unwrap(); - assert_eq!(front, &expected); - - // Left Front - let expected = create_ordered_multimap(&[ - ("Corner weight", "527 lbs"), - ("Ride height", "1.772 in"), - ("Shock defl", "1.070 in"), - ("Shock defl", "of"), - ("Shock defl", "1.969 in"), - ("Torsion bar defl", "0.377 in"), - ("Torsion bar turns", "5.000 Turns"), - ("Torsion bar O.D.", "13.90 mm"), - ("LS comp damping", "2 clicks"), - ("HS comp damping", "5 clicks"), - ("HS comp damp slope", "4 clicks"), - ("LS rbd damping", "4 clicks"), - ("HS rbd damping", "6 clicks"), - ("Camber", "-2.8 deg"), - ]); - let left_front = setup.get("Left Front").unwrap(); - assert_eq!(left_front, &expected); - - // Left Rear - let expected = create_ordered_multimap(&[ - ("Corner weight", "652 lbs"), - ("Ride height", "1.771 in"), - ("Shock defl", "1.598 in"), - ("Shock defl", "of"), - ("Shock defl", "2.953 in"), - ("Spring defl", "0.547 in"), - ("Spring defl", "of"), - ("Spring defl", "3.525 in"), - ("Spring perch offset", r#"2.146""#), - ("Spring rate", "600 lbs/in"), - ("LS comp damping", "2 clicks"), - ("HS comp damping", "5 clicks"), - ("HS comp damp slope", "4 clicks"), - ("LS rbd damping", "4 clicks"), - ("HS rbd damping", "6 clicks"), - ("Camber", "-1.8 deg"), - ("Toe-in", r#"+1/32""#), - ]); - let left_rear = setup.get("Left Rear").unwrap(); - assert_eq!(left_rear, &expected); - - // Right Front - let expected = create_ordered_multimap(&[ - ("Corner weight", "527 lbs"), - ("Ride height", "1.772 in"), - ("Shock defl", "1.070 in"), - ("Shock defl", "of"), - ("Shock defl", "1.969 in"), - ("Torsion bar defl", "0.377 in"), - ("Torsion bar turns", "5.000 Turns"), - ("Torsion bar O.D.", "13.90 mm"), - ("LS comp damping", "2 clicks"), - ("HS comp damping", "5 clicks"), - ("HS comp damp slope", "4 clicks"), - ("LS rbd damping", "4 clicks"), - ("HS rbd damping", "6 clicks"), - ("Camber", "-2.8 deg"), - ]); - let right_front = setup.get("Right Front").unwrap(); - assert_eq!(right_front, &expected); - - // Right Rear - let expected = create_ordered_multimap(&[ - ("Corner weight", "652 lbs"), - ("Ride height", "1.771 in"), - ("Shock defl", "1.598 in"), - ("Shock defl", "of"), - ("Shock defl", "2.953 in"), - ("Spring defl", "0.547 in"), - ("Spring defl", "of"), - ("Spring defl", "3.525 in"), - ("Spring perch offset", r#"2.146""#), - ("Spring rate", "600 lbs/in"), - ("LS comp damping", "2 clicks"), - ("HS comp damping", "5 clicks"), - ("HS comp damp slope", "4 clicks"), - ("LS rbd damping", "4 clicks"), - ("HS rbd damping", "6 clicks"), - ("Camber", "-1.8 deg"), - ("Toe-in", r#"+1/32""#), - ]); - let right_rear = setup.get("Right Rear").unwrap(); - assert_eq!(right_rear, &expected); - - // Rear - let expected = create_ordered_multimap(&[ - ("Third spring", "457 lbs/in"), - ("Third perch offset", r#"1.516""#), - ("Third spring defl", "0.538 in"), - ("Third spring defl", "of"), - ("Third spring defl", "3.753 in"), - ("Third slider defl", "2.928 in"), - ("Third slider defl", "of"), - ("Third slider defl", "5.906 in"), - ("ARB size", "Medium"), - ("ARB blades", "P4"), - ("Rear pushrod length", r#"6.614""#), - ("Third pin length", r#"7.126""#), - ("Cross weight", "50.0%"), - ]); - let rear = setup.get("Rear").unwrap(); - assert_eq!(rear, &expected); - - // Lighting - let expected = create_ordered_multimap(&[("Roof ID light color", "Blue")]); - let lighting = setup.get("Lighting").unwrap(); - assert_eq!(lighting, &expected); - - // Brake Spec - let expected = create_ordered_multimap(&[ - ("Pad compound", "Medium"), - ("Brake pressure bias", "49.2%"), - ]); - let brake_spec = setup.get("Brake Spec").unwrap(); - assert_eq!(brake_spec, &expected); - - // Fuel - let expected = create_ordered_multimap(&[("Fuel level", "19.8 gal")]); - let fuel = setup.get("Fuel").unwrap(); - assert_eq!(fuel, &expected); - - // Traction Control - let expected = create_ordered_multimap(&[ - ("Traction control gain", "5 (TC)"), - ("Traction control slip", "5 (TC)"), - ("Throttle shape", "1"), - ]); - let fuel = setup.get("Traction Control").unwrap(); - assert_eq!(fuel, &expected); - - // Gear Ratios - let expected = create_ordered_multimap(&[ - ("Gear stack", "Tall"), - ("Speed in first", "86.7 mph"), - ("Speed in second", "112.1 mph"), - ("Speed in third", "131.6 mph"), - ("Speed in forth", "156.3 mph"), - ("Speed in fifth", "182.7 mph"), - ("Speed in sixth", "210.2 mph"), - ]); - let gear_ratios = setup.get("Gear Ratios").unwrap(); - assert_eq!(gear_ratios, &expected); - - // Rear Diff Spec - let expected = create_ordered_multimap(&[ - ("Drive/coast ramp angles", "45/55"), - ("Clutch friction faces", "4"), - ("Preload", "55 ft-lbs"), - ]); - let rear_diff_spec = setup.get("Rear Diff Spec").unwrap(); - assert_eq!(rear_diff_spec, &expected); - } - - #[test] - fn test_setup_porche_911_gt3_r() { - let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); - let (track_name, car_name, setup) = - setup_from_html("./fixtures/baseline.htm", &config).unwrap(); - - assert_eq!(track_name, "Nürburgring Combined".to_string()); - assert_eq!(car_name, "Porsche 911 GT3 R".to_string()); - assert_eq!(setup.keys().len(), 12); - - // Left Front Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.5 psi"), - ("Last hot pressure", "20.5 psi"), - ("Last temps O M I", "112F"), - ("Last temps O M I", "112F"), - ("Last temps O M I", "112F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ]); - let left_front_tire = setup.get("Left Front Tire").unwrap(); - assert_eq!(left_front_tire, &expected); - - // Left Rear Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.5 psi"), - ("Last hot pressure", "20.5 psi"), - ("Last temps O M I", "112F"), - ("Last temps O M I", "112F"), - ("Last temps O M I", "112F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ]); - let left_rear_tire = setup.get("Left Rear Tire").unwrap(); - assert_eq!(left_rear_tire, &expected); - - // Right Front Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.5 psi"), - ("Last hot pressure", "20.5 psi"), - ("Last temps I M O", "112F"), - ("Last temps I M O", "112F"), - ("Last temps I M O", "112F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ]); - let right_front_tire = setup.get("Right Front Tire").unwrap(); - assert_eq!(right_front_tire, &expected); - - // Right Rear Tire - let expected = create_ordered_multimap(&[ - ("Starting pressure", "20.5 psi"), - ("Last hot pressure", "20.5 psi"), - ("Last temps I M O", "112F"), - ("Last temps I M O", "112F"), - ("Last temps I M O", "112F"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ("Tread remaining", "100%"), - ]); - let right_rear_tire = setup.get("Right Rear Tire").unwrap(); - assert_eq!(right_rear_tire, &expected); - - // Aero Balance Calc - let expected = create_ordered_multimap(&[ - ("Front RH at speed", r#"1.929""#), - ("Rear RH at speed", r#"2.835""#), - ("Wing setting", "7 degrees"), - ("Front downforce", "39.83%"), - ]); - let aero_balance_calc = setup.get("Aero Balance Calc").unwrap(); - assert_eq!(aero_balance_calc, &expected); - - // Front - let expected = create_ordered_multimap(&[ - ("ARB diameter", "45 mm"), - ("ARB setting", "Soft"), - ("Toe-in", r#"-2/32""#), - ("Front master cyl.", "0.811 in"), - ("Rear master cyl.", "0.811 in"), - ("Brake pads", "Medium friction"), - ("Fuel level", "15.9 gal"), - ("Cross weight", "50.0%"), - ]); - let front = setup.get("Front").unwrap(); - assert_eq!(front, &expected); - - // Left Front - let expected = create_ordered_multimap(&[ - ("Corner weight", "605 lbs"), - ("Ride height", "2.034 in"), - ("Spring perch offset", r#"2.441""#), - ("Spring rate", "1371 lbs/in"), - ("LS Comp damping", "-6 clicks"), - ("HS Comp damping", "-10 clicks"), - ("LS Rbd damping", "-8 clicks"), - ("HS Rbd damping", "-10 clicks"), - ("Camber", "-4.0 deg"), - ("Caster", "+7.6 deg"), - ]); - let left_front = setup.get("Left Front").unwrap(); - assert_eq!(left_front, &expected); - - // Left Rear - let expected = create_ordered_multimap(&[ - ("Corner weight", "945 lbs"), - ("Ride height", "3.026 in"), - ("Spring perch offset", r#"2.717""#), - ("Spring rate", "1600 lbs/in"), - ("LS Comp damping", "-6 clicks"), - ("HS Comp damping", "-10 clicks"), - ("LS Rbd damping", "-8 clicks"), - ("HS Rbd damping", "-10 clicks"), - ("Camber", "-3.4 deg"), - ("Toe-in", r#"+1/64""#), - ]); - let left_rear = setup.get("Left Rear").unwrap(); - assert_eq!(left_rear, &expected); - - // In-Car Dials - let expected = create_ordered_multimap(&[ - ("Display page", "Race 1"), - ("Brake pressure bias", "54.0%"), - ("Trac Ctrl (TCC) setting", "5 (TCC)"), - ("Trac Ctrl (TCR) setting", "5 (TCR)"), - ("Throttle Map setting", "4"), - ("ABS setting", "11 (ABS)"), - ("Engine map setting", "4 (MAP)"), - ("Night LED strips", "Blue"), - ]); - let in_car_dials = setup.get("In-Car Dials").unwrap(); - assert_eq!(in_car_dials, &expected); - - // Right Front - let expected = create_ordered_multimap(&[ - ("Corner weight", "605 lbs"), - ("Ride height", "2.034 in"), - ("Spring perch offset", r#"2.441""#), - ("Spring rate", "1371 lbs/in"), - ("LS Comp damping", "-6 clicks"), - ("HS Comp damping", "-10 clicks"), - ("LS Rbd damping", "-8 clicks"), - ("HS Rbd damping", "-10 clicks"), - ("Camber", "-4.0 deg"), - ("Caster", "+7.6 deg"), - ]); - let right_front = setup.get("Right Front").unwrap(); - assert_eq!(right_front, &expected); - - // Right Rear - let expected = create_ordered_multimap(&[ - ("Corner weight", "945 lbs"), - ("Ride height", "3.026 in"), - ("Spring perch offset", r#"2.717""#), - ("Spring rate", "1600 lbs/in"), - ("LS Comp damping", "-6 clicks"), - ("HS Comp damping", "-10 clicks"), - ("LS Rbd damping", "-8 clicks"), - ("HS Rbd damping", "-10 clicks"), - ("Camber", "-3.4 deg"), - ("Toe-in", r#"+1/64""#), - ]); - let right_rear = setup.get("Right Rear").unwrap(); - assert_eq!(right_rear, &expected); - - // Rear - let expected = create_ordered_multimap(&[ - ("ARB diameter", "35 mm"), - ("ARB setting", "Med"), - ("Diff preload", "74 ft-lbs"), - ("Wing setting", "7 degrees"), - ]); - let rear = setup.get("Rear").unwrap(); - assert_eq!(rear, &expected); - } -} diff --git a/src/setup/tests.rs b/src/setup/tests.rs new file mode 100644 index 0000000..d2ac0ab --- /dev/null +++ b/src/setup/tests.rs @@ -0,0 +1,940 @@ +use super::*; +use winit::dpi::PhysicalSize; + +fn create_ordered_multimap(list: &[(&str, &str)]) -> ListOrderedMultimap { + list.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() +} + +#[test] +fn test_load_dir() { + let mut config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + config.update_setups_path("./fixtures"); + let mut warnings = VecDeque::new(); + let setups = Setups::new(&mut warnings, &config); + + assert!(warnings.is_empty()); + + let tracks = setups.tracks(); + let cars = &tracks["Centripetal Circuit"]["Skip Barber Formula 2000"]; + assert_eq!(cars.len(), 1); + let SetupInfo { + setup: skip_barber, + name: file_name, + .. + } = &cars[0]; + assert_eq!(file_name, "skip_barber_centripetal"); + assert_eq!(skip_barber.keys().len(), 6); + + let cars = &tracks["Charlotte Motor Speedway - Legends Oval"]["Global Mazda MX-5 Cup"]; + assert_eq!(cars.len(), 1); + let SetupInfo { + setup: mx5, + name: file_name, + .. + } = &cars[0]; + assert_eq!(file_name, "mx5_charlotte_legends_oval"); + assert_eq!(mx5.keys().len(), 6); + + let cars = &tracks["Circuit des 24 Heures du Mans - 24 Heures du Mans"]["Dallara P217"]; + assert_eq!(cars.len(), 1); + let SetupInfo { + setup: dallara, + name: file_name, + .. + } = &cars[0]; + assert_eq!(file_name, "iracing_lemans_default"); + assert_eq!(dallara.keys().len(), 18); + + let cars = &tracks["Nürburgring Combined"]["Porsche 911 GT3 R"]; + assert_eq!(cars.len(), 1); + let SetupInfo { + setup: porche911, + name: file_name, + .. + } = &cars[0]; + assert_eq!(file_name, "baseline"); + assert_eq!(porche911.keys().len(), 12); + + assert_eq!(setups.tracks().len(), 4); +} + +#[test] +fn test_setup_skip_barber() { + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let (track_name, car_name, setup) = + setup_from_html("./fixtures/skip_barber_centripetal.htm", &config).unwrap(); + + assert_eq!(track_name, "Centripetal Circuit".to_string()); + assert_eq!(car_name, "Skip Barber Formula 2000".to_string()); + assert_eq!(setup.keys().len(), 6); + + // Front + let expected = create_ordered_multimap(&[("Brake bias", "54%")]); + let front = setup.get("Front").unwrap(); + assert_eq!(front, &expected); + + // Left Front + let expected = create_ordered_multimap(&[ + ("Cold pressure", "25.0 psi"), + ("Last hot pressure", "25.0 psi"), + ("Last temps O M I", "119F"), + ("Last temps O M I", "119F"), + ("Last temps O M I", "119F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "301 lbs"), + ("Ride height", "1.95 in"), + ("Spring perch offset", "5 x 1/16 in."), + ("Camber", "-1.6 deg"), + ("Caster", "+12.2 deg"), + ]); + let left_front = setup.get("Left Front").unwrap(); + assert_eq!(left_front, &expected); + + // Left Rear + let expected = create_ordered_multimap(&[ + ("Cold pressure", "25.0 psi"), + ("Last hot pressure", "25.0 psi"), + ("Last temps O M I", "119F"), + ("Last temps O M I", "119F"), + ("Last temps O M I", "119F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "438 lbs"), + ("Ride height", "3.20 in"), + ("Camber", "-2.1 deg"), + ]); + let left_rear = setup.get("Left Rear").unwrap(); + assert_eq!(left_rear, &expected); + + // Right Front + let expected = create_ordered_multimap(&[ + ("Cold pressure", "25.0 psi"), + ("Last hot pressure", "25.0 psi"), + ("Last temps I M O", "119F"), + ("Last temps I M O", "119F"), + ("Last temps I M O", "119F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "301 lbs"), + ("Ride height", "1.95 in"), + ("Spring perch offset", "5 x 1/16 in."), + ("Camber", "-1.6 deg"), + ("Caster", "+12.2 deg"), + ]); + let right_front = setup.get("Right Front").unwrap(); + assert_eq!(right_front, &expected); + + // Right Rear + let expected = create_ordered_multimap(&[ + ("Cold pressure", "25.0 psi"), + ("Last hot pressure", "25.0 psi"), + ("Last temps I M O", "119F"), + ("Last temps I M O", "119F"), + ("Last temps I M O", "119F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "438 lbs"), + ("Ride height", "3.20 in"), + ("Camber", "-2.1 deg"), + ]); + let right_rear = setup.get("Right Rear").unwrap(); + assert_eq!(right_rear, &expected); + + // Rear + let expected = create_ordered_multimap(&[("Fuel level", "4.2 gal"), ("Anti-roll bar", "6")]); + let rear = setup.get("Rear").unwrap(); + assert_eq!(rear, &expected); +} + +#[test] +fn test_setup_mx5() { + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let (track_name, car_name, setup) = + setup_from_html("./fixtures/mx5_charlotte_legends_oval.htm", &config).unwrap(); + + assert_eq!( + track_name, + "Charlotte Motor Speedway - Legends Oval".to_string() + ); + assert_eq!(car_name, "Global Mazda MX-5 Cup".to_string()); + assert_eq!(setup.keys().len(), 6); + + // Front + let expected = create_ordered_multimap(&[ + ("Toe-in", r#"-0/16""#), + ("Cross weight", "50.0%"), + ("Anti-roll bar", "Firm"), + ]); + let front = setup.get("Front").unwrap(); + assert_eq!(front, &expected); + + // Left Front + let expected = create_ordered_multimap(&[ + ("Cold pressure", "30.0 psi"), + ("Last hot pressure", "30.0 psi"), + ("Last temps O M I", "103F"), + ("Last temps O M I", "103F"), + ("Last temps O M I", "103F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "605 lbs"), + ("Ride height", "4.83 in"), + ("Spring perch offset", r#"2.563""#), + ("Bump stiffness", "+10 clicks"), + ("Rebound stiffness", "+8 clicks"), + ("Camber", "-2.7 deg"), + ]); + let left_front = setup.get("Left Front").unwrap(); + assert_eq!(left_front, &expected); + + // Left Rear + let expected = create_ordered_multimap(&[ + ("Cold pressure", "30.0 psi"), + ("Last hot pressure", "30.0 psi"), + ("Last temps O M I", "103F"), + ("Last temps O M I", "103F"), + ("Last temps O M I", "103F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "540 lbs"), + ("Ride height", "4.86 in"), + ("Spring perch offset", r#"1.625""#), + ("Bump stiffness", "+8 clicks"), + ("Rebound stiffness", "+10 clicks"), + ("Camber", "-2.7 deg"), + ]); + let left_rear = setup.get("Left Rear").unwrap(); + assert_eq!(left_rear, &expected); + + // Right Front + let expected = create_ordered_multimap(&[ + ("Cold pressure", "30.0 psi"), + ("Last hot pressure", "30.0 psi"), + ("Last temps I M O", "103F"), + ("Last temps I M O", "103F"), + ("Last temps I M O", "103F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "552 lbs"), + ("Ride height", "4.84 in"), + ("Spring perch offset", r#"2.781""#), + ("Bump stiffness", "+10 clicks"), + ("Rebound stiffness", "+8 clicks"), + ("Camber", "-2.7 deg"), + ]); + let right_front = setup.get("Right Front").unwrap(); + assert_eq!(right_front, &expected); + + // Right Rear + let expected = create_ordered_multimap(&[ + ("Cold pressure", "30.0 psi"), + ("Last hot pressure", "30.0 psi"), + ("Last temps I M O", "103F"), + ("Last temps I M O", "103F"), + ("Last temps I M O", "103F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Corner weight", "488 lbs"), + ("Ride height", "4.87 in"), + ("Spring perch offset", r#"1.844""#), + ("Bump stiffness", "+8 clicks"), + ("Rebound stiffness", "+10 clicks"), + ("Camber", "-2.7 deg"), + ]); + let right_rear = setup.get("Right Rear").unwrap(); + assert_eq!(right_rear, &expected); + + // Rear + let expected = create_ordered_multimap(&[ + ("Fuel level", "5.3 gal"), + ("Toe-in", r#"+2/16""#), + ("Anti-roll bar", "Unhooked"), + ]); + let rear = setup.get("Rear").unwrap(); + assert_eq!(rear, &expected); +} + +#[test] +fn test_setup_dallara_p217() { + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let (track_name, car_name, setup) = + setup_from_html("./fixtures/iracing_lemans_default.htm", &config).unwrap(); + + assert_eq!( + track_name, + "Circuit des 24 Heures du Mans - 24 Heures du Mans".to_string() + ); + assert_eq!(car_name, "Dallara P217".to_string()); + assert_eq!(setup.keys().len(), 18); + + // Left Front Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.0 psi"), + ("Last hot pressure", "22.0 psi"), + ("Last temps O M I", "178F"), + ("Last temps O M I", "182F"), + ("Last temps O M I", "187F"), + ("Tread remaining", "99%"), + ("Tread remaining", "98%"), + ("Tread remaining", "98%"), + ]); + let left_front_tire = setup.get("Left Front Tire").unwrap(); + assert_eq!(left_front_tire, &expected); + + // Left Rear Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.0 psi"), + ("Last hot pressure", "22.3 psi"), + ("Last temps O M I", "186F"), + ("Last temps O M I", "196F"), + ("Last temps O M I", "200F"), + ("Tread remaining", "98%"), + ("Tread remaining", "97%"), + ("Tread remaining", "97%"), + ]); + let left_rear_tire = setup.get("Left Rear Tire").unwrap(); + assert_eq!(left_rear_tire, &expected); + + // Right Front Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.0 psi"), + ("Last hot pressure", "21.8 psi"), + ("Last temps I M O", "183F"), + ("Last temps I M O", "179F"), + ("Last temps I M O", "173F"), + ("Tread remaining", "98%"), + ("Tread remaining", "98%"), + ("Tread remaining", "99%"), + ]); + let right_front_tire = setup.get("Right Front Tire").unwrap(); + assert_eq!(right_front_tire, &expected); + + // Right Rear Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.0 psi"), + ("Last hot pressure", "22.1 psi"), + ("Last temps I M O", "199F"), + ("Last temps I M O", "195F"), + ("Last temps I M O", "182F"), + ("Tread remaining", "97%"), + ("Tread remaining", "97%"), + ("Tread remaining", "98%"), + ]); + let right_rear_tire = setup.get("Right Rear Tire").unwrap(); + assert_eq!(right_rear_tire, &expected); + + // Aero Settings + let expected = create_ordered_multimap(&[ + ("Downforce trim", "Low"), + ("Rear wing angle", "12 deg"), + ("# of dive planes", "1"), + ("Wing gurney setting", "Off"), + ("Deck gurney setting", "On"), + ]); + let aero_settings = setup.get("Aero Settings").unwrap(); + assert_eq!(aero_settings, &expected); + + // Aero Calculator + let expected = create_ordered_multimap(&[ + ("Front RH at speed", r#"1.575""#), + ("Rear RH at speed", r#"1.181""#), + ("Downforce balance", "40.48%"), + ("L/D", "4.981"), + ]); + let aero_calculator = setup.get("Aero Calculator").unwrap(); + assert_eq!(aero_calculator, &expected); + + // Front + let expected = create_ordered_multimap(&[ + ("Third spring", "571 lbs/in"), + ("Third perch offset", r#"1.791""#), + ("Third spring defl", "0.292 in"), + ("Third spring defl", "of"), + ("Third spring defl", "3.090 in"), + ("Third slider defl", "1.988 in"), + ("Third slider defl", "of"), + ("Third slider defl", "3.937 in"), + ("ARB size", "Medium"), + ("ARB blades", "P2"), + ("Toe-in", r#"-1/32""#), + ("Third pin length", r#"7.913""#), + ("Front pushrod length", r#"7.520""#), + ("Power steering assist", "3"), + ("Steering ratio", "11.0"), + ("Display page", "Race1"), + ]); + let front = setup.get("Front").unwrap(); + assert_eq!(front, &expected); + + // Left Front + let expected = create_ordered_multimap(&[ + ("Corner weight", "527 lbs"), + ("Ride height", "1.772 in"), + ("Shock defl", "1.070 in"), + ("Shock defl", "of"), + ("Shock defl", "1.969 in"), + ("Torsion bar defl", "0.377 in"), + ("Torsion bar turns", "5.000 Turns"), + ("Torsion bar O.D.", "13.90 mm"), + ("LS comp damping", "2 clicks"), + ("HS comp damping", "5 clicks"), + ("HS comp damp slope", "4 clicks"), + ("LS rbd damping", "4 clicks"), + ("HS rbd damping", "6 clicks"), + ("Camber", "-2.8 deg"), + ]); + let left_front = setup.get("Left Front").unwrap(); + assert_eq!(left_front, &expected); + + // Left Rear + let expected = create_ordered_multimap(&[ + ("Corner weight", "652 lbs"), + ("Ride height", "1.771 in"), + ("Shock defl", "1.598 in"), + ("Shock defl", "of"), + ("Shock defl", "2.953 in"), + ("Spring defl", "0.547 in"), + ("Spring defl", "of"), + ("Spring defl", "3.525 in"), + ("Spring perch offset", r#"2.146""#), + ("Spring rate", "600 lbs/in"), + ("LS comp damping", "2 clicks"), + ("HS comp damping", "5 clicks"), + ("HS comp damp slope", "4 clicks"), + ("LS rbd damping", "4 clicks"), + ("HS rbd damping", "6 clicks"), + ("Camber", "-1.8 deg"), + ("Toe-in", r#"+1/32""#), + ]); + let left_rear = setup.get("Left Rear").unwrap(); + assert_eq!(left_rear, &expected); + + // Right Front + let expected = create_ordered_multimap(&[ + ("Corner weight", "527 lbs"), + ("Ride height", "1.772 in"), + ("Shock defl", "1.070 in"), + ("Shock defl", "of"), + ("Shock defl", "1.969 in"), + ("Torsion bar defl", "0.377 in"), + ("Torsion bar turns", "5.000 Turns"), + ("Torsion bar O.D.", "13.90 mm"), + ("LS comp damping", "2 clicks"), + ("HS comp damping", "5 clicks"), + ("HS comp damp slope", "4 clicks"), + ("LS rbd damping", "4 clicks"), + ("HS rbd damping", "6 clicks"), + ("Camber", "-2.8 deg"), + ]); + let right_front = setup.get("Right Front").unwrap(); + assert_eq!(right_front, &expected); + + // Right Rear + let expected = create_ordered_multimap(&[ + ("Corner weight", "652 lbs"), + ("Ride height", "1.771 in"), + ("Shock defl", "1.598 in"), + ("Shock defl", "of"), + ("Shock defl", "2.953 in"), + ("Spring defl", "0.547 in"), + ("Spring defl", "of"), + ("Spring defl", "3.525 in"), + ("Spring perch offset", r#"2.146""#), + ("Spring rate", "600 lbs/in"), + ("LS comp damping", "2 clicks"), + ("HS comp damping", "5 clicks"), + ("HS comp damp slope", "4 clicks"), + ("LS rbd damping", "4 clicks"), + ("HS rbd damping", "6 clicks"), + ("Camber", "-1.8 deg"), + ("Toe-in", r#"+1/32""#), + ]); + let right_rear = setup.get("Right Rear").unwrap(); + assert_eq!(right_rear, &expected); + + // Rear + let expected = create_ordered_multimap(&[ + ("Third spring", "457 lbs/in"), + ("Third perch offset", r#"1.516""#), + ("Third spring defl", "0.538 in"), + ("Third spring defl", "of"), + ("Third spring defl", "3.753 in"), + ("Third slider defl", "2.928 in"), + ("Third slider defl", "of"), + ("Third slider defl", "5.906 in"), + ("ARB size", "Medium"), + ("ARB blades", "P4"), + ("Rear pushrod length", r#"6.614""#), + ("Third pin length", r#"7.126""#), + ("Cross weight", "50.0%"), + ]); + let rear = setup.get("Rear").unwrap(); + assert_eq!(rear, &expected); + + // Lighting + let expected = create_ordered_multimap(&[("Roof ID light color", "Blue")]); + let lighting = setup.get("Lighting").unwrap(); + assert_eq!(lighting, &expected); + + // Brake Spec + let expected = + create_ordered_multimap(&[("Pad compound", "Medium"), ("Brake pressure bias", "49.2%")]); + let brake_spec = setup.get("Brake Spec").unwrap(); + assert_eq!(brake_spec, &expected); + + // Fuel + let expected = create_ordered_multimap(&[("Fuel level", "19.8 gal")]); + let fuel = setup.get("Fuel").unwrap(); + assert_eq!(fuel, &expected); + + // Traction Control + let expected = create_ordered_multimap(&[ + ("Traction control gain", "5 (TC)"), + ("Traction control slip", "5 (TC)"), + ("Throttle shape", "1"), + ]); + let traction_control = setup.get("Traction Control").unwrap(); + assert_eq!(traction_control, &expected); + + // Gear Ratios + let expected = create_ordered_multimap(&[ + ("Gear stack", "Tall"), + ("Speed in first", "86.7 mph"), + ("Speed in second", "112.1 mph"), + ("Speed in third", "131.6 mph"), + ("Speed in forth", "156.3 mph"), + ("Speed in fifth", "182.7 mph"), + ("Speed in sixth", "210.2 mph"), + ]); + let gear_ratios = setup.get("Gear Ratios").unwrap(); + assert_eq!(gear_ratios, &expected); + + // Rear Diff Spec + let expected = create_ordered_multimap(&[ + ("Drive/coast ramp angles", "45/55"), + ("Clutch friction faces", "4"), + ("Preload", "55 ft-lbs"), + ]); + let rear_diff_spec = setup.get("Rear Diff Spec").unwrap(); + assert_eq!(rear_diff_spec, &expected); +} + +#[test] +fn test_setup_porche_911_gt3_r() { + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let (track_name, car_name, setup) = + setup_from_html("./fixtures/baseline.htm", &config).unwrap(); + + assert_eq!(track_name, "Nürburgring Combined".to_string()); + assert_eq!(car_name, "Porsche 911 GT3 R".to_string()); + assert_eq!(setup.keys().len(), 12); + + // Left Front Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.5 psi"), + ("Last hot pressure", "20.5 psi"), + ("Last temps O M I", "112F"), + ("Last temps O M I", "112F"), + ("Last temps O M I", "112F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ]); + let left_front_tire = setup.get("Left Front Tire").unwrap(); + assert_eq!(left_front_tire, &expected); + + // Left Rear Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.5 psi"), + ("Last hot pressure", "20.5 psi"), + ("Last temps O M I", "112F"), + ("Last temps O M I", "112F"), + ("Last temps O M I", "112F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ]); + let left_rear_tire = setup.get("Left Rear Tire").unwrap(); + assert_eq!(left_rear_tire, &expected); + + // Right Front Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.5 psi"), + ("Last hot pressure", "20.5 psi"), + ("Last temps I M O", "112F"), + ("Last temps I M O", "112F"), + ("Last temps I M O", "112F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ]); + let right_front_tire = setup.get("Right Front Tire").unwrap(); + assert_eq!(right_front_tire, &expected); + + // Right Rear Tire + let expected = create_ordered_multimap(&[ + ("Starting pressure", "20.5 psi"), + ("Last hot pressure", "20.5 psi"), + ("Last temps I M O", "112F"), + ("Last temps I M O", "112F"), + ("Last temps I M O", "112F"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ("Tread remaining", "100%"), + ]); + let right_rear_tire = setup.get("Right Rear Tire").unwrap(); + assert_eq!(right_rear_tire, &expected); + + // Aero Balance Calc + let expected = create_ordered_multimap(&[ + ("Front RH at speed", r#"1.929""#), + ("Rear RH at speed", r#"2.835""#), + ("Wing setting", "7 degrees"), + ("Front downforce", "39.83%"), + ]); + let aero_balance_calc = setup.get("Aero Balance Calc").unwrap(); + assert_eq!(aero_balance_calc, &expected); + + // Front + let expected = create_ordered_multimap(&[ + ("ARB diameter", "45 mm"), + ("ARB setting", "Soft"), + ("Toe-in", r#"-2/32""#), + ("Front master cyl.", "0.811 in"), + ("Rear master cyl.", "0.811 in"), + ("Brake pads", "Medium friction"), + ("Fuel level", "15.9 gal"), + ("Cross weight", "50.0%"), + ]); + let front = setup.get("Front").unwrap(); + assert_eq!(front, &expected); + + // Left Front + let expected = create_ordered_multimap(&[ + ("Corner weight", "605 lbs"), + ("Ride height", "2.034 in"), + ("Spring perch offset", r#"2.441""#), + ("Spring rate", "1371 lbs/in"), + ("LS Comp damping", "-6 clicks"), + ("HS Comp damping", "-10 clicks"), + ("LS Rbd damping", "-8 clicks"), + ("HS Rbd damping", "-10 clicks"), + ("Camber", "-4.0 deg"), + ("Caster", "+7.6 deg"), + ]); + let left_front = setup.get("Left Front").unwrap(); + assert_eq!(left_front, &expected); + + // Left Rear + let expected = create_ordered_multimap(&[ + ("Corner weight", "945 lbs"), + ("Ride height", "3.026 in"), + ("Spring perch offset", r#"2.717""#), + ("Spring rate", "1600 lbs/in"), + ("LS Comp damping", "-6 clicks"), + ("HS Comp damping", "-10 clicks"), + ("LS Rbd damping", "-8 clicks"), + ("HS Rbd damping", "-10 clicks"), + ("Camber", "-3.4 deg"), + ("Toe-in", r#"+1/64""#), + ]); + let left_rear = setup.get("Left Rear").unwrap(); + assert_eq!(left_rear, &expected); + + // In-Car Dials + let expected = create_ordered_multimap(&[ + ("Display page", "Race 1"), + ("Brake pressure bias", "54.0%"), + ("Trac Ctrl (TCC) setting", "5 (TCC)"), + ("Trac Ctrl (TCR) setting", "5 (TCR)"), + ("Throttle Map setting", "4"), + ("ABS setting", "11 (ABS)"), + ("Engine map setting", "4 (MAP)"), + ("Night LED strips", "Blue"), + ]); + let in_car_dials = setup.get("In-Car Dials").unwrap(); + assert_eq!(in_car_dials, &expected); + + // Right Front + let expected = create_ordered_multimap(&[ + ("Corner weight", "605 lbs"), + ("Ride height", "2.034 in"), + ("Spring perch offset", r#"2.441""#), + ("Spring rate", "1371 lbs/in"), + ("LS Comp damping", "-6 clicks"), + ("HS Comp damping", "-10 clicks"), + ("LS Rbd damping", "-8 clicks"), + ("HS Rbd damping", "-10 clicks"), + ("Camber", "-4.0 deg"), + ("Caster", "+7.6 deg"), + ]); + let right_front = setup.get("Right Front").unwrap(); + assert_eq!(right_front, &expected); + + // Right Rear + let expected = create_ordered_multimap(&[ + ("Corner weight", "945 lbs"), + ("Ride height", "3.026 in"), + ("Spring perch offset", r#"2.717""#), + ("Spring rate", "1600 lbs/in"), + ("LS Comp damping", "-6 clicks"), + ("HS Comp damping", "-10 clicks"), + ("LS Rbd damping", "-8 clicks"), + ("HS Rbd damping", "-10 clicks"), + ("Camber", "-3.4 deg"), + ("Toe-in", r#"+1/64""#), + ]); + let right_rear = setup.get("Right Rear").unwrap(); + assert_eq!(right_rear, &expected); + + // Rear + let expected = create_ordered_multimap(&[ + ("ARB diameter", "35 mm"), + ("ARB setting", "Med"), + ("Diff preload", "74 ft-lbs"), + ("Wing setting", "7 degrees"), + ]); + let rear = setup.get("Rear").unwrap(); + assert_eq!(rear, &expected); +} + +#[test] +fn test_add_setup() { + use UpdateKind::*; + + fn assert_added(setups: &Setups, file_name: &str) { + let tracks = setups.tracks(); + assert_eq!(tracks.len(), 1); + assert_eq!(tracks["Nürburgring Combined"].len(), 1); + assert_eq!(tracks["Nürburgring Combined"]["Porsche 911 GT3 R"].len(), 1); + + let SetupInfo { setup, name, .. } = &tracks["Nürburgring Combined"]["Porsche 911 GT3 R"][0]; + + assert_eq!(name, file_name); + assert_eq!(setup.keys().len(), 12); + } + + let mut setups = Setups::default(); + assert!(setups.tracks.is_empty()); + + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let mut result = Vec::new(); + let path = Path::new("./fixtures/baseline.htm") + .canonicalize() + .expect("Cannot canonicalize path"); + + // Test adding a setup to an empty tree + setups.add(&mut result, &path, None, &config); + + assert_eq!( + &result, + &[AddedSetup( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string(), + 0 + )] + ); + assert_added(&setups, "baseline"); + + // Test adding an existing setup to the tree + result.clear(); + setups.add(&mut result, &path, None, &config); + + assert_eq!(&result, &[]); + assert_added(&setups, "baseline"); +} + +#[test] +fn test_remove_setup() { + use UpdateKind::*; + + fn assert_removed(setups: &Setups) { + let tracks = setups.tracks(); + assert_eq!(tracks.len(), 3); + assert!(tracks.contains_key("Centripetal Circuit")); + assert!(tracks.contains_key("Charlotte Motor Speedway - Legends Oval")); + assert!(tracks.contains_key("Circuit des 24 Heures du Mans - 24 Heures du Mans")); + } + + let mut config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + config.update_setups_path("./fixtures"); + let mut warnings = VecDeque::new(); + let mut setups = Setups::new(&mut warnings, &config); + + let tracks = setups.tracks(); + assert_eq!(tracks.len(), 4); + assert!(tracks.contains_key("Centripetal Circuit")); + assert!(tracks.contains_key("Charlotte Motor Speedway - Legends Oval")); + assert!(tracks.contains_key("Circuit des 24 Heures du Mans - 24 Heures du Mans")); + assert!(tracks.contains_key("Nürburgring Combined")); + + let mut result = Vec::new(); + let path = Path::new("./fixtures/baseline.htm") + .canonicalize() + .expect("Cannot canonicalize path"); + + // Test removing a setup from the tree + setups.remove(&mut result, &path); + + assert_eq!( + &result, + &[ + RemovedSetup( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string(), + 0 + ), + RemovedCar( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string() + ), + RemovedTrack("Nürburgring Combined".to_string()), + ] + ); + assert_removed(&setups); + + // Test removing a non-existent setup from the tree + result.clear(); + setups.remove(&mut result, &path); + + assert_eq!(&result, &[]); + assert_removed(&setups); +} + +#[test] +fn test_update_setup() { + use UpdateKind::*; + + let mut setups = Setups::default(); + assert!(setups.tracks.is_empty()); + + let config = Config::new("/tmp/some/path.toml", PhysicalSize::new(0, 0)); + let path1 = Path::new("./fixtures/baseline.htm") + .canonicalize() + .expect("Cannot canonicalize path"); + let path2 = Path::new("./fixtures/skip_barber_centripetal.htm") + .canonicalize() + .expect("Cannot canonicalize path"); + let path3 = tempfile::Builder::new() + .suffix(".html") + .tempfile() + .expect("Unable to create temp file") + .path() + .canonicalize() + .expect("Cannot canonicalize path"); + std::fs::copy(&path2, &path3).expect("Unable to copy file"); + let path4 = tempfile::Builder::new() + .suffix(".non-html-file") + .tempfile() + .expect("Unable to create temp file") + .path() + .canonicalize() + .expect("Cannot canonicalize path"); + + // Test adding a setup to an empty tree with Write + let event = hotwatch::Event::Create(path1.clone()); + let result = setups.update(&event, &config); + + assert_eq!( + &result, + &[AddedSetup( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string(), + 0 + )] + ); + assert_eq!(setups.tracks.len(), 1); + + // Test adding an existing setup to the tree + let event = hotwatch::Event::Write(path1.clone()); + let result = setups.update(&event, &config); + + assert_eq!(&result, &[]); + assert_eq!(setups.tracks.len(), 1); + + // Test adding a setup to the tree with Create + let event = hotwatch::Event::Create(path2.clone()); + let result = setups.update(&event, &config); + + assert_eq!( + &result, + &[AddedSetup( + "Centripetal Circuit".to_string(), + "Skip Barber Formula 2000".to_string(), + 0 + )] + ); + assert_eq!(setups.tracks.len(), 2); + + // Test removing a setup from the tree + let event = hotwatch::Event::Remove(path1); + let result = setups.update(&event, &config); + + assert_eq!( + &result, + &[ + RemovedSetup( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string(), + 0 + ), + RemovedCar( + "Nürburgring Combined".to_string(), + "Porsche 911 GT3 R".to_string() + ), + RemovedTrack("Nürburgring Combined".to_string()), + ] + ); + assert_eq!(setups.tracks.len(), 1); + + // Test renaming a setup in the tree + let name = &setups.tracks["Centripetal Circuit"]["Skip Barber Formula 2000"][0].name; + assert_eq!(name, "skip_barber_centripetal"); + + let event = hotwatch::Event::Rename(path2, path3.clone()); + let result = setups.update(&event, &config); + + assert_eq!(&result, &[]); + assert_eq!(setups.tracks.len(), 1); + + let name = &setups.tracks["Centripetal Circuit"]["Skip Barber Formula 2000"][0].name; + let expected_name = path3 + .as_path() + .file_stem() + .expect("Unable to get file stem") + .to_str() + .expect("Unable to convert &OsStr to &str"); + assert_eq!(name, expected_name); + + // Test renaming a setup in the tree to a non-html (unparseable) file + let event = hotwatch::Event::Rename(path3, path4); + let result = setups.update(&event, &config); + + assert_eq!( + &result, + &[ + RemovedSetup( + "Centripetal Circuit".to_string(), + "Skip Barber Formula 2000".to_string(), + 0 + ), + RemovedCar( + "Centripetal Circuit".to_string(), + "Skip Barber Formula 2000".to_string() + ), + RemovedTrack("Centripetal Circuit".to_string()), + ] + ); + assert!(setups.tracks.is_empty()); +} diff --git a/src/str_ext.rs b/src/str_ext.rs index 306ad6d..a44d935 100644 --- a/src/str_ext.rs +++ b/src/str_ext.rs @@ -1,6 +1,7 @@ //! String extension traits. use std::borrow::Cow; +use std::cmp::Ordering; use unicode_segmentation::UnicodeSegmentation; /// An extension trait for strings that adds a truncation method with ellipses. @@ -56,6 +57,38 @@ impl<'a> Capitalize<'a> for &'a str { } } +/// An extension trait for strings that adds "human sort" comparison methods. +pub(crate) trait HumanCompare { + fn human_compare(&self, other: &str) -> Ordering; +} + +impl HumanCompare for String { + fn human_compare(&self, other: &str) -> Ordering { + human_compare(self, other) + } +} + +impl HumanCompare for &String { + fn human_compare(&self, other: &str) -> Ordering { + human_compare(self, other) + } +} + +impl HumanCompare for &str { + fn human_compare(&self, other: &str) -> Ordering { + human_compare(self, other) + } +} + +fn human_compare(a: &str, b: &str) -> Ordering { + if a.starts_with('-') && b.starts_with('-') { + // Reverse parameter order when comparing negative numbers + human_sort::compare(b, a) + } else { + human_sort::compare(a, b) + } +} + #[cfg(test)] mod tests { use super::*; @@ -96,4 +129,35 @@ mod tests { Cow::from("You Know, I Find That I (Always) Shout A Lot! Sorry!"), ); } + + #[test] + fn test_human_compare_text() { + assert_eq!("a".human_compare("b"), Ordering::Less); + assert_eq!("ab".human_compare("abc"), Ordering::Less); + assert_eq!("abc".human_compare("abc"), Ordering::Equal); + } + + #[test] + fn test_human_compare_numbers() { + assert_eq!("1".human_compare("1"), Ordering::Equal); + assert_eq!("10".human_compare("10"), Ordering::Equal); + assert_eq!("1".human_compare("10"), Ordering::Less); + assert_eq!("10".human_compare("1"), Ordering::Greater); + + assert_eq!("1".human_compare("2"), Ordering::Less); + assert_eq!("10".human_compare("2"), Ordering::Greater); + assert_eq!("1".human_compare("-2"), Ordering::Greater); + assert_eq!("10".human_compare("-2"), Ordering::Greater); + assert_eq!("-1".human_compare("2"), Ordering::Less); + assert_eq!("-10".human_compare("2"), Ordering::Less); + assert_eq!("-1".human_compare("-2"), Ordering::Greater); + assert_eq!("-10".human_compare("-2"), Ordering::Less); + } + + #[test] + #[ignore = "Fractions are not yet supported"] + fn test_human_compare_fractions() { + assert_eq!("3/8".human_compare("1/2"), Ordering::Less); + assert_eq!("5/8".human_compare("1/2"), Ordering::Greater); + } }