I wrote and maintain some C++ code
to stream high quantities of data as fast as possible, and I try to use
splice
and sendfile
when available.
The availability of those system calls varies at runtime according to a number
of factors, and the code needs to be written to fall back to read
/write
loops depending on what the splice
and sendfile
syscalls say.
The tricky issue is unit testing: since the code path chosen depends on the kernel, the test suite will test one path or the other depending on the machine and filesystems where the tests are run.
It would be nice to be able to mock the syscalls, and replace them during tests, and it looks like I managed.
First I made catalogues of the mockable syscalls I want to be able to mock. One
with function pointers, for performance, and one with std::function
, for
flexibility:
/**
* Linux versions of syscalls to use for concrete implementations.
*/
struct ConcreteLinuxBackend
{
static ssize_t (*read)(int fd, void *buf, size_t count);
static ssize_t (*write)(int fd, const void *buf, size_t count);
static ssize_t (*writev)(int fd, const struct iovec *iov, int iovcnt);
static ssize_t (*sendfile)(int out_fd, int in_fd, off_t *offset, size_t count);
static ssize_t (*splice)(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);
static int (*poll)(struct pollfd *fds, nfds_t nfds, int timeout);
static ssize_t (*pread)(int fd, void *buf, size_t count, off_t offset);
};
/**
* Mockable versions of syscalls to use for testing concrete implementations.
*/
struct ConcreteTestingBackend
{
static std::function<ssize_t(int fd, void *buf, size_t count)> read;
static std::function<ssize_t(int fd, const void *buf, size_t count)> write;
static std::function<ssize_t(int fd, const struct iovec *iov, int iovcnt)> writev;
static std::function<ssize_t(int out_fd, int in_fd, off_t *offset, size_t count)> sendfile;
static std::function<ssize_t(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags)> splice;
static std::function<int(struct pollfd *fds, nfds_t nfds, int timeout)> poll;
static std::function<ssize_t(int fd, void *buf, size_t count, off_t offset)> pread;
static void reset();
};
Then I converted the code to templates, parameterized on the catalogue class.
Explicit template instantiation helps in making sure that one doesn't need to include template code in all sorts of places.
Finally, I can have a RAII class for mocking:
/**
* RAII mocking of syscalls for concrete stream implementations
*/
struct MockConcreteSyscalls
{
std::function<ssize_t(int fd, void *buf, size_t count)> orig_read;
std::function<ssize_t(int fd, const void *buf, size_t count)> orig_write;
std::function<ssize_t(int fd, const struct iovec *iov, int iovcnt)> orig_writev;
std::function<ssize_t(int out_fd, int in_fd, off_t *offset, size_t count)> orig_sendfile;
std::function<ssize_t(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags)> orig_splice;
std::function<int(struct pollfd *fds, nfds_t nfds, int timeout)> orig_poll;
std::function<ssize_t(int fd, void *buf, size_t count, off_t offset)> orig_pread;
MockConcreteSyscalls();
~MockConcreteSyscalls();
};
MockConcreteSyscalls::MockConcreteSyscalls()
: orig_read(ConcreteTestingBackend::read),
orig_write(ConcreteTestingBackend::write),
orig_writev(ConcreteTestingBackend::writev),
orig_sendfile(ConcreteTestingBackend::sendfile),
orig_splice(ConcreteTestingBackend::splice),
orig_poll(ConcreteTestingBackend::poll),
orig_pread(ConcreteTestingBackend::pread)
{
}
MockConcreteSyscalls::~MockConcreteSyscalls()
{
ConcreteTestingBackend::read = orig_read;
ConcreteTestingBackend::write = orig_write;
ConcreteTestingBackend::writev = orig_writev;
ConcreteTestingBackend::sendfile = orig_sendfile;
ConcreteTestingBackend::splice = orig_splice;
ConcreteTestingBackend::poll = orig_poll;
ConcreteTestingBackend::pread = orig_pread;
}
And here's the specialization to pretend sendfile
and splice
aren't
available:
/**
* Mock sendfile and splice as if they weren't available on this system
*/
struct DisableSendfileSplice : public MockConcreteSyscalls
{
DisableSendfileSplice();
};
DisableSendfileSplice::DisableSendfileSplice()
{
ConcreteTestingBackend::sendfile = [](int out_fd, int in_fd, off_t *offset, size_t count) -> ssize_t {
errno = EINVAL;
return -1;
};
ConcreteTestingBackend::splice = [](int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags) -> ssize_t {
errno = EINVAL;
return -1;
};
}
It's now also possible to reproduce in the test suite all sorts of system-related issues we might observe in production over time.