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}