So far we've made one very simple comment about the Rotor class - that it's an encryptor - but we've made no attempt to actually model the real rotor's behaviour. In order to do that, we need to more closely specify the rotor's properties and capabilities:
- A rotor has 26 inputs on its right hand side, and 26 outputs on its left hand side.
- Each input is connected to exactly one output. The way in which it connects is specified by the internal wiring of the rotor, and is a fixed property of any specific rotor type. Rotors are generally referred to by roman numerals.
- A rotor consists of an internal disk or "core" carrying the wiring, and an external ring, often referred to as a tyre, which carries the letter indicators.
- Each rotor has at least one carry notch on the left side of the ring, which can cause the next rotor up to move around one place.
- The ring can be moved relative to the core. The offset is referred to as the "Ring Setting" or "Ringstellung".
The functionality we've already contracted to provide, by agreeing to implement the Enigma_Encryptor_Interface is "Output value for given input value".
So, with the information we now have, can we implement and test our Rotor class?
Well, not quite.
We can see that the behaviour of a single rotor depends on both the identity of the rotor (and thereby its internal wiring), and its Ringstellung. We need to specify both of these to be able to simulate the rotor. We could specify both of these as configuration options to our Rotor class - and, leaving aside issues of design purity for now, let's do so.
For the moment, we don't want to break our existing unit test by changing the rotor's constructor, so let's add setRingOffset and setInputOutputMapping functions to our Enigma_Rotor class. For testing convenience, we'll also add relevant get* functions as well.
However, since we're going for pure TDD at the moment, we add these to the tests first.
Add a second function to tests/Enigma/RotorTest.php:function testSetAndRememberRingOffset ()
{
$rotor = new Enigma_Rotor();
$ringOffset=15;
$rotor->setRingOffset($ringOffset);
$this->assertSame($ringOffset,$rotor->getRingOffset());
}
Run the test, and of course it'll crash, because the functions aren't there. Adding them's easy enough:
Update library/Enigma/Rotor.php to read:<?php
class Enigma_Rotor implements Enigma_Encryptor_Interface
{
private $_ringOffset;
public function setRingOffset ($offset)
{
$this->_ringOffset = $offset;
}
public function getRingOffset ()
{
return $this->_ringOffset;
}
}
Run the tests, and they pass. Once more, we've gone red, green - but we won't refactor right now as the code's OK for our current needs. Instead, we go red again by updating RotorTest.php with:function testSetAndRememberCoreMapping ()
{
$rotor = new Enigma_Rotor();
$rotor->setCoreMapping($coreMapping);
$this->assertSame($coreMapping,$rotor->getCoreMapping());
}
and green by adding the following to Enigma_Rotor:private $_CoreMapping;
public function setCoreMapping ($Mapping)
{
$this->_coreMapping = $Mapping;
}
public function getCoreMapping ()
{
return $this->_coreMapping;
}
Whoa... green's great, but what use is an empty ring mapping?
Well, it's no use - and ideally our Rotor would reject it. However, if we decided we wanted that (additional) behaviour, we should define it as a new test.
For now we'll comment out testSetAndRememberCoreMapping as it's an invalid test and write a better one.
Now, we want to reject invalid data, but how do we define valid data? The simplest solution would be an associative array, of input position => output position. For readability, we'll work with an array of upper case letters, and for the sake of our "valid mapping" test, we'll just insist (for now) that it has 26 elements.
We now want to do at least two tests:
- Valid data is accepted.
- Invalid data is rejected.
We'll defined some really simple valid data for now - a "pass-through" rotor that maps any input character to itself. This may seem simple, but there is a historical relevance to it that we'll cover later.
So, we create a test function:function testSetAndRememberValidCoreIdentityMapping ()
{
$rotor = new Enigma_Rotor();
'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y','Z'=>'Z'
);
$rotor->setCoreMapping($coreMapping);
$this->assertSame($coreMapping,$rotor->getCoreMapping());
}
We can run the tests now, and they'll be green. We've not hit a red since our last green, but that doesn't matter; we'll prepare to hit one now:function testSetInvalidCoreMappingExplodes ()
{
$this->setExpectedException('Exception');
$rotor = new Enigma_Rotor();
'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y'
);
$rotor->setCoreMapping($coreMapping);
// $this->assertSame($coreMapping,$rotor->getCoreMapping());
}
We've made three changes here. We've chopped off the 'Z' mapping to make the whole mapping invalid, and we've told PHPUnit to expect an exception of type 'Exception'. Note that this expectation has to be specified before it's thrown (ideally, just before). We've also dropped the AssertSame as it's irrelevant if we can get the exception to throw
Running the tests now will give us a failure, however:silverbox:Enigma wechsler$ phpunit RotorTest.php
PHPUnit 3.4.5 by Sebastian Bergmann.
....F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) Enigma_RotorTest::testSetInvalidCoreMappingExplodes
Expected exception Exception
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
As we've not written any code to verify (and reject) our mapping, we get a "failure to fail". We can fix this by modifying setCoreMapping:public function setCoreMapping
(array $mapping) {
if(count($mapping)!=26) { throw new Exception("Mapping must have 26 elements");
}
$this->_coreMapping = $mapping;
}
after which we have green tests again.
Now, we'll commit that as we have our mapping behaviour neatly defined, and we want to sit back and think for a bit about validating our offsets.silverbox:tdd-enigma wechsler$ svn commit -m 'Verify rotor mapping setter/getter'
Sending library/Enigma/Rotor.php
Sending tests/Enigma/RotorTest.php
Transmitting file data ..
Committed revision 3.
We've put some basic testing on our rotor mapping - a bit too basic really, as we should really check that our mapping is exactly 26 upper-case characters, each to an upper-case character, and that each character appear on the input side and output side exactly once. Sounds tricky? Well guess what, I'm gonna leave that as an exercise to the reader! (You'll find some similar tests later on in this project, though). Do we really need to test quite this pedantically?
Well, testing a setter to this extent may verge on overkill, but by doing so (rather than just "writing what works") we’ve already considered one error condition we probably wouldn’t have spotted otherwise (the empty array). It’s also worth remembering that we’re not writing these tests just to confirm we can write the setter, but to test at any point in the future, when we’ve got thousands of lines of code in our application, that every single part of the app still works perfectly. If something breaks, and we’ve tested this thoroughly, we’ll know exactly what went wrong.
What we will test next though is a valid ring offset; we're going to be rolling these rotors around later on, and if they can get stuck in illogical positions we will get *very* confused.
So what defines a valid offset, and what does that offset mean? We could take a look at our system and say that we're looking at an offset in an array of mappings, and so that'll logically go from 0-25. But does that make sense with the hardware? Would it be meaningful to an original Enigma user or codebreaker?
Well, yes and no. It's *incredibly* tricky trying to nail down precise details for an 80-year old hardware device that was classified as Secret until 30 years ago, and most of the books on the topic weren't written for software engineers trying to write simulators. But you're lucky, because this book (well, series), is.
The "yes and no" answer lies in the fact that there were at least two dozen different types of Enigma device (see the Enigma Family Tree for details, and that different users (eg the German Army, or the Japanese Diplomatic Service) used them differently. We need to find a way to specify the offset that would make sense to all of them.
As it turns out, while most Enigma users appear to have used letters to indicate positions and offsets, the German Army (the 'Wehrmacht') tended to use devices with numbered (rather than lettered) rotors and may (so far as I can tell) also have specified ring positions numerically. So we'll start off with that convention.
(For the avoidance of confusion, the typical Wehrmacht machine, which I *believe* was the Type I, still had 26-position rotors. A later 10-position purely numeric system, the Z-series, was barely used).
So, we've said numeric's OK, but what numbers? Zero-indexed is natural to us, but these systems were all built before the computer age, and conventions and nomenclature were often different from what we're used to. Would 1-indexed make more sense?
Well, rather helpfully, the book Codebreakers: The Inside Story of Bletchley Park happens to have a picture of a daily settings sheet for a whole month, and in the numbers under "Ring Offsets" it contains one 26, and no zeros. (I'll see if Bletchley Park will let me reproduce such a sheet here at some point).
So, 1-26. We can add a test for this for now, and (since this installment's starting to get a little long), worry about what we mean by it later. We can be rather more confident about an alphabetic offset; only be A-Z can make any sense.
Writing the numeric test first gives us a chance to decide which conditions we should test. We know that:
- A rotor offset that's not numeric should fail (we'll ignore the character case for now and add that in the next iteration).
- Any rotor offset less than one must fail.
- Any rotor offset greater than 26 must fail.
Now, we could meet all these requirements just by rejecting *all* input (this is known as "smartarse TDD"), so let's also state what must pass:
- Any integer from 1 to 26 inclusive
Now, there's an infinite number of possible inputs, so which should we test against? One principle that will serve use well here is to test against "boundary conditions"; locations where the data is either just right, or just wrong.
For example:
0: just wrong; integer, but slightly too low
1: just right; integer, the lowest that passes
26: just right; integer, the highest that passes
27: just wrong; integer, just too high
0.99; just wrong; numeric, too low and not integer
25.99; just wrong; numeric, in range but not integer
The last two probably aren't pure boundary conditions, but they're still useful tests. We'll also add:
'A'; wrong datatype for our current spec - remember that we can update our tests as we update specs and system capabilities.
We can use these rules to build the following test:/**
* @dataProvider ringOffsetBoundariesDataProvider
* @param $ringOffset int (ideally!)
* @param $isOk bool
* @return null
*/
function testRingOffsetBoundaryConditions($ringOffset,$isOk)
{
if(!$isOk) {
$this->setExpectedException('Exception');
}
$rotor = new Enigma_Rotor();
$rotor->setRingOffset($ringOffset);
if($isOk) {
//Any test will do here to keep PHPUnit happy
// - if we got an unwanted exceptions, it'll have told us anyway.
$this->assertSame($ringOffset,$rotor->getRingOffset());
}
}
function ringOffsetBoundariesDataProvider()
{
);
}
Obviously there's something new going on here. We're running the same test with multiple datasets, so rather than rewrite the tests and just change the numbers and expectations, I've used PHPUnit's "Data Provider" capability. You can probably see how it works; you give your test function input parameters, and then name a function that'll provide thoses parameters in a special Docblock (/**) comment, ie/**
* @dataProvider functionNameGoesHere
*/
and in the named function, we provide an array-of-arrays of parameters to be used on each attempt.
So, if we test that code:silverbox:Enigma wechsler$ phpunit RotorTest.php
PHPUnit 3.4.5 by Sebastian Bergmann.
....F..FFFF
Time: 0 seconds, Memory: 5.75Mb
There were 5 failures:
1) Enigma_RotorTest::testRingOffsetBoundaryConditions with data set #0 (0, false)
Expected exception Exception
2) Enigma_RotorTest::testRingOffsetBoundaryConditions with data set #3 (27, false)
Expected exception Exception
3) Enigma_RotorTest::testRingOffsetBoundaryConditions with data set #4 (0.99, false)
Expected exception Exception
4) Enigma_RotorTest::testRingOffsetBoundaryConditions with data set #5 (25.99, false)
Expected exception Exception
5) Enigma_RotorTest::testRingOffsetBoundaryConditions with data set #6 ('A', false)
Expected exception Exception
FAILURES!
Tests: 11, Assertions: 11, Failures: 5.
Yup, red. To make it green's easier than writing the tests; a quick update to Enigma_Rotor::setRingOffset:public function setRingOffset ($offset)
{
if(!is_integer($offset) || ($offset<1) || ($offset>26)) { throw new Exception("Offset must be integer in range 1..26");
}
$this->_ringOffset = $offset;
}
And we're green! Quick, commit that; we've earned a rest:silverbox:tdd-enigma wechsler$ svn commit -m 'Added tests to ring offset'
Sending library/Enigma/Rotor.php
Sending tests/Enigma/RotorTest.php
Transmitting file data ..
Committed revision 4.
OK - that's been a long installment. Next time, we'll look at how to handle character offsets and what these offsets actually do for us, and then start seeing how our tested rotor actually behaves.