config/
parser_config.rs

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