Skip to content

Commit c31ba4b

Browse files
polidogclaude
andcommitted
test: Add unit tests and fix flaky tests
- Add 20 unit tests for procfs module (address parsing, TCP/UDP content parsing, cache management, display path logic) - Add 5 unit tests for list module (edge cases, options, port range) - Fix 3 flaky tests that failed when system tools (lsof, ss, netstat) are unavailable by accepting I/O errors in assertions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 34e2de2 commit c31ba4b

4 files changed

Lines changed: 322 additions & 0 deletions

File tree

src/commands/check.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ mod tests {
189189
|| error_msg.contains("ss")
190190
|| error_msg.contains("netstat")
191191
|| error_msg.contains("system tools")
192+
|| error_msg.contains("I/O error")
193+
|| error_msg.contains("No such file or directory")
192194
);
193195
}
194196
}
@@ -246,6 +248,8 @@ mod tests {
246248
|| error_msg.contains("ss")
247249
|| error_msg.contains("netstat")
248250
|| error_msg.contains("system tools")
251+
|| error_msg.contains("I/O error")
252+
|| error_msg.contains("No such file or directory")
249253
);
250254
}
251255
}

src/commands/kill.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ mod tests {
145145
|| error_msg.contains("ss")
146146
|| error_msg.contains("netstat")
147147
|| error_msg.contains("system tools")
148+
|| error_msg.contains("I/O error")
149+
|| error_msg.contains("No such file or directory")
148150
);
149151
}
150152
}

src/commands/list.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,4 +549,99 @@ mod tests {
549549
let s = String::from("toolong");
550550
assert_eq!(s.truncate_with_ellipsis(5), "to...");
551551
}
552+
553+
#[test]
554+
fn test_string_truncate_with_ellipsis_edge_cases() {
555+
// Empty string
556+
let s = String::from("");
557+
assert_eq!(s.truncate_with_ellipsis(5), "");
558+
559+
// Very small max_len (less than ellipsis) - returns just "..."
560+
let s = String::from("hello");
561+
assert_eq!(s.truncate_with_ellipsis(3), "...");
562+
563+
// max_len of 2 still returns "..." due to saturating_sub
564+
let s = String::from("hello");
565+
assert_eq!(s.truncate_with_ellipsis(2), "...");
566+
567+
// Single character with adequate max_len
568+
let s = String::from("a");
569+
assert_eq!(s.truncate_with_ellipsis(5), "a");
570+
571+
// Exactly 3 characters (same as string length, no truncation)
572+
let s = String::from("abc");
573+
assert_eq!(s.truncate_with_ellipsis(3), "abc");
574+
575+
// max_len of 4 for 5 char string
576+
let s = String::from("hello");
577+
assert_eq!(s.truncate_with_ellipsis(4), "h...");
578+
}
579+
580+
#[test]
581+
fn test_list_options_creation() {
582+
let options = ListOptions {
583+
ports_range: Some("3000-4000".to_string()),
584+
filter: Some("node".to_string()),
585+
sort: "port".to_string(),
586+
protocol: "tcp".to_string(),
587+
kill: false,
588+
quiet: false,
589+
json: false,
590+
watch: false,
591+
};
592+
593+
assert_eq!(options.ports_range, Some("3000-4000".to_string()));
594+
assert_eq!(options.filter, Some("node".to_string()));
595+
assert_eq!(options.sort, "port");
596+
assert_eq!(options.protocol, "tcp");
597+
assert!(!options.kill);
598+
assert!(!options.quiet);
599+
assert!(!options.json);
600+
assert!(!options.watch);
601+
}
602+
603+
#[test]
604+
fn test_list_options_debug() {
605+
let options = ListOptions {
606+
ports_range: None,
607+
filter: None,
608+
sort: "port".to_string(),
609+
protocol: "all".to_string(),
610+
kill: true,
611+
quiet: true,
612+
json: true,
613+
watch: true,
614+
};
615+
616+
// Test Debug trait
617+
let debug_str = format!("{:?}", options);
618+
assert!(debug_str.contains("ListOptions"));
619+
assert!(debug_str.contains("ports_range"));
620+
assert!(debug_str.contains("filter"));
621+
}
622+
623+
#[test]
624+
fn test_parse_port_range_boundary() {
625+
// Minimum port
626+
let result = ListCommand::parse_port_range("1-1");
627+
assert!(result.is_ok());
628+
assert_eq!(result.unwrap(), (1, 1));
629+
630+
// Maximum port
631+
let result = ListCommand::parse_port_range("65535-65535");
632+
assert!(result.is_ok());
633+
assert_eq!(result.unwrap(), (65535, 65535));
634+
635+
// Full range
636+
let result = ListCommand::parse_port_range("1-65535");
637+
assert!(result.is_ok());
638+
assert_eq!(result.unwrap(), (1, 65535));
639+
}
640+
641+
#[test]
642+
fn test_parse_port_range_overflow() {
643+
// Port number overflow
644+
let result = ListCommand::parse_port_range("0-70000");
645+
assert!(result.is_err());
646+
}
552647
}

src/port/procfs.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,224 @@ impl Default for ProcfsPortManager {
394394
Self::new()
395395
}
396396
}
397+
398+
#[cfg(test)]
399+
mod tests {
400+
use super::*;
401+
402+
#[test]
403+
fn test_procfs_port_manager_creation() {
404+
let manager = ProcfsPortManager::new();
405+
assert!(manager.pid_cache.is_empty());
406+
assert_eq!(manager.cache_ttl, std::time::Duration::from_secs(2));
407+
}
408+
409+
#[test]
410+
fn test_procfs_port_manager_default() {
411+
let manager = ProcfsPortManager::default();
412+
assert!(manager.pid_cache.is_empty());
413+
}
414+
415+
#[test]
416+
fn test_parse_ipv4_address_all_zeros() {
417+
let manager = ProcfsPortManager::new();
418+
let result = manager.parse_ipv4_address("00000000");
419+
assert_eq!(result, "*");
420+
}
421+
422+
#[test]
423+
fn test_parse_ipv4_address_localhost() {
424+
let manager = ProcfsPortManager::new();
425+
// 127.0.0.1 in little-endian hex: 0100007F
426+
let result = manager.parse_ipv4_address("0100007F");
427+
assert_eq!(result, "127.0.0.1");
428+
}
429+
430+
#[test]
431+
fn test_parse_ipv4_address_invalid_length() {
432+
let manager = ProcfsPortManager::new();
433+
let result = manager.parse_ipv4_address("00");
434+
assert_eq!(result, "*");
435+
}
436+
437+
#[test]
438+
fn test_parse_ipv6_address_all_zeros() {
439+
let manager = ProcfsPortManager::new();
440+
let result = manager.parse_ipv6_address("00000000000000000000000000000000");
441+
assert_eq!(result, "*");
442+
}
443+
444+
#[test]
445+
fn test_parse_ipv6_address_invalid_length() {
446+
let manager = ProcfsPortManager::new();
447+
let result = manager.parse_ipv6_address("0000");
448+
assert_eq!(result, "*");
449+
}
450+
451+
#[test]
452+
fn test_parse_ipv6_address_localhost() {
453+
let manager = ProcfsPortManager::new();
454+
// ::1 in hex: 00000000000000000000000000000001
455+
let result = manager.parse_ipv6_address("00000000000000000000000000000001");
456+
assert_eq!(result, "::1");
457+
}
458+
459+
#[test]
460+
fn test_parse_address_ipv4() {
461+
let manager = ProcfsPortManager::new();
462+
// Format: address:port in hex
463+
// 0.0.0.0:8080 -> 00000000:1F90
464+
let result = manager.parse_address("00000000:1F90", false);
465+
assert!(result.is_some());
466+
let (address, port) = result.unwrap();
467+
assert_eq!(address, "*");
468+
assert_eq!(port, 8080);
469+
}
470+
471+
#[test]
472+
fn test_parse_address_ipv4_localhost_port_3000() {
473+
let manager = ProcfsPortManager::new();
474+
// 127.0.0.1:3000 -> 0100007F:0BB8
475+
let result = manager.parse_address("0100007F:0BB8", false);
476+
assert!(result.is_some());
477+
let (address, port) = result.unwrap();
478+
assert_eq!(address, "127.0.0.1");
479+
assert_eq!(port, 3000);
480+
}
481+
482+
#[test]
483+
fn test_parse_address_ipv6() {
484+
let manager = ProcfsPortManager::new();
485+
// [::]:8080 -> 00000000000000000000000000000000:1F90
486+
let result = manager.parse_address("00000000000000000000000000000000:1F90", true);
487+
assert!(result.is_some());
488+
let (address, port) = result.unwrap();
489+
assert_eq!(address, "*");
490+
assert_eq!(port, 8080);
491+
}
492+
493+
#[test]
494+
fn test_parse_address_invalid() {
495+
let manager = ProcfsPortManager::new();
496+
// Missing colon
497+
let result = manager.parse_address("00000000", false);
498+
assert!(result.is_none());
499+
}
500+
501+
#[test]
502+
fn test_parse_tcp_content_empty() {
503+
let manager = ProcfsPortManager::new();
504+
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n";
505+
let result = manager.parse_tcp_content(content, false);
506+
assert!(result.is_ok());
507+
assert!(result.unwrap().is_empty());
508+
}
509+
510+
#[test]
511+
fn test_parse_tcp_content_listening() {
512+
let manager = ProcfsPortManager::new();
513+
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n 0: 00000000:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 12345 1 0000000000000000 100 0 0 10 0";
514+
let result = manager.parse_tcp_content(content, false);
515+
assert!(result.is_ok());
516+
let processes = result.unwrap();
517+
assert_eq!(processes.len(), 1);
518+
assert_eq!(processes[0].port, 8080);
519+
assert_eq!(processes[0].protocol, "tcp");
520+
assert_eq!(processes[0].address, "*");
521+
assert_eq!(processes[0].inode, Some(12345));
522+
}
523+
524+
#[test]
525+
fn test_parse_tcp_content_established_skipped() {
526+
let manager = ProcfsPortManager::new();
527+
// State 01 = ESTABLISHED, should be skipped
528+
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n 0: 00000000:1F90 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 12345 1 0000000000000000 100 0 0 10 0";
529+
let result = manager.parse_tcp_content(content, false);
530+
assert!(result.is_ok());
531+
assert!(result.unwrap().is_empty());
532+
}
533+
534+
#[test]
535+
fn test_parse_udp_content() {
536+
let manager = ProcfsPortManager::new();
537+
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops\n 0: 00000000:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 54321 2 0000000000000000 0";
538+
let result = manager.parse_udp_content(content, false);
539+
assert!(result.is_ok());
540+
let processes = result.unwrap();
541+
assert_eq!(processes.len(), 1);
542+
assert_eq!(processes[0].port, 53); // DNS port
543+
assert_eq!(processes[0].protocol, "udp");
544+
assert_eq!(processes[0].inode, Some(54321));
545+
}
546+
547+
#[test]
548+
fn test_get_display_path_dev_process() {
549+
let manager = ProcfsPortManager::new();
550+
let process_info = ProcessInfo {
551+
pid: 1234,
552+
name: "node".to_string(),
553+
command: "node /home/user/project/server.js".to_string(),
554+
executable_path: "/usr/bin/node".to_string(),
555+
working_directory: "/home/user/project".to_string(),
556+
port: 3000,
557+
protocol: "tcp".to_string(),
558+
address: "*".to_string(),
559+
inode: Some(12345),
560+
};
561+
let result = manager.get_display_path(&process_info);
562+
assert_eq!(result, "/home/user/project");
563+
}
564+
565+
#[test]
566+
fn test_get_display_path_system_process() {
567+
let manager = ProcfsPortManager::new();
568+
let process_info = ProcessInfo {
569+
pid: 1234,
570+
name: "nginx".to_string(),
571+
command: "nginx: master process".to_string(),
572+
executable_path: "/usr/sbin/nginx".to_string(),
573+
working_directory: "/".to_string(),
574+
port: 80,
575+
protocol: "tcp".to_string(),
576+
address: "*".to_string(),
577+
inode: Some(12345),
578+
};
579+
let result = manager.get_display_path(&process_info);
580+
assert_eq!(result, "/usr/sbin/nginx");
581+
}
582+
583+
#[test]
584+
fn test_clear_cache() {
585+
let mut manager = ProcfsPortManager::new();
586+
manager.pid_cache.insert(
587+
1234,
588+
ProcessDetails {
589+
name: "test".to_string(),
590+
command: "test".to_string(),
591+
executable_path: "/test".to_string(),
592+
working_directory: "/".to_string(),
593+
},
594+
);
595+
assert!(!manager.pid_cache.is_empty());
596+
597+
manager.clear_cache();
598+
assert!(manager.pid_cache.is_empty());
599+
}
600+
601+
#[test]
602+
fn test_is_listening_connection() {
603+
let manager = ProcfsPortManager::new();
604+
let process_info = ProcessInfo {
605+
pid: 1234,
606+
name: "test".to_string(),
607+
command: "test".to_string(),
608+
executable_path: "/test".to_string(),
609+
working_directory: "/".to_string(),
610+
port: 3000,
611+
protocol: "tcp".to_string(),
612+
address: "*".to_string(),
613+
inode: Some(12345),
614+
};
615+
assert!(manager.is_listening_connection(&process_info));
616+
}
617+
}

0 commit comments

Comments
 (0)