|
STORY
Goal
I wanted to create a desktop gadget to visualize the progress of unit tests run via PHPUnit.
I’ve named this project PHPUnicorn (by combining “PHPUnit” with “Unicorn pHAT”).
Hardware & Assembly
The hardware requirements for this project:
- Raspberry Pi Zero Wireless (W)
- MicroSD card flashed with Raspbian Lite
- 2 amp micro-USB power adapter
- Unicorn pHAT by Pimoroni
- A 2×20 header
- A soldering iron
Put the header between the Pi and Unicorn pHAT and solder it into place.
Load Raspbian onto the microSD card, configure networking, enable SSH, and install Python 3. The unicorn-hat Python library will also need to be installed.
Extending PHPUnit
PHPUnit allows you to easily add listeners via the phpunit.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<!-- Existing configuration here --->
<listeners>
<listener class="ColinODell\PHPUnicorn\PHPUnicornListener">
<arguments>
<string>192.168.80.93</string>
<string>5005</string>
</arguments>
</listener>
</listeners>
</phpunit>
Each listener is notified when tests begin, when they finish, and what the results are. Here’s the code I wrote to do just that:
<?php
namespace ColinODell\PHPUnicorn;
use Exception;
use PHPUnit_Framework_AssertionFailedError;
use PHPUnit_Framework_Test;
use PHPUnit_Framework_TestSuite;
use PHPUnit_Framework_Warning;
class PHPUnicornListener extends \PHPUnit_Framework_BaseTestListener
{
const NO_RESULT = 'N';
const ERROR = 'E';
const FAILURE = 'F';
const INCOMPLETE = 'I';
const RISKY = 'R';
const SKIPPED = 'S';
const PASSED = 'P';
const WARNING = 'W';
const TOTAL = 'T';
const COMPLETED = 'C';
private $currentTestPassed = false;
private $counts = [];
/**
* @var string
*/
private $host;
/**
* @var int
*/
private $port;
/**
* @var resource
*/
private $socket;
/**
* @param string $host
* @param int $port
*/
public function __construct($host, $port)
{
$this->host = $host;
$this->port = $port;
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
$this->resetCounts();
$this->broadcast();
}
/**
* Ensure socket is closed
*/
public function __destruct()
{
socket_close($this->socket);
$this->socket = null;
}
/**
* {@inheritdoc}
*/
public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
{
$this->counts[self::ERROR]++;
$this->currentTestPassed = false;
}
/**
* {@inheritdoc}
*/
public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {
$this->counts[self::FAILURE]++;
$this->currentTestPassed = false;
}
/**
* {@inheritdoc}
*/
public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time)
{
$this->counts[self::WARNING]++;
$this->currentTestPassed = false;
}
/**
* {@inheritdoc}
*/
public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
$this->counts[self::INCOMPLETE]++;
$this->currentTestPassed = false;
}
/**
* {@inheritdoc}
*/
public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) {
$this->counts[self::RISKY]++;
$this->currentTestPassed = false;
}
/**
* {@inheritdoc}
*/
public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
$this->counts[self::SKIPPED]++;
$this->currentTestPassed = false;
}
/**
* A test suite has started.
*
* {@inheritdoc}
*/
public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
// Test suites can contain child test suites. This function is always
// called with the top-most parent, so use its count method to determine
// how many tests there are (it'll count all sub-children recursively).
if ($this->counts[self::TOTAL] == 0) {
$this->counts[self::TOTAL] = $suite->count();
}
}
/**
* A test suite ended.
*
* {@inheritdoc}
*/
public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
// Send the results over to the Pi
$this->broadcast();
}
/**
* A single test started.
*
* {@inheritdoc}
*/
public function startTest(PHPUnit_Framework_Test $test)
{
// There's no method like addPassed(), so we'll assume the test passes
// unless one of the other add___() methods are called.
$this->currentTestPassed = true;
}
/**
* A test ended.
*
* {@inheritdoc}
*/
public function endTest(PHPUnit_Framework_Test $test, $time)
{
if ($this->currentTestPassed) {
$this->counts[self::PASSED]++;
}
$this->counts[self::COMPLETED]++;
$this->broadcast();
}
private function resetCounts()
{
$this->counts = [
self::NO_RESULT => 0,
self::ERROR => 0,
self::FAILURE => 0,
self::WARNING => 0,
self::INCOMPLETE => 0,
self::RISKY => 0,
self::SKIPPED => 0,
self::PASSED => 0,
self::TOTAL => 0,
self::COMPLETED => 0,
];
}
private function broadcast()
{
if ($this->counts[self::TOTAL] == 0) {
// Ask the Pi to clear the screen when we first start
$message = 'reset';
} else {
$message = json_encode($this->counts);
}
socket_sendto($this->socket, $message, strlen($message), 0, $this->host, $this->port);
}
}
The code is fairly straight-forward – as each test result comes in, we increment the corresponding result in $this->counts
and broadcast the latest counts to the Raspberry Pi via a UDP packet. These are encoded in a JSON string to keep the size low:
Read More: PHPUnicorn – Visualizing PHPUnit Tests