serial_unit_testing/tests/
test_case.rs

1/*
2 * File: tests/test_case.rs
3 * Date: 03.10.2018
4 * Author: MarkAtk
5 * 
6 * MIT License
7 * 
8 * Copyright (c) 2018 MarkAtk
9 * 
10 * Permission is hereby granted, free of charge, to any person obtaining a copy of
11 * this software and associated documentation files (the "Software"), to deal in
12 * the Software without restriction, including without limitation the rights to
13 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
14 * of the Software, and to permit persons to whom the Software is furnished to do
15 * so, subject to the following conditions:
16 * 
17 * The above copyright notice and this permission notice shall be included in all
18 * copies or substantial portions of the Software.
19 * 
20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 * SOFTWARE.
27 */
28
29use 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/// Settings for running a test.
39///
40/// All not set properties will be overwritten by the test suite.
41#[derive(Debug, Clone)]
42pub struct TestCaseSettings {
43    /// Ignore case for string comparison.
44    pub ignore_case: Option<bool>,
45    /// Repeat the test given times.
46    pub repeat: Option<u32>,
47    /// Wait given milliseconds before executing the test.
48    pub delay: Option<Duration>,
49    /// Timeout duration before the test will fail.
50    pub timeout: Option<Duration>,
51    /// Allow the test to fail.
52    pub allow_failure: Option<bool>,
53    /// Print additional information when executing the test.
54    pub verbose: Option<bool>
55}
56
57impl TestCaseSettings {
58    /// Merge test settings with other test settings.
59    ///
60    /// Properties will be set if own property is not set but other's is.
61    /// Own properties will not be overwritten.
62    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/// Test representing a check on the serial.
103#[derive(Debug)]
104pub struct TestCase {
105    /// Settings to use for the test.
106    pub settings: TestCaseSettings,
107    /// Text format of the input send to the serial.
108    pub input_format: utils::TextFormat,
109    /// Text format of the response received by the serial.
110    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    /// Create a new test.
122    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    /// Execute the test on given serial port.
137    ///
138    /// After running the test response and successful are set if no error occurred.
139    pub fn run(&mut self, serial: &mut Serial) -> Result<bool, String> {
140        // get input and desired output in correct format
141        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        // run test repeat + 1 times
168        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 delay is set wait before execution
177            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, &regex) {
187                Ok(res) => res,
188                Err(err) => return self.exit_run_with_error(err)
189            };
190
191            // check if response is correct
192            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    /// Check if the test was successful.
211    ///
212    /// If the test was not run before None will be returned.
213    pub fn is_successful(&self) -> Option<bool> {
214        self.successful
215    }
216
217    /// Get the error from running the test.
218    ///
219    /// If the test was not run before or no error occurred None will be returned.
220    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            // test passed
356            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}