Automatically running Rust unit tests
Jan 9 2015
Edit: These days there is the excellent cargo watch project. Use that.
Rust is a really cool new programming language from Mozilla. It features guaranteed memory safety with manual memory management, zero cost abstractions, pattern matching, and a type system that reminds me of Haskell, with algebraic data types and traits. Sounds cool, right?
If you’re like me, you probably want to write tests with your code, especially when working with a language that doesn’t have a REPL (yet…). You’ll probably also want the relevant tests to run automatically when you save a file after modification (test driven development, anyone?). Cargo, the rust package manager/build tool, doesn’t have ability to do this yet, but we can get more or less the same functionality using the lovely Ruby tool Guard.
I’m new to Rust, and the language has yet to reach version 1, so there may be situations in which this method doesn’t always work, but this will work for modules as they are described in the Rust book at time of writing (Rust 1.0 alpha).
TL;DR
$ gem install guard-shell
# Guardfile
guard :shell do
watch(/src\/.*\.rs/) do |m|
path = m.first
mod = unless %w(src/main.rs src/lib.rs).include? path
path.sub(/src\//, '')
.sub(/\/tests\.rs/, '.rs')
.sub(/\/mod\.rs/, '.rs')
.gsub(/\//, '::')
.sub(/\.rs/, '::tests')
end
puts "\n\n\n\n"
puts "\e[33mcargo test #{mod}\e[0m"
`cargo test #{mod}`
end
end
$ guard
The Process
Install Guard
Guard monitors the file system for changes. Let’s install the guard-shell, a guard that allows us to run shell commands when guard detects that we’ve modified a file.
$ gem install guard-shell
Alternative, put it in a Gemfile and manage Ruby dependencies with Bundler. This is probably a better idea long term.
In your Rust project directory run guard init shell
from the shell to create
a Guardfile. It’ll look something like this, preceded by a load of comments:
guard :shell do
watch(/(.*).txt/) {|m| `tail #{m[0]}` }
end
If you’re familiar with Ruby you’ll see that there is a watch
function being
called with a regexp, and into that function a block is being passed that
accepts one argument. The regexp here dictates which files we want the block to
be executed for, and the arg is an array of string paths of the files Guard has
detected a change in. Right now it just tail
s a file when it’s modified,
let’s make it do something useful.
Configure Guard
In Rust unit tests for a module are kept in a sub-module named ‘tests’, and
the directory tree structure mirrors the module tree structure. For example,
the module game
can be found in the file src/game/mod.rs
, and the module
game::player
can be found in the file src/game/player.rs
. Cool, so using
information we can work out what the module name would be from the path of the
modified file.
Let’s change that regexp to match Rust (.rs
) files within the source
directory.
guard :shell do
watch(/src\/(.*)\.rs/) {|m| `tail #{m[0]}` }
end
Now, let’s use the path string to create a string containing the name of the tests module we want to run for that file. See the comments for a step-by-step walk-through.
guard :shell do
watch(/src\/.*\.rs/) do |m|
# Get the path from the array
path = m.first
# Drop the 'src/' directory part
mod = path.sub(/src\//, '')
# Swap '/mod.rs' for '.rs', as the module name of those
# files is that of the parent directory.
.sub(/\/mod\.rs/, '.rs')
# Handle the tests that are in a file in the sub-directory,
# rather than in the same file.
.sub(/\/tests\.rs/, '.rs')
# Swap '/' directory separtors for '::' module separtors
.gsub(/\//, '::')
# Swap the file extension for '::tests', as the tests
# are contained within a sub-module called 'tests'
.sub(/\.rs/, '::tests')
end
end
And now that we have have the module name, we can run it with with
cargo test module::name
guard :shell do
watch(/src\/.*\.rs/) do |m|
path = m.first
mod = path.sub(/src\//, '')
.sub(/\/tests\.rs/, '.rs')
.sub(/\/mod\.rs/, '.rs')
.gsub(/\//, '::')
.sub(/\.rs/, '::tests')
`cargo test #{mod}`
end
end
However there is a problem here- it won’t work for the top level src/main.rs
or src/lib.rs
files. I’ve decided in this case I want to run all the tests
with cargo test
, so I’m going to add a conditional so that the module path is
not computed for these files.
guard :shell do
watch(/src\/.*\.rs/) do |m|
path = m.first
mod = unless %w(src/main.rs src/lib.rs).include? path
path.sub(/src\//, '')
.sub(/\/tests\.rs/, '.rs')
.sub(/\/mod\.rs/, '.rs')
.gsub(/\//, '::')
.sub(/\.rs/, '::tests')
end
`cargo test #{mod}`
end
end
And lastly, let’s print a few newlines and the module path in a colour so that we can see where each round of output starts a little easier.
guard :shell do
watch(/src\/.*\.rs/) do |m|
path = m.first
mod = unless %w(src/main.rs src/lib.rs).include? path
path.sub(/src\//, '')
.sub(/\/tests\.rs/, '.rs')
.sub(/\/mod\.rs/, '.rs')
.gsub(/\//, '::')
.sub(/\.rs/, '::tests')
end
puts "\n\n\n\n"
puts "\e[33mcargo test #{mod}\e[0m"
`cargo test #{mod}`
end
end
And that’s it. Run guard
from the project directory and you’re good to go.
:)
Cheers, Louis