I’m an inveterate Emacs user. It’s been my Swiss Army chainsaw of choice for over 33 years. Part of the beauty of Emacs is that it’s extensible and configurable. The relatively new1 package manager and ELPA package archive are a great start, but sometimes it’s better — or simply more fun — to roll your own.
This post was originally published on The Dirty Birds, the Chloe + Isabel tech team blog.
I’ve developed a few Emacs Lisp functions that let me run our Rails RSpec tests at varying levels of granularity and within various environments.
Granularity: what to test.
- Run all tests in a directory and its subdirectories.
- Run all tests in a single file, by default the one that’s in the current buffer.
- Run a single test or single context within a spec file.
Environment: where to run the test.
- Within an Emacs compilation buffer, which has features like jumping right to the source code referenced in any error message output can be used to jump directly to the relevant line of code.
- Within a shell running in an external terminal program such as the Mac OS X Terminal program or iTerm 2. Running within a shell instead of using compilation mode is necessary when you need to interact with the tests, for example if you insert a Byebug breakpoint in the code. To send text to these external programs, we’ll use AppleScript.
- Within a shell running inside Emacs. This is left as an exercise for the reader.
The first thing we need is the command we want to run. Variables that we
will fill in
The first line takes us to the root of the Rails project.
The second line empties out the log file without deleting it. If you move or delete a log file that Rails has open it won’t recreate a new one, so instead you can zero out the existing file by doing this. The third line runs the test.
The command to run the tests needs to specify a random seed and one or more
paths and optionally lines within files. The random seed is used by RSpec to
determine in what order tests are run. By default, we’ll use the value
$RANDOM which is a magic variable defined by most Unix shells that returns
a different value every time it is read. If a test fails sometimes but not
others, that could be because the order of the tests matters to the outcome.
(If that is the case, it’s a sign that the test needs to be rewritten so
that it has no external or intra-test data dependencies.) In that case you
may want to make sure you use the same random seed while debugging so that
the tests are run in the same order.
RSPEC_PATH_AND_LINE can be a directory (
$RAILS_ROOT/spec/models/foo.rb), or file plus line number
$RAILS_ROOT/spec/models/foo.rb:42). If a line number is specified, then
the test(s) containing that line is run. The line number doesn’t have to be
the first line of the test; any line in the test will do. Additionally, if
the line number is outside of any single test but within a
describe block, then all tests in that block will be run.
rspec command can actually take multiple dir/file name arguments, but
we’re only going to worry about running one right now.
Generating the Command
Here’s the elisp2 code that generates the command. The function
my-rails--rspec-command takes a seed value and a file name (or directory
name or file-plus-line-number) and returns a string containing the command
we want. It calls
find-rails-root to search for the Rails root directory
at or above the specified file’s directory. The two dashes in the function’s
name are a convention that tells the reader that this is a locally-used
internal function not intended for export or use by other code.
The function that finds the Rails root directory at or above a file’s
find-rails-root. It uses the predicate function
rails-root-p to determine if a directory is a Rails root directory. By
long-standing Lisp convention, the names of predicate functions (those that
return true or false) end with
-p, like Ruby’s convention of using
The seed option for the command is generated by this function:
One more addition lets us run the test(s) that are at point3:
Actually Running the Tests
Now that we can build the command we need, it’s simple to write functions that run the command for the current line, current file, or an entire directory.
There are two “environments” (a compilation buffer within emacs or an
external terminal program) and really only two granularities (whatever is at
point or a file/directory). We can abstract those options a bit by passing
around functions. One pair of functions takes a command string and executes
it in the proper environment. For those we use
#'send-to-iterm. The former function is built in to Emacs and the latter
is defined below.
We could theoretically create another pair that determine granularity by returning the proper final argument for the command string, but when I starting thinking about doing that it seemed to me that the code would become unnecessarily obtuse.
interactive is a special form4 that tells the function what types the
args are and how to retrieve or prompt for them. Since the seed argument for
all of these functions is numeric and since I usually want to use the
default value, we tell the functions to use their
numeric or prefix argument
for the seed parameter.
The final missing piece is
Writing this article caused me to review the code and improve it by refactoring it a bit. I even found a glaring error in the design: I assumed that the “current line” was to be found by asking the current buffer, even when calling a function with a completely different file.
I’m writing the book Emacs Mastery: Attaining Coding Supremacy, and hope to be finished later this year. (Writing a book takes a long time. Who knew?) In the mean time, enjoy Mastering Emacs by Mickey Petersen.