Various improvements to the serial package, including better error handling for out-of-range indices and improved test coverage for edge cases.

This commit is contained in:
2026-02-21 00:15:42 +01:00
parent 82679048e0
commit 4e933df49a
2 changed files with 250 additions and 44 deletions

View File

@@ -1,13 +1,14 @@
package serial_test
import (
"errors"
"testing"
"testing/quick"
"git.hoiting.org/micha/triplex/serial"
)
// Property: Decode(Encode(code)) == code (voor geldige codes)
// Property: Decode(Encode(code)) == code (for valid codes)
func TestEncodeDecodeRoundtrip(t *testing.T) {
f := func(idx uint32) bool {
if idx >= serial.MaxIndex {
@@ -32,7 +33,7 @@ func TestEncodeDecodeRoundtrip(t *testing.T) {
}
}
// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent op geldige indices)
// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent on valid indices)
func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
f := func(idx uint32) bool {
if idx >= serial.MaxIndex {
@@ -41,7 +42,7 @@ func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
code1, err := serial.Decode(idx)
if err != nil {
return true // decode mag indices weigeren (bv. forbidden pair)
return true // decode may reject indices (e.g. forbidden pair)
}
idx2, err := serial.Encode(code1)
@@ -62,8 +63,8 @@ func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
}
}
// Property: forbidden eerste twee letters komen nooit uit Decode
func TestForbiddenPairsNeverDecoded(t *testing.T) {
// Property: forbidden first three letters are never returned by Decode
func TestForbiddenTripletsNeverDecoded(t *testing.T) {
f := func(idx uint32) bool {
if idx >= serial.MaxIndex {
return true
@@ -71,21 +72,22 @@ func TestForbiddenPairsNeverDecoded(t *testing.T) {
code, err := serial.Decode(idx)
if err != nil {
return true // decode mag indices weigeren
return true // decode may reject indices
}
l1 := code[0]
l2 := code[1]
l3 := code[2]
return !serial.IsForbiddenPair(l1, l2)
return !serial.IsForbiddenTriplet(l1, l2, l3)
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("Forbidden pairs appeared in Decode: %v", err)
t.Fatalf("Forbidden triplets appeared in Decode: %v", err)
}
}
// Optional: basis-check op formaat LL-NNN-L-C
// Optional: basic check for format LLL-NNN-LC
func TestDecodedFormatLooksCorrect(t *testing.T) {
f := func(idx uint32) bool {
if idx >= serial.MaxIndex {
@@ -100,19 +102,19 @@ func TestDecodedFormatLooksCorrect(t *testing.T) {
if len(code) != 10 {
return false
}
if code[2] != '-' || code[6] != '-' || code[8] != '-' {
if code[3] != '-' || code[7] != '-' {
return false
}
// letters op posities 0,1,7,9
for _, p := range []int{0, 1, 7, 9} {
// letters at positions 0,1,2,8,9
for _, p := range []int{0, 1, 2, 8, 9} {
if code[p] < 'A' || code[p] > 'Z' {
return false
}
}
// cijfers op posities 3,4,5
for _, p := range []int{3, 4, 5} {
// digits at positions 4,5,6
for _, p := range []int{4, 5, 6} {
if code[p] < '0' || code[p] > '9' {
return false
}
@@ -125,3 +127,123 @@ func TestDecodedFormatLooksCorrect(t *testing.T) {
t.Fatalf("Decoded format property failed: %v", err)
}
}
func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
baseIdx := uint32(0x11223344)
idx := baseIdx % serial.MaxIndex
code, err := serial.Decode(idx)
if err != nil {
t.Fatalf("Decode failed for idx %d (derived from 0x11223344): %v", idx, err)
}
idx2, err := serial.Encode(code)
if err != nil {
t.Fatalf("Encode failed for code %q: %v", code, err)
}
if idx2 != idx {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDecodeOutOfRangeAtMaxIndex(t *testing.T) {
_, err := serial.Decode(serial.MaxIndex)
if !errors.Is(err, serial.ErrIndexOutOfRange) {
t.Fatalf("expected index out of range error, got: %v", err)
}
}
func TestDecodeOutOfRangeAtMaxIndexPlusHundred(t *testing.T) {
_, err := serial.Decode(serial.MaxIndex + 100)
if !errors.Is(err, serial.ErrIndexOutOfRange) {
t.Fatalf("expected index out of range error, got: %v", err)
}
}
func TestEncodeRejectsOldFormatWithThirdDash(t *testing.T) {
_, err := serial.Encode("AA-100-AA")
if !errors.Is(err, serial.ErrInvalidFormat) {
t.Fatalf("expected invalid format error, got: %v", err)
}
}
func TestEncodeRejectsLowercaseLetters(t *testing.T) {
_, err := serial.Encode("AAa-100-AA")
if !errors.Is(err, serial.ErrInvalidLetters) {
t.Fatalf("expected invalid letters error, got: %v", err)
}
}
func TestEncodeRejectsInvalidChecksum(t *testing.T) {
_, err := serial.Encode("AAA-100-AB")
if !errors.Is(err, serial.ErrInvalidChecksum) {
t.Fatalf("expected invalid checksum error, got: %v", err)
}
}
func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
_, err := serial.Encode("AAA-1A0-AA")
if !errors.Is(err, serial.ErrInvalidNumber) {
t.Fatalf("expected invalid number error, got: %v", err)
}
}
func TestRandomCodeRoundtrip(t *testing.T) {
for i := 0; i < 100; i++ {
code, idx, err := serial.RandomCode()
if err != nil {
t.Fatalf("RandomCode failed: %v", err)
}
idx2, err := serial.Encode(code)
if err != nil {
t.Fatalf("Encode failed for random code %q: %v", code, err)
}
if idx2 != idx {
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
}
decoded, err := serial.Decode(idx)
if err != nil {
t.Fatalf("Decode failed for idx %d: %v", idx, err)
}
if decoded != code {
t.Fatalf("random roundtrip mismatch: got %q, want %q", decoded, code)
}
}
}
func TestRandomCodeInvokesInUseCallback(t *testing.T) {
called := false
code, idx, err := serial.RandomCode(func(candidate uint32) bool {
called = true
return false
})
if err != nil {
t.Fatalf("RandomCode failed: %v", err)
}
if !called {
t.Fatalf("expected in-use callback to be called")
}
idx2, err := serial.Encode(code)
if err != nil {
t.Fatalf("Encode failed for random code %q: %v", code, err)
}
if idx2 != idx {
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
}
}
func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
_, _, err := serial.RandomCode(func(candidate uint32) bool {
return true
})
if !errors.Is(err, serial.ErrRandomGenerationFailed) {
t.Fatalf("expected random generation failed error, got: %v", err)
}
}