tdd-deciphered.com

created by @parsingphase

Chapter 11: Standard rotors

So far, every time we’ve picked up a rotor, we’ve specified its core mapping directly - something that’s going to be increasingly fiddly and error-prone if we have to effectively wire the rotors by hand each time we use them.

To reduce those errors, we’ll build a small factory to give ourselves easy access to copies the standard rotors.

The wiring of each of these rotors can conveniently be found at http://www.codesandciphers.org.uk/enigma/rotorspec.htm, thanks to the late and much missed Tony Sale. We’ll stand on the shoulders of giants by making use of that information here.

We need to build a factory that contains a simple template for each rotor and allows us to request each by name. We’ll also give this factory a QA department by writing some tests to ensure that its output is internally consistent.

First, we’ll define the structure of our factory and its core rotor building class:

class RotorFactory
{

    const ROTOR_ONE = 1;
    const ROTOR_TWO = 2;
    const ROTOR_THREE = 3;
    const ROTOR_FOUR = 4;
    const ROTOR_FIVE = 5;
    const ROTOR_SIX = 6;
    const ROTOR_SEVEN = 7;
    const ROTOR_EIGHT = 8;
    const ROTOR_BETA = 'B';
    const ROTOR_GAMMA = 'G';

    /**
     * @param mixed $instanceId A rotor name from the self::ROTOR_* constant list
     * @return Rotor
     */
    public function buildRotorInstance($instanceId)
    {}
}

and then we can write our ‘QA department’. One easy test springs to mind:

class RotorFactoryTest extends \PHPUnit_Framework_TestCase
{

    public function testFactoryBuildsRotorsWithValidName()
    {
        $factory = new RotorFactory();
        $this->assertTrue($factory instanceof RotorFactory);

        $rotor = $factory->buildRotorInstance(RotorFactory::ROTOR_ONE);
        $this->assertTrue($rotor instanceof Rotor);
    }

and another is almost as intuitive:

/**
 * @expectedException \InvalidArgumentException
 */
public function testFactoryRejectsBuildsWithBadName()
{
    $factory = new RotorFactory();
    $this->assertTrue($factory instanceof RotorFactory);

    $factory->buildRotorInstance('NOSUCHROTOR');
}

We could now just go an implement those trivially, but let’s stretch ourselves and think about what other QA we might want to apply to our rotors.

We could just ‘Rotor I must have this wiring, Rotor II must have this wiring” etc, but if we do that we’re just putting the rotor mapping spec in 2 places and seeing if it matches. Any error in one location could cause a matched error in the other place, so let’s step back and see what else we can say about the mappings that aren’t just “looks like X”.

We know that the core mapping:

  • Must be represented as a 26-part array
  • Must contain each letter as an input exactly once
  • Must contain each letter as an output exactly once

If we can create a dataProvider that presents each rotor once, we can implement that as:

/**
 * Make sure each rotor's mapping is coherent
 * @dataProvider supportedRotorsDataProvider
 *
 * @param Rotor $rotor Rotor to test
 */
function testCoherentRotorIdentities(Rotor $rotor)
{
    $mapping = $rotor->getCoreMapping();
    $this->assertTrue(is_array($mapping));
    $this->assertSame(26, count($mapping));

    $inputsSeen = array();
    $outputsSeen = array();

    foreach ($mapping as $i => $o) {
        $this->assertFalse(isset($inputsSeen[$i]), 'Input seen before');
        $this->assertFalse(isset($outputsSeen[$o]), 'Output seen before');
        $this->assertSame(
            1,
            preg_match('/^[A-Z]$/', $i),
            'Input "' . $i . '" must be upper-case letter'
        );
        $this->assertSame(
            1,
            preg_match('/^[A-Z]$/', $o),
            'Output must be upper-case letter'
        );
        $inputsSeen[$i] = true;
        $outputsSeen[$o] = true;
    }

    $this->assertSame(26, count($inputsSeen));
    $this->assertSame(26, count($outputsSeen));
}

If we implement our factory class as:

<?php
namespace Phase\Enigma;

class RotorFactory
{
    const ROTOR_ONE = 1;
    const ROTOR_TWO = 2;
    const ROTOR_THREE = 3;
    const ROTOR_FOUR = 4;
    const ROTOR_FIVE = 5;
    const ROTOR_SIX = 6;
    const ROTOR_SEVEN = 7;
    const ROTOR_EIGHT = 8;
    const ROTOR_BETA = 'B';
    const ROTOR_GAMMA = 'G';

    /**
     * Simple representation of output lists against inputs A-Z
     *
     * @var array
     */
    protected $rotorSpecStrings = [
        self::ROTOR_ONE => 'EKMFLGDQVZNTOWYHXUSPAIBRCJ',
        self::ROTOR_TWO => 'AJDKSIRUXBLHWTMCQGZNPYFVOE',
        self::ROTOR_THREE => 'BDFHJLCPRTXVZNYEIWGAKMUSQO',
        self::ROTOR_FOUR => 'ESOVPZJAYQUIRHXLNFTGKDCMWB',
        self::ROTOR_FIVE => 'VZBRGITYUPSDNHLXAWMJQOFECK',
        self::ROTOR_SIX => 'JPGVOUMFYQBENHZRDKASXLICTW',
        self::ROTOR_SEVEN => 'NZJHGRCXMYSWBOUFAIVLPEKQDT',
        self::ROTOR_EIGHT => 'FKQHTLXOCBJSPDZRAMEWNIUYGV',
        self::ROTOR_BETA => 'LEYJVCNIXWPBQMDRTAKZGFUHOS',
        self::ROTOR_GAMMA => 'FSOKANUERHMBTIYCWLQPZXVGJD'
    ];

    /**
     * @param mixed $instanceId A rotor name from the self::ROTOR_* constant list
     * @return Rotor
     */
    public function buildRotorInstance($instanceId)
    {
        $rotorSpecString = $this->getRotorSpecString($instanceId);
        $outputs = preg_split('//', $rotorSpecString, -1, PREG_SPLIT_NO_EMPTY);
        $inputs = preg_split('//', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', -1, PREG_SPLIT_NO_EMPTY);
        $coreMapping = array_combine($inputs, $outputs);
        $rotor = new Rotor();
        $rotor->setCoreMapping($coreMapping);
        $rotor->setRingOffset('A'); // useful default
        return $rotor;
    }

    public function getSupportedRotorIdentities()
    {
        return array_keys($this->rotorSpecStrings);
    }

    protected function getRotorSpecString($rotorId)
    {
        if (isset($this->rotorSpecStrings[$rotorId])) {
            return $this->rotorSpecStrings[$rotorId];
        } else {
            throw new \InvalidArgumentException;
        }
    }
}

then our dataProvider is:

public function supportedRotorsDataProvider()
{
    $factory = new RotorFactory();
    $rotorIds = $factory->getSupportedRotorIdentities();
    $parameterLists = [];
    foreach ($rotorIds as $rotorId) {
        $parameterLists[] = [$factory->buildRotorInstance($rotorId)];
    }
    return $parameterLists;
}

and we pass all the tests we’ve set ourselves.

I’ve tagged this as Chapter11-1-RotorFactory