From 2b10cc47ff252b7e450a00b93cd198c50dd0f695 Mon Sep 17 00:00:00 2001 From: Wim Hueskes Date: Tue, 2 Aug 2016 22:55:23 +0200 Subject: [PATCH] od: implement Read for MultifileReader also add tests and fix error handling --- src/od/mockstream.rs | 102 ++++++++++++++++++++++++++ src/od/multifilereader.rs | 146 ++++++++++++++++++++++++++++++-------- src/od/od.rs | 22 +++--- 3 files changed, 229 insertions(+), 41 deletions(-) create mode 100644 src/od/mockstream.rs diff --git a/src/od/mockstream.rs b/src/od/mockstream.rs new file mode 100644 index 000000000..4dd02e22f --- /dev/null +++ b/src/od/mockstream.rs @@ -0,0 +1,102 @@ +// https://github.com/lazy-bitfield/rust-mockstream/pull/2 + +use std::io::{Cursor, Read, Result, Error, ErrorKind}; +use std::error::Error as errorError; + +/// `FailingMockStream` mocks a stream which will fail upon read or write +/// +/// # Examples +/// +/// ``` +/// use std::io::{Cursor, Read}; +/// +/// struct CountIo {} +/// +/// impl CountIo { +/// fn read_data(&self, r: &mut Read) -> usize { +/// let mut count: usize = 0; +/// let mut retries = 3; +/// +/// loop { +/// let mut buffer = [0; 5]; +/// match r.read(&mut buffer) { +/// Err(_) => { +/// if retries == 0 { break; } +/// retries -= 1; +/// }, +/// Ok(0) => break, +/// Ok(n) => count += n, +/// } +/// } +/// count +/// } +/// } +/// +/// #[test] +/// fn test_io_retries() { +/// let mut c = Cursor::new(&b"1234"[..]) +/// .chain(FailingMockStream::new(ErrorKind::Other, "Failing", 3)) +/// .chain(Cursor::new(&b"5678"[..])); +/// +/// let sut = CountIo {}; +/// // this will fail unless read_data performs at least 3 retries on I/O errors +/// assert_eq!(8, sut.read_data(&mut c)); +/// } +/// ``` +#[derive(Clone)] +pub struct FailingMockStream { + kind: ErrorKind, + message: &'static str, + repeat_count: i32, +} + +impl FailingMockStream { + /// Creates a FailingMockStream + /// + /// When `read` or `write` is called, it will return an error `repeat_count` times. + /// `kind` and `message` can be specified to define the exact error. + pub fn new(kind: ErrorKind, message: &'static str, repeat_count: i32) -> FailingMockStream { + FailingMockStream { kind: kind, message: message, repeat_count: repeat_count, } + } + + fn error(&mut self) -> Result { + if self.repeat_count == 0 { + return Ok(0) + } + else { + if self.repeat_count > 0 { + self.repeat_count -= 1; + } + Err(Error::new(self.kind, self.message)) + } + } +} + +impl Read for FailingMockStream { + fn read(&mut self, _: &mut [u8]) -> Result { + self.error() + } +} + +#[test] +fn test_failing_mock_stream_read() { + let mut s = FailingMockStream::new(ErrorKind::BrokenPipe, "The dog ate the ethernet cable", 1); + let mut v = [0; 4]; + let error = s.read(v.as_mut()).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::BrokenPipe); + assert_eq!(error.description(), "The dog ate the ethernet cable"); + // after a single error, it will return Ok(0) + assert_eq!(s.read(v.as_mut()).unwrap(), 0); +} + +#[test] +fn test_failing_mock_stream_chain_interrupted() { + let mut c = Cursor::new(&b"abcd"[..]) + .chain(FailingMockStream::new(ErrorKind::Interrupted, "Interrupted", 5)) + .chain(Cursor::new(&b"ABCD"[..])); + + let mut v = [0; 8]; + c.read_exact(v.as_mut()).unwrap(); + assert_eq!(v, [0x61, 0x62, 0x63, 0x64, 0x41, 0x42, 0x43, 0x44]); + assert_eq!(c.read(v.as_mut()).unwrap(), 0); +} diff --git a/src/od/multifilereader.rs b/src/od/multifilereader.rs index c2bcb688e..6df298f2b 100644 --- a/src/od/multifilereader.rs +++ b/src/od/multifilereader.rs @@ -3,24 +3,26 @@ use std::io; use std::io::BufReader; use std::fs::File; use std::io::Write; +use std::vec::Vec; -#[derive(Debug)] pub enum InputSource<'a> { FileName(&'a str ), - Stdin + Stdin, + #[allow(dead_code)] + Stream(Box), } // MultifileReader - concatenate all our input, file or stdin. pub struct MultifileReader<'a> { - ni: std::slice::Iter<'a, InputSource<'a>>, + ni: Vec>, curr_file: Option>, pub any_err: bool, } impl<'b> MultifileReader<'b> { - pub fn new<'a>(fnames: &'a [InputSource]) -> MultifileReader<'a> { + pub fn new<'a>(fnames: Vec>) -> MultifileReader<'a> { let mut mf = MultifileReader { - ni: fnames.iter(), + ni: fnames, curr_file: None, // normally this means done; call next_file() any_err: false, }; @@ -31,47 +33,50 @@ impl<'b> MultifileReader<'b> { fn next_file(&mut self) { // loop retries with subsequent files if err - normally 'loops' once loop { - match self.ni.next() { - None => { + if self.ni.len() == 0 { self.curr_file = None; return; + } + match self.ni.remove(0) { + InputSource::Stdin => { + self.curr_file = Some(Box::new(BufReader::new(std::io::stdin()))); + return; } - Some(input) => { - match *input { - InputSource::Stdin => { - self.curr_file = Some(Box::new(BufReader::new(std::io::stdin()))); + InputSource::FileName(fname) => { + match File::open(fname) { + Ok(f) => { + self.curr_file = Some(Box::new(BufReader::new(f))); return; } - InputSource::FileName(fname) => { - match File::open(fname) { - Ok(f) => { - self.curr_file = Some(Box::new(BufReader::new(f))); - return; - } - Err(e) => { - // If any file can't be opened, - // print an error at the time that the file is needed, - // then move on the the next file. - // This matches the behavior of the original `od` - eprintln!("{}: '{}': {}", - executable!().split("::").next().unwrap(), // remove module - fname, e); - self.any_err = true - } - } + Err(e) => { + // If any file can't be opened, + // print an error at the time that the file is needed, + // then move on the the next file. + // This matches the behavior of the original `od` + eprintln!("{}: '{}': {}", + executable!().split("::").next().unwrap(), // remove module + fname, e); + self.any_err = true } } } + InputSource::Stream(s) => { + self.curr_file = Some(s); + return; + } } } } +} + +impl<'b> io::Read for MultifileReader<'b> { // Fill buf with bytes read from the list of files // Returns Ok() // Handles io errors itself, thus always returns OK // Fills the provided buffer completely, unless it has run out of input. // If any call returns short (< buf.len()), all subsequent calls will return Ok<0> - pub fn f_read(&mut self, buf: &mut [u8]) -> io::Result { + fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut xfrd = 0; // while buffer we are filling is not full.. May go thru several files. 'fillloop: while xfrd < buf.len() { @@ -83,7 +88,13 @@ impl<'b> MultifileReader<'b> { xfrd += match curr_file.read(&mut buf[xfrd..]) { Ok(0) => break, Ok(n) => n, - Err(e) => panic!("file error: {}", e), + Err(e) => { + eprintln!("{}: I/O: {}", + executable!().split("::").next().unwrap(), // remove module + e); + self.any_err = true; + break; + }, }; if xfrd == buf.len() { // transferred all that was asked for. @@ -97,3 +108,78 @@ impl<'b> MultifileReader<'b> { Ok(xfrd) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Cursor, Read, ErrorKind}; + use mockstream::*; + + #[test] + fn test_multi_file_reader_one_read() { + let mut inputs = Vec::new(); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let mut v = [0; 10]; + + let mut sut = MultifileReader::new(inputs); + + assert_eq!(sut.read(v.as_mut()).unwrap(), 8); + assert_eq!(v, [0x61, 0x62, 0x63, 0x64, 0x41, 0x42, 0x43, 0x44, 0, 0]); + assert_eq!(sut.read(v.as_mut()).unwrap(), 0); + } + + #[test] + fn test_multi_file_reader_two_reads() { + let mut inputs = Vec::new(); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let mut v = [0; 5]; + + let mut sut = MultifileReader::new(inputs); + + assert_eq!(sut.read(v.as_mut()).unwrap(), 5); + assert_eq!(v, [0x61, 0x62, 0x63, 0x64, 0x41]); + assert_eq!(sut.read(v.as_mut()).unwrap(), 3); + assert_eq!(v, [0x42, 0x43, 0x44, 0x64, 0x41]); // last two bytes are not overwritten + } + + #[test] + fn test_multi_file_reader_read_error() { + let c = Cursor::new(&b"1234"[..]) + .chain(FailingMockStream::new(ErrorKind::Other, "Failing", 1)) + .chain(Cursor::new(&b"5678"[..])); + let mut inputs = Vec::new(); + inputs.push(InputSource::Stream(Box::new(c))); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + let mut v = [0; 5]; + + let mut sut = MultifileReader::new(inputs); + + assert_eq!(sut.read(v.as_mut()).unwrap(), 5); + assert_eq!(v, [49, 50, 51, 52, 65]); + assert_eq!(sut.read(v.as_mut()).unwrap(), 3); + assert_eq!(v, [66, 67, 68, 52, 65]); // last two bytes are not overwritten + + // note: no retry on i/o error, so 5678 is missing + } + + #[test] + fn test_multi_file_reader_read_error_at_start() { + let mut inputs = Vec::new(); + inputs.push(InputSource::Stream(Box::new(FailingMockStream::new(ErrorKind::Other, "Failing", 1)))); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"abcd"[..])))); + inputs.push(InputSource::Stream(Box::new(FailingMockStream::new(ErrorKind::Other, "Failing", 1)))); + inputs.push(InputSource::Stream(Box::new(Cursor::new(&b"ABCD"[..])))); + inputs.push(InputSource::Stream(Box::new(FailingMockStream::new(ErrorKind::Other, "Failing", 1)))); + let mut v = [0; 5]; + + let mut sut = MultifileReader::new(inputs); + + assert_eq!(sut.read(v.as_mut()).unwrap(), 5); + assert_eq!(v, [0x61, 0x62, 0x63, 0x64, 0x41]); + assert_eq!(sut.read(v.as_mut()).unwrap(), 3); + assert_eq!(v, [0x42, 0x43, 0x44, 0x64, 0x41]); // last two bytes are not overwritten + } + +} diff --git a/src/od/od.rs b/src/od/od.rs index 827c48816..c8ab51f2e 100644 --- a/src/od/od.rs +++ b/src/od/od.rs @@ -22,8 +22,11 @@ mod formatteriteminfo; mod prn_int; mod prn_char; mod prn_float; +#[cfg(test)] +mod mockstream; use std::cmp; +use std::io::Read; use std::io::Write; use unindent::*; use byteorder_io::*; @@ -134,8 +137,7 @@ pub fn uumain(args: Vec) -> i32 { }; // Gather up file names - args which don't start with '-' - let stdnionly = [InputSource::Stdin]; - let inputs = args[1..] + let mut inputs = args[1..] .iter() .filter_map(|w| match w as &str { "--" => Some(InputSource::Stdin), @@ -143,12 +145,10 @@ pub fn uumain(args: Vec) -> i32 { x => Some(InputSource::FileName(x)), }) .collect::>(); - // If no input files named, use stdin. - let inputs = if inputs.len() == 0 { - &stdnionly[..] - } else { - &inputs[..] - }; + if inputs.len() == 0 { + inputs.push(InputSource::Stdin); + } + // Gather up format flags, we don't use getopts becase we need keep them in order. let flags = args[1..] .iter() @@ -216,11 +216,11 @@ pub fn uumain(args: Vec) -> i32 { let output_duplicates = matches.opt_present("v"); - odfunc(line_bytes, input_offset_base, byte_order, &inputs, &formats[..], output_duplicates) + odfunc(line_bytes, input_offset_base, byte_order, inputs, &formats[..], output_duplicates) } fn odfunc(line_bytes: usize, input_offset_base: Radix, byte_order: ByteOrder, - fnames: &[InputSource], formats: &[FormatterItemInfo], output_duplicates: bool) -> i32 { + fnames: Vec, formats: &[FormatterItemInfo], output_duplicates: bool) -> i32 { let mut mf = MultifileReader::new(fnames); let mut addr = 0; @@ -270,7 +270,7 @@ fn odfunc(line_bytes: usize, input_offset_base: Radix, byte_order: ByteOrder, // print each line data (or multi-format raster of several lines describing the same data). // TODO: we need to read more data in case a multi-byte sequence starts at the end of the line - match mf.f_read(bytes.as_mut_slice()) { + match mf.read(bytes.as_mut_slice()) { Ok(0) => { if input_offset_base != Radix::NoPrefix { print!("{}\n", print_with_radix(input_offset_base, addr)); // print final offset