Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing to a file or String in Rust

Tags:

rust

TL;DR: I want to implement trait std::io::Write that outputs to a memory buffer, ideally String, for unit-testing purposes.

I must be missing something simple.

Similar to another question, Writing to a file or stdout in Rust, I am working on a code that can work with any std::io::Write implementation.

It operates on structure defined like this:

pub struct MyStructure {
    writer: Box<dyn Write>,
}

Now, it's easy to create instance writing to either a file or stdout:

impl MyStructure {
    pub fn use_stdout() -> Self {
        let writer = Box::new(std::io::stdout());
        MyStructure { writer }
    }

    pub fn use_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let writer = Box::new(File::create(path)?);
        Ok(MyStructure { writer })
    }
    
    pub fn printit(&mut self) -> Result<()> {
        self.writer.write(b"hello")?;
        Ok(())
    }
}

But for unit testing, I also need to have a way to run the business logic (here represented by method printit()) and trap its output, so that its content can be checked in the test.

I cannot figure out how to implement this. This playground code shows how I would like to use it, but it does not compile because it breaks borrowing rules.

// invalid code - does not compile!
fn main() {
    let mut buf = Vec::new(); // This buffer should receive output
    let mut x2 = MyStructure { writer: Box::new(buf) };
    x2.printit().unwrap();
    
    // now, get the collected output
    let output = std::str::from_utf8(buf.as_slice()).unwrap().to_string();
    
    // here I want to analyze the output, for instance in unit-test asserts
    println!("Output to string was {}", output);
}

Any idea how to write the code correctly? I.e., how to implement a writer on top of a memory structure (String, Vec, ...) that can be accessed afterwards?

like image 825
Petr Kozelka Avatar asked Jul 21 '20 23:07

Petr Kozelka


Video Answer


1 Answers

Something like this does work:

let mut buf = Vec::new();

{
   // Use the buffer by a mutable reference
   //
   // Also, we're doing it inside another scope
   // to help the borrow checker

   let mut x2 = MyStructure { writer: Box::new(&mut buf) };
   x2.printit().unwrap();
}

let output = std::str::from_utf8(buf.as_slice()).unwrap().to_string();
println!("Output to string was {}", output);

However, in order for this to work, you need to modify your type and add a lifetime parameter:

pub struct MyStructure<'a> {
    writer: Box<dyn Write + 'a>,
}

Note that in your case (where you omit the + 'a part) the compiler assumes that you use 'static as the lifetime of the trait object:

// Same as your original variant
pub struct MyStructure {
    writer: Box<dyn Write + 'static>
}

This limits the set of types which could be used here, in particular, you cannot use any kinds of borrowed references. Therefore, for maximum genericity we have to be explicit here and define a lifetime parameter.

Also note that depending on your use case, you can use generics instead of trait objects:

pub struct MyStructure<W: Write> {
    writer: W
}

In this case the types are fully visible at any point of your program, and therefore no additional lifetime annotation is needed.

like image 143
Vladimir Matveev Avatar answered Nov 15 '22 10:11

Vladimir Matveev