diff --git a/plugboard/plugboard_test.go b/plugboard/plugboard_test.go index c73157d..6aa5df1 100644 --- a/plugboard/plugboard_test.go +++ b/plugboard/plugboard_test.go @@ -1,7 +1,6 @@ package plugboard import ( - "reflect" "testing" "github.com/atbu/ultra/conversion" @@ -48,46 +47,34 @@ func TestUnevenPlugboardString(t *testing.T) { } } -func generatePlugboardPair(a rune, b rune) PlugboardPair { - return PlugboardPair{int(a - 65), int(b - 65)} -} - func TestPlugboardConstructor(t *testing.T) { - tests := []struct { - input string - expectedOutput *Plugboard - }{ - {"ABCD", &Plugboard{[]PlugboardPair{ - generatePlugboardPair('A', 'B'), - generatePlugboardPair('C', 'D'), - }}}, - {"RKWDPT", &Plugboard{[]PlugboardPair{ - generatePlugboardPair('R', 'K'), - generatePlugboardPair('W', 'D'), - generatePlugboardPair('P', 'T'), - }}}, - {"QPALZMWOXNEICBRUVFGH", &Plugboard{[]PlugboardPair{ - generatePlugboardPair('Q', 'P'), - generatePlugboardPair('A', 'L'), - generatePlugboardPair('Z', 'M'), - generatePlugboardPair('W', 'O'), - generatePlugboardPair('X', 'N'), - generatePlugboardPair('E', 'I'), - generatePlugboardPair('C', 'B'), - generatePlugboardPair('R', 'U'), - generatePlugboardPair('V', 'F'), - generatePlugboardPair('G', 'H'), - }}}, + tests := []string{ + "ABCD", + "RKWDPT", + "QPALZMWOXNEICBRUVFGH", } - for _, testData := range tests { - got, err := New(testData.input) - if !reflect.DeepEqual(got, testData.expectedOutput) { - t.Errorf("New() in plugboard package returned unexpected output - got=%v, expected=%v", got, testData.expectedOutput) - } + for _, input := range tests { + got, err := New(input) if err != nil { t.Errorf("New() in plugboard package returned unexpected error - got=%v, expected=%v", err, nil) } + + // Each pair in the plugboard is derived from two consecutive characters in the input string, so we can + // derive the expected pairs from the input rather than hand-listing them. + wantPairs := len(input) / 2 + if len(got.Configuration) != wantPairs { + t.Errorf("New() in plugboard package returned unexpected number of pairs - got=%v, expected=%v", len(got.Configuration), wantPairs) + continue + } + + for i, pair := range got.Configuration { + wantA := conversion.ConvertCharToIndex(rune(input[i*2])) + wantB := conversion.ConvertCharToIndex(rune(input[i*2+1])) + if pair.a != wantA || pair.b != wantB { + t.Errorf("New() in plugboard package returned unexpected pair at index %d - got=%v, expected=%v", i, pair, PlugboardPair{wantA, wantB}) + } + } } } diff --git a/reflector/reflector_test.go b/reflector/reflector_test.go index e5d688e..8a9fb5b 100644 --- a/reflector/reflector_test.go +++ b/reflector/reflector_test.go @@ -1,165 +1,28 @@ package reflector import ( - "reflect" "testing" "github.com/atbu/ultra/conversion" ) -// todo to structure better - func TestConstructor(t *testing.T) { tests := []struct { - input ReflectorConfiguration - expectedOutput *Reflector + input ReflectorConfiguration + wiringString string }{ - {ReflectorA, &Reflector{[26]int{ - 4, - 9, - 12, - 25, - 0, - 11, - 24, - 23, - 21, - 1, - 22, - 5, - 2, - 17, - 16, - 20, - 14, - 13, - 19, - 18, - 15, - 8, - 10, - 7, - 6, - 3, - }}}, - {ReflectorB, &Reflector{[26]int{ - 24, - 17, - 20, - 7, - 16, - 18, - 11, - 3, - 15, - 23, - 13, - 6, - 14, - 10, - 12, - 8, - 4, - 1, - 5, - 25, - 2, - 22, - 21, - 9, - 0, - 19, - }}}, - {ReflectorC, &Reflector{[26]int{ - 5, - 21, - 15, - 9, - 8, - 0, - 14, - 24, - 4, - 3, - 17, - 25, - 23, - 22, - 6, - 2, - 19, - 10, - 20, - 16, - 18, - 1, - 13, - 12, - 7, - 11, - }}}, - {ReflectorNarrowB, &Reflector{[26]int{ - 4, - 13, - 10, - 16, - 0, - 20, - 24, - 22, - 9, - 8, - 2, - 14, - 15, - 1, - 11, - 12, - 3, - 23, - 25, - 21, - 5, - 19, - 7, - 17, - 6, - 18, - }}}, - {ReflectorNarrowC, &Reflector{[26]int{ - 17, - 3, - 14, - 1, - 9, - 13, - 19, - 10, - 21, - 4, - 7, - 12, - 11, - 5, - 2, - 22, - 25, - 0, - 23, - 6, - 24, - 8, - 15, - 18, - 20, - 16, - }}}, + {ReflectorA, "EJMZALYXVBWFCRQUONTSPIKHGD"}, + {ReflectorB, "YRUHQSLDPXNGOKMIEBFZCWVJAT"}, + {ReflectorC, "FVPJIAOYEDRZXWGCTKUQSBNMHL"}, + {ReflectorNarrowB, "ENKQAUYWJICOPBLMDXZVFTHRGS"}, + {ReflectorNarrowC, "RDOBJNTKVEHMLFCWZAXGYIPSUQ"}, } for _, testData := range tests { got := New(testData.input) - if !reflect.DeepEqual(got, testData.expectedOutput) { - t.Errorf("New() in reflector package returned unexpected output - got=%v, expected=%v", got, testData.expectedOutput) + wantWiring := conversion.ConvertWiringStringToArray(testData.wiringString) + if got.Wiring != wantWiring { + t.Errorf("New() in reflector package returned unexpected wiring - got=%v, expected=%v", got.Wiring, wantWiring) } } } diff --git a/rotor/fourth_rotor_test.go b/rotor/fourth_rotor_test.go index 4cfdea6..2eb10fe 100644 --- a/rotor/fourth_rotor_test.go +++ b/rotor/fourth_rotor_test.go @@ -1 +1,80 @@ package rotor + +import ( + "testing" + + "github.com/atbu/ultra/conversion" +) + +func TestNewFourthRotor(t *testing.T) { + tests := []struct { + configuration FourthRotorConfiguration + wiringString string + startingPosition rune + }{ + {FourthRotorBeta, "LEYJVCNIXWPBQMDRTAKZGFUHOS", 'A'}, + {FourthRotorGamma, "FSOKANUERHMBTIYCWLQPZXVGJD", 'K'}, + } + + for _, testData := range tests { + got := NewFourthRotor(testData.configuration, testData.startingPosition) + + wantWiring := conversion.ConvertWiringStringToArray(testData.wiringString) + if got.Wiring != wantWiring { + t.Errorf("NewFourthRotor() in rotor package returned unexpected wiring - got=%v, expected=%v", got.Wiring, wantWiring) + } + + wantInverseWiring := conversion.InverseWiringArray(wantWiring) + if got.InverseWiring != wantInverseWiring { + t.Errorf("NewFourthRotor() in rotor package returned unexpected inverse wiring - got=%v, expected=%v", got.InverseWiring, wantInverseWiring) + } + + wantPosition := conversion.ConvertCharToIndex(testData.startingPosition) + if got.Position != wantPosition { + t.Errorf("NewFourthRotor() in rotor package returned unexpected position - got=%v, expected=%v", got.Position, wantPosition) + } + } +} + +func TestFourthRotorMapSignal(t *testing.T) { + tests := []struct { + configuration FourthRotorConfiguration + startingPosition rune + input rune + inverse bool + expectedOutput rune + }{ + // With position 'A' the delta is zero, so the signal maps straight through the wiring. + {FourthRotorBeta, 'A', 'A', false, 'L'}, + {FourthRotorGamma, 'A', 'A', false, 'F'}, + // Inverse mapping with a zero delta uses the inverse wiring. Beta maps 'A'->'L', so 'L' maps back to 'A'. + {FourthRotorBeta, 'A', 'L', true, 'A'}, + } + + for _, testData := range tests { + rotor := NewFourthRotor(testData.configuration, testData.startingPosition) + got := rotor.MapSignal(conversion.ConvertCharToIndex(testData.input), testData.inverse) + expectedOutput := conversion.ConvertCharToIndex(testData.expectedOutput) + if got != expectedOutput { + t.Errorf("MapSignal() in rotor package returned unexpected output - got=%v, expected=%v", got, expectedOutput) + } + } +} + +// TestFourthRotorMapSignalRoundTrip verifies that mapping a signal forward and then back through a fourth rotor +// returns the original signal, across a range of positions. This exercises the position delta wrap-around math +// without needing hand-computed expected values. +func TestFourthRotorMapSignalRoundTrip(t *testing.T) { + positions := []rune{'A', 'F', 'N', 'Z'} + + for _, position := range positions { + rotor := NewFourthRotor(FourthRotorGamma, position) + for signal := 0; signal < 26; signal++ { + forward := rotor.MapSignal(signal, false) + got := rotor.MapSignal(forward, true) + if got != signal { + t.Errorf("MapSignal() round-trip in rotor package failed for position=%c signal=%d - got=%v, expected=%v", position, signal, got, signal) + } + } + } +} diff --git a/rotor/rotor_test.go b/rotor/rotor_test.go index 4cfdea6..26b2a36 100644 --- a/rotor/rotor_test.go +++ b/rotor/rotor_test.go @@ -1 +1,155 @@ package rotor + +import ( + "reflect" + "testing" + + "github.com/atbu/ultra/conversion" +) + +func TestNewRotor(t *testing.T) { + tests := []struct { + configuration RotorConfiguration + wiringString string + notches []rune + startingPosition rune + ringSetting rune + }{ + {RotorI, "EKMFLGDQVZNTOWYHXUSPAIBRCJ", []rune{'Q'}, 'A', 'A'}, + {RotorII, "AJDKSIRUXBLHWTMCQGZNPYFVOE", []rune{'E'}, 'A', 'A'}, + {RotorIII, "BDFHJLCPRTXVZNYEIWGAKMUSQO", []rune{'V'}, 'M', 'F'}, + {RotorIV, "ESOVPZJAYQUIRHXLNFTGKDCMWB", []rune{'J'}, 'Z', 'B'}, + {RotorV, "VZBRGITYUPSDNHLXAWMJQOFECK", []rune{'Z'}, 'A', 'A'}, + {RotorVI, "JPGVOUMFYQBENHZRDKASXLICTW", []rune{'Z', 'M'}, 'K', 'C'}, + {RotorVII, "NZJHGRCXMYSWBOUFAIVLPEKQDT", []rune{'Z', 'M'}, 'A', 'A'}, + {RotorVIII, "FKQHTLXOCBJSPDZRAMEWNIUYGV", []rune{'Z', 'M'}, 'Y', 'X'}, + } + + for _, testData := range tests { + got := NewRotor(testData.configuration, testData.startingPosition, testData.ringSetting) + + wantWiring := conversion.ConvertWiringStringToArray(testData.wiringString) + if got.Wiring != wantWiring { + t.Errorf("NewRotor() in rotor package returned unexpected wiring - got=%v, expected=%v", got.Wiring, wantWiring) + } + + wantInverseWiring := conversion.InverseWiringArray(wantWiring) + if got.InverseWiring != wantInverseWiring { + t.Errorf("NewRotor() in rotor package returned unexpected inverse wiring - got=%v, expected=%v", got.InverseWiring, wantInverseWiring) + } + + var wantNotches []int + for _, notch := range testData.notches { + wantNotches = append(wantNotches, conversion.ConvertCharToIndex(notch)) + } + if !reflect.DeepEqual(got.Notches, wantNotches) { + t.Errorf("NewRotor() in rotor package returned unexpected notches - got=%v, expected=%v", got.Notches, wantNotches) + } + + wantPosition := conversion.ConvertCharToIndex(testData.startingPosition) + if got.Position != wantPosition { + t.Errorf("NewRotor() in rotor package returned unexpected position - got=%v, expected=%v", got.Position, wantPosition) + } + + wantRingSetting := conversion.ConvertCharToIndex(testData.ringSetting) + if got.RingSetting != wantRingSetting { + t.Errorf("NewRotor() in rotor package returned unexpected ring setting - got=%v, expected=%v", got.RingSetting, wantRingSetting) + } + } +} + +func TestRotate(t *testing.T) { + tests := []struct { + startingPosition rune + expectedPosition int + }{ + // A normal step simply increments the position by one. + {'A', conversion.ConvertCharToIndex('B')}, + {'M', conversion.ConvertCharToIndex('N')}, + // Rotating from 'Z' (index 25) must wrap around back to 'A' (index 0). + {'Z', conversion.ConvertCharToIndex('A')}, + } + + for _, testData := range tests { + rotor := NewRotor(RotorI, testData.startingPosition, 'A') + rotor.Rotate() + if rotor.Position != testData.expectedPosition { + t.Errorf("Rotate() in rotor package returned unexpected position - got=%v, expected=%v", rotor.Position, testData.expectedPosition) + } + } +} + +func TestMapSignal(t *testing.T) { + tests := []struct { + configuration RotorConfiguration + startingPosition rune + ringSetting rune + input rune + inverse bool + expectedOutput rune + }{ + // With position 'A' and ring setting 'A' the delta is zero, so the signal maps straight through the wiring. + {RotorI, 'A', 'A', 'A', false, 'E'}, + {RotorII, 'A', 'A', 'A', false, 'A'}, + // Inverse mapping with a zero delta uses the inverse wiring. RotorI maps 'A'->'E', so 'E' maps back to 'A'. + {RotorI, 'A', 'A', 'E', true, 'A'}, + // A non-zero position offsets the signal (Enigma stepping math). + {RotorI, 'B', 'A', 'A', false, 'J'}, + // A non-zero ring setting offsets the signal in the opposite direction. + {RotorI, 'A', 'B', 'A', false, 'K'}, + } + + for _, testData := range tests { + rotor := NewRotor(testData.configuration, testData.startingPosition, testData.ringSetting) + got := rotor.MapSignal(conversion.ConvertCharToIndex(testData.input), testData.inverse) + expectedOutput := conversion.ConvertCharToIndex(testData.expectedOutput) + if got != expectedOutput { + t.Errorf("MapSignal() in rotor package returned unexpected output - got=%v, expected=%v", got, expectedOutput) + } + } +} + +// TestMapSignalRoundTrip verifies that mapping a signal forward and then back through a rotor returns the original +// signal for every starting signal, across a range of positions and ring settings. This exercises the position / +// ring-setting delta wrap-around math without needing hand-computed expected values. +func TestMapSignalRoundTrip(t *testing.T) { + positions := []rune{'A', 'F', 'N', 'Z'} + ringSettings := []rune{'A', 'C', 'T', 'Z'} + + for _, position := range positions { + for _, ringSetting := range ringSettings { + rotor := NewRotor(RotorIII, position, ringSetting) + for signal := 0; signal < 26; signal++ { + forward := rotor.MapSignal(signal, false) + got := rotor.MapSignal(forward, true) + if got != signal { + t.Errorf("MapSignal() round-trip in rotor package failed for position=%c ringSetting=%c signal=%d - got=%v, expected=%v", position, ringSetting, signal, got, signal) + } + } + } + } +} + +func TestIsInNotch(t *testing.T) { + tests := []struct { + configuration RotorConfiguration + startingPosition rune + expectedOutput bool + }{ + // RotorI has a single notch at 'Q'. + {RotorI, 'Q', true}, + {RotorI, 'A', false}, + // RotorVI has two notches, at 'Z' and 'M'. Both must be recognised. + {RotorVI, 'Z', true}, + {RotorVI, 'M', true}, + {RotorVI, 'A', false}, + } + + for _, testData := range tests { + rotor := NewRotor(testData.configuration, testData.startingPosition, 'A') + got := rotor.IsInNotch() + if got != testData.expectedOutput { + t.Errorf("IsInNotch() in rotor package returned unexpected output - got=%v, expected=%v", got, testData.expectedOutput) + } + } +}