You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/wordkey/generate_test.go

217 lines
7.0 KiB

package wordkey
import (
"bytes"
"crypto/sha256"
"fmt"
"math"
"strings"
"testing"
)
// The built-in lists and their SHA-256, as lists/README.md records them. A
// change of a list changes the hash an app pins, so it is never silent.
var listHashes = map[string]string{
"en": "6d557f0693958fb5e650b68b5bee585eb82cf4da32965505c789e924743bc522",
"es": "ff77b487765c000da97cca58fe94a2cdb947303e7a07460614d7d95d800034fe",
}
func TestBuiltInLists(t *testing.T) {
if got := strings.Join(Languages(), " "); got != "en es" {
t.Fatalf("Languages() = %q", got)
}
for lang, want := range listHashes {
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(lists[lang]))); got != want {
t.Errorf("list %s: SHA-256 %s, want %s", lang, got, want)
}
words, err := List(lang)
if err != nil {
t.Fatal(err)
}
if len(words) != 7776 {
t.Errorf("list %s: %d words, want 7776", lang, len(words))
}
}
if _, err := List("xx"); err == nil || !strings.Contains(err.Error(), `no word list for "xx"; the lists are en, es`) {
t.Errorf("List(xx): %v", err)
}
// The list of the EFF keeps its order, so that the word at position i
// is the one of the dice of i in base 6: 11111 is the first, and 66666
// the last.
en, _ := List("en")
if en[0] != "abacus" || en[1] != "abdomen" || en[7775] != "zoom" {
t.Errorf("list en: %q, %q, %q", en[0], en[1], en[7775])
}
}
func TestCheckList(t *testing.T) {
// pal followed by three letters: palaaa, palaab, …
base := make([]string, MinListSize)
for i := range base {
base[i] = "pal" + string([]rune{'a' + rune(i/676), 'a' + rune(i/26%26), 'a' + rune(i%26)})
}
if err := CheckList("es", base); err != nil {
t.Fatal(err)
}
if err := CheckList("xx", base); err == nil || err.Error() != `no alphabet for the language "xx"` {
t.Errorf("CheckList(xx): %v", err)
}
with := func(i int, w string) []string {
l := append([]string(nil), base...)
l[i] = w
return l
}
for _, c := range []struct {
list []string
want string
}{
{base[:MinListSize-1], "2047 words, fewer than 2048"},
{with(5, "dos palabras"), `line 6, "dos palabras", is not one word`},
{with(5, " "), "is not one word"},
{with(5, "mi"), `"mi", is not one word of 3 or more letters`},
{with(5, "casa​"), "invisible character U+200B"},
// Only the letters of the alphabet of the language, as the list
// writes them: no capitals, no Cyrillic U+0441 that looks like a Latin
// c, no digits, no carriage return of a file with CRLF lines.
{with(5, "Palaaf"), `line 6, "Palaaf", holds U+0050, which is not in the alphabet of "es"`},
{with(5, string(rune(0x0441))+"asa"), "holds U+0441, which is not in the alphabet"},
{with(5, "pal1"), "holds U+0031"},
{with(5, "palaaf\r"), `line 6, "palaaf\r", holds U+000D`},
{with(5, base[4]), `line 6, "palaae", is the same word as "palaae"`},
{with(5, "palaáe"), `line 6, "palaáe", is the same word as "palaae"`},
{append(with(0, "papá"), "papa"), `"papa", is the same word as "papá"`},
} {
if err := CheckList("es", c.list); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("CheckList: %v, want %q", err, c.want)
}
}
// The hyphen of the compound words of the list of the EFF is a letter of
// en and not of es; an accent is a letter of es and not of en.
if err := CheckList("en", with(5, "t-shirt")); err != nil {
t.Errorf("CheckList(en) with t-shirt: %v", err)
}
for _, c := range []struct{ lang, word, want string }{
{"es", "t-shirt", `line 6, "t-shirt", holds U+002D, which is not in the alphabet of "es"`},
{"en", "palaáf", `line 6, "palaáf", holds U+00E1, which is not in the alphabet of "en"`},
} {
if err := CheckList(c.lang, with(5, c.word)); err == nil || err.Error() != c.want {
t.Errorf("CheckList(%s) with %q: %v, want %q", c.lang, c.word, err, c.want)
}
}
}
func TestGenerate(t *testing.T) {
list, err := List("es")
if err != nil {
t.Fatal(err)
}
words, err := Generate(list, DefaultCount, nil)
if err != nil {
t.Fatal(err)
}
if len(words) != DefaultCount {
t.Fatalf("%d words", len(words))
}
if err := Check(Normalize(strings.Join(words, " "))); err != nil {
t.Errorf("generated words %q: %v", words, err)
}
seen := map[string]bool{}
for _, w := range words {
if seen[w] {
t.Errorf("%q drawn twice", w)
}
seen[w] = true
}
// The same random bytes draw the same words: Generate reads nothing else.
var seed []byte
for i := range 8 {
h := sha256.Sum256([]byte{byte(i)})
seed = append(seed, h[:]...)
}
a, err := Generate(list, 6, bytes.NewReader(seed))
if err != nil {
t.Fatal(err)
}
b, err := Generate(list, 6, bytes.NewReader(seed))
if err != nil || len(a) != 6 || strings.Join(a, " ") != strings.Join(b, " ") {
t.Errorf("%q and %q: %v", a, b, err)
}
// An index drawn twice is drawn again: each pair of bytes is an index
// below 8192, and 0 1 is index 1.
again := []byte{0, 1, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6}
if w, err := Generate(list, 6, bytes.NewReader(again)); err != nil || strings.Join(w, " ") != strings.Join(list[1:7], " ") {
t.Errorf("Generate with index 1 twice: %q, %v", w, err)
}
// Bytes that only ever give two indices, 1793 and 2081, run out.
if _, err := Generate(list, 6, bytes.NewReader(bytes.Repeat([]byte{7, 1, 200, 33}, 64))); err == nil || !strings.Contains(err.Error(), "EOF") {
t.Errorf("Generate with two indices: %v", err)
}
for _, c := range []struct {
n int
want string
}{
{5, "at least 6 words, not 5"},
{len(list), "7776 words of a list of 7776"},
} {
if _, err := Generate(list, c.n, nil); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("Generate(%d): %v, want %q", c.n, err, c.want)
}
}
if _, err := Generate(list, 6, bytes.NewReader(nil)); err == nil || !strings.Contains(err.Error(), "EOF") {
t.Errorf("Generate without random bytes: %v", err)
}
}
func TestBits(t *testing.T) {
if b := Bits(7776, 1); b != math.Log2(7776) {
t.Errorf("Bits(7776, 1) = %v", b)
}
// 7 words of 7776 are a little under 90.5 bits, and 6 of 2048, the
// fewest that Generate draws, a little under 66.
for _, c := range []struct {
size, count int
low, high float64
}{
{7776, 0, 0, 0},
{7776, 7, 90.469, 90.470},
{7776, 8, 103.393, 103.394},
{2048, 6, 65.989, 65.990},
} {
if b := Bits(c.size, c.count); b < c.low || b > c.high {
t.Errorf("Bits(%d, %d) = %v", c.size, c.count, b)
}
}
}
// Every word is about as likely: over 7776·40 draws of one word, each index
// falls in its bucket of 64 between 0.8 and 1.2 times the mean.
func TestGenerateUniform(t *testing.T) {
if testing.Short() {
t.Skip("slow")
}
list, _ := List("es")
index := make(map[string]int, len(list))
for i, w := range list {
index[w] = i
}
const buckets = 64
var count [buckets]int
draws := 0
for draws < len(list)*40 {
words, err := Generate(list, 6, nil)
if err != nil {
t.Fatal(err)
}
for _, w := range words {
count[index[w]*buckets/len(list)]++
draws++
}
}
mean := float64(draws) / buckets
for i, c := range count {
if float64(c) < 0.8*mean || float64(c) > 1.2*mean {
t.Errorf("bucket %d: %d draws, mean %.0f", i, c, mean)
}
}
}

Powered by TurnKey Linux.