Mock for Box<std::io::Write>

I have a function that takes a Box<Write> as a parameter so I can pass either a file or stdout() as the parameter. I'm looking to mock this with just a plain buffer, so I can then assert the correct thing was written in my unit test. Sample code here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=67cb88dcc739df78cce40b9a87ae6709

The problem I'm facing is that I cannot deref my Box<Write> as a Cursor so I can then call into_inner() and examine the underlying buffer. Thoughts on how to mock this?

I don't see a way of getting the original buffer back after type erasing it.

If you were really really desperate about this, you could wrap the Vec in Arc<Mutex<Vec>> and a newtype, and implement Write on that.

Or unsafely (very unsafely) keep a raw pointer to Box<Cursor>, and use it afterwards (Box::from_raw). Make sure to mem::forget Box<dyn Write> to avoid double free. This is actually unsafe, because there's no guarantee that the function didn't replace the Box<dyn Write> with another instance and freed your cursor, but if it's your code and your test, it might not be a problem.


Can you change the function to take &mut dyn Write instead of &mut Box<dyn Write>? The extra layers of indirection don't help.

Taking a reference to a box is the same kind of mistake as using &Vec<T> instead of &[T].

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=67cb88dcc739df78cce40b9a87ae6709

3 Likes

Take a generic type instead:

use std::io::*;

fn foo(mut output_writer: impl Write) {
    output_writer.write(&[0]).expect("Error writing");
}

fn main() {
    let mut output_buffer = Vec::new();

    foo(&mut output_buffer);

    assert_eq!(output_buffer, [0]);
}

playground

1 Like

Thanks both @kornel and @shepmaster. I went with changing my foo function to simply taking impl Write. I guess I was (still am) confused about how/when to leverage Box vs generics. My code for handling a file or stdin() (or stdout()) for example, is as follows:

    let input_reader : Box<dyn BufRead> = if input == "-" {
        Box::new(BufReader::new(stdin()))
    } else {
        Box::new(BufReader::new(fs::OpenOptions::new()
            .read(true)
            .write(false)
            .create(false)
            .open(input).expect("Error opening input file")))
    };

Where input is a command-line arg that specifies either the name of a file, or "-" for stdin(). This is, I believe, equivalent to a BufRead pointer in something like C++. Is this the right way to do this?

Box<dyn Foo> is a pointer, and it's roughly equivalent to a new Foo() with all virtual methods in C++.

impl Foo argument is closer to C++ templates and will "copy'n'paste" the method for every type you use it with, which gives most optimized code, but if used with many types, it bloats the executable.

Rust supports dynamic dispatch for on-stack objects too. If you can have it limited to a scope, I'd do:

use std::io::stdin;

let mut stdio;
let mut file;
let read: &mut dyn Read = if input == "-" {
        stdio = stdin();
        &mut stdio
    } else {
        file = File::open(input).expect("Error opening input file");
        &mut file
    };
let input_reader = BufReader::new(read);
1 Like

The issue in your code was with you requiring a Box<dyn Write> when you just needed "write"/mut access on the Write-able thinggy. This means that a &mut dyn Write would have sufficed, as @kornel pointed out. And it turns out, that in this case requiring a Box<dyn Write> had a nasty side effect:

  • dyn Write is an erased type that has forgotten everything about itself, except that it can be Written to (and dropped). So it has forgotten all the other properties it had, such as being a Cursor that can be converted back to its inner Vec.

  • That's why once you get a Box<dyn Write> you can do nothing with it except writing to it or dropping it.

To get a "temporary" view of the cursor as a dyn Write, rather than using a pointer with ownership semantics, use a pointer with borrowing semantics, such as &dyn Write or &mut dyn Write. And since &dyn Write won't give us the &mut API required to write to it, all we have left is &mut dyn Write, as suggested:

use ::std::io::*;

fn foo (output_writer: &mut (dyn Write))
{
    output_writer
        .write(&[42, 27])
        .expect("Error writing")
    ;
}

fn main ()
{
    let mut output_cursor = Cursor::new(vec![]);
    let output_writer: &mut (dyn Write) = &mut output_cursor;
    foo(output_writer);
    // want to test the contents of output_buffer here
    dbg!(output_cursor.into_inner());
}
1 Like