Refactor tests to put match tests in lib.rs

Also replaces Lipsum with something a bit more readable
master
Nick Krichevsky 2021-11-07 19:41:58 -05:00
parent 121d7f215f
commit d98ab5877d
6 changed files with 170 additions and 143 deletions

7
Cargo.lock generated
View File

@ -192,6 +192,7 @@ name = "hl"
version = "0.1.0"
dependencies = [
"grep",
"stringreader",
"termion",
"test-case",
"thiserror",
@ -356,6 +357,12 @@ dependencies = [
"serde",
]
[[package]]
name = "stringreader"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913e7b03d63752f6cdd2df77da36749d82669904798fe8944b9ec3d23f159905"
[[package]]
name = "syn"
version = "1.0.81"

View File

@ -12,3 +12,4 @@ termion = "1"
[dev-dependencies]
test-case = "1.2.1"
stringreader = "0.1"

View File

@ -74,3 +74,97 @@ pub fn scan_pattern_to_printer<R: Read, P: Printer>(
searcher.search_reader(matcher, reader, context_sink)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
use stringreader::StringReader;
use test_case::test_case;
use testutil::mock_print::MockPrinter;
const SEARCH_TEXT: &str = "The \"computable\" numbers may be described briefly as the real \n\
numbers whose expressions as a decimal are calculable by finite means. \n\
Although the subject of this paper is ostensibly the computable numbers. \n\
it is almost equally easy to define and investigate computable functions \n\
of an integral variable or a real or computable variable, computable \n\
predicates, and so forth. The fundamental problems involved are, \n\
however, the same in each case, and I have chosen the computable numbers \n\
for explicit treatment as involving the least cumbrous technique. I hope \n\
shortly to give an account of the relations of the computable numbers, \n\
functions, and so forth to one another. This will include a development \n\
of the theory of functions of a real variable expressed in terms of \n\
computable numbers. According to my definition, a number is computable \n\
if its decimal can be written down by a machine.\n";
#[test]
fn test_highlights_matches() {
let mock_printer = MockPrinter::default();
let mut lipsum_reader = StringReader::new(SEARCH_TEXT);
let res = scan_pattern_to_printer(
&mut lipsum_reader,
r#""?computable"?\snumbers"#,
&mock_printer,
);
if let Err(err) = res {
panic!("failed to search: {}", err)
}
let colored_messages = mock_printer.colored_messages.borrow();
#[rustfmt::skip]
let expected_colored_messages = [
"The \"computable\" numbers may be described briefly as the real \n".to_string(),
"Although the subject of this paper is ostensibly the computable numbers. \n".to_string(),
"however, the same in each case, and I have chosen the computable numbers \n".to_string(),
"shortly to give an account of the relations of the computable numbers, \n".to_string(),
"computable numbers. According to my definition, a number is computable \n".to_string(),
];
assert!(
testutil::are_slices_eq(&colored_messages, &expected_colored_messages),
"(expected) {:?} != (actual) {:?}",
expected_colored_messages,
colored_messages,
);
let uncolored_messages = mock_printer.messages.borrow();
#[rustfmt::skip]
let expected_uncolored_messages = [
"numbers whose expressions as a decimal are calculable by finite means. \n".to_string(),
"it is almost equally easy to define and investigate computable functions \n".to_string(),
"of an integral variable or a real or computable variable, computable \n".to_string(),
"predicates, and so forth. The fundamental problems involved are, \n".to_string(),
"for explicit treatment as involving the least cumbrous technique. I hope \n".to_string(),
"functions, and so forth to one another. This will include a development \n".to_string(),
"of the theory of functions of a real variable expressed in terms of \n".to_string(),
"if its decimal can be written down by a machine.\n".to_string(),
];
assert!(
testutil::are_slices_eq(&uncolored_messages, &expected_uncolored_messages),
"(expected) {:?} != (actual) {:?}",
expected_uncolored_messages,
colored_messages,
);
}
#[test_case(".", 0, 1; "failure on first match will only attempt to print that match")]
#[test_case("hello I am alan turing", 1, 0; "never matching will only attempt to print the first line")]
fn test_does_not_attempt_to_print_after_broken_pipe_error(
pattern: &str,
num_uncolored_messages: usize,
num_colored_messages: usize,
) {
let mut mock_printer = MockPrinter::default();
let broken_pipe_err =
print::Error::from(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe"));
mock_printer.fail_next(broken_pipe_err);
let mut lipsum_reader = StringReader::new(SEARCH_TEXT);
let res = scan_pattern_to_printer(&mut lipsum_reader, pattern, &mock_printer);
assert!(!res.is_err(), "failed to search: {:?}", res.unwrap_err());
assert_eq!(
num_colored_messages,
mock_printer.colored_messages.borrow().len()
);
assert_eq!(num_uncolored_messages, mock_printer.messages.borrow().len());
}
}

View File

@ -115,151 +115,16 @@ impl<P: Printer> Sink for ContextPrintingSink<P> {
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::mock_print::MockPrinter;
use grep::regex::RegexMatcher;
use grep::searcher::SearcherBuilder;
use std::cell::RefCell;
use std::fmt;
use std::io;
use test_case::test_case;
#[derive(Default)]
struct MockPrinter {
messages: RefCell<Vec<String>>,
colored_messages: RefCell<Vec<String>>,
next_error: RefCell<Option<print::Error>>,
}
impl MockPrinter {
fn fail_next(&mut self, error: print::Error) {
self.next_error.replace(Some(error));
}
}
impl Printer for &MockPrinter {
fn print<S: fmt::Display>(&self, msg: S) -> print::Result {
self.messages.borrow_mut().push(msg.to_string());
if self.next_error.borrow().is_some() {
Err(self.next_error.replace(None).unwrap())
} else {
Ok(())
}
}
fn colored_print<S: fmt::Display, C: color::Color>(
&self,
_color: color::Fg<C>,
msg: S,
) -> print::Result {
// Unfortunately, termion colors don't implement PartialEq, so checking for the exact color is not
// feasible unless we wanted to write a wrapper, which I don't care enough to just for unit testing
self.colored_messages.borrow_mut().push(msg.to_string());
if self.next_error.borrow().is_some() {
Err(self.next_error.replace(None).unwrap())
} else {
Ok(())
}
}
}
const LIPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel magna a magna porta \n\
viverra eu ac metus. Integer auctor enim id turpis mollis, quis sagittis nulla accumsan. Nam sagittis lorem ut \n\
elit convallis ultricies. Suspendisse sed lobortis enim. Nulla lobortis tristique maximus. Mauris fermentum urna \n\
id ex finibus commodo. Aliquam erat volutpat. Maecenas tristique erat vel consectetur varius. Fusce a \n\
condimentum orci. Praesent at rhoncus felis, et tempus nulla. Morbi consectetur, elit quis interdum tincidunt, \n\
felis risus malesuada elit, non feugiat tortor velit vel risus. Nullam a odio sodales, iaculis quam sit amet, \n\
molestie dolor. Praesent et nibh id nisl convallis hendrerit ac sed sapien. Fusce tempus venenatis odio, \n\
ut maximus nisi egestas vel.";
fn are_slices_eq<T: PartialEq>(v1: &[T], v2: &[T]) -> bool {
if v1.len() != v2.len() {
return false;
}
// https://stackoverflow.com/a/29504547
let len = v1.len();
v1.iter().zip(v2).filter(|&(a, b)| a == b).count() == len
}
#[test]
fn test_highlights_matches() {
let matcher = RegexMatcher::new("Integer").expect("regexp doesn't compile");
let mock_printer = MockPrinter::default();
let sink = ContextPrintingSink {
printer: &mock_printer,
};
let res = SearcherBuilder::new().passthru(true).build().search_slice(
matcher,
LIPSUM.as_bytes(),
sink,
);
if let Err(err) = res {
panic!("failed to search: {}", err)
}
let colored_messages = mock_printer.colored_messages.borrow();
let expected_colored_messages = ["viverra eu ac metus. Integer auctor enim id turpis mollis, quis sagittis nulla accumsan. Nam sagittis lorem ut \n".to_string()];
assert!(
are_slices_eq(&colored_messages, &expected_colored_messages),
"(expected) {:?} != (actual) {:?}",
expected_colored_messages,
colored_messages,
);
let uncolored_messages = mock_printer.messages.borrow();
let expected_uncolored_messages = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel magna a magna porta \n".to_string(),
// only missing the line that's in expected
"elit convallis ultricies. Suspendisse sed lobortis enim. Nulla lobortis tristique maximus. Mauris fermentum urna \n".to_string(),
"id ex finibus commodo. Aliquam erat volutpat. Maecenas tristique erat vel consectetur varius. Fusce a \n".to_string(),
"condimentum orci. Praesent at rhoncus felis, et tempus nulla. Morbi consectetur, elit quis interdum tincidunt, \n".to_string(),
"felis risus malesuada elit, non feugiat tortor velit vel risus. Nullam a odio sodales, iaculis quam sit amet, \n".to_string(),
"molestie dolor. Praesent et nibh id nisl convallis hendrerit ac sed sapien. Fusce tempus venenatis odio, \n".to_string(),
"ut maximus nisi egestas vel.".to_string()
];
assert!(
are_slices_eq(&uncolored_messages, &expected_uncolored_messages),
"(expected) {:?} != (actual) {:?}",
expected_uncolored_messages,
colored_messages,
);
}
#[test_case(".", 0, 1; "failure on first match will only attempt to print that match")]
#[test_case("this doesn't appear in lorem ipsum", 1, 0; "never matching will only attempt to print the first line")]
fn test_does_not_attempt_to_print_after_broken_pipe_error(
pattern: &str,
num_uncolored_messages: usize,
num_colored_messages: usize,
) {
let matcher = RegexMatcher::new(pattern).expect("regexp doesn't compile");
let mut mock_printer = MockPrinter::default();
let broken_pipe_err =
print::Error::from(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe"));
mock_printer.fail_next(broken_pipe_err);
let sink = ContextPrintingSink {
printer: &mock_printer,
};
let res = SearcherBuilder::new().passthru(true).build().search_slice(
matcher,
LIPSUM.as_bytes(),
sink,
);
assert!(!res.is_err(), "failed to search: {:?}", res.unwrap_err());
assert_eq!(
num_colored_messages,
mock_printer.colored_messages.borrow().len()
);
assert_eq!(num_uncolored_messages, mock_printer.messages.borrow().len());
}
const SEARCH_TEXT: &str = "The quick \n\
brown fox \n\
jumped over \n\
the lazy \n\
dog.";
// TODO: This is a bit overkill for a single setting, and could probably be simplified
enum RequiredSearcherSettings {
@ -274,7 +139,7 @@ mod tests {
) {
// This must be wrapped so we can safely use `panic::catch_unwind`
let perform_search = || {
let matcher = RegexMatcher::new("Integer").expect("regexp doesn't compile");
let matcher = RegexMatcher::new("fox").expect("regexp doesn't compile");
let mock_printer = MockPrinter::default();
let sink = ContextPrintingSink {
@ -289,7 +154,7 @@ mod tests {
}
let mut searcher = builder.build();
searcher.search_slice(matcher, LIPSUM.as_bytes(), sink)
searcher.search_slice(matcher, SEARCH_TEXT.as_bytes(), sink)
};
if valid {

13
src/testutil.rs Normal file
View File

@ -0,0 +1,13 @@
#![cfg(test)]
#![allow(dead_code)]
pub(crate) mod mock_print;
pub(crate) fn are_slices_eq<T: PartialEq>(v1: &[T], v2: &[T]) -> bool {
if v1.len() != v2.len() {
return false;
}
// https://stackoverflow.com/a/29504547
let len = v1.len();
v1.iter().zip(v2).filter(|&(a, b)| a == b).count() == len
}

View File

@ -0,0 +1,47 @@
#![cfg(test)]
use crate::print;
use crate::print::Printer;
use std::cell::RefCell;
use std::fmt;
use termion::color;
#[derive(Default)]
pub(crate) struct MockPrinter {
pub(crate) messages: RefCell<Vec<String>>,
pub(crate) colored_messages: RefCell<Vec<String>>,
next_error: RefCell<Option<print::Error>>,
}
impl MockPrinter {
pub(crate) fn fail_next(&mut self, error: print::Error) {
self.next_error.replace(Some(error));
}
}
impl Printer for &MockPrinter {
fn print<S: fmt::Display>(&self, msg: S) -> print::Result {
self.messages.borrow_mut().push(msg.to_string());
if self.next_error.borrow().is_some() {
Err(self.next_error.replace(None).unwrap())
} else {
Ok(())
}
}
fn colored_print<S: fmt::Display, C: color::Color>(
&self,
_color: color::Fg<C>,
msg: S,
) -> print::Result {
// Unfortunately, termion colors don't implement PartialEq, so checking for the exact color is not
// feasible unless we wanted to write a wrapper, which I don't care enough to just for unit testing
self.colored_messages.borrow_mut().push(msg.to_string());
if self.next_error.borrow().is_some() {
Err(self.next_error.replace(None).unwrap())
} else {
Ok(())
}
}
}