I picked Lua for my hobby project deliberately. I chose Lua because it’s a fast and small interpreted language. I also chose the language because it is a smaller ecosystem than the Python ecosystem that I usually work in.
On the Tiobe Index, Python is in the pole position at #1, while Lua is not even in the top 25. Lua sits at 30th rank on the index.
From my point of view, this means that there are more unexplored areas for the Lua ecosystem. While Python is great for grabbing packages that are at the ready, this story is not so similar for Lua. Since my project is all about exploring the language space, having unexplored areas leaves a lot of options open, if I’m willing to put in the work.
On the flip side, trailblazing comes at a cost for some things that are unrelated to my project, but I would like to have for nicer developer ergonomics.
This has led me to do some yak shaving of the Lua ecosystem.
Yak Shave: Doing a task that leads to another task that leads to another task and so until you find yourself working on tasks that are distant from your original goal.
The First Yak - Testing
My first yak shave came about with my test tools. In the Lua ecosystem, Busted seems to be one of popular unit testing libraries. When I write my code in Vim, I like to have my primary source file in one split window and my unit test file in an adjacent split. I use vim-test to trigger my unit tests directly in Vim. This feedback loop is so fast because I never have to leave my text editor while working with my code.
On my Python projects,
I can press my leader key
(mine is mapped to the space bar),
followed by t
to run the exact unit test
that my cursor is nearest to.
If you can’t run a single unit test
with a keyboard shortcut,
then I strongly recommend you try it out.
I think it might transform your workflow
as much as it transformed mine
once upon a time.
Anyway,
when I tried the same thing as I started on the template engine
of my
Atlas
web framework,
my shortcut ran the entire file of tests
instead of a single test.
I went to the vim-test source code and confirmed that, sure enough,
the :TestNearest
command was only able to detect the test file name.
Therefore,
the granularity of my test command was limited to whatever test file
I tried to run.
This constraint made decent sense to me because of the way Busted works. Unlike pytest, Busted is a Behavior Driven Development (BDD) style test framework. In practice, that means that instead of having test code like:
def test_some_function():
result = do_stuff()
assert result == 42
The test code looks like:
describe("Thing", function()
it("does stuff", function()
local result = do_stuff()
assert.equal(42, result)
end)
end)
I’ll argue that the former style is easier for test tools
to find individual tests
than the latter style.
The reason is because test_some_function
is a clear identifier
that can be discovered by testing tools.
In contrast,
the function in the BDD-style is it
.
When you have more than one test in pytest,
you’ll need functions with different names
or else Python will think there is only a single function definition.
With Busted, tests will have multiple it
calls
and that is perfectly normal.
The support for pytest in vim-test will put together the unique identifier
(e.g., test_file.py::test_some_function
)
and invoke pytest with that identifier
as the argument.
At first glance,
it doesn’t seem like there is much that could be done
to work around this problem for Busted.
Then I found that Busted has a --filter
option.
The filter option takes a Lua pattern
and runs tests whose string description matches the pattern.
So,
I concluded that I can’t match on the it
function name,
but I could possibly match
on the "does stuff"
description.
My solution wouldn’t be perfect because two tests could have the same description,
but it would be better than running the whole file.
The outcome of my effort was the
vim-test/vim-test#598
Pull Request.
Essentially,
the feature does a Vim regex search to find the description
of a test,
then transforms that description
into a Lua pattern
that can be passed to the --filter
option.
Along the way,
I learned a few things.
- Writing Vimscript is painful.
- Lua patterns have a number of escape characters that must be matched
if you want to match an exact description.
The escape character is a
%
instead of a\
like many other languages. This creates funny scenarios because Vim treats%
as the current file name when executing external commands. - If you have to deal with escaping across multiple languages and a shell, good luck. My head wanted to explode at times while trying to reason about all the escaping.
- The Moon dialect of Lua is pretty nice. vim-test’s Lua support works for Moon too, so I had to devise a pattern that worked with both dialects.
- The vim-test test suite uses vim-flavor, which is written in Ruby.
For those keeping score at home, that means that, to add this feature, I had to work with:
- Lua
- Vimscript
- Shell
- Moon
- and Ruby
This Yak was hairy. Happily, the feature got merged, and I can run individual unit tests in my project now with a couple of keystrokes.
The Second Yak - Pre-commit Linting
I’m a huge believer in using linting tools that can either automate away drudgery like code formatting or spot problems using static analysis techniques like finding unused variables. Code tools are what give developers most of our superpowers as we put computers to work on tasks that we stink at.
I set up Continuous Integration early in my project on GitHub Actions to keep me honest. My GitHub Actions setup:
- runs all my unit tests,
- builds my Luarock (Lua’s packaging format),
- statically analyzes with Luacheck,
- and checks code formatting with LuaFormatter.
Having CI is great. Having CI that is often failing on you is not great. I kept making mistakes because I didn’t have all my linters set up in Vim.
Wouldn’t it be great if I could run my lint checks before I push code to GitHub? Most certainly!
Thankfully,
I know of a great tool
to help with this:
pre-commit.
pre-commit installs a Git hook
that will cause the tool to run every time
that I run git commit
on my project.
When the pre-commit tool runs,
it can execute a set of user defined hooks
that I set in a .pre-commit-config.yaml
file.
Super,
I can add Luacheck and LuaFormatter
to pre-commit
and prevent an entire category of errors
from reaching GitHub Actions.
I checked the list of support languages and…
Lua wasn’t there.
Then I checked the list of supported hooks.
Luacheck and LuaFormatter were there,
but the definitions use the system
option.
This means that the hooks could work,
but the hooks assume that you have a working luacheck
and lua-format
on your PATH
.
One of my favorite things about pre-commit is that most
of the hooks will do all the setup for you.
system
is the escape hatch
that punts on that quality.
I wanted native hooks to exist
because I thought that would be nicer
for future Lua pre-commit users.
I made a small contribution to pre-commit years ago, but not to support a new language. Looking at the source, I saw that adding a new language was a well scoped activity if I could implement the required hooks that plug into the core of pre-commit.
With my
pre-commit/pre-commit#2158 PR,
I helped get Lua added as a supported language.
Ultimately,
the diff for that change is not huge,
but I’m grateful
for all the help
that the maintainer,
Anthony Sottile,
provided while working through the change set.
The process of figuring out package isolation
with Luarock trees,
setting the right LUA_PATH
and LUA_CPATH
,
and getting appropriate test coverage
was a fairly involved process.
To close out my effort, I added the docs for pre-commit.com. I believe that a feature doesn’t really exist unless it’s documented and users know it is available.
Once pre-commit 2.17.0 was out, I could add hook definitions to Luacheck and LuaFormatter. By adding the hook definitions to the core projects, users can use pre-commit directly from the canonical source repos. Fortunately, hook definitions are the easiest part of the process. I completed my pre-commit yak shave with the lunarmodules/luacheck#48 and Koihik/LuaFormatter#236 PRs.
What’s Next?
I think I’m done yak shaving for a while. The tools that I now have in place are mostly sufficient. There is one more project that I might pursue in the future to improve LuaCov and enforce a coverage percentage on my code, but I’ve hacked together a workaround for now.
My next goal in my project is to design a logging scheme
for Atlas.
I’ve been using the very lazy strategy
of calling print
for some debug logging,
but the output shows up in my test runs
because Busted doesn’t capture stdout.
The thing that I enjoyed about these yak shaves (aside from the very tangible benefits that I made for myself and the community) is that this deepens my knowledge of the Lua language ecosystem. My hope is that this knowledge will benefit me as a build out my web framework.
Thanks for reading! Have questions? Let me know on X at @mblayman.