Edit to add: The ideas in this post are now implemented in a library. Feel free to read this post for understanding the concepts, but you’re probably better off just using that.
The theme of this fort was that we were going to work on a C project. People made the mistake of listening to me, and one thing lead to another, and the result is Conch, a security free curses interface to our terrible Twitter clone, Bugle. Here’s a sneak peek:
This post is mostly not about Conch, except that working on it was what lead to this concept.
We used check for testing a lot of the implementation, which was fine if a little laborious. However testing the front end (oh god, why do we have front end code written in C?) proved challenging. ncurses was not our friend here.
I tried using pexpect for this, which is like expect but written in python instead of TCL, but ran into a bunch of problems. It has an ANSI virtual terminal, but for whatever reason (this might have been my fault) it got very confused and ended up with lots of problems with partial drawing of things and leaving the screen in a wrong state.
So I put my thinking cap on, read some man pages, and applied some twisted imagination to the problem and came up with a solution that works great, albeit at some small cost to my dignity and sense of taste.
The solution is this: What I need is a robust virtual terminal I can control and inspect the state of.
Fortunately I have one. It’s called tmux.
Tmux is pretty great. It has a whole bunch of functionality, is a rock solid virtual terminal that is widely used with a wide variety of programs, and you can control it all externally via the command line. Putting these all together lead to a bunch of primitive operations out of which I could basically build selenium style testing for the console.
I’m still figuring out the details. When I do I’ll probably turn this into a library for testing console applications rather than the current ad-hoc thing I have, but basically there’s a relatively small set of operations you can build this testing out of:
- First we allocate a unique ID for our test. This should be long and random to avoid conflicting with existing sessions. Henceforth it will be called $ID.
- “tmux -L $ID new-session -d <your shell command>” will start your program running in a fresh tmux session under your id. The -d is necessary because you will not be starting this from a controlling terminal in your program, so you want it to start detached. If you want you can specify width and height with -x and -y respectively.
- At the end, “tmux -L $ID kill-server” will shut down all sessions in your test tmux server, including the child processes.
- In order to capture the current contents of your tmux session you can run: “tmux -L $ID capture-pane; tmux -L $ID show-buffer; tmux -L $ID delete-buffer”. This will save a “screenshot” of the currently active pane (of which there is only one if you’ve just used these commands) to a new paste buffer, print the paste buffer to stdout, then delete the buffer.
- In order to send key presses to the running program you can use “tmux -L $ID send-key <some-char>”. These can either be ascii characters or a variety of control ones. e.g. PageUp, PageDown and Enter do what you expect. Adding C- as a modifier will hold down control, so e.g. C-c and C-d would be Control-c and Control-d with their usual interpretations (send an interrupt to the running program, send EOF).
- In order to send non-ascii or larger text you can use do “tmux -L $ID set-buffer <my text>; tmux -L $ID paste-buffer; tmux -L $ID delete-buffer”, which will set a paste buffer, paste it to the active pane, and then delete the buffer.
(Some of the above is not actually what I did, because I figured out some better ways using commands I’d previously missed while writing this post).
The main things that are hard to do with this, and why for now this is a blog post rather than a piece of open source software, is getting the PID and exit code out for the program you’ve started and resizing the window. I know how to do both of those (running a manager process and starting inside a pane respectively), but it’s fiddly and I haven’t got the details right. When I do, expect all of the above to be baked into a library.