0

Summary

I have an application that starts another process and transfers its StdOut/StdErr to a log file using the log crate. My application transfers the output line by line (buf_read.read_line()). As it can be any arbitrary process, my application makes the assumption that the other process may be malicious and may try to print to stdout/sterr enormous amounts of data without a single newline, thus causing OOM in my application. Hence my application limits the number of bytes the BufReader can read at a time using BufReader.take().

The problem

Ignoring all the details about chunking the input, how can I test that my logger was called X times with the correct parameters ? Let's assume my app has read one huge line and has split it in 3 parts like the MCVE below.

MCVE:

use std::thread::JoinHandle;

fn main() {
    let handle = start_transfer_thread(&|x| {
        println!("X={}", x);
    }).join();
}

fn start_transfer_thread<F>(logger: &'static F) -> JoinHandle<()> where F: Send + Sync + Fn(&str) -> () {
    std::thread::spawn(move || {
        logger("1");
        logger("2");
        logger("3");
    })
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_logged_in_order() {
        let result = start_transfer_thread(&|x| {
            match x {
                "1" => (),
                "2" => (),
                "3" => (),
                x => panic!("unexpected token: {}", x)
            }
        }).join();
        assert!(result.is_ok());
    }
}
Svetlin Zarev
  • 14,713
  • 4
  • 53
  • 82
  • Why not just test your code with some long input and check the result ???!!! – Stargateur Apr 22 '19 at 07:30
  • I did that and I know that it works, but I want to have an automated test. Testing by hand is not an option. – Svetlin Zarev Apr 22 '19 at 07:33
  • You can get the logger from a trait object and Inject it as mock logger which implements the same trait. In your mock logger, you can add your desired logic and get the call count from it. – Akiner Alkan Apr 22 '19 at 07:52
  • 1
    What you're describing is called a Spy. https://blog.cleancoder.com/uncle-bob/2014/05/14/TheLittleMocker.html – Kraylog Apr 22 '19 at 07:58
  • I know what a `spy` is, the question was how to do it with plain rust. In the end I replaced the function/closure with a trait object. Now in the tests I'm able to pass another implementation that accumulates the values using a channel. But still I have no idea how to do it if was a function/closure as in my MCVE. – Svetlin Zarev Apr 22 '19 at 08:03
  • @Kraylog, I was just describing the blog :) Could not wanted to just call it spy, instead wanted to give insight about implementation – Akiner Alkan Apr 22 '19 at 08:27
  • @Shepmaster The linked duplicate does not answer the question. It makes the assumption that the function belongs to a struct which can conditionally contain the test state. My function is not associated with a struct, hence I cannot conditionally add a new field to the struct. As I had control over the whole codebase I easily changed it to use a trait instead of function reference, but that might not have been possible if I was not in control of the API. – Svetlin Zarev Apr 22 '19 at 14:47
  • I don’t understand how you answer differs from [this one](https://stackoverflow.com/a/54584732/155423). Both use traits to solve the problem (although you chose to use dynamic dispatch) – Shepmaster Apr 22 '19 at 14:49

1 Answers1

1

I was able to do this by replacing the function/closure with a trait object:

trait Logger: Send + Sync {
    fn log(&mut self, log_name: &str, data: &str);
}

struct StandardLogger;

impl Logger for StandardLogger {
    fn log(&mut self, log_name: &str, data: &str) {
        log::logger().log(
            &log::Record::builder()
                .level(log::Level::Info)
                .target(log_name)
                .args(format_args!("{}", data))
                .build(),
        );
    }
}

For the tests I use another implementation:

struct DummyLogger {
    tx: Mutex<Sender<String>>,
}

impl DummyLogger {
    pub fn new() -> (DummyLogger, Receiver<String>) {
        let (tx, rx) = std::sync::mpsc::channel();
        let logger = DummyLogger { tx: Mutex::new(tx) };
        (logger, rx)
    }
}

impl Logger for DummyLogger {
    fn log(&mut self, log_name: &str, data: &str) {
        let tx = self.tx.lock().unwrap();
        tx.send(data.to_owned());
    }
}

Which allows me to verify that it was both called the correct number of times, with the correct parameters:

let actual: Vec<String> = rx.iter().collect();
assert_eq!(actual, vec!["1", "2", "3", "4"]);
Ömer Erden
  • 7,680
  • 5
  • 36
  • 45
Svetlin Zarev
  • 14,713
  • 4
  • 53
  • 82