tdd-deciphered.com

created by @parsingphase

Chapter 12: Rotor turnover, part 2

Having defined our collection of rotors and slots, we now need to look at how they work together. We’ve already alluded to the fact that a wheel can cause the one next to it to turn over, but how and when does this happen?

It turns out that the mechanism isn’t quite what you’d expect. The simplest assumption is that the rotors work like a car odometer, with a complete rotation of each rotor nudging the one to its right up one position. It turns out that while you can model the system this way, it only works most of the time.

The actual mechanism is a set of pawls (think “pushy fingers”), one for each rotor, that try to push that rotor around on each keypress by engaging with the teeth on the right side of the rotor. However, each pawl is wide enough that it’s usually blocked from engaging with those teeth by the toothless left-hand side of the rotor to its right (if there’s a rotor there)

The remaining pawls are dependent on a notch on the toothless side of their right rotor which allows them to engage with the cogs of the left rotor.

So pawl.canPush = leftRotor.hasNotchOnRightSideInCurrentPosition AND (rightRotor.isAbsent OR rightRotor.hasNotchOnLeftSideInCurrentPosition).

Simplifying this because there’s always a notch on the right-hand side (it’s a full cogwheel), pawl.canPush = (rightRotor.isAbsent OR rightRotor.hasNotchOnLeftSideInCurrentPosition)

Expanding to the two cases of the rightmost pawl (rightRotor.isAbsent is TRUE) and the remaining pawls (rightRotor.isAbsent is FALSE):

For the rightmost pawl: pawl.canPush = TRUE

For all other pawls: pawl.canPush = rightRotor.hasNotchOnLeftSideInCurrentPosition

If pawl.canPush, then it will push both its left and right rotors on the next keypress. Note that the notches are on the rotor rings, so we can ignore the ring offset when we check where they are.

The first step in implementing this is to be able to get the notch positions of all rotors. Each rotor has one or two notch positions (a couple of later ones had no notches), which correspond to the rotor offset in the slot at which a pawl would be able to engage with the notch. We’ll need to add these positions to our Rotor factory, together with a setter and getter to access them.

Setter and getter tests:

/**
 * @dataProvider validNotchSettingsProvider
 * @param string[] $notchPositions One or more character positions
 */
public function testValidNotchSetter($notchPositions)
{
    $rotor = new Rotor();
    $rotor->setNotchPositions($notchPositions);
    $this->assertSame($notchPositions, $rotor->getNotchPositions());
}

public function validNotchSettingsProvider()
{
    return [
        [['A']], // allow one notch
        [['A', 'J']], // allow two notches
        [['Z']] // check the upper limit
    ];
}

/**
 * @dataProvider invalidNotchSettingsProvider
 * @param string[] $notchPositions One or more character positions
 * @expectedException \InvalidArgumentException
 */
public function testInvalidNotchSetter($notchPositions)
{
    $rotor = new Rotor();
    $rotor->setNotchPositions($notchPositions);
}


public function invalidNotchSettingsProvider()
{
    return [
        [['']], // non-character
        [['AB']], // non-single-character
        [['A', 'J', 'Z']], // don't allow three notches
   ];
}

and implementation:

class Rotor implements EncryptorInterface
{
//…
    /**
     * @var string[] One or two single char positions determining the notch offsets
     */
    protected $notchPositions;
//…
    /**
     * @return \string[]
     */
    public function getNotchPositions()
    {
        return $this->notchPositions;
    }

    /**
     * @param \string[] $notchPositions
     */
    public function setNotchPositions($notchPositions)
    {
        if (is_array($notchPositions) && (count($notchPositions) < 3)) {
            foreach ($notchPositions as $position) {
                if (!preg_match('/^[A-Z]$/', $position)) {
                    throw new \InvalidArgumentException;
                }
            }
            $this->notchPositions = $notchPositions;
        } else {
            throw new \InvalidArgumentException;
        }
    }
//…

And add a test to RotorFactoryTest to ensure that the rotor positions we get match the constraint noted above (we can re-use our supportedRotorsDataProvider):

/**
 * Make sure each rotor's notch positions are coherent
 * @dataProvider supportedRotorsDataProvider
 *
 * @param Rotor $rotor Rotor to test
 */
public function testAllRotorNotches(Rotor $rotor)
{
    $notches = $rotor->getNotchPositions();
    $this->assertTrue(is_array($notches));
    $this->assertTrue(count($notches) < 3);
    foreach ($notches as $position) {
        $this->assertRegExp('/^[A-Z]$/', $position);
    }
}

And the implementation in RotorFactory:

protected $rotorNotchPositions = [
    self::ROTOR_ONE => ['Q'],
    self::ROTOR_TWO => ['E'],
    self::ROTOR_THREE => ['V'],
    self::ROTOR_FOUR => ['J'],
    self::ROTOR_FIVE => ['Z'],
    self::ROTOR_SIX => ['Z', 'M'],
    self::ROTOR_SEVEN => ['Z', 'M'],
    self::ROTOR_EIGHT => ['Z', 'M'],
    self::ROTOR_BETA => [],
    self::ROTOR_GAMMA => []
];

/**
 * @param mixed $instanceId A rotor name from the self::ROTOR_* constant list
 * @return Rotor
 */
public function buildRotorInstance($instanceId)
{
//...
    $rotor->setNotchPositions($this->rotorNotchPositions[$instanceId]);
//...
}

In the next chapter we’ll see how we need to combine multiple elements to be able to simulate this mechanism fully.

Tag: Chapter12-1-RotorNotches