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