How can I test my filesystem code?


I'm working on a storage engine for a little embedded database. The storage engine saves data to a file, and needs to do that atomically. (Every write succeeds or doesn't succeed). If the program crashes, the user should never lose data.

I'm using a scheme that I think should work. The file is made up of fixed size pages. Every page has a checksum. Writes use a "double buffering" type system where data is alternately written to a secondary page when necessary to make sure there's always a backup copy of all data when updates happen.

Anyway, the details don't matter. Its quite complex, and I think it'll be correct.

But experience has taught me that thinking my code is correct is very different from actually having correct code.

So, I want something to test all the edge cases. To do that, I want to write a little fuzzer which issues random write calls to my API. Along the way, the system should randomly inject "crashes" / filesystem write failures, in accordance with the guarantees provided by the operating system. (So, if fsync() succeeds, then all writes issued before the fsync can't be lost. But any writes since then may or may not have completed). After a simulated crash, I should be able to read back the file. All committed data should be there, and the file should never end up in a corrupt state.

Any ideas or libraries to help do this in rust?

I'm imagining a trait which wraps std::fs. For testing, I want to provide my library code with a mocked implementation of the filesystem which randomly generates write failures. But I can't find anything like that on cargo.

I can imagine other approaches which could work out-of-process like:

  • Running my testing process in a wrapper which provides a different glibc. Then randomly segfault the child process (my test) and reload it to make sure everything looks good.
  • Using FUSE to mount an intentionally buggy testing filesystem, and send all the file IO into that.

But both of those approaches feel much more complicated to setup and run. And they'd probably run much slower.


1 Like

There is this crate:

which I think is something like what you want (haven't used it myself; it's on my "list of interesting crates that I should try out someday")

The dumbest option is to use unit tests with

#[cfg(not(test))] use std::fs;
#[cfg(test)] use my_mock_fs as fs;

Integration tests are tougher, since you can't configure the library with different code at build time, so you either have to runtime configure the library or test against the real fs (which you probably should for fuzzing!)


Thanks for the link. Unfortunately it looks like rsfs isn't designed to support injecting faults. Calls to the memory backed versions of read and write are infallible.

I could probably fork the library and add what I'm after, but given how big that library is, it'd probably be faster to make my own filesystem wrapper from scratch which just wraps the handful of filesystem functions I'm actually using.