mod_installer/config/
parser_config.rs

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