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(MutationCommandConfig),
18 Config(ConfigCommandConfig),
21 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 #[clap(default_value = ".")]
46 pub path: String,
47}
48
49#[derive(Args, Debug, Clone)]
50pub struct ConfigCommandConfig {
51 #[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 let (sender, receiver) = std::sync::mpsc::channel();
73 std::thread::spawn(move || {
75 sender.send(f()).expect("Could not send message");
76 });
77 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 let output_dir = Path::new(config.path.as_str()).join("mutant-kraken-dist");
152 if output_dir.exists() {
153 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 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}