package util import ( "math" "strings" "testing" ) func TestRandomStringLengthAndAlphabet(t *testing.T) { for _, length := range []int{1, 6, 18, 24, 32, 48, 64, 300} { value, err := RandomString(length) if err != nil { t.Fatalf("RandomString(%d): %v", length, err) } if len(value) != length { t.Errorf("RandomString(%d) вернул строку длины %d", length, len(value)) } for _, r := range value { if !strings.ContainsRune(charset, r) { t.Errorf("RandomString(%d) вернул символ %q вне алфавита", length, r) } } } } func TestRandomStringRejectsNonPositiveLength(t *testing.T) { for _, length := range []int{0, -1} { if _, err := RandomString(length); err == nil { t.Errorf("RandomString(%d) не отклонён", length) } } } // Проверка отсутствия modulo bias. // // Прежняя реализация делала charset[b % 62] по байту напрямую. 256 = 4×62 + 8, // поэтому первые восемь символов алфавита выпадали пятью значениями байта из // 256, а остальные 54 — четырьмя: перекос примерно на 25%. // // Тест сравнивает наблюдаемые частоты первых восьми символов алфавита с // остальными. На выборке в 200 000 символов ожидаемая частота каждого — около // 3226; перекос в 25% дал бы у «a»…«h» примерно 4032 против 3226, то есть // разницу в сотни раз больше статистического шума. Порог взят с большим // запасом, чтобы тест не был флаки: он ловит систематическое смещение, а не // проверяет качество генератора случайных чисел. func TestRandomStringHasNoModuloBias(t *testing.T) { const sampleSize = 200000 value, err := RandomString(sampleSize) if err != nil { t.Fatalf("RandomString: %v", err) } counts := make(map[rune]int, len(charset)) for _, r := range value { counts[r]++ } expected := float64(sampleSize) / float64(len(charset)) // Восемь символов, которые смещённая реализация выдавала чаще остальных. biasedPrefix := charset[:256%len(charset)] var prefixTotal, restTotal float64 for _, r := range charset { if strings.ContainsRune(biasedPrefix, r) { prefixTotal += float64(counts[r]) } else { restTotal += float64(counts[r]) } } prefixMean := prefixTotal / float64(len(biasedPrefix)) restMean := restTotal / float64(len(charset)-len(biasedPrefix)) // Смещённая реализация давала prefixMean/restMean = 5/4 = 1.25. // Допуск 5% оставляет между порогом и дефектом кратный зазор. ratio := prefixMean / restMean if math.Abs(ratio-1) > 0.05 { t.Errorf( "распределение смещено: начало алфавита %.1f, остальные %.1f, отношение %.4f", prefixMean, restMean, ratio, ) } // Ни один символ алфавита не должен пропасть или доминировать. for _, r := range charset { count := float64(counts[r]) if math.Abs(count-expected)/expected > 0.15 { t.Errorf("символ %q встретился %d раз при ожидаемых %.0f", r, counts[r], expected) } } }