t_trace 0.1.3

High-performance, command-line statistics tracker for your shell
Documentation
use anyhow::{Result, anyhow, bail};
use std::fmt;

#[derive(Debug, PartialEq)]
pub enum Request {
    Stop,
    HealthCheck,
    CommandBegin { pid: u32, command: String },
    CommandEnd { pid: u32, exit_code: i32 },
    GetStats,
    SaveStats,
}

impl Request {
    pub fn from_str(s: &str) -> Result<Self> {
        let s = s.trim();

        if s == "STOP" {
            return Ok(Request::Stop);
        }
        if s == "HEALTH_CHECK" {
            return Ok(Request::HealthCheck);
        }
        if s == "GET_STATS" {
            return Ok(Request::GetStats);
        }
        if s == "SAVE_STATS" {
            return Ok(Request::SaveStats);
        }

        let mut parts = s.splitn(3, ' ');
        let verb = parts.next().ok_or_else(|| anyhow!("Missing verb"))?;
        let pid_str = parts.next().ok_or_else(|| anyhow!("Missing PID"))?;
        let pid = pid_str.parse::<u32>()?;

        match verb {
            "COMMAND_BEGIN" => {
                let command = parts
                    .next()
                    .ok_or_else(|| anyhow!("Missing command string"))?
                    .to_string();
                Ok(Request::CommandBegin { pid, command })
            }
            "COMMAND_END" => {
                let exit_code_str = parts.next().ok_or_else(|| anyhow!("Missing exit code"))?;
                let exit_code = exit_code_str.parse::<i32>()?;
                Ok(Request::CommandEnd { pid, exit_code })
            }
            _ => bail!("Unknown verb: {}", verb),
        }
    }
}

impl fmt::Display for Request {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Request::Stop => write!(f, "STOP"),
            Request::HealthCheck => write!(f, "HEALTH_CHECK"),
            Request::CommandBegin { pid, command } => {
                write!(f, "COMMAND_BEGIN {} {}", pid, command)
            }
            Request::CommandEnd { pid, exit_code } => {
                write!(f, "COMMAND_END {} {}", pid, exit_code)
            }
            Request::GetStats => write!(f, "GET_STATS"),
            Request::SaveStats => write!(f, "SAVE_STATS"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_command_begin_request_parsing() {
        let input = "COMMAND_BEGIN 1234 ls -l /home/user";
        let expected = Request::CommandBegin {
            pid: 1234,
            command: "ls -l /home/user".to_string(),
        };
        assert_eq!(Request::from_str(input).unwrap(), expected);
    }

    #[test]
    fn test_command_end_request_parsing() {
        let input = "COMMAND_END 5678 0";
        let expected = Request::CommandEnd {
            pid: 5678,
            exit_code: 0,
        };
        assert_eq!(Request::from_str(input).unwrap(), expected);
    }

    #[test]
    fn test_command_end_request_parsing_with_negative_exit_code() {
        let input = "COMMAND_END 5678 -1";
        let expected = Request::CommandEnd {
            pid: 5678,
            exit_code: -1,
        };
        assert_eq!(Request::from_str(input).unwrap(), expected);
    }

    #[test]
    fn test_request_formatting() {
        let begin_req = Request::CommandBegin {
            pid: 1234,
            command: "git commit -m \"a message\"".to_string(),
        };
        let end_req = Request::CommandEnd {
            pid: 5678,
            exit_code: 0,
        };

        let expected_begin = "COMMAND_BEGIN 1234 git commit -m \"a message\"";
        let expected_end = "COMMAND_END 5678 0";

        assert_eq!(begin_req.to_string(), expected_begin);
        assert_eq!(end_req.to_string(), expected_end);
    }

    #[test]
    fn test_parsing_fails_on_unknown_verb() {
        let input = "FOO 1234 bar";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_empty_input() {
        let input = "";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_missing_pid() {
        let input = "COMMAND_BEGIN";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_invalid_pid() {
        let input = "COMMAND_BEGIN not_a_pid ls";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_missing_command_string_for_start() {
        let input = "COMMAND_BEGIN 1234";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_missing_exit_code_for_end() {
        let input = "COMMAND_END 1234";
        assert!(Request::from_str(input).is_err());
    }

    #[test]
    fn test_parsing_fails_on_invalid_exit_code() {
        let input = "COMMAND_END 1234 not_an_int";
        assert!(Request::from_str(input).is_err());
    }
}