├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── coverage.yml │ ├── lint-soft.yml │ └── lint.yml ├── .gitignore ├── .golangci-soft.yml ├── .golangci.yml ├── LICENSE ├── README.md ├── errors.go ├── examples ├── space │ └── space.go └── tokens │ └── tokens.go ├── go.mod ├── go.sum ├── toktok.go └── toktok_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: muesli 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | go-version: [~1.11, ^1] 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Download Go modules 23 | run: go mod download 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | coverage: 6 | strategy: 7 | matrix: 8 | go-version: [^1] 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Coverage 23 | env: 24 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: | 26 | go test -race -covermode atomic -coverprofile=profile.cov ./... 27 | GO111MODULE=off go get github.com/mattn/goveralls 28 | $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github 29 | -------------------------------------------------------------------------------- /.github/workflows/lint-soft.yml: -------------------------------------------------------------------------------- 1 | name: lint-soft 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint-soft 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: golangci-lint command line arguments. 21 | args: --config .golangci-soft.yml --issues-exit-code=0 22 | # Optional: show only new issues if it's a pull request. The default value is `false`. 23 | only-new-issues: true 24 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: golangci-lint command line arguments. 21 | #args: 22 | # Optional: show only new issues if it's a pull request. The default value is `false`. 23 | only-new-issues: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Backup files 17 | *~ -------------------------------------------------------------------------------- /.golangci-soft.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | issues: 5 | include: 6 | - EXC0001 7 | - EXC0005 8 | - EXC0011 9 | - EXC0012 10 | - EXC0013 11 | 12 | max-issues-per-linter: 0 13 | max-same-issues: 0 14 | 15 | linters: 16 | enable: 17 | # - dupl 18 | - exhaustive 19 | # - exhaustivestruct 20 | - goconst 21 | - godot 22 | - godox 23 | - gomnd 24 | - gomoddirectives 25 | - goprintffuncname 26 | - ifshort 27 | # - lll 28 | - misspell 29 | - nakedret 30 | - nestif 31 | - noctx 32 | - nolintlint 33 | - prealloc 34 | - wrapcheck 35 | 36 | # disable default linters, they are already enabled in .golangci.yml 37 | disable: 38 | - deadcode 39 | - errcheck 40 | - gosimple 41 | - govet 42 | - ineffassign 43 | - staticcheck 44 | - structcheck 45 | - typecheck 46 | - unused 47 | - varcheck 48 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | issues: 5 | include: 6 | - EXC0001 7 | - EXC0005 8 | - EXC0011 9 | - EXC0012 10 | - EXC0013 11 | 12 | max-issues-per-linter: 0 13 | max-same-issues: 0 14 | 15 | linters: 16 | enable: 17 | - bodyclose 18 | - exportloopref 19 | - goimports 20 | - gosec 21 | - nilerr 22 | - predeclared 23 | - revive 24 | - rowserrcheck 25 | - sqlclosecheck 26 | - tparallel 27 | - unconvert 28 | - unparam 29 | - whitespace 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | toktok 2 | ====== 3 | 4 | [![Latest Release](https://img.shields.io/github/release/muesli/toktok.svg)](https://github.com/muesli/toktok/releases) 5 | [![Build Status](https://travis-ci.org/muesli/toktok.svg?branch=master)](https://travis-ci.org/muesli/toktok) 6 | [![Coverage Status](https://coveralls.io/repos/github/muesli/toktok/badge.svg?branch=master)](https://coveralls.io/github/muesli/toktok?branch=master) 7 | [![Go ReportCard](https://goreportcard.com/badge/muesli/toktok)](https://goreportcard.com/report/muesli/toktok) 8 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/muesli/toktok) 9 | 10 | A human-friendly token generator 11 | 12 | Creates tokens which avoid characters that can be easily misinterpreted, like '1' and 'I' or '8' and 'B', as well as 13 | repeated characters within the token. It also compares newly generated tokens to all previously generated ones and 14 | guarantees a safety distance between the tokens, so they become resilient to typos or other human entry errors. 15 | 16 | ## Installation 17 | 18 | Make sure you have a working Go environment (Go 1.11 or higher is required). 19 | See the [install instructions](https://golang.org/doc/install.html). 20 | 21 | To install toktok, simply run: 22 | 23 | go get github.com/muesli/toktok 24 | 25 | Compiling toktok is easy, simply run: 26 | 27 | git clone https://github.com/muesli/toktok.git 28 | cd toktok 29 | go build && go test -v 30 | 31 | ## Example 32 | ```go 33 | package main 34 | 35 | import ( 36 | "fmt" 37 | 38 | "github.com/muesli/toktok" 39 | ) 40 | 41 | func main() { 42 | // Generate a new token bucket. 43 | // Each generated token will be 8 characters long. 44 | bucket, _ := toktok.NewBucket(8) 45 | 46 | // Generate a bunch of tokens with a safety distance of 4. 47 | // Distance is calculated by insertion cost (1), deletion cost (1) and 48 | // substitution cost (2). 49 | for i := 0; i < 9; i++ { 50 | token, _ := bucket.NewToken(4) 51 | fmt.Printf("Generated Token %d: %s\n", i, token) 52 | } 53 | 54 | // One more token that we will tamper with. 55 | token, _ := bucket.NewToken(4) 56 | fmt.Printf("Generated Token 9: %s\n", token) 57 | token = "_" + token[1:7] + "_" 58 | 59 | // Find the closest match for the faulty token. 60 | match, distance := bucket.Resolve(token) 61 | fmt.Printf("Best match for '%s' is token '%s' with distance %d\n", token, match, distance) 62 | } 63 | ``` 64 | 65 | ## Result 66 | ``` 67 | Generated Token 0: J3KPC9YF 68 | Generated Token 1: PXTWDC9P 69 | Generated Token 2: WNANK4FU 70 | ... 71 | Generated Token 9: Y3NCDFWN 72 | Best match for '_3NCDFW_' is token 'Y3NCDFWN' with distance 4 73 | ``` 74 | 75 | ## Feedback 76 | 77 | Got some feedback or suggestions? Please open an issue or drop me a note! 78 | 79 | * [Twitter](https://twitter.com/mueslix) 80 | * [The Fediverse](https://mastodon.social/@fribbledom) 81 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Human-friendly token generator 3 | * Copyright (c) 2017-2022, Christian Muehlhaeuser 4 | * 5 | * For license see LICENSE 6 | */ 7 | 8 | package toktok 9 | 10 | import ( 11 | "errors" 12 | ) 13 | 14 | var ( 15 | // ErrTokenLengthTooSmall gets returned when the token length is too small 16 | ErrTokenLengthTooSmall = errors.New("Token length is too small") 17 | // ErrTooFewRunes gets returned when the set of runes is too small 18 | ErrTooFewRunes = errors.New("Not enough runes") 19 | // ErrDupeRunes gets returned when the set of runes contains a dupe 20 | ErrDupeRunes = errors.New("Dupe in runes") 21 | // ErrDistanceTooSmall gets returned when the required distance is too small 22 | ErrDistanceTooSmall = errors.New("Distance must be at least 1") 23 | // ErrTokenSpaceExhausted gets returned when the token space has been exhausted 24 | ErrTokenSpaceExhausted = errors.New("Token space exhausted. Use longer tokens, more runes or a smaller distance") 25 | ) 26 | -------------------------------------------------------------------------------- /examples/space/space.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muesli/toktok" 7 | ) 8 | 9 | func main() { 10 | for n := uint(4); n < 10; n++ { 11 | fmt.Println("Probing token space for length", n) 12 | fmt.Println("================================") 13 | for dist := 3; dist <= 5; dist++ { 14 | fmt.Printf("Probing with distance %d\n", dist) 15 | 16 | // Generate a new token bucket. Each generated token will be n characters long 17 | bucket, _ := toktok.NewBucket(n) 18 | 19 | // Generate a bunch of tokens with a safety distance of 4 20 | // Distance is calculated by insertion cost (1), deletion cost (1) and substitution cost (2) 21 | for i := 0; bucket.EstimatedFillPercentage() < 2.00 || i < 20000; i++ { 22 | _, err := bucket.NewToken(dist) 23 | if err != nil { 24 | break 25 | } 26 | 27 | if i >= 1024 && (i&(i-1)) == 0 { 28 | fmt.Printf("Generated %d tokens, estimated space for %d tokens (%.2f%% full)\n", 29 | i, bucket.EstimatedTokenSpace(), bucket.EstimatedFillPercentage()) 30 | } 31 | } 32 | 33 | fmt.Printf("Finished probing for length %d with distance %d: estimated space for %d tokens\n\n", n, dist, bucket.EstimatedTokenSpace()) 34 | } 35 | fmt.Println() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/tokens/tokens.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muesli/toktok" 7 | ) 8 | 9 | func main() { 10 | // Generate a new token bucket. 11 | // Each generated token will be 8 characters long. 12 | bucket, _ := toktok.NewBucket(8) 13 | 14 | // Generate a bunch of tokens with a safety distance of 4. 15 | // Distance is calculated by insertion cost (1), deletion cost (1) and 16 | // substitution cost (2). 17 | for i := 0; i < 9; i++ { 18 | token, _ := bucket.NewToken(4) 19 | fmt.Printf("Generated Token %d: %s\n", i, token) 20 | } 21 | 22 | // One more token that we will tamper with. 23 | token, _ := bucket.NewToken(4) 24 | fmt.Printf("Generated Token 9: %s\n", token) 25 | token = "_" + token[1:7] + "_" 26 | 27 | // Find the closest match for the faulty token. 28 | match, distance := bucket.Resolve(token) 29 | fmt.Printf("Best match for '%s' is token '%s' with distance %d\n", token, match, distance) 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muesli/toktok 2 | 3 | go 1.15 4 | 5 | require github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 2 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 3 | -------------------------------------------------------------------------------- /toktok.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Human-friendly token generator 3 | * Copyright (c) 2017-2022, Christian Muehlhaeuser 4 | * 5 | * For license see LICENSE 6 | */ 7 | 8 | package toktok 9 | 10 | import ( 11 | "math/rand" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/xrash/smetrics" 17 | ) 18 | 19 | // Bucket tracks all the generated tokens and lets you create new, unique tokens. 20 | type Bucket struct { 21 | length uint 22 | runes []rune 23 | 24 | tokens map[string]struct{} 25 | tries []uint64 26 | 27 | sync.RWMutex 28 | } 29 | 30 | // NewBucket returns a new bucket, which will contain tokens of tokenLength. 31 | func NewBucket(tokenLength uint) (Bucket, error) { 32 | // Problematic chars: 33 | // - A and 4 34 | // - B and 8 35 | // - G and 6 36 | // - I and 1 37 | // - O and Q, 0 38 | // - Q and O, 0 39 | // - S and 5 40 | // - U and V 41 | // - Z and 2, 7 42 | return NewBucketWithRunes(tokenLength, "ABCDEFHJKLMNPRSTUWXY369") 43 | } 44 | 45 | // NewBucketWithRunes returns a new bucket and lets you define which runes will 46 | // be used for token generation. 47 | func NewBucketWithRunes(tokenLength uint, runes string) (Bucket, error) { 48 | runes = strings.ToUpper(runes) 49 | for _, r := range runes { 50 | if strings.Count(runes, string(r)) > 1 { 51 | return Bucket{}, ErrDupeRunes 52 | } 53 | } 54 | 55 | if tokenLength < 2 { 56 | return Bucket{}, ErrTokenLengthTooSmall 57 | } 58 | if len(runes) < 4 { 59 | return Bucket{}, ErrTooFewRunes 60 | } 61 | 62 | return Bucket{ 63 | length: tokenLength, 64 | runes: []rune(runes), 65 | tokens: make(map[string]struct{}), 66 | }, nil 67 | } 68 | 69 | // LoadTokens adds previously generated tokens to the Bucket. 70 | func (bucket *Bucket) LoadTokens(tokens []string) { 71 | bucket.Lock() 72 | defer bucket.Unlock() 73 | 74 | for _, v := range tokens { 75 | bucket.tokens[v] = struct{}{} 76 | } 77 | } 78 | 79 | // NewToken returns a new token with a minimal safety distance to all other 80 | // existing tokens. 81 | func (bucket *Bucket) NewToken(distance int) (string, error) { 82 | if distance < 1 { 83 | return "", ErrDistanceTooSmall 84 | } 85 | 86 | c, i, err := bucket.generate(distance) 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | bucket.Lock() 92 | defer bucket.Unlock() 93 | 94 | bucket.tokens[c] = struct{}{} 95 | 96 | bucket.tries = append(bucket.tries, uint64(i)) 97 | if len(bucket.tries) > 5000 { 98 | bucket.tries = bucket.tries[1:] 99 | } 100 | 101 | return c, nil 102 | } 103 | 104 | // Resolve tries to find the matching original token for a potentially corrupted 105 | // token. 106 | func (bucket *Bucket) Resolve(code string) (string, int) { 107 | distance := 65536 108 | 109 | bucket.RLock() 110 | defer bucket.RUnlock() 111 | 112 | code = strings.ToUpper(code) 113 | // try to find a perfect match first 114 | _, ok := bucket.tokens[code] 115 | if ok { 116 | return code, 0 117 | } 118 | 119 | var t string 120 | // find the closest match 121 | for token := range bucket.tokens { 122 | if hd := smetrics.WagnerFischer(code, token, 1, 1, 2); hd <= distance { 123 | if hd == distance { 124 | // duplicate distance, ignore the previous result as it's not unique enough 125 | t = "" 126 | } else { 127 | t = token 128 | distance = hd 129 | } 130 | } 131 | } 132 | 133 | return t, distance 134 | } 135 | 136 | // Count returns how many tokens are currently in this Bucket. 137 | func (bucket *Bucket) Count() uint64 { 138 | bucket.Lock() 139 | defer bucket.Unlock() 140 | 141 | return uint64(len(bucket.tokens)) 142 | } 143 | 144 | // EstimatedFillPercentage returns how full the Bucket approximately is. 145 | func (bucket *Bucket) EstimatedFillPercentage() float64 { 146 | bucket.Lock() 147 | defer bucket.Unlock() 148 | 149 | if len(bucket.tries) == 0 { 150 | return 0 151 | } 152 | 153 | tries := uint64(0) 154 | for _, v := range bucket.tries { 155 | tries += v 156 | } 157 | 158 | return 100.0 - (100.0 / (float64(tries) / float64(len(bucket.tries)))) 159 | } 160 | 161 | // EstimatedTokenSpace returns the total estimated token space available in this 162 | // Bucket. 163 | func (bucket *Bucket) EstimatedTokenSpace() uint64 { 164 | return uint64(float64(bucket.Count()) * (100.0 / bucket.EstimatedFillPercentage())) 165 | } 166 | 167 | func (bucket *Bucket) generate(distance int) (string, int, error) { 168 | bucket.RLock() 169 | defer bucket.RUnlock() 170 | 171 | var c string 172 | i := 0 173 | for { 174 | i++ 175 | if i == 100 { 176 | return "", i, ErrTokenSpaceExhausted 177 | } 178 | 179 | c = GenerateToken(bucket.length, bucket.runes) 180 | 181 | dupe := false 182 | for token := range bucket.tokens { 183 | if hd := smetrics.WagnerFischer(c, token, 1, 1, 2); hd <= distance { 184 | dupe = true 185 | break 186 | } 187 | } 188 | if !dupe { 189 | break 190 | } 191 | } 192 | 193 | return c, i, nil 194 | } 195 | 196 | // GenerateToken generates a new token of length n with the defined rune-set 197 | // letterRunes. 198 | func GenerateToken(n uint, letterRunes []rune) string { 199 | l := len(letterRunes) 200 | b := make([]rune, n) 201 | 202 | for i := range b { 203 | var lastrune rune 204 | if i > 0 { 205 | lastrune = b[i-1] 206 | } 207 | b[i] = lastrune 208 | for lastrune == b[i] { 209 | b[i] = letterRunes[rand.Intn(l)] //nolint:gosec 210 | } 211 | } 212 | 213 | return string(b) 214 | } 215 | 216 | func init() { 217 | rand.Seed(time.Now().UnixNano()) 218 | } 219 | -------------------------------------------------------------------------------- /toktok_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Human-friendly token generator 3 | * Copyright (c) 2017-2022, Christian Muehlhaeuser 4 | * 5 | * For license see LICENSE 6 | */ 7 | 8 | package toktok 9 | 10 | import ( 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestTokenGen(t *testing.T) { 16 | length := uint(8) 17 | bucket, err := NewBucket(length) 18 | if err != nil { 19 | t.Error("Error creating new token bucket:", err) 20 | } 21 | 22 | tok, err := bucket.NewToken(4) 23 | if err != nil { 24 | t.Errorf("Unexpected error %v", err) 25 | } 26 | if len(tok) != int(length) { 27 | t.Errorf("Wrong token length, expected %d, got %d", length, len(tok)) 28 | } 29 | } 30 | 31 | func TestTokenCount(t *testing.T) { 32 | bucket, _ := NewBucket(8) 33 | bucket.NewToken(1) 34 | 35 | if bucket.Count() != 1 { 36 | t.Errorf("Expected Count() to return 1, got %d", bucket.Count()) 37 | } 38 | } 39 | 40 | func TestTokenLoad(t *testing.T) { 41 | code1, code2 := "ABCDEFGH", "IJKLMNOP" 42 | tokens := []string{code1, code2} 43 | bucket, _ := NewBucket(8) 44 | bucket.LoadTokens(tokens) 45 | 46 | if bucket.Count() != 2 { 47 | t.Errorf("Expected Count() to return 2, got %d", bucket.Count()) 48 | } 49 | tok, _ := bucket.Resolve(code1) 50 | if tok != code1 { 51 | t.Errorf("Expected Token '%s', got '%s'", code1, tok) 52 | } 53 | } 54 | 55 | func TestTokenEstimations(t *testing.T) { 56 | bucket, _ := NewBucket(7) 57 | if bucket.EstimatedFillPercentage() != 0 { 58 | t.Errorf("Expected zero fill-rate estimate, got %f", bucket.EstimatedFillPercentage()) 59 | } 60 | 61 | for i := 0; i < 5001; i++ { 62 | bucket.NewToken(4) 63 | } 64 | 65 | if bucket.EstimatedFillPercentage() <= 0 { 66 | t.Errorf("Expected positive fill-rate estimate, got %f", bucket.EstimatedFillPercentage()) 67 | } 68 | if bucket.EstimatedTokenSpace() <= 0 { 69 | t.Errorf("Expected positive token space estimate, got %d", bucket.EstimatedTokenSpace()) 70 | } 71 | } 72 | 73 | func TestTokenError(t *testing.T) { 74 | _, err := NewBucket(1) 75 | if err != ErrTokenLengthTooSmall { 76 | t.Errorf("Expected error %v, got %v", ErrTokenLengthTooSmall, err) 77 | } 78 | 79 | _, err = NewBucketWithRunes(8, "ABC") 80 | if err != ErrTooFewRunes { 81 | t.Errorf("Expected error %v, got %v", ErrTooFewRunes, err) 82 | } 83 | 84 | _, err = NewBucketWithRunes(8, "ABCDabcd") 85 | if err != ErrDupeRunes { 86 | t.Errorf("Expected error %v, got %v", ErrDupeRunes, err) 87 | } 88 | 89 | bucket, _ := NewBucket(4) 90 | _, err = bucket.NewToken(0) 91 | if err != ErrDistanceTooSmall { 92 | t.Errorf("Expected error %v, got %v", ErrDistanceTooSmall, err) 93 | } 94 | 95 | for i := 0; i < 256; i++ { 96 | _, err = bucket.NewToken(4) 97 | if err != nil { 98 | break 99 | } 100 | } 101 | if err != ErrTokenSpaceExhausted { 102 | t.Errorf("Expected error %v, got %v", ErrTokenSpaceExhausted, err) 103 | } 104 | } 105 | 106 | func TestTokenResolve(t *testing.T) { 107 | bucket, _ := NewBucket(8) 108 | 109 | var tok string 110 | for i := 0; i < 32; i++ { 111 | gtok, err := bucket.NewToken(4) 112 | if err != nil { 113 | t.Errorf("Unexpected error %v", err) 114 | } 115 | if i == 0 { 116 | tok = gtok 117 | } 118 | } 119 | 120 | ntok, dist := bucket.Resolve(tok) 121 | if ntok != tok { 122 | t.Errorf("Token mismatch, expected %v, got %v", tok, ntok) 123 | } 124 | if dist != 0 { 125 | t.Errorf("Wrong distance returned, expected 0, got %d", dist) 126 | } 127 | 128 | ntok, dist = bucket.Resolve(strings.ToLower(tok)) 129 | if ntok != tok { 130 | t.Errorf("Lowercase token mismatch, expected %v, got %v", tok, ntok) 131 | } 132 | if dist != 0 { 133 | t.Errorf("Wrong distance returned, expected 0, got %d", dist) 134 | } 135 | } 136 | 137 | func TestTokenFaultyResolve(t *testing.T) { 138 | bucket, _ := NewBucket(8) 139 | 140 | var tok, ttok string 141 | for i := 0; i < 32; i++ { 142 | gtok, err := bucket.NewToken(4) 143 | if err != nil { 144 | t.Errorf("Unexpected error %v", err) 145 | } 146 | if i == 0 { 147 | tok = gtok 148 | ttok = tok 149 | } 150 | } 151 | 152 | // replace char in token 153 | ttok = " " + ttok[1:] 154 | 155 | ntok, dist := bucket.Resolve(ttok) 156 | if ntok != tok { 157 | t.Errorf("Token mismatch, expected %v, got %v", tok, ntok) 158 | } 159 | if dist != 2 { 160 | t.Errorf("Wrong distance returned, expected 2, got %d", dist) 161 | } 162 | 163 | // insert char in token 164 | ttok = tok + " " 165 | 166 | ntok, dist = bucket.Resolve(ttok) 167 | if ntok != tok { 168 | t.Errorf("Token mismatch, expected %v, got %v", tok, ntok) 169 | } 170 | if dist != 1 { 171 | t.Errorf("Wrong distance returned, expected 1, got %d", dist) 172 | } 173 | 174 | // remove char in token 175 | ttok = tok[1:] 176 | 177 | ntok, dist = bucket.Resolve(ttok) 178 | if ntok != tok { 179 | t.Errorf("Token mismatch, expected %v, got %v", tok, ntok) 180 | } 181 | if dist != 1 { 182 | t.Errorf("Wrong distance returned, expected 1, got %d", dist) 183 | } 184 | } 185 | 186 | func BenchmarkCodeGen(b *testing.B) { 187 | bucket, _ := NewBucket(8) 188 | 189 | for i := 0; i < b.N; i++ { 190 | bucket.NewToken(4) 191 | } 192 | } 193 | --------------------------------------------------------------------------------