hide_glue/file_reader.rs
1//! A text file reader
2use std::fmt;
3use std::fs::File;
4use std::io::SeekFrom::Start;
5use std::io::{Read, Seek};
6use std::path::Path;
7
8const FAILED_READ_SENTINEL: usize = 1234567890;
9// We need a second one to ensure inequality for the PartialEq trait.
10const FAILED_READ_SENTINEL_2: usize = 12345678901;
11
12pub struct TextFileReader<'a> {
13 filepath: &'a Path,
14 file: File,
15 size: u64,
16}
17
18/// [`TextFileReader`] simplifies reading a file for assertions. The struct implements
19/// [`PartialEq`] and [`fmt::Debug`] to simplify debugging.
20///
21/// # Usage
22///
23/// ```no_run
24/// use std::fs::OpenOptions;
25/// use std::path::Path;
26/// use hide_glue::file_reader::TextFileReader;
27///
28/// let test_output_path = Path::new("/temporary/file/path");
29/// let expected_output_path = Path::new("tests/fixtures/expected.txt");
30/// let test_output = OpenOptions::new()
31/// .write(true)
32/// .create_new(true)
33/// .open(test_output_path).unwrap();
34///
35/// // write some text to the file; don't forget to flush the buffers!
36///
37/// assert_eq!(
38/// TextFileReader::new(test_output_path)
39/// .expect("Failed to read test_output_path file"),
40/// TextFileReader::new(expected_output_path)
41/// .expect("Failed to read expected_output_path file")
42/// );
43/// ```
44impl<'a> TextFileReader<'a> {
45 /// Open the file and record its length.
46 pub fn new(filepath: &'a Path) -> Result<Self, Box<dyn std::error::Error>> {
47 let file = File::open(filepath)?;
48 let size = file.metadata()?.len();
49 Ok(Self {
50 filepath,
51 file,
52 size,
53 })
54 }
55}
56
57impl fmt::Debug for TextFileReader<'_> {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
59 writeln!(f, "{}", self.filepath.to_string_lossy()).unwrap();
60 let mut my_file = &self.file;
61 my_file.seek(Start(0)).unwrap();
62 let buffer: &mut [u8] = &mut [0; 1024];
63 loop {
64 // Because the interface does not allow reporting errors, write the error to the debug
65 // text output. This ought to be safe because the debug output of this type should not
66 // be mission-critical. If you are reading this comment and cursing this decision,
67 // then you have made a mistake by depending on this output.
68 match my_file.read(buffer) {
69 Ok(bytes_read) => {
70 if bytes_read > 0 {
71 write!(f, "{}", String::from_utf8_lossy(&buffer[0..bytes_read])).unwrap();
72 } else {
73 break;
74 }
75 }
76 Err(err) => {
77 write!(
78 f,
79 "Error: failed to read source file {}: {}",
80 self.filepath.to_string_lossy(),
81 err
82 )
83 .unwrap();
84 return Err(fmt::Error);
85 }
86 }
87 }
88 Ok(())
89 }
90}
91
92impl PartialEq for TextFileReader<'_> {
93 fn eq(&self, other: &Self) -> bool {
94 if self.size != other.size {
95 return false;
96 }
97 let mut my_file = &self.file;
98 let mut other_file = &other.file;
99 my_file.seek(Start(0)).unwrap();
100 other_file.seek(Start(0)).unwrap();
101 let my_buffer: &mut [u8] = &mut [0; 1024];
102 let other_buffer: &mut [u8] = &mut [0; 1024];
103 loop {
104 // It would be good if there were a way to report the read failure here without
105 // breaking the Trait interface.
106 let my_bytes_read = my_file.read(my_buffer).unwrap_or(FAILED_READ_SENTINEL);
107 let other_bytes_read = other_file
108 .read(other_buffer)
109 .unwrap_or(FAILED_READ_SENTINEL_2);
110 if my_bytes_read == other_bytes_read {
111 if my_buffer != other_buffer {
112 return false;
113 }
114 } else {
115 return false;
116 }
117 if my_bytes_read == 0 {
118 break;
119 }
120 }
121 true
122 }
123}