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