TDD-Deciphered.com

Part 8: Putting the Rotors in place

16/01/2010
In the last installment, we got the rotors working. Now, we'll work on the framework they sit in, starting with the rotor slots

As we did with the Rotors themselves, we need to analyse the behaviour we need from the slots:
- Any time a letter is sent to the right-hand rotor in slot 1, it will be turned over by one position before the electrical signal is sent.
- If the rotor in a slot is in its pre-turnover position, and it turns over, it will cause the rotor to its left to turn over before the electrical signal is passed.
- The position of the rotor in the slot must also be taken into account in routing the electrical signal.

From this, we can see that each slot will need to know:
- What position it's in (ie, which slot number it is)
- Whether its rotor is in the pre-turnover position
- Whether the rotor to its right (if it has one) will cause it to turn over
- What rotary position its rotor is in.

We can best model the behaviour of this system in two phases; the mechanical phase and the electrical phase. We can test these both separately and together, and we can also test the slots separately and as a set.

Once we can test both phases for the whole set of rotors, we'll have simulated the complete device except for the reflector and plugboard, both of which are comparatively simple elements.

The easiest place for us to start is with the electrical effects of the rotary position of a rotor in a slot, which is functionally the same as the position of the rotor's core within the ring.
We can use the data and notation already seen above to designate what happens when we drop Rotor I, with its core set to an offset of 26/Z, into a slot at position "A" (the default position).
We'll extend the diagrams we used before:

If we input a 'V', we get:



ie input V, rotor position A, core position Z, output A

If we rotate the wheel two steps (keeping the core position), we get:



ie input V, rotor position C, core position Z, output Z

This gives up two testable datasets which, if we skip over the details of setters and so on, gives us tests that might look like this:
  1. <?php
  2. require_once ('../../library/initialise.php');
  3.  
  4. class Enigma_RotorSlotTest extends PHPUnit_Framework_TestCase
  5. {
  6.  
  7. /**
  8.   * @dataProvider singleSlotRotorOneRingOffsetOneDataProvider
  9.   * @param $rotorOffset
  10.   * @param $input
  11.   * @param $output
  12.   */
  13. function testSingleSlotRotorOneRingOffsetOne( $rotorOffset, $input, $output )
  14. {
  15. $rotor = new Enigma_Rotor();
  16. $coreMapping=array(
  17. 'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D',
  18. 'H'=>'Q','I'=>'V','J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W',
  19. 'O'=>'Y','P'=>'H','Q'=>'X','R'=>'U','S'=>'S','T'=>'P','U'=>'A',
  20. 'V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
  21. );
  22.  
  23. $rotor->setRingOffset(1);
  24. $rotor->setCoreMapping($coreMapping);
  25.  
  26. $slot=new Enigma_RotorSlot();
  27. $slot->loadRotor($rotor);
  28. $slot->setRotorOffset($rotorOffset);
  29.  
  30. $slotOutput=$slot->getOutputCharacterForInputCharacter($input);
  31.  
  32. $this->assertSame($output,$slotOutput);
  33. }
  34.  
  35. function singleSlotRotorOneRingOffsetOneDataProvider()
  36. {
  37. return array(
  38. array('A','V','A'),
  39. array('C','V','O')
  40. );
  41. }
  42. }
Creating the code to fill these tests is very similar to the code we created for the rotors themselves; feel free to work through it yourselves, but the net result will be a class much like:
  1. <?php
  2. class Enigma_RotorSlot implements Enigma_Encryptor_Interface
  3. {
  4.  
  5. /**
  6.   *
  7.   * @var Enigma_Rotor_Abstract
  8.   */
  9. private $_rotor;
  10. private $_rotorOffset;
  11.  
  12. public function loadRotor($rotor)
  13. {
  14. $this->_rotor=$rotor;
  15. }
  16.  
  17. public function setRotorOffset ($offset)
  18. {
  19. if(preg_match('/^[A-Z]$/',$offset)) {
  20. $offset=$this->_charToAlphabetPosition($offset);
  21. } else if(!is_integer($offset) || ($offset<1) || ($offset>26)) {
  22. throw new Exception("Offset must be integer in range 1..26");
  23. }
  24.  
  25. $this->_rotorOffset = $offset;
  26. }
  27.  
  28. public function getOutputCharacterForInputCharacter ($inputCharacter)
  29. {
  30. $inputCharacter = strtoupper($inputCharacter);
  31. $rotorInputCharacter = $this->_getCharacterOffsetBy(
  32. $inputCharacter,
  33. $this->_rotorOffset-1
  34. );
  35. $rotorOutputCharacter = $this->_rotor->getOutputCharacterForInputCharacter(
  36. $rotorInputCharacter
  37. );
  38. $outputCharacter = $this->_getCharacterOffsetBy(
  39. $rotorOutputCharacter, 0 - ($this->_rotorOffset - 1)
  40. );
  41. return ($outputCharacter);
  42. }
  43.  
  44. private function _getCharacterOffsetBy ($character, $offset)
  45. {
  46. if(!is_numeric($offset)) {
  47. throw new Exception('Character offsets must be numeric');
  48. }
  49. $offset=(26+$offset)%26;
  50. $charAsInt = $this->_charToAlphabetPosition($character);
  51.  
  52. $newInteger = (26 + $charAsInt + $offset) % 26;
  53. if($newInteger==0) {
  54. $newInteger=26;
  55. }
  56.  
  57. $newCharacter = $this->_alphabetPositionToCharacter($newInteger);
  58. return ($newCharacter);
  59. }
  60.  
  61. private function _charToAlphabetPosition ($char)
  62. {
  63. if(!preg_match('/^[A-Z]$/',$char)) {
  64. throw new Exception ("Bad Character '$char'");
  65. }
  66. $position= (ord($char)-64);
  67.  
  68. return $position;
  69. }
  70.  
  71. private function _alphabetPositionToCharacter ($position)
  72. {
  73. if(($position<1) || ($position>26)){
  74. throw new Exception ("Bad alphaoffset '$position'");
  75. }
  76. $char= (chr($position+64));
  77.  
  78. return $char;
  79. }
  80.  
  81. }
That was easy enough... so we'll make checkin 7 to save our position.

Now, to simulate the mechanical part, we need to consider how the mechanical interaction works. For the rightmost rotor (slot 1), the motion of pushing down the key will also push around the rotor in slot one. If this rotor is in its turnover position, when it turns it will also push around the rotor in slot 2, and so on. At the end of its downward mechanical motion the key completes the electrical circuit and so the signal current flows after all turnovers have taken place.

In terms of functional requirements, this means:

The rightmost rotor will turn over on every keypress, but not all rotors will turn over on every keypress.
The action of the remaining rotors depends on the identity and position of the rotor in the slot to their right, therefore:
There needs to be some sort of programmatic structure that relates the positions and behaviours of the slots and their rotors.

So, how to do this? One way of looking at the problem is that each slot needs to see what slot (and rotor) below it are doing. The ideal pattern for this is the Observer Pattern, which will give us the following observation network:

Slot 3 watches for a "turnover" notification from slot 2; when it receives it, it turns its own rotor.
Slot 2 watches for a turnover notification from slot 1; when it receives it, it turns over its own rotor; if this rotor is now in its turnover position, it sends its own turnover notification.
Slot 1 observes nothing (although it could observe the keyboard, we're not simulating that, so we'll accept a direct turnover call from an as-yet-undefined object that handles the device's input). If its rotor is in turnover position after this, it'll send its notification.

Before we start on this structure, we'll handle the simple case of the first rotor. This simply needs an "increment rotor position" function that we'll trigger manually. We just need to test that the rotor offset after calling this is one more than it was beforehand.
We can use the following test:
  1. /**
  2.   * @dataProvider offsetIncrementDataProvider
  3.   */
  4. function testRotorOffsetIncrement($originalOffset,$newOffset)
  5. {
  6. $rotor = new Enigma_Rotor();
  7. $coreMapping=array(
  8. 'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D','H'=>'Q','I'=>'V',
  9. 'J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W','O'=>'Y','P'=>'H','Q'=>'X','R'=>'U',
  10. 'S'=>'S','T'=>'P','U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
  11. );
  12.  
  13. $rotor->setRingOffset(1);
  14. $rotor->setCoreMapping($coreMapping);
  15.  
  16. $slot=new Enigma_RotorSlot();
  17. $slot->loadRotor($rotor);
  18. $slot->setRotorOffset($originalOffset);
  19.  
  20. $this->assertSame($originalOffset,$slot->getRotorOffset());
  21. $slot->incrementRotorOffset();
  22. $this->assertSame($newOffset,$slot->getRotorOffset());
  23. }
  24.  
  25. function offsetIncrementDataProvider()
  26. {
  27. return array(
  28. array(1,2),
  29. array(13,14),
  30. array(26,1)
  31. );
  32. }
We'll go with numeric offsets as that's how we store them internally.

Note that we need to add two new functions to pass these tests:
Enigma_RotorSlot::getRotorOffset()
Enigma_RotorSlot::incrementRotorOffset()

These are trivial:
  1. public function getRotorOffset()
  2. {
  3. return($this->_rotorOffset);
  4. }
  5.  
  6. public function incrementRotorOffset()
  7. {
  8. $this->_rotorOffset++;
  9. if($this->_rotorOffset>26) {
  10. $this->_rotorOffset=1;
  11. }
  12. }
So, now we can roll over one rotor. How do we roll over the lot? How do we test this?
It's time to put some of our elements together into the beginnings of the working system that we described in our initial design. We designated the "Enigma_Machine" class for this purpose, so now we need to start building it.

However, if we're now working with the whole box, how are we going to test it at a unit level? Can we still get an accurate idea of what's going on within the system without having to compromise the design of the Enigma_Machine class?

So far, our design specification for that class consists of the following:
  1. <?php
  2. $outChar=$myEnigma->getOutputCharacterForInputCharacter($inChar);
  3. ?>
That doesn't give us much to work on. We can suppose we'll need to add methods to drop rotors into slots and set up the plugboard, but if we weren't testing, we'd have no reason to (for example) track the notifications between rotors, or care where the rollover points on the rotors are. An accurate model of the system just has to accept an input and configuration, display the letters in the rotor windows, and tell us which lamp's lit. It doesn't, to look at it another way, have to tell us why; the device isn't designed to be self-testing.

So, if we wanted to build, or test, a real Enigma machine, how would we do that? As with any complex electrical or mechanical device, we'd need special tools and test equipment. We can build code equivalents of these tools; test rigs that can check parts of the system but won't be shipped with the working device.

(As a historical note, the real enigma devices did ship with two test tools; a set of contacts to test bulbs, and a pair of sockets to test the plugboard cables. These were maintenance tools to handle mechanical failure - hopefully not a problem we'll have with our software version.)

Before we start building our test rig, however, we should do some housekeeping on our code. So far, every time we've set up a rotor we've specified the internal mapping. German signallers never had to do that; they took one of a small set of wheels out of the box, turned them to the required core offset and dropped them into a slot. To better model this, and to keep our lives simpler when we're setting up the device, we can model these rotors as specific classes.

The core mapping of all standard "service" rotors can be found online at http://www.codesandciphers.org.uk/enigma/rotorspec.htm

From this, we can build classes like the following:
  1. <?php
  2. final class Enigma_Rotor_I extends Enigma_Rotor
  3. {
  4. protected $_coreMapping=array(
  5. 'A'=>'E','B'=>'K','C'=>'M','D'=>'F','E'=>'L','F'=>'G','G'=>'D','H'=>'Q','I'=>'V',
  6. 'J'=>'Z','K'=>'N','L'=>'T','M'=>'O','N'=>'W','O'=>'Y','P'=>'H','Q'=>'X','R'=>'U',
  7. 'S'=>'S','T'=>'P','U'=>'A','V'=>'I','W'=>'B','X'=>'R','Y'=>'C','Z'=>'J'
  8. );
  9.  
  10. }
The class is final as it represents a fixed, solid entity. It has no methods beyond those it inherits from Enigma_Rotor. The only data it needs of its own is its core mapping (and, as we'll see later, its rollover notch position, but we'll leave that for now).

We've already written tests that use a rotor with this mapping, so we ought to be able to drop it straight into those tests.

We copy Enigma_RotorTest::testRotorIMappingOffset() to Enigma_RotorTest::testRealRotorIOffset() and replace the verbose setup of the mapping with a simple call to instantiate this new class. Our new test function is:
  1. /**
  2.   * @dataProvider rotorIDataProvider
  3.   * @param $testInputCharacter
  4.   * @param $testOutputCharacter
  5.   * @return null
  6.   */
  7. function testRealRotorIOffset ($offset,$testInputCharacter,$testOutputCharacter)
  8. {
  9. $rotor = new Enigma_Rotor_I();
  10. $rotor->setRingOffset($offset);
  11. $this->assertSame(
  12. $testOutputCharacter,
  13. $rotor->getOutputCharacterForInputCharacter($testInputCharacter)
  14. );
  15. }
However, if we run that, we get some very obscure errors, for example:
silverbox:Enigma wechsler$ phpunit RotorTest.php 
PHPUnit 3.4.5 by Sebastian Bergmann.

...................EEE

Time: 0 seconds, Memory: 5.75Mb

There were 3 errors:

1) Enigma_RotorTest::testRealRotorIOffset with data set #0 (1, 'V', 'I')
Exception: Bad Character ''

/Users/wechsler/tdd-enigma/library/Enigma/Rotor.php:69
/Users/wechsler/tdd-enigma/library/Enigma/Rotor.php:57
/Users/wechsler/tdd-enigma/library/Enigma/Rotor.php:47
/Users/wechsler/tdd-enigma/tests/Enigma/RotorTest.php:200

[...]

FAILURES!
Tests: 22, Assertions: 19, Errors: 3.
silverbox:Enigma wechsler$ phpunit RotorTest.php 
PHPUnit 3.4.5 by Sebastian Bergmann.

......................

Time: 0 seconds, Memory: 5.75Mb

OK (22 tests, 22 assertions)
The problem's a simple but important one. Our $_coreMapping member is marked private in the Enigma_Rotor base class. This means that our inheriting class can't override it, and we're trying to run with an empty rotor mapping. If we fix this to be protected in the base class, and re-run all out unit tests, all is clean.

However, we still have some "accidents of history" in our code. In particular, we have the setCoreMapping() function, which makes no sense for our new, immutable wheel. We need to do a little housekeeping here, and split our rotor classes up to allow us to keep developing and testing.

We'll create a new parent class, Enigma_Rotor_Abstract, which contains everything common to all rotors.

We'll move our setCoreMapping function to a new subclass of Enigma_Rotor_Abstract, called Enigma_Rotor_Configurable. This doesn't represent any real-world rotor, but is useful for testing; we can consider it a test tool as mentioned above.

Once we've done that, we'll have an abstract class Enigma_Rotor_Abstract implements Enigma_Encryptor_Interface and an Enigma_Router_Configurable class:
  1. <?php
  2. class Enigma_Rotor_Configurable extends Enigma_Rotor_Abstract
  3. {
  4.  
  5. public function setCoreMapping (array $mapping)
  6. {
  7. if (count($mapping) != 26) {
  8. throw new Exception("Mapping must have 26 elements");
  9. }
  10. $this->_coreMapping = $mapping;
  11. }
  12. }
However, if we run our tests with no other changes, we'll fail immediately. Note that we can run all tests in a directory by just putting the directory name as the argument to PHPunit:
silverbox:Enigma wechsler$ phpunit .
PHPUnit 3.4.5 by Sebastian Bergmann.

Fatal error: Class 'Enigma_Rotor' not found in /Users/wechsler/tdd-enigma/tests/Enigma/RotorSlotTest.php on line 16
We can solve this in RotorSlotTest.php with a quick search and replace from Enigma_Rotor to Enigma_Rotor_Configurable (taking care not to rename Enigma_RotorSlot when we do so).

We then need to do much the same in RotorTest.php, which will then remind us we also need to fix the inheritance of Enigma_Rotor_I (to extend Enigma_Rotor_Abstract).

Having done those changes, all the tests pass again. We've just done a refactoring which changed almost every file in our library, but thanks to our tests we have a high degree of certainty that we've broken no functionality in the process.

And that seems like a good time to to a checkin, and relax. (NB; this is revision 9; I used one in the refactoring above for safety's sake).

Next time, we'll create final classes for the five basic rotors used in the 'Service' Enigma, and look at the mechanical phase of the device's operation.

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

Sponsored links to recommended books: