more refactoring
This commit is contained in:
259
cart_id_test.go
Normal file
259
cart_id_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEncodeDecodeBase62RoundTrip verifies encodeBase62/decodeBase62 are inverse.
|
||||
func TestEncodeDecodeBase62RoundTrip(t *testing.T) {
|
||||
mrand.Seed(42)
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Random 64-bit value
|
||||
v := mrand.Uint64()
|
||||
s := encodeBase62(v)
|
||||
dec, ok := decodeBase62(s)
|
||||
if !ok {
|
||||
t.Fatalf("decodeBase62 failed for %d encoded=%s", v, s)
|
||||
}
|
||||
if dec != v {
|
||||
t.Fatalf("round trip mismatch: have %d got %d (encoded=%s)", v, dec, s)
|
||||
}
|
||||
}
|
||||
// Explicit zero test
|
||||
if s := encodeBase62(0); s != "0" {
|
||||
t.Fatalf("expected encodeBase62(0) == \"0\", got %q", s)
|
||||
}
|
||||
if v, ok := decodeBase62("0"); !ok || v != 0 {
|
||||
t.Fatalf("decodeBase62(0) unexpected result v=%d ok=%v", v, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewCartIDUniqueness generates a number of IDs and checks for duplicates.
|
||||
func TestNewCartIDUniqueness(t *testing.T) {
|
||||
const n = 10000
|
||||
seen := make(map[string]struct{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
id, err := NewCartID()
|
||||
if err != nil {
|
||||
t.Fatalf("NewCartID error: %v", err)
|
||||
}
|
||||
s := id.String()
|
||||
if _, exists := seen[s]; exists {
|
||||
t.Fatalf("duplicate CartID generated: %s", s)
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
if id.IsZero() {
|
||||
t.Fatalf("NewCartID returned zero value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseCartIDValidation tests parsing of valid and invalid base62 strings.
|
||||
func TestParseCartIDValidation(t *testing.T) {
|
||||
id, err := NewCartID()
|
||||
if err != nil {
|
||||
t.Fatalf("NewCartID error: %v", err)
|
||||
}
|
||||
parsed, ok := ParseCartID(id.String())
|
||||
if !ok {
|
||||
t.Fatalf("ParseCartID failed for valid id %s", id)
|
||||
}
|
||||
if parsed.raw != id.raw {
|
||||
t.Fatalf("parsed raw mismatch: %d vs %d", parsed.raw, id.raw)
|
||||
}
|
||||
|
||||
if _, ok := ParseCartID(""); ok {
|
||||
t.Fatalf("expected empty string to be invalid")
|
||||
}
|
||||
// Invalid char ('-')
|
||||
if _, ok := ParseCartID("abc-123"); ok {
|
||||
t.Fatalf("expected invalid chars to fail parse")
|
||||
}
|
||||
// Overly long ( >16 )
|
||||
if _, ok := ParseCartID("1234567890abcdefg"); ok {
|
||||
t.Fatalf("expected overly long string to fail parse")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFallbackDeterminism ensures fallback hashing is deterministic.
|
||||
func TestFallbackDeterminism(t *testing.T) {
|
||||
inputs := []string{
|
||||
"legacy-cart-1",
|
||||
"legacy-cart-2",
|
||||
"UPPER_lower_123",
|
||||
"🚀unicode", // unicode bytes (will hash byte sequence)
|
||||
}
|
||||
for _, in := range inputs {
|
||||
a := FallbackFromString(in)
|
||||
b := FallbackFromString(in)
|
||||
if a.raw != b.raw || a.String() != b.String() {
|
||||
t.Fatalf("fallback mismatch for %q: %+v vs %+v", in, a, b)
|
||||
}
|
||||
}
|
||||
// Distinct inputs should almost always differ; sample check
|
||||
a := FallbackFromString("distinct-A")
|
||||
b := FallbackFromString("distinct-B")
|
||||
if a.raw == b.raw {
|
||||
t.Fatalf("unexpected identical fallback hashes for distinct inputs")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCanonicalizeIncomingBehavior covers main control flow branches.
|
||||
func TestCanonicalizeIncomingBehavior(t *testing.T) {
|
||||
// Empty => new id
|
||||
id1, generated, err := CanonicalizeIncoming("")
|
||||
if err != nil || !generated || id1.IsZero() {
|
||||
t.Fatalf("CanonicalizeIncoming empty failed: id=%v gen=%v err=%v", id1, generated, err)
|
||||
}
|
||||
|
||||
// Valid base62 => parse; no generation
|
||||
id2, gen2, err := CanonicalizeIncoming(id1.String())
|
||||
if err != nil || gen2 || id2.raw != id1.raw {
|
||||
t.Fatalf("CanonicalizeIncoming parse mismatch: id2=%v gen2=%v err=%v", id2, gen2, err)
|
||||
}
|
||||
|
||||
// Legacy-like random containing invalid chars -> fallback
|
||||
fallbackInput := "legacy\x00\x00padding"
|
||||
id3, gen3, err := CanonicalizeIncoming(fallbackInput)
|
||||
if err != nil || gen3 {
|
||||
t.Fatalf("CanonicalizeIncoming fallback unexpected: id3=%v gen3=%v err=%v", id3, gen3, err)
|
||||
}
|
||||
|
||||
// Deterministic fallback
|
||||
id4, _, _ := CanonicalizeIncoming(fallbackInput)
|
||||
if id3.raw != id4.raw {
|
||||
t.Fatalf("fallback canonicalization not deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpgradeLegacyCartId ensures mapping of old CartId is stable.
|
||||
func TestUpgradeLegacyCartId(t *testing.T) {
|
||||
var legacy CartId
|
||||
copy(legacy[:], []byte("legacy-123456789")) // 15 bytes + padding
|
||||
up1 := UpgradeLegacyCartId(legacy)
|
||||
up2 := UpgradeLegacyCartId(legacy)
|
||||
if up1.raw != up2.raw {
|
||||
t.Fatalf("UpgradeLegacyCartId not deterministic: %v vs %v", up1, up2)
|
||||
}
|
||||
if up1.String() != up2.String() {
|
||||
t.Fatalf("UpgradeLegacyCartId string mismatch: %s vs %s", up1, up2)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkNewCartID gives a rough idea of generation cost.
|
||||
func BenchmarkNewCartID(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := NewCartID(); err != nil {
|
||||
b.Fatalf("error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkEncodeBase62 measures encode speed in isolation.
|
||||
func BenchmarkEncodeBase62(b *testing.B) {
|
||||
// Random sample of values
|
||||
samples := make([]uint64, 1024)
|
||||
for i := range samples {
|
||||
var buf [8]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
b.Fatalf("rand: %v", err)
|
||||
}
|
||||
samples[i] = binary.BigEndian.Uint64(buf[:])
|
||||
}
|
||||
b.ResetTimer()
|
||||
var sink string
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = encodeBase62(samples[i%len(samples)])
|
||||
}
|
||||
_ = sink
|
||||
}
|
||||
|
||||
// BenchmarkDecodeBase62 measures decode speed.
|
||||
func BenchmarkDecodeBase62(b *testing.B) {
|
||||
// Pre-encode
|
||||
encoded := make([]string, 1024)
|
||||
for i := range encoded {
|
||||
encoded[i] = encodeBase62(uint64(i)<<32 | uint64(i))
|
||||
}
|
||||
b.ResetTimer()
|
||||
var sum uint64
|
||||
for i := 0; i < b.N; i++ {
|
||||
v, ok := decodeBase62(encoded[i%len(encoded)])
|
||||
if !ok {
|
||||
b.Fatalf("decode failed")
|
||||
}
|
||||
sum ^= v
|
||||
}
|
||||
_ = sum
|
||||
}
|
||||
|
||||
// TestLookupNDeterminism (ring integration smoke test) ensures LookupN
|
||||
// returns distinct hosts and stable ordering for a fixed ring.
|
||||
func TestLookupNDeterminism(t *testing.T) {
|
||||
rb := NewRingBuilder().WithEpoch(1).WithVnodesPerHost(8).WithHosts([]string{"a", "b", "c"})
|
||||
ring := rb.Build()
|
||||
if ring.Empty() {
|
||||
t.Fatalf("expected non-empty ring")
|
||||
}
|
||||
id := MustNewCartID()
|
||||
owners1 := ring.LookupN(id.Raw(), 3)
|
||||
owners2 := ring.LookupN(id.Raw(), 3)
|
||||
if len(owners1) != len(owners2) {
|
||||
t.Fatalf("LookupN length mismatch")
|
||||
}
|
||||
for i := range owners1 {
|
||||
if owners1[i].Host != owners2[i].Host {
|
||||
t.Fatalf("LookupN ordering instability at %d: %v vs %v", i, owners1[i], owners2[i])
|
||||
}
|
||||
}
|
||||
// Distinct host constraint
|
||||
seen := map[string]struct{}{}
|
||||
for _, v := range owners1 {
|
||||
if _, ok := seen[v.Host]; ok {
|
||||
t.Fatalf("duplicate host in LookupN result: %v", owners1)
|
||||
}
|
||||
seen[v.Host] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRingFingerprintChanges ensures fingerprint updates with membership changes.
|
||||
func TestRingFingerprintChanges(t *testing.T) {
|
||||
b1 := NewRingBuilder().WithEpoch(1).WithHosts([]string{"node1", "node2"})
|
||||
r1 := b1.Build()
|
||||
b2 := NewRingBuilder().WithEpoch(2).WithHosts([]string{"node1", "node2", "node3"})
|
||||
r2 := b2.Build()
|
||||
if r1.Fingerprint() == r2.Fingerprint() {
|
||||
t.Fatalf("expected differing fingerprints after host set change")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRingDiffHosts verifies added/removed host detection.
|
||||
func TestRingDiffHosts(t *testing.T) {
|
||||
r1 := NewRingBuilder().WithEpoch(1).WithHosts([]string{"a", "b"}).Build()
|
||||
r2 := NewRingBuilder().WithEpoch(2).WithHosts([]string{"b", "c"}).Build()
|
||||
added, removed := r1.DiffHosts(r2)
|
||||
if fmt.Sprintf("%v", added) != "[c]" {
|
||||
t.Fatalf("expected added [c], got %v", added)
|
||||
}
|
||||
if fmt.Sprintf("%v", removed) != "[a]" {
|
||||
t.Fatalf("expected removed [a], got %v", removed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRingLookupConsistency ensures direct Lookup and LookupID are aligned.
|
||||
func TestRingLookupConsistency(t *testing.T) {
|
||||
ring := NewRingBuilder().WithEpoch(1).WithHosts([]string{"alpha", "beta"}).WithVnodesPerHost(4).Build()
|
||||
id, _ := ParseCartID("1")
|
||||
if id.IsZero() {
|
||||
t.Fatalf("expected parsed id non-zero")
|
||||
}
|
||||
v1 := ring.Lookup(id.Raw())
|
||||
v2 := ring.LookupID(id)
|
||||
if v1.Host != v2.Host || v1.Hash != v2.Hash {
|
||||
t.Fatalf("Lookup vs LookupID mismatch: %+v vs %+v", v1, v2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user