Files
triplex/serial/serial_test.go
2026-03-02 04:17:36 +01:00

542 lines
12 KiB
Go

// Copyright (c) 2026 Micha Hoiting
package serial_test
import (
"errors"
"fmt"
"testing"
"testing/quick"
"git.hoiting.org/micha/triplex/serial"
)
// Property: Encode(Decode(code)) == code (for valid codes)
func TestEncodeDecodeRoundtrip(t *testing.T) {
f := func(idx uint) bool {
if idx >= serial.MaxIndex {
return true
}
code, err := serial.Encode(idx)
if err != nil {
return false
}
idx2, err := serial.Decode(code)
if err != nil {
return false
}
return idx == idx2
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("Decode/Encode roundtrip failed: %v", err)
}
}
// Property: Encode(Decode(Encode(i))) == Encode(i) (idempotent on valid indices)
func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
f := func(idx uint) bool {
if idx >= serial.MaxIndex {
return true
}
code1, err := serial.Encode(idx)
if err != nil {
return true // decode may reject indices (e.g. forbidden pair)
}
idx2, err := serial.Decode(code1)
if err != nil {
return false
}
code2, err := serial.Encode(idx2)
if err != nil {
return false
}
return code1 == code2
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("Encode/Decode/Encode idempotence failed: %v", err)
}
}
// Property: forbidden first three letters are never returned by Encode
func TestForbiddenTripletsNeverDecoded(t *testing.T) {
f := func(idx uint) bool {
if idx >= serial.MaxIndex {
return true
}
code, err := serial.Encode(idx)
if err != nil {
return true // decode may reject indices
}
l1 := code[0]
l2 := code[1]
l3 := code[2]
return !serial.IsForbiddenTriplet(l1, l2, l3)
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("Forbidden triplets appeared in Encode: %v", err)
}
}
// Optional: basic check for format LLL-NNN-LLC
func TestDecodedFormatLooksCorrect(t *testing.T) {
f := func(idx uint) bool {
if idx >= serial.MaxIndex {
return true
}
code, err := serial.Encode(idx)
if err != nil {
return true
}
if len(code) != 11 {
return false
}
if code[3] != '-' || code[7] != '-' {
return false
}
// letters at positions 0,1,2,8,9,10
for _, p := range []int{0, 1, 2, 8, 9, 10} {
if code[p] < 'A' || code[p] > 'Z' {
return false
}
}
// digits at positions 4,5,6
for _, p := range []int{4, 5, 6} {
if code[p] < '0' || code[p] > '9' {
return false
}
if code[0] == '0' {
return false
}
}
return true
}
if err := quick.Check(f, nil); err != nil {
t.Fatalf("Decoded format property failed: %v", err)
}
}
func TestEncodeSpecificIndex0(t *testing.T) {
code := "AAA-100-AA"
idx := uint(0)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex1(t *testing.T) {
code := "AAA-100-AB"
idx := uint(1)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex20(t *testing.T) {
code := "AAA-100-AZ"
idx := uint(20)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex21(t *testing.T) {
code := "AAA-100-BA"
idx := uint(21)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex42(t *testing.T) {
code := "AAA-100-CA"
idx := uint(42)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex441(t *testing.T) {
code := "AAA-101-AA"
idx := uint(441)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex396900(t *testing.T) {
code := "AAB-100-AA"
idx := uint(396900)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex8334900(t *testing.T) {
code := "ABA-100-AA"
idx := uint(8334900)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestEncodeSpecificIndex175032900(t *testing.T) {
code := "BAA-100-AA"
idx := uint(175032900)
code2, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d: %v", idx, err)
}
// Check excluding the checksum letter
if code != code2[0:10] {
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
}
}
func TestDencodeSpecificIndex0(t *testing.T) {
code := "AAA-100-AAA"
idx := uint(0)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex21(t *testing.T) {
code := "AAA-100-BAV"
idx := uint(21)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex1(t *testing.T) {
code := "AAA-100-ABB"
idx := uint(1)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex441(t *testing.T) {
code := "AAA-101-AAZ"
idx := uint(441)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex396900(t *testing.T) {
code := "AAB-100-AAK"
idx := uint(396900)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex8334900(t *testing.T) {
code := "ABA-100-AAC"
idx := uint(8334900)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestDencodeSpecificIndex175032900(t *testing.T) {
code := "BAA-100-AAQ"
idx := uint(175032900)
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode failed for code %q: %v", code, err)
}
if idx != idx2 {
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
}
}
func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
baseIdx := uint(0x11223344)
idx := baseIdx % serial.MaxIndex
code, err := serial.Encode(idx)
if err != nil {
t.Fatalf("Encode failed for idx %d (derived from 0x11223344): %v", idx, err)
}
idx2, err := serial.Decode(code)
if err != nil {
t.Fatalf("Decode 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.Encode(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.Encode(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.Decode("AA-100-AAA")
if !errors.Is(err, serial.ErrInvalidFormat) {
t.Fatalf("expected invalid format error, got: %v", err)
}
}
func TestEncodeRejectsLowercaseLetters(t *testing.T) {
_, err := serial.Decode("AAa-100-AAA")
if !errors.Is(err, serial.ErrInvalidLetter) {
t.Fatalf("expected invalid letters error, got: %v", err)
}
}
func TestEncodeRejectsInvalidChecksum(t *testing.T) {
_, err := serial.Decode("AAA-100-AAB")
if !errors.Is(err, serial.ErrInvalidChecksum) {
t.Fatalf("expected invalid checksum error, got: %v", err)
}
}
func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
_, err := serial.Decode("AAA-1A0-AAA")
if !errors.Is(err, serial.ErrInvalidNumber) {
t.Fatalf("expected invalid number error, got: %v", err)
}
}
func TestCompleteCodeRoundtrip(t *testing.T) {
original, err := serial.Encode(123456)
if err != nil {
t.Fatalf("Encode failed: %v", err)
}
withoutChecksum := original[:len(original)-1]
full, idx, err := serial.CompleteCode(withoutChecksum)
if err != nil {
t.Fatalf("CompleteCode failed: %v", err)
}
if full != original {
t.Fatalf("completed code mismatch: got %q, want %q", full, original)
}
idx2, err := serial.Decode(full)
if err != nil {
t.Fatalf("Decode failed for completed code %q: %v", full, err)
}
if idx2 != idx {
t.Fatalf("idx mismatch: got %d, want %d", idx, idx2)
}
}
func TestCompleteCodeRejectsInvalidFormat(t *testing.T) {
_, _, err := serial.CompleteCode("AAA-10-AA")
if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) {
t.Fatalf("expected invalid no-checksum format 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.Decode(code)
if err != nil {
t.Fatalf("Decode 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.Encode(idx)
if err != nil {
t.Fatalf("Encode 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 uint) 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.Decode(code)
if err != nil {
t.Fatalf("Decode 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 uint) bool {
return true
})
if !errors.Is(err, serial.ErrNoAvailableIndex) {
t.Fatalf("expected no available index error, got: %v", err)
}
}
func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) {
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
MaxAttempts: 1,
IsInUse: func(candidate uint) bool {
return true
},
})
if !errors.Is(err, serial.ErrNoAvailableIndex) {
t.Fatalf("expected no available index error, got: %v", err)
}
}
func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) {
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
RandomIndex: func(max uint) (uint, error) {
return 0, fmt.Errorf("rng down")
},
})
if !errors.Is(err, serial.ErrRandomSourceFailed) {
t.Fatalf("expected random source failed error, got: %v", err)
}
}