mutant_kraken/
config.rs

1use std::{fs, io::BufReader, path::Path};
2
3use serde::{Deserialize, Serialize};
4
5use crate::mutation_tool::MutationOperators;
6
7#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
8pub struct MutantKrakenConfig {
9    pub general: GeneralConfig,
10    pub ignore: IgnoreConfig,
11    pub threading: ThreadingConfig,
12    pub output: OutputConfig,
13    pub logging: LoggingConfig,
14}
15
16#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
17pub struct GeneralConfig {
18    /// The time in seconds to wait for the mutation tool to finish
19    /// before killing the process
20    pub timeout: Option<u64>,
21
22    #[serde(default)]
23    pub operators: Vec<MutationOperators>,
24}
25
26impl Default for GeneralConfig {
27    /// Set Default timeout of 5 minutes
28    fn default() -> Self {
29        use MutationOperators::*;
30        Self {
31            timeout: None,
32            operators: vec![
33                ArithmeticReplacementOperator,
34                UnaryRemovalOperator,
35                LogicalReplacementOperator,
36                RelationalReplacementOperator,
37                AssignmentReplacementOperator,
38                UnaryReplacementOperator,
39                NotNullAssertionOperator,
40                ElvisRemoveOperator,
41                ElvisLiteralChangeOperator,
42                LiteralChangeOperator,
43                ExceptionChangeOperator,
44                WhenRemoveBranchOperator,
45                RemoveLabelOperator,
46                FunctionalBinaryReplacementOperator,
47                FunctionalReplacementOperator,
48            ],
49        }
50    }
51}
52
53#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
54pub struct LoggingConfig {
55    pub log_level: String,
56}
57
58#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
59pub struct IgnoreConfig {
60    pub ignore_files: Vec<String>,
61    pub ignore_directories: Vec<String>,
62}
63
64#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
65pub struct ThreadingConfig {
66    pub max_threads: usize,
67}
68
69#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
70pub struct OutputConfig {
71    pub display_end_table: bool,
72}
73
74impl MutantKrakenConfig {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    pub fn load_config<P: AsRef<Path>>(path: P) -> Self {
80        let mut config = match fs::File::open(path.as_ref().join("mutantkraken.config.json")) {
81            Ok(file) => {
82                let buffer_reader = BufReader::new(file);
83                match serde_json::from_reader(buffer_reader) {
84                    Ok(config) => config,
85                    Err(e) => {
86                        println!("[WARNING] ⚠️  Could not parse config file, view logs for error.");
87                        tracing::warn!(
88                            "Could not parse config file, using default config. Error: {}",
89                            e
90                        );
91                        Self::default()
92                    }
93                }
94            }
95            Err(_) => {
96                println!("[WARNING] ⚠️  Could not find mutantkraken.config.json file in root directory, using default config.");
97                Self::default()
98            }
99        };
100
101        if config.general.operators.is_empty() {
102            println!("[WARNING] ⚠️  No mutation operators specified in config file, using default operators.");
103            config.general.operators = GeneralConfig::default().operators;
104        }
105
106        config
107    }
108}
109
110impl Default for IgnoreConfig {
111    fn default() -> Self {
112        Self {
113            ignore_files: vec![
114                r#"^.*Test\.[^.]*$"#.to_string(), // Ignore all files that end with Test.*
115            ],
116            ignore_directories: vec![
117                "dist".into(),
118                "build".into(),
119                "bin".into(),
120                ".gradle".into(),
121                ".idea".into(),
122                "gradle".into(),
123            ],
124        }
125    }
126}
127
128impl Default for ThreadingConfig {
129    fn default() -> Self {
130        Self { max_threads: 30 }
131    }
132}
133
134impl Default for LoggingConfig {
135    fn default() -> Self {
136        Self {
137            log_level: "info".into(),
138        }
139    }
140}
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::{env::temp_dir, fs::File, io::Write};
145    use MutationOperators::*;
146    #[test]
147    fn test_default_general_config() {
148        let default_general = GeneralConfig::default();
149        assert_eq!(default_general.timeout, None);
150        assert_eq!(
151            default_general.operators,
152            vec![
153                ArithmeticReplacementOperator,
154                UnaryRemovalOperator,
155                LogicalReplacementOperator,
156                RelationalReplacementOperator,
157                AssignmentReplacementOperator,
158                UnaryReplacementOperator,
159                NotNullAssertionOperator,
160                ElvisRemoveOperator,
161                ElvisLiteralChangeOperator,
162                LiteralChangeOperator,
163                ExceptionChangeOperator,
164                WhenRemoveBranchOperator,
165                RemoveLabelOperator,
166                FunctionalBinaryReplacementOperator,
167                FunctionalReplacementOperator,
168            ]
169        );
170    }
171
172    #[test]
173    fn test_default_ignore_config() {
174        let default_ignore = IgnoreConfig::default();
175        assert_eq!(default_ignore.ignore_files.len(), 1);
176        assert_eq!(default_ignore.ignore_directories.len(), 6);
177    }
178
179    #[test]
180    fn test_default_threading_config() {
181        let default_threading = ThreadingConfig::default();
182        assert_eq!(default_threading.max_threads, 30);
183    }
184
185    #[test]
186    fn test_default_logging_config() {
187        let default_logging = LoggingConfig::default();
188        assert_eq!(default_logging.log_level, "info");
189    }
190
191    #[test]
192    fn test_default_output_config() {
193        let default_output = OutputConfig::default();
194        assert_eq!(default_output.display_end_table, false);
195    }
196
197    #[test]
198    fn test_new_mutantkraken_config() {
199        let config = MutantKrakenConfig::new();
200        assert_eq!(config.general.timeout, None);
201        assert_eq!(
202            config.general.operators,
203            vec![
204                ArithmeticReplacementOperator,
205                UnaryRemovalOperator,
206                LogicalReplacementOperator,
207                RelationalReplacementOperator,
208                AssignmentReplacementOperator,
209                UnaryReplacementOperator,
210                NotNullAssertionOperator,
211                ElvisRemoveOperator,
212                ElvisLiteralChangeOperator,
213                LiteralChangeOperator,
214                ExceptionChangeOperator,
215                WhenRemoveBranchOperator,
216                RemoveLabelOperator,
217                FunctionalBinaryReplacementOperator,
218                FunctionalReplacementOperator,
219            ]
220        );
221        assert_eq!(config.ignore.ignore_files.len(), 1);
222        assert_eq!(config.ignore.ignore_directories.len(), 6);
223        assert_eq!(config.threading.max_threads, 30);
224        assert_eq!(config.output.display_end_table, false);
225        assert_eq!(config.logging.log_level, "info");
226    }
227
228    #[test]
229    fn test_load_config_from_valid_file() {
230        let temp_dir = temp_dir();
231        let file_path = temp_dir.join("mutantkraken.config.json");
232        let mut file = File::create(file_path).expect("Failed to create temporary file");
233
234        // Create a valid JSON content
235        let config = MutantKrakenConfig {
236            general: GeneralConfig {
237                timeout: Some(10),
238                operators: vec![
239                    MutationOperators::UnaryRemovalOperator,
240                    MutationOperators::AssignmentReplacementOperator,
241                ],
242            },
243            ignore: IgnoreConfig {
244                ignore_files: vec!["file1".into(), "file2".into()],
245                ignore_directories: vec!["dir1".into(), "dir2".into()],
246            },
247            threading: ThreadingConfig { max_threads: 42 },
248            output: OutputConfig {
249                display_end_table: true,
250            },
251            logging: LoggingConfig {
252                log_level: "debug".into(),
253            },
254        };
255
256        let config_json = serde_json::to_string_pretty(&config).unwrap();
257        file.write_all(config_json.as_bytes())
258            .expect("Failed to write to temporary file");
259
260        let config = MutantKrakenConfig::load_config(temp_dir);
261        assert_eq!(config.general.timeout, Some(10));
262        assert_eq!(
263            config.general.operators,
264            vec![
265                MutationOperators::UnaryRemovalOperator,
266                MutationOperators::AssignmentReplacementOperator
267            ]
268        );
269        assert_eq!(config.ignore.ignore_files, vec!["file1", "file2"]);
270        assert_eq!(config.ignore.ignore_directories, vec!["dir1", "dir2"]);
271        assert_eq!(config.threading.max_threads, 42);
272        assert_eq!(config.output.display_end_table, true);
273        assert_eq!(config.logging.log_level, "debug");
274    }
275
276    #[test]
277    fn test_load_config_from_invalid_file() {
278        // Create an invalid JSON content
279        let temp_dir = temp_dir().join("invalid_config");
280        // Create the temporary directory
281        fs::create_dir_all(&temp_dir).expect("Failed to create temporary directory");
282        // Create the temporary file
283        let file_path = temp_dir.join("mutantkraken.config.json");
284        let mut file = File::create(file_path).expect("Failed to create temporary file");
285
286        let invalid_json_content = r#"
287            {
288                "general": {
289                    "timeout": "invalid_timeout_value"
290                }
291            }
292        "#;
293
294        writeln!(file, "{}", invalid_json_content).expect("Failed to write to temporary file");
295        let config = MutantKrakenConfig::load_config(temp_dir);
296        // Since the JSON is invalid, it should fall back to default values
297        assert_eq!(config.general.timeout, None);
298        assert_eq!(
299            config.general.operators,
300            vec![
301                ArithmeticReplacementOperator,
302                UnaryRemovalOperator,
303                LogicalReplacementOperator,
304                RelationalReplacementOperator,
305                AssignmentReplacementOperator,
306                UnaryReplacementOperator,
307                NotNullAssertionOperator,
308                ElvisRemoveOperator,
309                ElvisLiteralChangeOperator,
310                LiteralChangeOperator,
311                ExceptionChangeOperator,
312                WhenRemoveBranchOperator,
313                RemoveLabelOperator,
314                FunctionalBinaryReplacementOperator,
315                FunctionalReplacementOperator,
316            ]
317        );
318    }
319
320    #[test]
321    fn test_load_config_from_missing_file() {
322        // Create a temp directory
323        let temp_dir = temp_dir().join("missing_config");
324        // Create the temp directory
325        fs::create_dir_all(&temp_dir).expect("Failed to create temporary directory");
326        // Test loading config from a missing file
327        let config = MutantKrakenConfig::load_config(temp_dir);
328        // Since the file is missing, it should fall back to default values
329        assert_eq!(config.general.timeout, None);
330        assert_eq!(
331            config.general.operators,
332            vec![
333                ArithmeticReplacementOperator,
334                UnaryRemovalOperator,
335                LogicalReplacementOperator,
336                RelationalReplacementOperator,
337                AssignmentReplacementOperator,
338                UnaryReplacementOperator,
339                NotNullAssertionOperator,
340                ElvisRemoveOperator,
341                ElvisLiteralChangeOperator,
342                LiteralChangeOperator,
343                ExceptionChangeOperator,
344                WhenRemoveBranchOperator,
345                RemoveLabelOperator,
346                FunctionalBinaryReplacementOperator,
347                FunctionalReplacementOperator,
348            ]
349        );
350    }
351}