TDD-Deciphered.com

Part 9: Implementing the real rotors

16/01/2010

We can now go on to populate the first five rotors that were provided with the German Military Enigma Machines, rotors I through V, as classes Enigma_Rotor_I to Enigma_Rotor_V.

That's a lot of very fragile data, of course, and we need a good way to check it. How can we do this?

There are two risks:
1) We get the data completely wrong; ie, we populate data for the wrong wheel.
This is reasonably unlikely, and we can visually check it quite easily.

2) We make a minor error in copying the rotor specification into our class; for example we miss an input/output pair, or get the wrong output for an input.
If we were to try and check this directly, we'd end up comparing the data we type in for the class with the data we type in for the tests - not ideal. But we can do some fairly smart heuristic tests:

i) For each wheel, each letter must appear as an input exactly once, and as an output exactly once.
ii) Each wheel must have exactly 26 I/O pairs.

These are easy to code, work for all current and future rotors, and are reasonably immune from human error.

We add this test code to RotorTest.php:
  1. /**
  2.   * Make sure each rotor's mapping is coherent
  3.   * @return null
  4.   * @dataProvider fiveRotorsDataProvider
  5.   */
  6. function testCoherentRotorIdentities(Enigma_Rotor_Abstract $rotor)
  7. {
  8. $rotor=new Enigma_Rotor_I;
  9.  
  10. $mapping=$rotor->getCoreMapping();
  11. $this->assertTrue(is_array($mapping));
  12. $this->assertSame(26,count($mapping));
  13.  
  14. $inputsSeen=array();
  15. $outputsSeen=array();
  16.  
  17. foreach($mapping as $i=>$o) {
  18. $this->assertFalse(isset($inputsSeen[$i]),'Input seen before');
  19. $this->assertFalse(isset($outputsSeen[$o]),'Ouput seen before');
  20. $this->assertSame(
  21. 1,
  22. preg_match('/^[A-Z]$/',$i),
  23. 'Input "'.$i.'" must be upper-case letter'
  24. );
  25. $this->assertSame(
  26. 1,
  27. preg_match('/^[A-Z]$/',$o),
  28. 'Output must be upper-case letter'
  29. );
  30. $inputsSeen[$i]=true;
  31. $outputsSeen[$o]=true;
  32. }
  33.  
  34. $this->assertSame(26,count($inputsSeen));
  35. $this->assertSame(26,count($outputsSeen));
  36.  
  37. }
  38.  
  39. function fiveRotorsDataProvider()
  40. {
  41. return(array(
  42. array(new Enigma_Rotor_I),
  43. array(new Enigma_Rotor_II),
  44. array(new Enigma_Rotor_III),
  45. array(new Enigma_Rotor_IV),
  46. array(new Enigma_Rotor_V),
  47. ));
  48. }
And, if we've built our rotors correctly, we get clean passes.

So, we've now built a full set of rotors. Now, we can get back to the task of testing their rollover behaviour in the required array of slots.

To recap, we first want to ensure that, if the rotor in slot 1 is in its pre-turnover position, incrementing its position should cause slot 2 to turn its rotor too.

The main problem at this point, however, is that we've not yet implemented the idea of a turnover position. These positions, for the five rotors we've built so far, follow the mnemonic coined at Bletchley Park: "Royal Flags Wave Kings Above". (These are the post-turnover positions).

There are a few ways we could implement this, but we'll take our clue from the mechanical properties of the system; pegs at a given position on the wheel, which, when the wheel is in the appropriate position, cause a "knock-on". Most wheels have one peg; some have two, and the pegs were movable in some (experimental?) models.

It's the position of the peg in the slot that counts, so we'll get the slot to check the wheel, and have the slot trigger a turnover when a rotor has a peg at the current position.

So, rotor 1 needs to respond to hasPegAtPosition($position) with a Boolean answer. Since all rotors have this behaviour, we put it in the Enigma_Rotor_Abstract class, and, as with the core mapping, store the position as a private property. (For our test rotors, Enigma_Rotor_Configurable, we'll assume a default position of "A" for now, until we need to do something smarter).

So, Enigma_Rotor_Abstract gains:
  1. protected $_pegPositions=array();
  2.  
  3. /**
  4.   * Do we go "tick" here?
  5.   * @return bool
  6.   */
  7. public function hasPegAtPosition($position) {
  8. return(in_array($position,$this->_pegPositions));
  9. }
  10.  
  11. and Enigma_Rotor_I gains
  12.  
  13. protected $_pegPositions = array(‘R');
  14.  
  15. and so on for the other rotors.
  16.  
  17. We can test these wheels in RotorTest with:
We can similarly give the slots the ability to check for turnover, with a knockOnDue() function in Enigma_Rotor_Slot. To test this, we'll need to load a slot with a known rotor in a known position.


(Note: We treat KnockOn as the cause, and Turnover as the effect)

In RotorSlotTest.php, we put very similar tests to the above:
  1. /**
  2.   * Check that slots know when to trigger a "Knock-On"
  3.   *
  4.   * @dataProvider fiveRotorsTurnoverDataProvider
  5.   * @param $rotor Enigma_Rotor_Abstract
  6.   * @param $offsetTurnoverDue
  7.   * @param $offsetNoTurnover
  8.   * @return null
  9.   */
  10. function testSlotKnockOnDue(Enigma_Rotor_Abstract $rotor,
  11. $offsetTurnoverDue,$offsetNoTurnover)
  12. {
  13. $slot=new Enigma_RotorSlot();
  14. $slot->loadRotor($rotor);
  15.  
  16. $slot->setRotorOffset($offsetTurnoverDue);
  17. $this->assertTrue($slot->turnoverDue());
  18.  
  19. $slot->setRotorOffset ($offsetNoTurnover);
  20. $this->assertFalse($slot->turnoverDue());
  21. }
  22.  
  23.  
  24. function fiveRotorsTurnoverDataProvider()
  25. {
  26. return(array(
  27. array(new Enigma_Rotor_I,'R','Z'),
  28. array(new Enigma_Rotor_II,'F','X'),
  29. array(new Enigma_Rotor_III,'W','P'),
  30. array(new Enigma_Rotor_IV,'K','N'),
  31. array(new Enigma_Rotor_V,'A','F'),
  32. ));
  33. }
and allow ourselves to pass them by adding:
  1. public function turnoverDue()
  2. {
  3. return($this->_rotor->hasPegAtPosition($this->getRotorOffsetAsCharacter()));
  4. }
  5.  
  6. public function getRotorOffsetAsCharacter ()
  7. {
  8. return $this->_alphabetPositionToCharacter($this->getRotorOffset());
  9. }
to the Enigma_RotorSlot class.

We then need to link the rotor and slots, and the electrical and mechanical stages together. Remembering that the mechanical phase happens first:

Increment slot 1's rotor position.
Is this slot's rotor now in the knock-on position?
If so:
Cause slot 2's rotor position to increment.
Is slot 2's rotor in the knock-on position... etc

Note that slot2 can only cause a knock-on in slot3 if it itself is being moved on - so it makes sense to put this check-and-trigger in the Enigma_RotorSlot::incrementRotorOffset() function.

We'll implement this simply in the Enigma_RotorSlot class as follows:
  1. public function incrementRotorOffset()
  2. {
  3. $this->_rotorOffset++;
  4. if($this->_rotorOffset>26) {
  5. $this->_rotorOffset=1;
  6. }
  7.  
  8. //If we're in turnover position, notify the next slot
  9. if($this->turnoverDue()) {
  10. $this->notifyTurnoverObserver();
  11. }
  12. }
  13.  
  14. public function registerTurnoverObserver(Enigma_RotorSlot $observer)
  15. {
  16. $this->_turnoverObserver=$observer;
  17. }
  18.  
  19. public function notifyTurnoverObserver()
  20. {
  21. if($this->_turnoverObserver) {
  22. $this->_turnoverObserver->incrementRotorOffset();
  23. }
  24. }
How do we test this though? If it works correctly, it'll cause a turnover in the next rotor up, but we'll have to set and then read this rotor's position – we're testing an indirect effect, which is slightly risky.

However, as this is the best we know for now, let's give it a try.
  1. function testNextRotorKnockOnSimple()
  2. {
  3. //Three slots, as per a basic enigma device
  4. $rotorSlots=array(new Enigma_RotorSlot, new Enigma_RotorSlot, new Enigma_RotorSlot);
  5.  
  6. //each slot is observed by next one up
  7. $rotorSlots[1]->registerTurnoverObserver($rotorSlots[2]);
  8. $rotorSlots[0]->registerTurnoverObserver($rotorSlots[1]);
  9.  
  10. //load the slots
  11. $rotorSlots[0]->loadRotor(new Enigma_Rotor_I);
  12. $rotorSlots[1]->loadRotor(new Enigma_Rotor_II);
  13. $rotorSlots[2]->loadRotor(new Enigma_Rotor_III);
  14.  
  15. //put slot 0's wheel slightly before its turnover position
  16. $rotorSlots[0]->setRotorOffset('P');
  17. //put other wheels in known position
  18. $rotorSlots[1]->setRotorOffset('A');
  19. $rotorSlots[2]->setRotorOffset('A');
  20.  
  21. //Check 0 and 1 are in expected positions:
  22. $this->assertSame('P',$rotorSlots[0]->getRotorOffsetAsCharacter());
  23. $this->assertSame('A',$rotorSlots[1]->getRotorOffsetAsCharacter());
  24. $this->assertSame('A',$rotorSlots[2]->getRotorOffsetAsCharacter());
  25.  
  26. //Turn over slot 0
  27. $rotorSlots[0]->incrementRotorOffset();
  28. //0 moves, 1 stays still
  29. $this->assertSame('Q',$rotorSlots[0]->getRotorOffsetAsCharacter());
  30. $this->assertSame('A',$rotorSlots[1]->getRotorOffsetAsCharacter());
  31. $this->assertSame('A',$rotorSlots[2]->getRotorOffsetAsCharacter());
  32.  
  33. //Turn over slot 0
  34. $rotorSlots[0]->incrementRotorOffset();
  35. //Bottom 2 move this time
  36. $this->assertSame('R',$rotorSlots[0]->getRotorOffsetAsCharacter());
  37. $this->assertSame('B',$rotorSlots[1]->getRotorOffsetAsCharacter());
  38. $this->assertSame('A',$rotorSlots[2]->getRotorOffsetAsCharacter());
  39. }
That looks promising (and works!), but how could we ask "has slot[1]'s rotor had incrementRotorOffset called once?"

One option would be to add code to the Enigma_RotorSlot class to count the number of times incrementRotorOffset class was called, and then check that afterwards – but this would bulk out the class with test-only code and impair its functionality. We should be able to test the class without modifying it.

What if we had a piece of test kit we could drop in slot[1] which we told "Expect one call to incrementRotorOffset"? Well, if we could tell it what we meant by "expect" (or when to stop expecting) that might work, but again the code could be complex if we have to build it by hand. Plus, our registerTurnoverObserver() function demands that its argument is an Enigma_RotorSlot, so we'd have to extend that class.

However, PHPUnit provides exactly that functionality internally, for any class. It's called "Mocking", and we use to create a "Mock Object" that replicates the slot's functionality. Instead of the mass of hand-built code, we implement it as follows:
  1. $mockRotor=$this->getMock('Enigma_RotorSlot', array('incrementRotorOffset','setRotorOffset'));
  2.  
  3. $mockRotor->expects($this->once())->method('incrementRotorOffset');
Our first line says "Create a fake Enigma_RotorSlot with incrementRotorOffsets() and setRotorOffset() functions". The second says "This class' incrementRotorOffset() function must be called once in the current test function".

Those are about the only changes we need to make to our test function:
  1. function testNextRotorKnockOnMock()
  2. {
  3. $mockRotor=$this->getMock(
  4. 'Enigma_RotorSlot',
  5. array('incrementRotorOffset','setRotorOffset')
  6. );
  7.  
  8. $mockRotor->expects($this->once())->method('incrementRotorOffset');
  9.  
  10. $rotorSlots=array(new Enigma_RotorSlot, $mockRotor, new Enigma_RotorSlot);
  11.  
  12. //each slot is observed by next one up
  13. $rotorSlots[1]->registerTurnoverObserver($rotorSlots[2]);
  14. $rotorSlots[0]->registerTurnoverObserver($rotorSlots[1]);
  15.  
  16. //load the slots
  17. $rotorSlots[0]->loadRotor(new Enigma_Rotor_I);
  18. $rotorSlots[1]->loadRotor(new Enigma_Rotor_II);
  19. $rotorSlots[2]->loadRotor(new Enigma_Rotor_III);
  20.  
  21. //put slot 0's wheel slightly before its turnover position
  22. $rotorSlots[0]->setRotorOffset('P');
  23. //put other wheels in known position
  24. $rotorSlots[1]->setRotorOffset('A');
  25. $rotorSlots[2]->setRotorOffset('A');
  26.  
  27. //Turn over slot 0
  28. $rotorSlots[0]->incrementRotorOffset();
  29. $rotorSlots[0]->incrementRotorOffset();
  30. }
This is a simple application of this capability (and one we could have been able to work around), but shows how it can be used. However, it works, and with that we've tested all the mechanical functionality of the Enigma system.

Next time, we'll finish up the electical parts of the system, and look at how to assemble the components into a working machine.

All content copyright Richard George (richard@phase.org), 2009-2010

Sponsored links to recommended books: