From 3af95235e6177e741e76deeb9f043edfce473044 Mon Sep 17 00:00:00 2001 From: Micheal Smith Date: Mon, 10 Nov 2025 05:26:59 -0600 Subject: [PATCH] Added some tests at least for the broadcast buffering. --- Cargo.lock | 126 +++++++++++++++++++++++++++- Cargo.toml | 36 ++++++-- src/event_manager.rs | 190 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 341 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4280434..a074606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -172,6 +181,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cargo-husky" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" + [[package]] name = "cc" version = "1.2.43" @@ -858,6 +873,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1666,6 +1687,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1823,6 +1853,41 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.24" @@ -1899,6 +1964,7 @@ name = "robotnik" version = "0.1.0" dependencies = [ "better-panic", + "cargo-husky", "clap", "color-eyre", "config", @@ -1907,6 +1973,7 @@ dependencies = [ "genai", "human-panic", "irc", + "rstest", "serde", "serde_json", "tokio", @@ -1926,6 +1993,36 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rstest" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -1948,6 +2045,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2070,6 +2176,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2489,7 +2601,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -2537,6 +2649,18 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + [[package]] name = "toml_parser" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index ed0f02d..ff168eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,26 +4,46 @@ version = "0.1.0" edition = "2024" [dependencies] -# TODO: make this a dev and/or debug dependency later. better-panic = "0.3.0" -clap = { version = "4.5", features = [ "derive" ] } color-eyre = "0.6.3" -config = { version = "0.15", features = [ "toml" ] } directories = "6.0" futures = "0.3" human-panic = "2.0" genai = "0.4.3" irc = "1.1" -serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1", features = [ "io-util", "macros", "net", "rt-multi-thread", "sync" ] } tracing = "0.1" tracing-subscriber = "0.3" + [dependencies.clap] + version = "4.5" + features = [ "derive" ] + + [dependencies.config] + version = "0.15" + features = [ "toml" ] + + [dependencies.serde] + version = "1.0" + features = [ "derive" ] + + [dependencies.tokio] + version = "1" + features = [ "io-util", "macros", "net", "rt-multi-thread", "sync" ] + +[dev-dependencies] +rstest = "0.24" + + [dev-dependencies.cargo-husky] + version = "1" + features = [ + "run-cargo-check", + "run-cargo-clippy", +] + [profile.release] strip = true -opt-level = "z" # Optimize for size -lto = true # Link-time optimization +opt-level = "z" +lto = true codegen-units = 1 -# Comment this if unwinding is needed. Compiling without release works too. panic = "abort" diff --git a/src/event_manager.rs b/src/event_manager.rs index 8838eae..9b7fcf8 100644 --- a/src/event_manager.rs +++ b/src/event_manager.rs @@ -6,7 +6,7 @@ use tokio::{ net::{UnixListener, UnixStream}, sync::{RwLock, broadcast}, }; -use tracing::error; +use tracing::{error, info}; use crate::event::Event; @@ -51,7 +51,8 @@ impl EventManager { loop { match listener.accept().await { - Ok((stream, _)) => { + Ok((stream, _addr)) => { + info!("New broadcast subscriber"); // Spawn a new stream for the plugin. The loop // runs recursively from there. let broadcaster = Arc::clone(&self); @@ -87,3 +88,188 @@ impl EventManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[tokio::test] + async fn test_new_event_manager_has_empty_buffer() { + let manager = EventManager::new().unwrap(); + let events = manager.events.read().await; + assert_eq!(events.len(), 0); + } + + #[tokio::test] + async fn test_broadcast_adds_event_to_buffer() { + let manager = EventManager::new().unwrap(); + let event = Event::new("test message"); + + manager.broadcast(&event).await.unwrap(); + + let events = manager.events.read().await; + assert_eq!(events.len(), 1); + assert!(events[0].contains("test message")); + assert!(events[0].ends_with('\n')); + } + + #[tokio::test] + async fn test_broadcast_serializes_event_as_json() { + let manager = EventManager::new().unwrap(); + let event = Event::new("hello world"); + + manager.broadcast(&event).await.unwrap(); + + let events = manager.events.read().await; + let stored = &events[0]; + + // Should be valid JSON + let parsed: serde_json::Value = serde_json::from_str(stored.trim()).unwrap(); + assert_eq!(parsed["message"], "hello world"); + } + + #[rstest] + #[case(1)] + #[case(10)] + #[case(100)] + #[case(999)] + #[tokio::test] + async fn test_buffer_holds_events_below_max(#[case] count: usize) { + let manager = EventManager::new().unwrap(); + + for i in 0..count { + let event = Event::new(format!("event {}", i)); + manager.broadcast(&event).await.unwrap(); + } + + let events = manager.events.read().await; + assert_eq!(events.len(), count); + } + + #[tokio::test] + async fn test_buffer_at_exactly_max_capacity() { + let manager = EventManager::new().unwrap(); + + // Fill to exactly EVENT_BUF_MAX (1000) + for i in 0..EVENT_BUF_MAX { + let event = Event::new(format!("event {}", i)); + manager.broadcast(&event).await.unwrap(); + } + + let events = manager.events.read().await; + assert_eq!(events.len(), EVENT_BUF_MAX); + assert!(events[0].contains("event 0")); + assert!(events[EVENT_BUF_MAX - 1].contains("event 999")); + } + + #[rstest] + #[case(1)] + #[case(10)] + #[case(100)] + #[case(500)] + #[tokio::test] + async fn test_buffer_overflow_evicts_oldest_fifo(#[case] overflow: usize) { + let manager = EventManager::new().unwrap(); + let total = EVENT_BUF_MAX + overflow; + + // Broadcast more events than buffer can hold + for i in 0..total { + let event = Event::new(format!("event {}", i)); + manager.broadcast(&event).await.unwrap(); + } + + let events = manager.events.read().await; + + // Buffer should still be at max capacity + assert_eq!(events.len(), EVENT_BUF_MAX); + + // Oldest events (0 through overflow-1) should be evicted + // Buffer should contain events [overflow..total) + let first_event = &events[0]; + let last_event = &events[EVENT_BUF_MAX - 1]; + + assert!(first_event.contains(&format!("event {}", overflow))); + assert!(last_event.contains(&format!("event {}", total - 1))); + + // Verify the evicted events are NOT in the buffer + let buffer_string = events.iter().cloned().collect::(); + assert!(!buffer_string.contains(r#""message":"event 0""#)); + } + + #[tokio::test] + async fn test_multiple_broadcasts_maintain_order() { + let manager = EventManager::new().unwrap(); + let messages = vec!["first", "second", "third", "fourth", "fifth"]; + + for msg in &messages { + let event = Event::new(*msg); + manager.broadcast(&event).await.unwrap(); + } + + let events = manager.events.read().await; + assert_eq!(events.len(), messages.len()); + + for (i, expected) in messages.iter().enumerate() { + assert!(events[i].contains(expected)); + } + } + + #[tokio::test] + async fn test_buffer_wraparound_maintains_newest_events() { + let manager = EventManager::new().unwrap(); + + // Fill buffer completely + for i in 0..EVENT_BUF_MAX { + let event = Event::new(format!("old {}", i)); + manager.broadcast(&event).await.unwrap(); + } + + // Add 5 more events + for i in 0..5 { + let event = Event::new(format!("new {}", i)); + manager.broadcast(&event).await.unwrap(); + } + + let events = manager.events.read().await; + assert_eq!(events.len(), EVENT_BUF_MAX); + + // First 5 old events should be gone + let buffer_string = events.iter().cloned().collect::(); + assert!(!buffer_string.contains(r#""message":"old 0""#)); + assert!(!buffer_string.contains(r#""message":"old 4""#)); + + // But old 5 should still be there (now at the front) + assert!(events[0].contains("old 5")); + + // New events should be at the end + assert!(events[EVENT_BUF_MAX - 5].contains("new 0")); + assert!(events[EVENT_BUF_MAX - 1].contains("new 4")); + } + + #[tokio::test] + async fn test_concurrent_broadcasts_all_stored() { + let manager = Arc::new(EventManager::new().unwrap()); + let mut handles = vec![]; + + // Spawn 10 concurrent tasks, each broadcasting 10 events + for task_id in 0..10 { + let manager_clone = Arc::clone(&manager); + let handle = tokio::spawn(async move { + for i in 0..10 { + let event = Event::new(format!("task {} event {}", task_id, i)); + manager_clone.broadcast(&event).await.unwrap(); + } + }); + handles.push(handle); + } + + // Wait for all tasks to complete + for handle in handles { + handle.await.unwrap(); + } + + let events = manager.events.read().await; + assert_eq!(events.len(), 100); + } +}