use clap::{Parser, ValueEnum};
use clap_verbosity_flag::{Verbosity, WarnLevel};
use core::fmt;
use crate::signal::KillportSignal;
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
pub enum Mode {
Auto,
Process,
Container,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let variant = match *self {
Mode::Auto => "auto",
Mode::Process => "process",
Mode::Container => "container",
};
write!(f, "{}", variant)
}
}
pub fn service_descriptors(mode: Mode) -> (&'static str, &'static str) {
match mode {
Mode::Auto => ("service", "services"),
Mode::Process => ("process", "processes"),
Mode::Container => ("container", "containers"),
}
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct KillPortArgs {
#[arg(
name = "ports",
help = "The list of port numbers to kill processes or containers on",
required = true
)]
pub ports: Vec<u16>,
#[arg(
long,
short = 'm',
help = "Mode of operation: auto (default, kill both), process (only processes), container (only containers)",
default_value_t = Mode::Auto)]
pub mode: Mode,
#[arg(
long,
short = 's',
name = "SIG",
help = "SIG is a signal name",
default_value = "sigkill",
value_parser = parse_signal
)]
pub signal: KillportSignal,
#[command(flatten)]
pub verbose: Verbosity<WarnLevel>,
#[arg(
long,
help = "Perform a dry run without killing any processes or containers"
)]
pub dry_run: bool,
#[arg(
long,
help = "Exit successfully even if no matching process or container is found"
)]
pub no_fail: bool,
}
fn parse_signal(arg: &str) -> Result<KillportSignal, std::io::Error> {
arg.to_uppercase().parse()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mode_display_auto() {
assert_eq!(Mode::Auto.to_string(), "auto");
}
#[test]
fn test_mode_display_process() {
assert_eq!(Mode::Process.to_string(), "process");
}
#[test]
fn test_mode_display_container() {
assert_eq!(Mode::Container.to_string(), "container");
}
#[test]
fn test_service_descriptors_auto() {
assert_eq!(service_descriptors(Mode::Auto), ("service", "services"));
}
#[test]
fn test_service_descriptors_process() {
assert_eq!(service_descriptors(Mode::Process), ("process", "processes"));
}
#[test]
fn test_service_descriptors_container() {
assert_eq!(
service_descriptors(Mode::Container),
("container", "containers")
);
}
#[test]
fn test_parse_signal_sigkill() {
assert!(parse_signal("sigkill").is_ok());
}
#[test]
fn test_parse_signal_uppercase() {
assert!(parse_signal("SIGTERM").is_ok());
}
#[test]
fn test_parse_signal_mixed_case() {
assert!(parse_signal("SigInt").is_ok());
}
#[cfg(unix)]
#[test]
fn test_parse_signal_invalid() {
assert!(parse_signal("NOTASIGNAL").is_err());
}
#[cfg(unix)]
#[test]
fn test_parse_signal_empty() {
assert!(parse_signal("").is_err());
}
#[test]
fn test_cli_args_single_port() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert_eq!(args.ports, vec![8080]);
}
#[test]
fn test_cli_args_multiple_ports() {
let args = KillPortArgs::try_parse_from(["killport", "80", "443", "8080"]).unwrap();
assert_eq!(args.ports, vec![80, 443, 8080]);
}
#[test]
fn test_cli_args_no_ports_fails() {
assert!(KillPortArgs::try_parse_from(["killport"]).is_err());
}
#[test]
fn test_cli_args_port_zero() {
let args = KillPortArgs::try_parse_from(["killport", "0"]).unwrap();
assert_eq!(args.ports, vec![0]);
}
#[test]
fn test_cli_args_port_65535() {
let args = KillPortArgs::try_parse_from(["killport", "65535"]).unwrap();
assert_eq!(args.ports, vec![65535]);
}
#[test]
fn test_cli_args_port_overflow() {
assert!(KillPortArgs::try_parse_from(["killport", "65536"]).is_err());
}
#[test]
fn test_cli_args_port_negative() {
assert!(KillPortArgs::try_parse_from(["killport", "-1"]).is_err());
}
#[test]
fn test_cli_args_port_string() {
assert!(KillPortArgs::try_parse_from(["killport", "abc"]).is_err());
}
#[test]
fn test_cli_args_mode_auto() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "--mode", "auto"]).unwrap();
assert_eq!(args.mode, Mode::Auto);
}
#[test]
fn test_cli_args_mode_process() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "--mode", "process"]).unwrap();
assert_eq!(args.mode, Mode::Process);
}
#[test]
fn test_cli_args_mode_container() {
let args =
KillPortArgs::try_parse_from(["killport", "8080", "--mode", "container"]).unwrap();
assert_eq!(args.mode, Mode::Container);
}
#[test]
fn test_cli_args_mode_invalid() {
assert!(KillPortArgs::try_parse_from(["killport", "8080", "--mode", "foobar"]).is_err());
}
#[test]
fn test_cli_args_mode_short_flag() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-m", "process"]).unwrap();
assert_eq!(args.mode, Mode::Process);
}
#[test]
fn test_cli_args_mode_default() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert_eq!(args.mode, Mode::Auto);
}
#[test]
fn test_cli_args_signal_default() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert_eq!(args.signal.to_string().to_uppercase(), "SIGKILL");
}
#[test]
fn test_cli_args_signal_custom() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-s", "sigterm"]).unwrap();
assert_eq!(args.signal.to_string().to_uppercase(), "SIGTERM");
}
#[test]
fn test_cli_args_dry_run_present() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "--dry-run"]).unwrap();
assert!(args.dry_run);
}
#[test]
fn test_cli_args_dry_run_absent() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert!(!args.dry_run);
}
#[test]
fn test_cli_args_no_fail_present() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "--no-fail"]).unwrap();
assert!(args.no_fail);
}
#[test]
fn test_cli_args_no_fail_absent() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert!(!args.no_fail);
}
#[test]
fn test_cli_args_combined_flags() {
let args = KillPortArgs::try_parse_from([
"killport",
"-m",
"process",
"-s",
"sigterm",
"--dry-run",
"8080",
"8081",
])
.unwrap();
assert_eq!(args.ports, vec![8080, 8081]);
assert_eq!(args.mode, Mode::Process);
assert_eq!(args.signal.to_string().to_uppercase(), "SIGTERM");
assert!(args.dry_run);
}
#[test]
fn test_cli_args_help() {
let result = KillPortArgs::try_parse_from(["killport", "--help"]);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn test_cli_args_version() {
let result = KillPortArgs::try_parse_from(["killport", "--version"]);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
#[test]
fn test_verbosity_default_is_warn() {
let args = KillPortArgs::try_parse_from(["killport", "8080"]).unwrap();
assert_eq!(args.verbose.log_level(), Some(log::Level::Warn));
}
#[test]
fn test_verbosity_single_v_is_info() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-v"]).unwrap();
assert_eq!(args.verbose.log_level(), Some(log::Level::Info));
}
#[test]
fn test_verbosity_double_v_is_debug() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-vv"]).unwrap();
assert_eq!(args.verbose.log_level(), Some(log::Level::Debug));
}
#[test]
fn test_verbosity_triple_v_is_trace() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-vvv"]).unwrap();
assert_eq!(args.verbose.log_level(), Some(log::Level::Trace));
}
#[test]
fn test_verbosity_single_q_is_error() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-q"]).unwrap();
assert_eq!(args.verbose.log_level(), Some(log::Level::Error));
}
#[test]
fn test_verbosity_double_q_is_off() {
let args = KillPortArgs::try_parse_from(["killport", "8080", "-qq"]).unwrap();
assert_eq!(args.verbose.log_level(), None);
}
#[test]
fn test_mode_clone() {
let mode = Mode::Auto;
let cloned = mode;
assert_eq!(mode, cloned);
}
#[test]
fn test_mode_debug() {
let debug_str = format!("{:?}", Mode::Auto);
assert_eq!(debug_str, "Auto");
}
#[test]
fn test_mode_equality() {
assert_eq!(Mode::Auto, Mode::Auto);
assert_ne!(Mode::Auto, Mode::Process);
assert_ne!(Mode::Process, Mode::Container);
}
}