package id_test import ( "apigo.cc/go/encoding" "apigo.cc/go/id" "math" "strings" "testing" ) func TestMakeID(t *testing.T) { uid := id.MakeID(10) if len(uid) != 10 { t.Errorf("expected length 10, got %d", len(uid)) } } func TestGetForMysql(t *testing.T) { uid := id.DefaultIDMaker.GetForMysql(10) if len(uid) != 10 { t.Errorf("expected length 10, got %d", len(uid)) } } func TestGetForPostgreSQL(t *testing.T) { uid := id.DefaultIDMaker.GetForPostgreSQL(10) if len(uid) != 10 { t.Errorf("expected length 10, got %d", len(uid)) } } func TestPrintIDs(t *testing.T) { t.Log("=== Normal Mode (Get) ===") for size := 6; size <= 12; size++ { t.Logf("--- Size: %d ---", size) for i := 0; i < 10; i++ { uid := id.DefaultIDMaker.Get(size) t.Logf(" #%d: %s", i+1, uid) } } t.Log("=== MySQL Mode (GetForMysql) ===") for size := 6; size <= 12; size++ { t.Logf("--- Size: %d ---", size) for i := 0; i < 10; i++ { uid := id.DefaultIDMaker.GetForMysql(size) t.Logf(" #%d: %s", i+1, uid) } } } // UnexchangeInt is the inverse of ExchangeInt func UnexchangeInt(buf []byte) []byte { size := len(buf) if size <= 1 { return buf } res := make([]byte, size) buf2_i := 0 buf2_ai := 0 buf2_ri := size - 1 // In ExchangeInt: // for i := 0; i < size; i++ { // if i%2 == 0 { // buf2[buf2_i] = buf[buf2_ri]; buf2_ri-- // } else { // buf2[buf2_i] = buf[buf2_ai]; buf2_ai++ // } // buf2_i++ // } // We reverse this mapping. for i := 0; i < size; i++ { if i%2 == 0 { res[buf2_ri] = buf[buf2_i] buf2_ri-- } else { res[buf2_ai] = buf[buf2_i] buf2_ai++ } buf2_i++ } return res } // UnhashInt is the inverse of HashInt func UnhashInt(buf []byte, digits string, decodeMap [256]int, radix int) []byte { if len(buf) == 0 { return buf } orderedDigits := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" defaultDigits := "9ukH1grX75TQS6LzpFAjIivsdZoO0mc8NBwnyYDhtMWEC2V3KaGxfJRPqe4lbU" // F is the non-linear cross-charset update function for prevP F := func(p int) int { char := defaultDigits[p] return strings.IndexByte(orderedDigits, char) } // 1. Undo Round 2 (Backward propagation, right to left) prevP := (len(buf) * 31) % radix for i := len(buf) - 1; i >= 0; i-- { newChar := buf[i] newP := decodeMap[newChar] if newP < 0 { newP = 0 } p := (newP - prevP + radix) % radix buf[i] = digits[p] prevP = F(newP) } // 2. Undo Round 1 (Forward propagation, left to right) prevP = (len(buf) * 17) % radix for i := 0; i < len(buf); i++ { newChar := buf[i] newP := decodeMap[newChar] if newP < 0 { newP = 0 } p := (newP - prevP + radix) % radix buf[i] = digits[p] prevP = F(newP) } return buf } func TestReversibility(t *testing.T) { digits := "9ukH1grX75TQS6LzpFAjIivsdZoO0mc8NBwnyYDhtMWEC2V3KaGxfJRPqe4lbU" var decodeMap [256]int for i := 0; i < 256; i++ { decodeMap[i] = -1 } for i, c := range digits { decodeMap[c] = i } radix := len(digits) // We generate 100,000 sequential combinations to check if they map uniquely t.Log("Testing reversibility of HashInt and ExchangeInt...") // Create some arbitrary input for size := 5; size <= 15; size++ { // Test multiple inputs for each size for step := 0; step < 1000; step++ { original := make([]byte, size) for i := 0; i < size; i++ { // Fill with pseudo-random digits from the digit set original[i] = digits[(step+i)%radix] } // Copy for modification temp := make([]byte, size) copy(temp, original) // Forward transformation: Exchange then Hash // We simulate what id.go does: encoding.HashInt(encoding.ExchangeInt(uid)) // Since encoding.ExchangeInt returns a new slice, and encoding.HashInt modifies in-place exchanged := encoding.ExchangeInt(temp) hashed := encoding.HashInt(exchanged) // Backward transformation: Unhash then Unexchange unhashed := UnhashInt(hashed, digits, decodeMap, radix) unexchanged := UnexchangeInt(unhashed) for i := 0; i < size; i++ { if unexchanged[i] != original[i] { t.Fatalf("Reversibility failed for size %d, step %d! Expected %s, got %s", size, step, string(original), string(unexchanged)) } } } } t.Log("Reversibility verified! The transformation is a perfect BIJECTION (no collisions possible on same-length unique inputs before truncation).") } func TestCollisionRate(t *testing.T) { // We simulate high concurrency generation of IDs for lengths 6 to 12. // We verify if the distribution behaves as a mathematically ideal random distribution. importMath := func(n float64, space float64) float64 { ratio := n / space if ratio < 1e-4 { return (n * n) / (2.0 * space) } importMathExp := space * (1.0 - math.Exp(-ratio)) return n - importMathExp } for size := 6; size <= 10; size++ { seen := make(map[string]int) collisions := 0 n := 100000 // Test 100,000 IDs if size == 6 { n = 1000000 // Test 1,000,000 IDs for size 6 to see actual collisions } else if size >= 9 { n = 500000 } logCount := 0 for i := 0; i < n; i++ { uid := id.DefaultIDMaker.Get(size) if prevIdx, found := seen[uid]; found { collisions++ if size == 6 && logCount < 10 { t.Logf("Coll size 6: ID=%s found at iteration %d and %d", uid, prevIdx, i) logCount++ } } else { seen[uid] = i } } space := math.Pow(62, float64(size)) expected := importMath(float64(n), space) t.Logf("Size %2d: Space %12.0f, N = %d | Actual Collisions: %d (Rate: %.5f%%) | Expected Collisions: %.2f (Rate: %.5f%%)", size, space, n, collisions, float64(collisions)/float64(n)*100, expected, expected/float64(n)*100) } } func TestCheckHashDiff(t *testing.T) { // Let's generate two concurrent IDs with consecutive secIndex // Normal mode (size = 6) // We want to see: // 1. The original uid (before Exchange and Hash) // 2. The exchanged uid // 3. The hashed uid // 4. The final 6-character truncated version // We stub the process of making UID sec := uint64(14776336 + 100) // arbitrary time sec intEncoder := encoding.DefaultIntEncoder secBytes := intEncoder.EncodeInt(sec) // 5 bytes for idx := uint64(1); idx <= 5; idx++ { secIndex := idx inSecIndexBytes := intEncoder.EncodeInt(secIndex) m := min(uint64(len(inSecIndexBytes)), 5) secTagVal := uint64(0)*5 + (m - 1) var uid = make([]byte, 0, 6) uid = intEncoder.AppendInt(uid, secTagVal) uid = append(uid, secBytes...) uid = append(uid, inSecIndexBytes...) uid = intEncoder.FillInt(uid, 6) orig := string(uid) exchanged := encoding.ExchangeInt(uid) exchStr := string(exchanged) // Hash modifies in-place hashed := encoding.HashInt(exchanged) hashStr := string(hashed) truncated := hashStr[:6] t.Logf("idx=%d | Orig: %s (len=%d) | Exch: %s | Hashed: %s | Truncated: %s", idx, orig, len(orig), exchStr, hashStr, truncated) } } func TestPrintCollisions(t *testing.T) { seen := make(map[string]struct { sec uint64 secIndex uint64 orig string exchanged string hashed string }) intEncoder := encoding.DefaultIntEncoder size := 6 t.Logf("Printing collisions for size = %d across two adjacent seconds...", size) count := 0 // Second 1 sec1 := uint64(14776336 + 100) for secIndex := uint64(1); secIndex <= 50000; secIndex++ { secBytes := intEncoder.EncodeInt(sec1) inSecIndexBytes := intEncoder.EncodeInt(secIndex) m := min(uint64(len(inSecIndexBytes)), 5) secTagVal := uint64(0)*5 + (m - 1) var uid = make([]byte, 0, size) uid = intEncoder.AppendInt(uid, secTagVal) uid = append(uid, secBytes...) uid = append(uid, inSecIndexBytes...) uid = intEncoder.FillInt(uid, size) orig := string(uid) exchanged := encoding.ExchangeInt(uid) exchStr := string(exchanged) hashed := encoding.HashInt(exchanged) hashStr := string(hashed) finalID := hashStr[:size] seen[finalID] = struct { sec uint64 secIndex uint64 orig string exchanged string hashed string }{sec: sec1, secIndex: secIndex, orig: orig, exchanged: exchStr, hashed: hashStr} } // Second 2 (Adjacent second) sec2 := sec1 + 1 for secIndex := uint64(1); secIndex <= 50000; secIndex++ { secBytes := intEncoder.EncodeInt(sec2) inSecIndexBytes := intEncoder.EncodeInt(secIndex) m := min(uint64(len(inSecIndexBytes)), 5) secTagVal := uint64(0)*5 + (m - 1) var uid = make([]byte, 0, size) uid = intEncoder.AppendInt(uid, secTagVal) uid = append(uid, secBytes...) uid = append(uid, inSecIndexBytes...) uid = intEncoder.FillInt(uid, size) orig := string(uid) exchanged := encoding.ExchangeInt(uid) exchStr := string(exchanged) hashed := encoding.HashInt(exchanged) hashStr := string(hashed) finalID := hashStr[:size] if prev, found := seen[finalID]; found { t.Logf("Coll! ID: %s", finalID) t.Logf(" Pair 1: sec=%d, idx=%d | Orig: %s | Exch: %s | Hashed: %s", prev.sec, prev.secIndex, prev.orig, prev.exchanged, prev.hashed) t.Logf(" Pair 2: sec=%d, idx=%d | Orig: %s | Exch: %s | Hashed: %s", sec2, secIndex, orig, exchStr, hashStr) count++ if count >= 10 { break } } } t.Logf("Total collision pairs logged. Simulated adjacent-second collision counts: %d out of 50,000", count) } func BenchmarkIDMaker(b *testing.B) { b.Run("MakeID-10", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = id.MakeID(10) } }) b.Run("GetForMysql-10", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = id.DefaultIDMaker.GetForMysql(10) } }) }