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