config/
parser_config.rs

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