mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 09:29:07 +02:00
Adds PickTuple, which draws an adjective and a noun to form a single DNS label such as "brave-otter". The existing single-word generator kept a per-cluster "taken" set and guessed uniqueness up front; a later commit replaces that with a database constraint and a retry, so PickTuple deliberately takes no taken set and has no fallback suffix. The adjectives live in their own pool rather than reusing the noun list, which is almost entirely nouns -- drawing twice from it produces "millet-hammock", which reads as noise rather than a name. Tests assert the curation contract the pool depends on: duplicate-free, DNS-safe, and disjoint from the nouns.
181 lines
6.2 KiB
Go
181 lines
6.2 KiB
Go
package labelgen
|
|
|
|
import (
|
|
"math/rand"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPickUnique_DeterministicWithSeededRng locks the property the
|
|
// caller relies on: same seed + same taken set → same pick. Without
|
|
// that, the bootstrap flow can't reproduce a label across retries.
|
|
func TestPickUnique_DeterministicWithSeededRng(t *testing.T) {
|
|
taken := map[string]struct{}{}
|
|
|
|
rngA := rand.New(rand.NewSource(42))
|
|
rngB := rand.New(rand.NewSource(42))
|
|
|
|
a := PickUnique(rngA, taken, "abcd")
|
|
b := PickUnique(rngB, taken, "abcd")
|
|
|
|
assert.Equal(t, a, b, "Same seed and taken set must produce identical pick")
|
|
}
|
|
|
|
// TestPickUnique_AvoidsTakenWordsWhenMostAreReserved seeds taken with
|
|
// every word in the pool except a handful and confirms PickUnique
|
|
// finds one of the remaining free entries instead of returning the
|
|
// fallback form.
|
|
func TestPickUnique_AvoidsTakenWordsWhenMostAreReserved(t *testing.T) {
|
|
pool := uniqueWords()
|
|
require.NotEmpty(t, pool, "wordlist must be populated for the test to mean anything")
|
|
|
|
free := map[string]struct{}{
|
|
pool[0]: {},
|
|
pool[len(pool)/2]: {},
|
|
pool[len(pool)-1]: {},
|
|
}
|
|
|
|
taken := make(map[string]struct{}, len(pool))
|
|
for _, w := range pool {
|
|
if _, ok := free[w]; ok {
|
|
continue
|
|
}
|
|
taken[w] = struct{}{}
|
|
}
|
|
|
|
rng := rand.New(rand.NewSource(7))
|
|
got := PickUnique(rng, taken, "abcd")
|
|
|
|
_, isFree := free[got]
|
|
assert.True(t, isFree, "PickUnique must return one of the free words; got %q", got)
|
|
assert.NotContains(t, got, "-", "Free pick must not be the suffix fallback form")
|
|
}
|
|
|
|
// TestPickUnique_FallsBackWhenAllReserved exhausts the pool and
|
|
// confirms PickUnique appends the supplied suffix instead of
|
|
// returning a duplicate.
|
|
func TestPickUnique_FallsBackWhenAllReserved(t *testing.T) {
|
|
pool := uniqueWords()
|
|
|
|
taken := make(map[string]struct{}, len(pool))
|
|
for _, w := range pool {
|
|
taken[w] = struct{}{}
|
|
}
|
|
|
|
rng := rand.New(rand.NewSource(99))
|
|
got := PickUnique(rng, taken, "abcd")
|
|
|
|
assert.True(t, strings.HasSuffix(got, "-abcd"), "Exhausted pool must produce <word>-<suffix>; got %q", got)
|
|
|
|
prefix := strings.TrimSuffix(got, "-abcd")
|
|
found := false
|
|
for _, w := range pool {
|
|
if w == prefix {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Fallback prefix must be drawn from the wordlist; got %q", prefix)
|
|
}
|
|
|
|
// TestUniqueWords_DropsDuplicates guards against authoring slips in
|
|
// words.go: every entry must be unique and DNS-safe.
|
|
func TestUniqueWords_DropsDuplicates(t *testing.T) {
|
|
pool := uniqueWords()
|
|
seen := make(map[string]struct{}, len(pool))
|
|
for _, w := range pool {
|
|
_, dup := seen[w]
|
|
assert.False(t, dup, "Duplicate entry %q in deduplicated pool", w)
|
|
seen[w] = struct{}{}
|
|
assert.GreaterOrEqual(t, len(w), 4, "Word %q is shorter than 4 chars", w)
|
|
assert.LessOrEqual(t, len(w), 12, "Word %q is longer than 12 chars", w)
|
|
for _, r := range w {
|
|
ok := r >= 'a' && r <= 'z'
|
|
assert.True(t, ok, "Word %q contains non-lowercase-ASCII rune %q", w, r)
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(t, len(pool), 500, "Pool must contain at least 500 unique words")
|
|
}
|
|
|
|
// TestPickTuple_ShapeAndPoolMembership locks the wire-visible shape: an
|
|
// adjective and a noun, each from its own pool, joined by a single hyphen so
|
|
// the result stays one DNS label.
|
|
func TestPickTuple_ShapeAndPoolMembership(t *testing.T) {
|
|
nouns := uniqueWords()
|
|
inNouns := make(map[string]struct{}, len(nouns))
|
|
for _, w := range nouns {
|
|
inNouns[w] = struct{}{}
|
|
}
|
|
inAdjectives := make(map[string]struct{}, len(adjectives))
|
|
for _, a := range adjectives {
|
|
inAdjectives[a] = struct{}{}
|
|
}
|
|
|
|
rng := rand.New(rand.NewSource(7))
|
|
for i := 0; i < 200; i++ {
|
|
got := PickTuple(rng)
|
|
|
|
parts := strings.Split(got, "-")
|
|
require.Len(t, parts, 2, "PickTuple must produce exactly two hyphen-joined words; got %q", got)
|
|
|
|
_, adjOK := inAdjectives[parts[0]]
|
|
assert.True(t, adjOK, "First half must be an adjective; %q not in adjectives (from %q)", parts[0], got)
|
|
_, nounOK := inNouns[parts[1]]
|
|
assert.True(t, nounOK, "Second half must be a noun; %q not in words (from %q)", parts[1], got)
|
|
|
|
assert.LessOrEqual(t, len(got), 63, "Label must fit a DNS label; got %q (%d chars)", got, len(got))
|
|
}
|
|
}
|
|
|
|
// TestAdjectives_AreDisjointFromNouns keeps the namespace a clean product and
|
|
// prevents nonsense like "azure-azure": a handful of the noun pool's entries
|
|
// are adjectival, and any overlap would let the same word land on both sides.
|
|
func TestAdjectives_AreDisjointFromNouns(t *testing.T) {
|
|
nouns := make(map[string]struct{}, len(uniqueWords()))
|
|
for _, w := range uniqueWords() {
|
|
nouns[w] = struct{}{}
|
|
}
|
|
for _, a := range adjectives {
|
|
_, clash := nouns[a]
|
|
assert.False(t, clash, "Adjective %q also appears in the noun pool; remove it from one list", a)
|
|
}
|
|
}
|
|
|
|
// TestAdjectives_AreDNSSafeAndDeduplicated mirrors the curation contract stated
|
|
// in words.go: lowercase ASCII, 4-12 chars, no digits or hyphens, no repeats.
|
|
func TestAdjectives_AreDNSSafeAndDeduplicated(t *testing.T) {
|
|
seen := make(map[string]struct{}, len(adjectives))
|
|
for _, a := range adjectives {
|
|
_, dup := seen[a]
|
|
assert.False(t, dup, "Duplicate adjective %q", a)
|
|
seen[a] = struct{}{}
|
|
|
|
assert.Regexp(t, `^[a-z]{4,12}$`, a, "Adjective %q must be 4-12 lowercase ASCII letters", a)
|
|
}
|
|
assert.Greater(t, len(adjectives), 150, "Adjective pool too small to give a useful namespace")
|
|
}
|
|
|
|
// TestPickTuple_DeterministicWithSeededRng documents that generation is a pure
|
|
// function of the rng, which is what makes allocation retries reproducible in tests.
|
|
func TestPickTuple_DeterministicWithSeededRng(t *testing.T) {
|
|
a := PickTuple(rand.New(rand.NewSource(42)))
|
|
b := PickTuple(rand.New(rand.NewSource(42)))
|
|
assert.Equal(t, a, b, "Same seed must yield the same tuple")
|
|
}
|
|
|
|
// TestPickTuple_SpansALargeNamespace guards the reason we moved to tuples: a
|
|
// single-word pool caps the GLOBAL namespace at 857. Drawing many tuples must
|
|
// yield overwhelmingly distinct values.
|
|
func TestPickTuple_SpansALargeNamespace(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(11))
|
|
seen := make(map[string]struct{}, 2000)
|
|
for i := 0; i < 2000; i++ {
|
|
seen[PickTuple(rng)] = struct{}{}
|
|
}
|
|
assert.Greater(t, len(seen), 1900,
|
|
"2000 draws should be nearly all distinct across a ~200k namespace; got %d unique", len(seen))
|
|
}
|