mutant_kraken/
cli.rs

1use std::{path::Path, time::Duration};
2
3use clap::{Args, CommandFactory, Parser, Subcommand};
4use tracing_appender::non_blocking::WorkerGuard;
5
6use crate::{
7    config::MutantKrakenConfig,
8    error::{self, MutantKrakenError},
9    mutation_tool::MutationToolBuilder,
10};
11
12#[derive(Subcommand, Debug, Clone)]
13pub enum Commands {
14    /// Mutate the files in the given path
15    /// If no path is given, the current directory will be used
16    /// A csv file will be generated along with a html table
17    Mutate(MutationCommandConfig),
18    /// Display help text on how to setup the config file
19    /// or create a config file in the current directory
20    Config(ConfigCommandConfig),
21    /// Clean the mutant-kraken-dist directory
22    /// This will delete all files in the directory
23    /// This is useful if you want to remove all the files
24    Clean(MutationCommandConfig),
25}
26
27const ABOUT: &str = include_str!("../assets/about.txt");
28
29#[derive(Parser, Debug)]
30#[command(
31    author,
32    version,
33    about = ABOUT,
34    long_about = None
35)]
36pub struct Cli {
37    #[command(subcommand)]
38    pub command: Commands,
39}
40
41#[derive(Args, Debug, Clone, PartialEq, Eq)]
42pub struct MutationCommandConfig {
43    /// The path to the files to be mutated
44    /// Error will be thrown if the path is not a directory
45    #[clap(default_value = ".")]
46    pub path: String,
47}
48
49#[derive(Args, Debug, Clone)]
50pub struct ConfigCommandConfig {
51    /// Create a config file in the current directory
52    #[clap(long, short, default_value = "false")]
53    pub setup: bool,
54}
55
56impl Default for MutationCommandConfig {
57    fn default() -> Self {
58        Self {
59            path: std::env::current_dir()
60                .expect("Could not get the current working directory")
61                .display()
62                .to_string(),
63        }
64    }
65}
66
67pub fn run_with_timeout<F>(mut f: F, timeout: Duration) -> error::Result<()>
68where
69    F: FnMut() -> error::Result<()> + Send + 'static,
70{
71    // Create a channel to send a message when the function is done
72    let (sender, receiver) = std::sync::mpsc::channel();
73    // Spawn a thread to run the function
74    std::thread::spawn(move || {
75        sender.send(f()).expect("Could not send message");
76    });
77    // Wait for the function to finish or timeout
78    match receiver.recv_timeout(timeout) {
79        Ok(res) => res,
80        Err(_) => Err(MutantKrakenError::Error(format!(
81            "Timeout reached, mutation tool took longer than {} seconds to finish",
82            timeout.as_secs()
83        ))),
84    }
85}
86
87pub fn run_cli() {
88    let _guard: WorkerGuard;
89    tracing::info!("Starting mutant Kraken");
90
91    let args = Cli::parse();
92    let mutate_tool_builder = MutationToolBuilder::new();
93
94    match args.command {
95        Commands::Mutate(mutate_config) => {
96            let config = MutantKrakenConfig::load_config(mutate_config.path.clone());
97            _guard = setup_logging(&config.logging.log_level, mutate_config.path.clone());
98            let mut tool = mutate_tool_builder
99                .set_mutate_config(mutate_config)
100                .set_general_config(config)
101                .set_mutation_comment(true)
102                .build();
103            let res = match tool.mutantkraken_config.general.timeout {
104                Some(timeout) => {
105                    run_with_timeout(move || tool.mutate(), Duration::from_secs(timeout))
106                }
107                None => tool.mutate(),
108            };
109            if let Err(e) = res {
110                let error_msg = match e {
111                    error::MutantKrakenError::FileReadingError(msg) => msg,
112                    error::MutantKrakenError::MutationGenerationError => {
113                        "Error Generating Mutations".into()
114                    }
115                    error::MutantKrakenError::MutationGatheringError => {
116                        "Error Gathering Mutations".into()
117                    }
118                    error::MutantKrakenError::MutationBuildTestError => {
119                        "Error Building and Testing Mutations".into()
120                    }
121                    error::MutantKrakenError::ConversionError => "Error Converting".into(),
122                    error::MutantKrakenError::Error(msg) => msg,
123                };
124                Cli::command()
125                    .error(clap::error::ErrorKind::Io, error_msg)
126                    .exit();
127            }
128        }
129        Commands::Config(config) => {
130            if config.setup {
131                let config_file_path = Path::new("mutantkraken.config.json");
132                if config_file_path.exists() {
133                    println!("Config file already exists");
134                } else {
135                    std::fs::write(config_file_path, include_str!("../assets/config.json"))
136                        .expect("Could not write config file");
137                    println!("Config file created");
138                }
139            } else {
140                println!("Config file setup instructions:");
141                println!(
142                    "1. Create a file named mutantkraken.config.json in the root of your project"
143                );
144                println!("2. Copy the following into the file:");
145                println!("{}", include_str!("../assets/config.json"));
146                println!("3. Edit the config file to your liking");
147            }
148        }
149        Commands::Clean(config) => {
150            // Check to see if the output directory exists
151            let output_dir = Path::new(config.path.as_str()).join("mutant-kraken-dist");
152            if output_dir.exists() {
153                // Delete the output directory
154                std::fs::remove_dir_all(output_dir).expect("Could not delete output directory");
155            }
156        }
157    }
158}
159
160fn setup_logging(log_level: &str, dir: String) -> WorkerGuard {
161    let log_level = match log_level.to_lowercase().as_str() {
162        "trace" => tracing::Level::TRACE,
163        "debug" => tracing::Level::DEBUG,
164        "info" => tracing::Level::INFO,
165        "warn" => tracing::Level::WARN,
166        "error" => tracing::Level::ERROR,
167        _ => tracing::Level::INFO,
168    };
169    // Create dist log folder if it doesn't exist
170    let log_dir = Path::new(dir.as_str())
171        .join("mutant-kraken-dist")
172        .join("logs");
173    std::fs::create_dir_all(&log_dir).expect("Could not create log directory");
174    let file_appender = tracing_appender::rolling::never(log_dir, "mutant-kraken.log");
175    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
176    tracing_subscriber::fmt()
177        .with_max_level(log_level)
178        .with_ansi(false)
179        .with_target(false)
180        .with_writer(non_blocking)
181        .with_thread_ids(true)
182        .init();
183    guard
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use clap::CommandFactory;
190
191    #[test]
192    fn verify_cli_parse() {
193        Cli::command().debug_assert();
194    }
195}