Colin O'Dell
Published © MIT

PHPUnicorn - Visualizing PHPUnit Tests

Watch PHPUnit tests run in real-time on a Unicorn pHAT (through a Rasperry Pi Zero W).

BeginnerFull instructions provided1 hour4,005
PHPUnicorn - Visualizing PHPUnit Tests

Things used in this project

Hardware components

Raspberry Pi Zero Wireless
Raspberry Pi Zero Wireless
×1
2A Micro-USB Power Adapter
×1
Unicorn pHAT
Pimoroni Unicorn pHAT
×1

Software apps and online services

PHPUnit

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Code

PHPUnicornListener.php

PHP
Custom listener for PHPUnit to collect test stats and send them along to the Pi
<?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);
    }
}

display.py

Python
Python 3 script to receive the test stats and render them on a Unicorn pHAT display
#!/usr/bin/env python

import json
import socket
import time
import unicornhat as unicorn

UDP_IP = "0.0.0.0"
UDP_PORT = 5005

# Define constants for each JSON key
NO_RESULT = 'N'
ERROR = 'E'
FAILURE = 'F'
WARNING = 'W'
INCOMPLETE = 'I'
RISKY = 'R'
SKIPPED = 'S'
PASSED = 'P'
TOTAL = 'T'
COMPLETED = 'C'

TOTAL_PIXELS = 4*8


unicorn.set_layout(unicorn.PHAT)
unicorn.rotation(0)
unicorn.brightness(0.5)

sock = socket.socket(socket.AF_INET, # Internet
                     socket.SOCK_DGRAM) # UDP
sock.bind((UDP_IP, UDP_PORT))

time.sleep(1)

def set_pixel(i, r, g, b):
    # Don't render more pixels than we have
    if i > TOTAL_PIXELS - 1:
        return

    # Determine the X/Y coordinates based on pixel index
    x = int(i % 8)
    y = int((i - x) / 8)

    unicorn.set_pixel(x, y, r, g, b)


while True:
    data, addr = sock.recvfrom(1024)

    unicorn.set_all(0, 0, 0)

    data = data.decode("utf-8")
    if data == "reset":
        unicorn.show()
        continue

    data = json.loads(data)
    original_data = data.copy()
    print(data)

    test_count = data[TOTAL]
    # how many tests count towards a pixel?
    tests_per_pixel = test_count / TOTAL_PIXELS

    for i in range(0, TOTAL_PIXELS):
        # Render test results in order of severity
        if data[ERROR] > 0:
            data[ERROR] -= tests_per_pixel
            set_pixel(i, 255, 0, 0)
            continue
        elif data[FAILURE] > 0:
            data[FAILURE] -= tests_per_pixel
            set_pixel(i, 200, 0, 0)
            continue
        elif data[WARNING] > 0:
            data[WARNING] -= tests_per_pixel
            set_pixel(i, 255, 255, 0)
            continue
        elif data[INCOMPLETE] > 0:
            data[INCOMPLETE] -= tests_per_pixel
            set_pixel(i, 170, 170, 0)
            continue
        elif data[RISKY] > 0:
            data[RISKY] -= tests_per_pixel
            set_pixel(i, 255, 255, 0)
            continue
        elif data[SKIPPED] > 0:
            data[SKIPPED] -= tests_per_pixel
            set_pixel(i, 64, 64, 64)
            continue
        elif data[PASSED] > 0:
            data[PASSED] -= tests_per_pixel
            set_pixel(i, 0, 255, 0)
            continue

    # Update the display once the pixel buffers are set
    unicorn.show()

Credits

Colin O'Dell

Colin O'Dell

1 project • 43 followers
Lead Web Developer at Unleashed Technologies. Author of league/commonmark. Conference speaker. Arduino enthusiast.

Comments