Skip to content

Commit 0603a96

Browse files
committed
chore: add tests
1 parent f96c03b commit 0603a96

File tree

9 files changed

+468
-41
lines changed

9 files changed

+468
-41
lines changed

AGENTS.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44
- `cargo build` - Build the project
55
- `cargo build --release` - Build optimized release version
66
- `cargo test` - Run all tests
7+
- `cargo test <test_name>` - Run a single test (e.g., `cargo test test_validate_cli_with_all_flag`)
78
- `cargo fmt` - Format code
89
- `cargo clippy` - Run linting checks
910

1011
## Code Style
11-
- Use `snake_case` for functions, variables, and modules
12-
- Use `PascalCase` for structs and enums
13-
- Import external crates at top, then `use` statements grouped by visibility
14-
- Error handling: Use `anyhow::Result<T>` for public APIs, `?` operator for propagation
15-
- String formatting: Use `format!()` macro for dynamic strings, string literals for static
12+
- Naming: `snake_case` for functions/variables/modules, `PascalCase` for structs/enums, `SCREAMING_SNAKE_CASE` for constants
13+
- Imports: External crates first, then local `mod` and `use` statements, group by visibility (pub before private)
14+
- Error handling: `anyhow::Result<T>` for fallible functions, `anyhow::bail!()` for errors, `?` for propagation, `.context()` for error context
15+
- String formatting: `format!()` for dynamic, string literals for static, `String::with_capacity()` for buffers
16+
- Function signatures: Accept `&str` slices, return owned `String` only when necessary
17+
- Testing: Place unit tests in `#[cfg(test)] mod tests` at bottom of each file, use descriptive names like `test_<function>_<scenario>`
1618

1719
## Architecture
1820
- Modular structure: `src/` contains `main.rs`, `cli.rs`, `sketchybar.rs`, and `stats/` module
19-
- Stats module exports individual stat functions via `mod.rs`
20-
- CLI uses `clap` with `Parser` derive macro
21-
- Async main with `tokio` runtime for event loop
22-
23-
## Dependencies
24-
- Core: `anyhow`, `clap`, `sysinfo`, `tokio`
25-
- Build: `cc` for C bindings
26-
- Platform: macOS only (`#[cfg(target_os = "macos")]`)
21+
- Stats module: Individual stat files (`cpu.rs`, `disk.rs`, etc.) with public functions exported via `mod.rs`
22+
- CLI: `clap` with `Parser` derive, validation in separate `validate_cli()` function, constants for defaults/limits
23+
- Async runtime: `tokio::main` macro, `tokio::select!` for concurrent operations, `tokio::time::sleep` for intervals
24+
- Platform: macOS only - use `#[cfg(target_os = "macos")]` for platform-specific code

src/cli.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,175 @@ pub fn all_system_flags() -> Vec<&'static str> {
138138
pub fn all_uptime_flags() -> Vec<&'static str> {
139139
vec!["week", "day", "hour", "min", "sec"]
140140
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use super::*;
145+
146+
#[test]
147+
fn test_validate_cli_with_all_flag() {
148+
let cli = Cli {
149+
all: true,
150+
cpu: None,
151+
disk: None,
152+
memory: None,
153+
network: None,
154+
system: None,
155+
uptime: None,
156+
interval: DEFAULT_INTERVAL,
157+
network_refresh_rate: DEFAULT_NETWORK_REFRESH_RATE,
158+
bar: None,
159+
verbose: false,
160+
no_units: false,
161+
};
162+
assert!(validate_cli(&cli).is_ok());
163+
}
164+
165+
#[test]
166+
fn test_validate_cli_with_cpu_flag() {
167+
let cli = Cli {
168+
all: false,
169+
cpu: Some(vec!["usage".to_string()]),
170+
disk: None,
171+
memory: None,
172+
network: None,
173+
system: None,
174+
uptime: None,
175+
interval: DEFAULT_INTERVAL,
176+
network_refresh_rate: DEFAULT_NETWORK_REFRESH_RATE,
177+
bar: None,
178+
verbose: false,
179+
no_units: false,
180+
};
181+
assert!(validate_cli(&cli).is_ok());
182+
}
183+
184+
#[test]
185+
fn test_validate_cli_no_flags() {
186+
let cli = Cli {
187+
all: false,
188+
cpu: None,
189+
disk: None,
190+
memory: None,
191+
network: None,
192+
system: None,
193+
uptime: None,
194+
interval: DEFAULT_INTERVAL,
195+
network_refresh_rate: DEFAULT_NETWORK_REFRESH_RATE,
196+
bar: None,
197+
verbose: false,
198+
no_units: false,
199+
};
200+
assert!(validate_cli(&cli).is_err());
201+
}
202+
203+
#[test]
204+
fn test_validate_cli_interval_too_low() {
205+
let cli = Cli {
206+
all: true,
207+
cpu: None,
208+
disk: None,
209+
memory: None,
210+
network: None,
211+
system: None,
212+
uptime: None,
213+
interval: 0,
214+
network_refresh_rate: DEFAULT_NETWORK_REFRESH_RATE,
215+
bar: None,
216+
verbose: false,
217+
no_units: false,
218+
};
219+
assert!(validate_cli(&cli).is_err());
220+
}
221+
222+
#[test]
223+
fn test_validate_cli_interval_too_high() {
224+
let cli = Cli {
225+
all: true,
226+
cpu: None,
227+
disk: None,
228+
memory: None,
229+
network: None,
230+
system: None,
231+
uptime: None,
232+
interval: MAX_INTERVAL + 1,
233+
network_refresh_rate: DEFAULT_NETWORK_REFRESH_RATE,
234+
bar: None,
235+
verbose: false,
236+
no_units: false,
237+
};
238+
assert!(validate_cli(&cli).is_err());
239+
}
240+
241+
#[test]
242+
fn test_validate_cli_network_refresh_rate_too_low() {
243+
let cli = Cli {
244+
all: true,
245+
cpu: None,
246+
disk: None,
247+
memory: None,
248+
network: None,
249+
system: None,
250+
uptime: None,
251+
interval: DEFAULT_INTERVAL,
252+
network_refresh_rate: 0,
253+
bar: None,
254+
verbose: false,
255+
no_units: false,
256+
};
257+
assert!(validate_cli(&cli).is_err());
258+
}
259+
260+
#[test]
261+
fn test_validate_cli_network_refresh_rate_too_high() {
262+
let cli = Cli {
263+
all: true,
264+
cpu: None,
265+
disk: None,
266+
memory: None,
267+
network: None,
268+
system: None,
269+
uptime: None,
270+
interval: DEFAULT_INTERVAL,
271+
network_refresh_rate: MAX_NETWORK_REFRESH_RATE + 1,
272+
bar: None,
273+
verbose: false,
274+
no_units: false,
275+
};
276+
assert!(validate_cli(&cli).is_err());
277+
}
278+
279+
#[test]
280+
fn test_all_cpu_flags_returns_correct_flags() {
281+
let flags = all_cpu_flags();
282+
assert_eq!(flags, vec!["count", "frequency", "temperature", "usage"]);
283+
}
284+
285+
#[test]
286+
fn test_all_disk_flags_returns_correct_flags() {
287+
let flags = all_disk_flags();
288+
assert_eq!(flags, vec!["count", "free", "total", "usage", "used"]);
289+
}
290+
291+
#[test]
292+
fn test_all_memory_flags_contains_ram_and_swap() {
293+
let flags = all_memory_flags();
294+
assert!(flags.contains(&"ram_available"));
295+
assert!(flags.contains(&"swp_total"));
296+
assert_eq!(flags.len(), 8);
297+
}
298+
299+
#[test]
300+
fn test_all_system_flags_returns_correct_flags() {
301+
let flags = all_system_flags();
302+
assert!(flags.contains(&"arch"));
303+
assert!(flags.contains(&"distro"));
304+
assert_eq!(flags.len(), 7);
305+
}
306+
307+
#[test]
308+
fn test_all_uptime_flags_returns_correct_flags() {
309+
let flags = all_uptime_flags();
310+
assert_eq!(flags, vec!["week", "day", "hour", "min", "sec"]);
311+
}
312+
}

src/main.rs

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,20 @@ struct ProcessedFlags<'a> {
1818
uptime_flags: Option<&'a [String]>,
1919
}
2020

21-
impl<'a> ProcessedFlags<'a> {
22-
fn cpu_flag_refs(&self) -> Option<Vec<&str>> {
23-
self.cpu_flags
24-
.map(|flags| flags.iter().map(String::as_str).collect())
25-
}
26-
27-
fn disk_flag_refs(&self) -> Option<Vec<&str>> {
28-
self.disk_flags
29-
.map(|flags| flags.iter().map(String::as_str).collect())
30-
}
31-
32-
fn memory_flag_refs(&self) -> Option<Vec<&str>> {
33-
self.memory_flags
34-
.map(|flags| flags.iter().map(String::as_str).collect())
35-
}
21+
macro_rules! flag_refs_method {
22+
($method_name:ident, $field:ident) => {
23+
fn $method_name(&self) -> Option<Vec<&str>> {
24+
self.$field
25+
.map(|flags| flags.iter().map(String::as_str).collect())
26+
}
27+
};
28+
}
3629

37-
fn uptime_flag_refs(&self) -> Option<Vec<&str>> {
38-
self.uptime_flags
39-
.map(|flags| flags.iter().map(String::as_str).collect())
40-
}
30+
impl<'a> ProcessedFlags<'a> {
31+
flag_refs_method!(cpu_flag_refs, cpu_flags);
32+
flag_refs_method!(disk_flag_refs, disk_flags);
33+
flag_refs_method!(memory_flag_refs, memory_flags);
34+
flag_refs_method!(uptime_flag_refs, uptime_flags);
4135
}
4236

4337
struct StatsContext<'a> {
@@ -69,20 +63,24 @@ fn validate_network_interfaces(
6963
) -> Result<()> {
7064
let available_interfaces: Vec<String> = networks.keys().map(|name| name.to_string()).collect();
7165

66+
if available_interfaces.is_empty() {
67+
anyhow::bail!("No network interfaces available on this system");
68+
}
69+
7270
for interface in requested_interfaces {
73-
if !available_interfaces.contains(interface) && verbose {
74-
eprintln!(
75-
"Warning: Network interface '{}' not found. Available interfaces: {}",
71+
if !available_interfaces.contains(interface) {
72+
let msg = format!(
73+
"Network interface '{}' not found. Available interfaces: {}",
7674
interface,
7775
available_interfaces.join(", ")
7876
);
77+
if verbose {
78+
eprintln!("Warning: {}", msg);
79+
}
80+
anyhow::bail!("{}", msg);
7981
}
8082
}
8183

82-
if available_interfaces.is_empty() {
83-
anyhow::bail!("No network interfaces available on this system");
84-
}
85-
8684
Ok(())
8785
}
8886

@@ -272,3 +270,68 @@ async fn main() -> Result<()> {
272270

273271
Ok(())
274272
}
273+
274+
#[cfg(test)]
275+
mod tests {
276+
use super::*;
277+
278+
#[test]
279+
fn test_process_cli_flags() {
280+
let cli = cli::Cli {
281+
all: false,
282+
cpu: Some(vec!["usage".to_string()]),
283+
disk: None,
284+
memory: Some(vec!["ram_total".to_string()]),
285+
network: None,
286+
system: None,
287+
uptime: None,
288+
interval: 5,
289+
network_refresh_rate: 5,
290+
bar: None,
291+
verbose: false,
292+
no_units: false,
293+
};
294+
295+
let flags = process_cli_flags(&cli);
296+
297+
assert!(flags.cpu_flags.is_some());
298+
assert!(flags.disk_flags.is_none());
299+
assert!(flags.memory_flags.is_some());
300+
assert!(flags.network_flags.is_none());
301+
}
302+
303+
#[test]
304+
fn test_processed_flags_cpu_flag_refs() {
305+
let cpu_flags = vec!["usage".to_string(), "count".to_string()];
306+
let flags = ProcessedFlags {
307+
cpu_flags: Some(&cpu_flags),
308+
disk_flags: None,
309+
memory_flags: None,
310+
network_flags: None,
311+
uptime_flags: None,
312+
};
313+
314+
let refs = flags.cpu_flag_refs();
315+
assert!(refs.is_some());
316+
let refs_vec = refs.unwrap();
317+
assert_eq!(refs_vec.len(), 2);
318+
assert_eq!(refs_vec[0], "usage");
319+
assert_eq!(refs_vec[1], "count");
320+
}
321+
322+
#[test]
323+
fn test_processed_flags_returns_none_when_empty() {
324+
let flags = ProcessedFlags {
325+
cpu_flags: None,
326+
disk_flags: None,
327+
memory_flags: None,
328+
network_flags: None,
329+
uptime_flags: None,
330+
};
331+
332+
assert!(flags.cpu_flag_refs().is_none());
333+
assert!(flags.disk_flag_refs().is_none());
334+
assert!(flags.memory_flag_refs().is_none());
335+
assert!(flags.uptime_flag_refs().is_none());
336+
}
337+
}

src/sketchybar.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use tokio::sync::Mutex;
66
use tokio::time::{Duration, Instant};
77

88
// Modified from sketchybar-rs (https://github.com/johnallen3d/sketchybar-rs)
9+
const PORT_REFRESH_INTERVAL_SECS: u64 = 300;
10+
911
#[link(name = "sketchybar", kind = "static")]
1012
extern "C" {
1113
fn sketchybar(message: *const c_char, bar_name: *const c_char) -> *mut c_char;
@@ -34,7 +36,7 @@ impl Sketchybar {
3436
bar_name: c_string,
3537
port_state: Mutex::new(PortState {
3638
last_refresh: Instant::now(),
37-
refresh_interval: Duration::from_secs(300), // 5 minutes
39+
refresh_interval: Duration::from_secs(PORT_REFRESH_INTERVAL_SECS),
3840
}),
3941
})
4042
}

0 commit comments

Comments
 (0)