Who Watches the Watchmen?
I'm a contributor to the Sculpin open source project. Sculpin is a PHP-based static site generator that helps you create blogs or informational websites. Posts and pages can be written in Markdown or HTML, and it uses the Twig template library to provide enhanced functionality. The best part is, the resulting output is static - it can be hosted on any HTTP server, including AWS Simple Storage Service (S3), without having to worry about maintaining databases or other headaches.
When users create sites in Sculpin, they often use it in --watch
mode. In this mode, Sculpin runs as a simple PHP server that watches the filesystem for any changes to your blog posts, pages, and templates. When it detects a change, it regenerates the affected HTML files. It's not speedy enough in this mode to serve the site to the Internet - plus, it only has a single process - but it's great for development and local testing.
Earlier this week, a bug was reported on Sculpin's develop
branch, where the watch process wasn't properly applying changes to inherited templates. I had to go to some creative lengths to solve it, and while I'm not entirely certain that my proposed solution is the proper one, it contains at least one thing worth blogging about.
Most PHP scripts are a single process, which makes it difficult to test things that are "blocking". Sculpin's watch mode is one of these things. In the existing tests, Sculpin is tested by invoking it with a call to exec()
, but without the --watch
flag. It runs, finishes, and exits, and then the tests go examine the output.
If Sculpin were launched from exec()
with the watch flag, it would never return, because Sculpin's watch mode doesn't self-terminate. Additionally, while Sculpin was being executed, the PHPunit tests would be stuck waiting for the return that will never come.
At first, experienced readers might think the same thing I did: How about using the &
shell flag on the exec()
call to fork the process off and let it run on its own?
The drawback to this is that your test environment will end up with dozens of zombie sculpins hogging all your system resources.
What's needed is a way to fork a shell command into its own thread without allowing the thread to outlive your unit tests. And after a bit of research on Packagist, I found exactly what I needed.
The Symfony Process Component allows you to do all kinds of things with shell commands, and one of those things is start and stop them asynchronously - without blocking your script's execution.
It turned out I didn't even need to install anything, because that package was already included in Sculpin's dependencies. I hadn't realized that at first, though, which led to an amusing tweet when I tested it in an empty folder:
This feels like getting a hole-in-one. Dependency Golf? #php #composer #symfony pic.twitter.com/ovWcSlQ20B
— Kevin Boyd (@Beryllium9) February 22, 2018
Basically, symfony/process
has zero dependencies on its own, so you can quickly bake it into any project that needs it. Even better, I only needed three lines of code to enable it.
use \Symfony\Component\Process\Process;
$process = new Process('bin/sculpin generate --watch');
$process->start();
// do tests here
// make sure to use sleep(1)
// to give time to detect filesystem changes & perform updates
$process->stop(0); // stop the process immediately at the end of the test
The actual command string passed to Process was slightly different in the tests, because the test suite needs to do things like create a temporary folder for holding the test fixtures, but the above is the relevant logic. A) Instantiate the process with a command string, B) start it, C) do your thing, D) stop it. Very straightforward.
I was extremely pleased that it all came together so smoothly. Right away, I was able to prove that the problem existed, and that my proposed solution successfully addressed it. Plus, if it ever breaks again in the future, the tests will catch it.
Published: February 22, 2018
Tags: sculpin, coding, dev, development, php, howto, utilities, debugging, testing, command-line, phpunit