Serialising Rust tests
14 Jan 2019
Originally posted at https://tech.labs.oliverwyman.com/blog/2019/01/14/serialising-rust-tests/
I'm once again prodding the potboiler tests
and a couple of the tests I was doing wanted to mess around with the shared database. This had the problem that multiple tests would collide with each other, as the default for Rust testing is to run everything in parallel
. This is unusual, but good in many ways. With most languages, you write a bunch of tests which run in serial, and then eventually you realise they're really slow and have to figure out a way to make your very serial-dependant tests now run in parallel, which usually involves picking a new test runner. Rust has a built-in testing framework, but doesn't yet have the option to pick a different test framework
so we can't do that. Officially, we could just use "--test-threads=1"
to make all our tests serial, but that's not really what we want. Usually what you want is a subset of tests serialised, and everything else parallel, so you're sacrificing the minimum amount of speed.
The nice way to do this would be with some sort of attribute, and a number of people have suggested the idea
, but no-one appears to have actually built it yet. One of the things that was limiting this was the lack of stable "procedural attribute macros"
, but now that Rust 1.30 stabilised them
this is now more possible.
Let's step back a bit. So, procedural macros
in Rust are used to take some chunk of source code (in the form of a stream of AST tokens) and rewrite it into another set of AST tokens. This means that at the simplest level I can write code that can add things before/after your code, a bit like Python decorators
. That's the most basic example, and you can do arbitrary source changes, but I only need that level for the purposes of this. Dealing with the full AST tree on your own is pretty deep magic stuff, but two crates make your life much easier: syn
. Syn lets you take that stream of AST tokens and make it into a tree of source code that's much easier to figure out, and quote
lets you generate new token streams from templated Rust code.
What that all means in practice is the core of the #[serial]
attribute looks like this
Basically, use syn::parse
to parse out the tree, fish out the items we need, and then use quote!
to reassemble things. Note the useful magic of things like #(#attrs)
(the newline and * after it are part of it) which magically reprints any attributes on the test function back out again, and the use of #block
which is the entire original contents of the test function.
This is all published
so you just need to add the following to your Cargo.toml
and then use serial_test_derive::serial;
and now you can use the attribute. For extra fun, if you need differing subsets of tests to be serial with each other, but not care about the other set, then you can use #[serial(some_name)]
and any tests with the same some_name
will be serial with each other (omitting it gets you the default blank name). End result of all of this is code like this:
As always, source code is in the usual sort of place