From 2ee5d766bf7363c1d88744a6926fc9d6f78fac72 Mon Sep 17 00:00:00 2001 From: Leonardo Forchini Date: Mon, 5 Oct 2026 08:50:10 +0100 Subject: [PATCH] Add proptest dependency Proptest allows for checking general properties instead of just single instances. This commit modifies some tests to use proptest. Previously, the thread_id <-> vq mapping was tested by creating some thread ids, having a fixed number of VQs, then checking those specific inputs were reasonably assigned. Now, we generate 0..8 ids and 0..128 vqs and ensure any combination assigns VQs reasonably. Similarly, proptest is used to ensure that `next_qemu_iothread_id` really does pick the lowest thread_id, no matter what has already been assigned. Signed-off-by: Leonardo Forchini --- Cargo.lock | 184 ++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/backends/qemu/helpers.rs | 114 +++++++++++++++++----- 3 files changed, 269 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3281c619..8f391e29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,21 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -455,6 +470,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -528,6 +549,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -536,7 +569,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -660,6 +693,7 @@ dependencies = [ "json-patch", "linkme", "procfs", + "proptest", "ratatui", "regex", "rstest", @@ -1087,6 +1121,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1126,6 +1169,31 @@ dependencies = [ "hex", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -1135,6 +1203,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1149,17 +1223,27 @@ checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ "autocfg 0.1.8", "libc", - "rand_chacha", + "rand_chacha 0.1.1", "rand_core 0.4.3", "rand_hc", "rand_isaac", "rand_jitter", "rand_os", "rand_pcg", - "rand_xorshift", + "rand_xorshift 0.1.1", "winapi", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -1170,6 +1254,16 @@ dependencies = [ "rand_core 0.3.2", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.3.2" @@ -1185,6 +1279,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e5937858e6fd18cd595d558f90bb5de3b72ae23f9e3763af0e805949b04ef60" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -1247,6 +1350,15 @@ dependencies = [ "rand_core 0.3.2", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "ratatui" version = "0.30.2" @@ -1423,6 +1535,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -1570,7 +1694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49d57902bb128e5e38b5218d3681215ae3e322d99f65d5420e9849730d2ea372" dependencies = [ "num", - "rand", + "rand 0.6.5", ] [[package]] @@ -1629,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -1879,6 +2003,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.26" @@ -1952,12 +2082,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -2058,6 +2206,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zbus" version = "5.12.0" @@ -2114,6 +2268,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 945f6d45..2c82a4d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,3 +101,4 @@ rstest = "0.26" tempfile = "3" test-log = { version = "0.2", features = ["trace"] } tokio = { version = "1", features = ["test-util"] } +proptest = "1" diff --git a/src/backends/qemu/helpers.rs b/src/backends/qemu/helpers.rs index eda816ae..1b120f63 100644 --- a/src/backends/qemu/helpers.rs +++ b/src/backends/qemu/helpers.rs @@ -320,44 +320,108 @@ pub fn next_qemu_iothread_id(existing: &[String]) -> String { #[cfg(test)] mod tests { + use proptest::prelude::*; + use super::*; - /// Test that vQ round-robin mapping spreads queues across IOThreads - /// as evenly as possible. + proptest! { + /// Test that vQ round-robin mapping spreads queues across IOThreads + /// as evenly as possible. + #[test] + fn test_qemu_round_robin_partitions_queues( + ids in prop::collection::vec("[a-z]{1,4}", 0..8), + vq_count in 0u16..128, + ) { + // We run a mapping on arbitrary IOThread ids and number of queues. + let mapping = round_robin_vq_mapping(&ids, vq_count); + if ids.is_empty() || vq_count == 0 { + prop_assert!(mapping.is_empty()); + return Ok(()); + } + + // Check every queue has been assigned once. + prop_assert_eq!(mapping.len(), ids.len()); + let mut seen = vec![false; usize::from(vq_count)]; + for (idx, entry) in mapping.iter().enumerate() { + prop_assert_eq!(&entry.iothread, &ids[idx]); + for &vq in &entry.vqs { + prop_assert_eq!(usize::from(vq) % ids.len(), idx); + prop_assert!(!seen[usize::from(vq)]); + seen[usize::from(vq)] = true; + } + } + prop_assert!(seen.into_iter().all(|present| present)); + + // Check queues are evenly distributed. + let sizes: Vec = mapping.iter().map(|entry| entry.vqs.len()).collect(); + let min = sizes.iter().copied().min().unwrap(); + let max = sizes.iter().copied().max().unwrap(); + prop_assert!(max - min <= 1); + } + + /// Ensure the next IOThread to have a queue assigned is the one with + /// the lowest id. + #[test] + fn test_qemu_next_id_is_the_lowest_missing_iot( + // IOThreads with assigned queues, these are excluded from the search. + present in prop::collection::btree_set(0u32..1024, 0..32), + ) { + let ids: Vec = present.iter().map(|n| format!("iot{n}")).collect(); + // Find the thread with the lowest id. + let expected = (0..1024).find(|n| !present.contains(n)).unwrap(); + prop_assert_eq!(next_qemu_iothread_id(&ids), format!("iot{expected}")); + } + } + + /// Test that managed `iotN` threads are kept in id order and other ids are + /// dropped. #[test] - fn round_robin_distributes_evenly() { - let ids = vec!["iot0".to_string(), "iot1".to_string(), "iot2".to_string()]; - let mapping = round_robin_vq_mapping(&ids, 7); - let counts: Vec = mapping.iter().map(|m| m.vqs.len()).collect(); - assert_eq!(counts, vec![3, 2, 2]); - assert_eq!(mapping[0].vqs, vec![0, 3, 6]); - assert_eq!(mapping[1].vqs, vec![1, 4]); - assert_eq!(mapping[2].vqs, vec![2, 5]); + fn test_qemu_topology_keeps_managed_threads_in_id_order() { + let body = r#" +qemu_iothread_info{id="iot1",tid="124"} 1 +qemu_iothread_info{id="dirtybitmap",tid="999"} 1 +qemu_iothread_info{id="iot0",tid="123"} 1 +"#; + let topo = QemuTopology::new(body); + assert_eq!(topo.iothreads, vec!["iot0", "iot1"]); + assert_eq!(topo.iothread_tids.get("iot0"), Some(&123)); + assert_eq!(topo.iothread_tids.get("iot1"), Some(&124)); + assert_eq!(topo.iothread_tids.len(), 2); } - /// Test that `next_qemu_iothread_id` fills the lowest missing - /// `iotN` id. + /// Test that `thread_id` and `path` are accepted in place of `tid` and + /// `device`. #[test] - fn next_id_fills_gaps_in_order() { - let ids = vec!["iot0".to_string(), "iot2".to_string()]; - assert_eq!(next_qemu_iothread_id(&ids), "iot1"); - let ids = vec!["iot0".to_string(), "iot1".to_string()]; - assert_eq!(next_qemu_iothread_id(&ids), "iot2"); + fn test_qemu_topology_accepts_label_aliases() { + let body = r#" +qemu_iothread_info{id="iot0",thread_id="321"} 1 +qemu_virtio_scsi_num_queues{path="/machine/peripheral/scsi0"} 4 +"#; + let topo = QemuTopology::new(body); + assert_eq!(topo.iothreads, vec!["iot0"]); + assert_eq!(topo.iothread_tids.get("iot0"), Some(&321)); + assert_eq!(topo.device_path, "/machine/peripheral/scsi0"); + assert_eq!(topo.vq_count, 4); } - /// Test that prometheus scrape text yields IOThread ids, TIDs, and - /// the virtio-scsi device path. + /// Test that the first device wins, noise lines are ignored, and a later + /// thread is kept. + // TODO make sure this is intended #[test] - fn topology_parses_prometheus_body() { - let body = r#"# HELP foo -qemu_iothread_info{id="iot0",tid="123"} 1 -qemu_iothread_info{id="iot1",tid="124"} 1 -qemu_iothread_info{id="dirtybitmap",tid="999"} 1 + fn test_qemu_topology_keeps_the_first_device_and_later_threads() { + let body = r#" +# HELP foo +not a metric +qemu_virtio_scsi_num_queues{device=""} 3 +qemu_iothread_info{id="iot7",tid="nope"} 1 qemu_virtio_scsi_num_queues{device="/machine/peripheral/scsi0"} 4 +qemu_iothread_info{id="iot0",tid="123"} 1 +qemu_virtio_scsi_num_queues{device="/machine/peripheral/scsi1"} 99 "#; let topo = QemuTopology::new(body); - assert_eq!(topo.iothreads, vec!["iot0", "iot1"]); + assert_eq!(topo.iothreads, vec!["iot0"]); assert_eq!(topo.iothread_tids.get("iot0"), Some(&123)); + assert_eq!(topo.iothread_tids.len(), 1); assert_eq!(topo.device_path, "/machine/peripheral/scsi0"); assert_eq!(topo.vq_count, 4); assert!(topo.vq_inflight.is_empty());