serial_unit_testing/tests/
test_case.rs1use std::str;
30use std::time::Duration;
31use std::thread::sleep;
32#[cfg(feature = "colored-tests")]
33use colored::*;
34use regex::Regex;
35use crate::serial::Serial;
36use crate::utils;
37
38#[derive(Debug, Clone)]
42pub struct TestCaseSettings {
43 pub ignore_case: Option<bool>,
45 pub repeat: Option<u32>,
47 pub delay: Option<Duration>,
49 pub timeout: Option<Duration>,
51 pub allow_failure: Option<bool>,
53 pub verbose: Option<bool>
55}
56
57impl TestCaseSettings {
58 pub fn merge_weak(&mut self, other: &TestCaseSettings) {
63 if self.ignore_case.is_none() && other.ignore_case.is_some() {
64 self.ignore_case = other.ignore_case;
65 }
66
67 if self.repeat.is_none() && other.repeat.is_some() {
68 self.repeat = other.repeat;
69 }
70
71 if self.delay.is_none() && other.delay.is_some() {
72 self.delay = other.delay;
73 }
74
75 if self.timeout.is_none() && other.timeout.is_some() {
76 self.timeout = other.timeout;
77 }
78
79 if self.allow_failure.is_none() && other.allow_failure.is_some() {
80 self.allow_failure = other.allow_failure;
81 }
82
83 if self.verbose.is_none() && other.verbose.is_some() {
84 self.verbose = other.verbose;
85 }
86 }
87}
88
89impl Default for TestCaseSettings {
90 fn default() -> TestCaseSettings {
91 TestCaseSettings {
92 ignore_case: None,
93 repeat: None,
94 delay: None,
95 timeout: None,
96 allow_failure: None,
97 verbose: None
98 }
99 }
100}
101
102#[derive(Debug)]
104pub struct TestCase {
105 pub settings: TestCaseSettings,
107 pub input_format: utils::TextFormat,
109 pub output_format: utils::TextFormat,
111
112 name: String,
113 input: String,
114 output: String,
115 response: Option<String>,
116 successful: Option<bool>,
117 error: Option<String>
118}
119
120impl TestCase {
121 pub fn new(name: String, input: String, output: String) -> TestCase {
123 TestCase {
124 name,
125 input,
126 output,
127 settings: Default::default(),
128 input_format: utils::TextFormat::Text,
129 output_format: utils::TextFormat::Text,
130 response: None,
131 successful: None,
132 error: None
133 }
134 }
135
136 pub fn run(&mut self, serial: &mut Serial) -> Result<bool, String> {
140 let input: String;
142 let mut output: String;
143
144 if self.input_format == utils::TextFormat::Text {
145 input = self.descape_string(&self.input);
146 } else {
147 input = self.input.clone();
148 }
149
150 if self.output_format == utils::TextFormat::Text {
151 output = self.descape_string(&self.output);
152 } else {
153 output = self.output.clone();
154 }
155
156 if self.settings.ignore_case.unwrap_or(false) {
157 output = output.to_lowercase();
158 } else if self.output_format == utils::TextFormat::Hex {
159 output = output.to_uppercase();
160 }
161
162 let regex = match Regex::new(&output) {
163 Ok(regex) => regex,
164 Err(_) => return self.exit_run_with_error(format!("Error in regex"))
165 };
166
167 let mut repeat = 1;
169 let mut success: bool = false;
170
171 if let Some(count) = self.settings.repeat {
172 repeat += count;
173 }
174
175 for _ in 0..repeat {
176 if let Some(delay) = self.settings.delay {
178 sleep(delay);
179 }
180
181 match serial.write_format(&input, self.input_format) {
182 Ok(_) => (),
183 Err(e) => return self.exit_run_with_error(format!("Unable to write to serial port: {}", e))
184 };
185
186 let response = match self.read_response(serial, ®ex) {
187 Ok(res) => res,
188 Err(err) => return self.exit_run_with_error(err)
189 };
190
191 if let Some(mat) = regex.find(&response) {
193 success = mat.start() == 0 && mat.end() == response.len();
194 } else {
195 success = false;
196 }
197
198 self.response = Some(response);
199
200 if success == false {
201 break;
202 }
203 }
204
205 self.successful = Some(success);
206
207 Ok(success)
208 }
209
210 pub fn is_successful(&self) -> Option<bool> {
214 self.successful
215 }
216
217 pub fn error(&self) -> Option<String> {
221 self.error.clone()
222 }
223
224 fn read_response(&mut self, serial: &mut Serial, regex: &Regex) -> Result<String, String> {
225 let mut response = String::new();
226
227 loop {
228 let response_chunk;
229
230 if let Some(timeout) = self.settings.timeout {
231 response_chunk = serial.read_with_timeout(timeout);
232 } else {
233 response_chunk = serial.read();
234 }
235
236 match response_chunk {
237 Ok(bytes) => {
238 let mut new_text = match self.output_format {
239 utils::TextFormat::Text => str::from_utf8(bytes).unwrap().to_string(),
240 _ => match utils::radix_string(bytes, &self.output_format) {
241 Ok(text) => text,
242 Err(e) => return Err(format!("Error converting response {}", e))
243 }
244 };
245
246 if self.settings.ignore_case.unwrap_or(false) {
247 new_text = new_text.to_lowercase();
248 }
249
250 response.push_str(new_text.as_str());
251
252 if regex.find(&response).is_some() {
253 break;
254 }
255 },
256 Err(e) if e.is_timeout() => {
257 if response.len() == 0 {
258 return Err("Connection timed out".to_string());
259 }
260
261 break;
262 },
263 Err(e) => return Err(format!("Error while running test {}", e))
264 }
265 }
266
267 Ok(response)
268 }
269
270 fn title(&self) -> String {
271 if self.name.is_empty() == false {
272 format!("{} \"{}\"", self.name, self.input)
273 } else {
274 self.input.clone()
275 }
276 }
277
278 fn descape_string(&self, text: &str) -> String {
279 let mut response = String::new();
280 let mut descape_next_char = false;
281 let mut iterator = text.chars();
282
283 loop {
284 match iterator.next() {
285 Some('t') if descape_next_char => response.push('\t'),
286 Some('r') if descape_next_char => response.push('\r'),
287 Some('n') if descape_next_char => response.push('\n'),
288 Some('\\') if descape_next_char == false => {
289 descape_next_char = true;
290
291 continue;
292 },
293 Some(ch) => response.push(ch),
294 None => break
295 };
296
297 descape_next_char = false;
298 }
299
300 response
301 }
302
303 fn exit_run_with_error(&mut self, err: String) -> Result<bool, String> {
304 self.error = Some(err.clone());
305
306 Err(err)
307 }
308
309 #[cfg(feature = "colored-tests")]
310 fn red_text(text: &str) -> ColoredString {
311 text.red()
312 }
313
314 #[cfg(not(feature = "colored-tests"))]
315 fn red_text(text: &str) -> String {
316 text.to_string()
317 }
318
319 #[cfg(feature = "colored-tests")]
320 fn green_text(text: &str) -> ColoredString {
321 text.green()
322 }
323
324 #[cfg(not(feature = "colored-tests"))]
325 fn green_text(text: &str) -> String {
326 text.to_string()
327 }
328
329 #[cfg(feature = "colored-tests")]
330 fn yellow_text(text: &str) -> ColoredString {
331 text.yellow()
332 }
333
334 #[cfg(not(feature = "colored-tests"))]
335 fn yellow_text(text: &str) -> String {
336 text.to_string()
337 }
338}
339
340impl ToString for TestCase {
341 fn to_string(&self) -> String {
342 if let Some(err) = &self.error {
343 return format!("{}...{} {}", self.title(), TestCase::red_text("Error:"), err);
344 }
345
346 if let Some(successful) = self.successful {
347 if successful == false && self.settings.allow_failure.unwrap_or(false) == false {
348 return if let Some(ref response) = self.response {
349 format!("{}...{}, expected '{}' but received '{}'", self.title(), TestCase::red_text("Failed"), self.output, response)
350 } else {
351 format!("{}...{}, expected '{}' but received nothing", self.title(), TestCase::red_text("Failed"), self.output)
352 };
353 }
354
355 let repeat = if let Some(count) = self.settings.repeat {
357 format!(" ({}x)", count)
358 } else {
359 String::new()
360 };
361
362 let verbose = if self.settings.verbose.unwrap_or(false) {
363 if let Some(ref response) = self.response {
364 format!(", response: '{}'", response)
365 } else {
366 format!(", no response")
367 }
368 } else {
369 String::new()
370 };
371
372 let result = if successful {
373 format!("{}", TestCase::green_text("OK"))
374 } else {
375 format!("{} (failed)", TestCase::yellow_text("OK"))
376 };
377
378 format!("{}...{}{}{}", self.title(), result, repeat, verbose)
379 } else {
380 format!("{}", self.title())
381 }
382 }
383}