mod_installer/config/
parser_config.rs

1use crate::{config::meta::Metadata, state::State};
2
3use serde_derive::{Deserialize, Serialize};
4
5pub(crate) const PARSER_CONFIG_LOCATION: &str = "parser";
6
7#[derive(Debug, PartialEq, Serialize, Deserialize)]
8pub(crate) struct ParserConfig {
9    pub(crate) in_progress_words: Vec<String>,
10    pub(crate) useful_status_words: Vec<String>,
11    pub(crate) choice_words: Vec<String>,
12    pub(crate) choice_phrase: Vec<String>,
13    pub(crate) completed_with_warnings: Vec<String>,
14    pub(crate) failed_with_error: Vec<String>,
15    pub(crate) finished: Vec<String>,
16    pub(crate) metadata: Metadata,
17}
18
19impl Default for ParserConfig {
20    fn default() -> Self {
21        Self {
22            in_progress_words: vec!["installing".to_string(), "creating".to_string()],
23            useful_status_words: vec![
24                "copied".to_string(),
25                "copying".to_string(),
26                "creating".to_string(),
27                "installed".to_string(),
28                "installing".to_string(),
29                "patched".to_string(),
30                "patching".to_string(),
31                "processed".to_string(),
32                "processing".to_string(),
33            ],
34            choice_words: vec![
35                "choice".to_string(),
36                "choose".to_string(),
37                "select".to_string(),
38                "enter".to_string(),
39            ],
40            choice_phrase: vec![
41                "do you want".to_string(),
42                "would you like".to_string(),
43                "answer [y]es or [n]o.".to_string(),
44                "is this correct? [y]es or [n]o".to_string(),
45            ],
46            completed_with_warnings: vec!["installed with warnings".to_string()],
47            failed_with_error: vec![
48                "not installed due to errors".to_string(),
49                "installation aborted".to_string(),
50            ],
51            finished: vec![
52                "successfully installed".to_string(),
53                "process ended".to_string(),
54            ],
55            metadata: Metadata::default(),
56        }
57    }
58}
59
60impl ParserConfig {
61    pub fn string_looks_like_question(&self, weidu_output: &str) -> bool {
62        let comparable_output = weidu_output.trim().to_ascii_lowercase();
63        // installing|creating
64        for progress_word in self.in_progress_words.iter() {
65            if comparable_output.contains(progress_word) {
66                return false;
67            }
68        }
69
70        for question in self.choice_phrase.iter() {
71            if comparable_output.contains(question) {
72                return true;
73            }
74        }
75
76        for question in self.choice_words.iter() {
77            for word in comparable_output.split_whitespace() {
78                if word
79                    .chars()
80                    .filter(|c| c.is_alphabetic())
81                    .collect::<String>()
82                    == *question
83                {
84                    return true;
85                }
86            }
87        }
88
89        false
90    }
91
92    pub fn detect_weidu_finished_state(&self, weidu_output: &str) -> State {
93        let comparable_output = weidu_output.trim().to_lowercase();
94        let failure = self.failed_with_error.iter().fold(false, |acc, fail_case| {
95            comparable_output.contains(fail_case) || acc
96        });
97        if failure {
98            return State::CompletedWithErrors {
99                error_details: comparable_output,
100            };
101        }
102        let warning = self
103            .completed_with_warnings
104            .iter()
105            .fold(false, |acc, warn_case| {
106                comparable_output.contains(warn_case) || acc
107            });
108        if warning {
109            return State::CompletedWithWarnings;
110        }
111        let finished = self.finished.iter().fold(false, |acc, success_case| {
112            comparable_output.contains(success_case) || acc
113        });
114        if finished {
115            return State::Completed;
116        }
117        State::InProgress
118    }
119}
120
121#[cfg(test)]
122mod tests {
123
124    use super::*;
125    use pretty_assertions::assert_eq;
126    use std::{error::Error, path::Path, result::Result};
127
128    #[test]
129    fn test_exit_warnings() -> Result<(), Box<dyn Error>> {
130        let config = ParserConfig::default();
131        let test = "INSTALLED WITH WARNINGS     Additional equipment for Thieves and Bards";
132        assert_eq!(config.string_looks_like_question(test), false);
133        assert_eq!(
134            config.detect_weidu_finished_state(test),
135            State::CompletedWithWarnings
136        );
137        Ok(())
138    }
139
140    #[test]
141    fn test_exit_success() -> Result<(), Box<dyn Error>> {
142        let config = ParserConfig::default();
143        let test = "SUCCESSFULLY INSTALLED      Jan's Extended Quest";
144        assert_eq!(config.string_looks_like_question(test), false);
145        assert_eq!(config.detect_weidu_finished_state(test), State::Completed);
146        Ok(())
147    }
148
149    #[test]
150    fn is_a_question() -> Result<(), Box<dyn Error>> {
151        let config = ParserConfig::default();
152        let tests = vec![
153            "Enter the full path to your Baldur's Gate installation then press Enter.",
154            "Enter the full path to your BG:EE+SoD installation then press Enter.\
155Example: C:\\Program Files (x86)\\BeamDog\\Games\\00806",
156            "[N]o, [Q]uit or choose one:",
157            "Please enter the chance for items to randomly not be randomised as a integet number (e.g. 10 for 10%)",
158            "Is this correct? [Y]es or [N]o",
159        ];
160        for test in tests {
161            assert_eq!(
162                config.string_looks_like_question(test),
163                true,
164                "String {} doesn't look like a question",
165                test
166            );
167            assert_eq!(config.detect_weidu_finished_state(test), State::InProgress);
168            assert_eq!(
169                config.useful_status_words.contains(&test.to_string()),
170                false,
171                "String {} looks like useful status words, it should only look like a question",
172                test
173            )
174        }
175        Ok(())
176    }
177
178    #[test]
179    fn is_not_a_question() -> Result<(), Box<dyn Error>> {
180        let config = ParserConfig::default();
181        let tests = vec![
182            "FAILURE:",
183            "NOT INSTALLED DUE TO ERRORS The BG1 NPC Project: Required Modifications",
184            "Creating epilogues. Too many epilogues... Why are there so many options here?",
185            "Including file(s) spellchoices_defensive/vanilla/ENCHANTER.TPH",
186        ];
187        for test in tests {
188            assert_eq!(
189                config.string_looks_like_question(test),
190                false,
191                "String {} does look like a question",
192                test
193            );
194        }
195        Ok(())
196    }
197
198    #[test]
199    fn load_config() -> Result<(), Box<dyn Error>> {
200        let root = std::env::current_dir()?;
201        let config_path = Path::join(&root, Path::new("example_config.toml"));
202        let config: ParserConfig = confy::load_path(config_path)?;
203        let mut expected = ParserConfig::default();
204        expected.metadata = config.metadata.clone();
205        assert_eq!(expected, config);
206        Ok(())
207    }
208
209    #[test]
210    fn failure() -> Result<(), Box<dyn Error>> {
211        let config = ParserConfig::default();
212        let tests = vec![
213            "not installed due to errors the bg1 npc project: required modifications",
214            "installation aborted merge dlc into game -> merge all available dlcs",
215        ];
216        for input in tests {
217            assert_eq!(
218                config.detect_weidu_finished_state(input),
219                State::CompletedWithErrors {
220                    error_details: input.to_string(),
221                },
222                "Input {} did not fail",
223                input
224            );
225        }
226        Ok(())
227    }
228}