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 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}