diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07f9baa..b074220 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.35 + version: v1.43 env: VERBOSE: "true" diff --git a/.golangci.yml b/.golangci.yml index b5356f5..bceefbb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,6 +9,8 @@ linters-settings: govet: check-shadowing: true enable-all: true + disable: + - fieldalignment lll: line-length: 80 tab-width: 4 @@ -20,43 +22,52 @@ linters-settings: linters: disable-all: true enable: + - asciicheck - bodyclose - deadcode - depguard - - dupl + - durationcheck - errcheck + - errorlint + - exhaustive + - exportloopref - funlen - gochecknoinits - goconst - gocritic - gocyclo - - goerr113 + - godot + - gofumpt - goimports - - golint - goprintffuncname - gosec - gosimple - govet + - importas - ineffassign - lll - misspell - nakedret + - nilerr - nlreturn - noctx - nolintlint - - scopelint + - prealloc + - predeclared + - revive + - rowserrcheck - sqlclosecheck - staticcheck - structcheck - typecheck - unconvert + - unparam - unused - varcheck + - wastedassign - whitespace issues: - include: - # - EXC0002 # disable excluding of issues about comments from golint exclude: - Using the variable on range scope `tt` in function literal - Using the variable on range scope `tc` in function literal @@ -71,6 +82,12 @@ issues: - source: "`json:" linters: - lll + - source: "`xml:" + linters: + - lll + - source: "`yaml:" + linters: + - lll run: timeout: 2m diff --git a/Makefile b/Makefile index 1598645..fd74751 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,6 @@ SHELL := env \ # TOOLS += $(TOOLDIR)/gobin -gobin: $(TOOLDIR)/gobin $(TOOLDIR)/gobin: GO111MODULE=off go get -u github.com/myitcv/gobin @@ -43,16 +42,14 @@ $(TOOLDIR)/gobin: define tool # 1: binary-name, 2: go-import-path TOOLS += $(TOOLDIR)/$(1) -.PHONY: $(1) -$(1): $(TOOLDIR)/$(1) - $(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile gobin $(V) "$(2)" endef $(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) -$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports)) -$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35)) +$(eval $(call tool,gofumpt,mvdan.cc/gofumpt)) +$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43)) $(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) .PHONY: tools @@ -62,8 +59,8 @@ tools: $(TOOLS) # Development # -TEST ?= $$(go list ./... | grep -v 'vendor') BENCH ?= . +TESTARGS ?= .PHONY: clean clean: @@ -72,24 +69,24 @@ clean: .PHONY: test test: - go test $(V) -count=1 $(TESTARGS) $(TEST) + go test $(V) -count=1 -race $(TESTARGS) ./... .PHONY: test-deps test-deps: go test all .PHONY: lint -lint: golangci-lint - GOGC=off golangci-lint $(V) run +lint: $(TOOLDIR)/golangci-lint + golangci-lint $(V) run .PHONY: format -format: gofumports - gofumports -w . +format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt + goimports -w . && gofumpt -w . .SILENT: bench .PHONY: bench bench: - go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST) + go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./... # # Coverage @@ -115,16 +112,14 @@ coverage.out: $(SOURCES) .PHONY: deps deps: - $(info Downloading dependencies) go mod download .PHONY: deps-update deps-update: - $(info Downloading dependencies) - go get -u ./... + go get -u -t ./... .PHONY: deps-analyze -deps-analyze: gomod +deps-analyze: $(TOOLDIR)/gomod gomod analyze .PHONY: tidy @@ -160,7 +155,7 @@ check-tidy: # Serve docs .PHONY: docs -docs: godoc +docs: $(TOOLDIR)/godoc $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) @godoc -http=127.0.0.1:6060 @@ -169,5 +164,14 @@ docs: godoc # .PHONY: new-version -new-version: +new-version: check-npx npx standard-version + +.PHONY: next-version +next-version: check-npx + npx standard-version --dry-run + +.PHONY: check-npx +check-npx: + $(if $(shell which npx),,\ + $(error No npx found in PATH, please install NodeJS)) diff --git a/README.md b/README.md index efbb4aa..365760d 100644 --- a/README.md +++ b/README.md @@ -36,41 +36,94 @@ alt="GitHub issues">

-```go -s, _ := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g== -s, _ := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ -s, _ := rands.Hex(16) // => 956e2ec9e7f19ddd58bb935826926531 -s, _ := rands.Alphanumeric(16) // => Fvk1PkrmG5crgOjT -s, _ := rands.Alphabetic(16) // => XEJIzcZufHkuUmRM -s, _ := rands.Upper(16) // => UMAGAFPPNDRGLUPZ -s, _ := rands.UpperNumeric(16) // => DF0CQS0TK9CPUO3E -s, _ := rands.Lower(16) // => ocsmggykzrxzfwgt -s, _ := rands.LowerNumeric(16) // => rwlv7a1p7klqffs5 -s, _ := rands.Numeric(16) // => 9403373143598295 +## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package -s, _ := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb -s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三 +`rands` is intended for use in production code where random data generation is +required. All functions have a error return value which should be checked. -s, _ := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h -s, _ := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785 +For tests there is the `randsmust` package, which has all the same functions but +with single return values, and they panic in the event of an error. -n, _ := rands.Int(2147483647) // => 1334400235 -n, _ := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 - -b, _ := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] -``` - -## Import +### Import ``` import "github.com/jimeh/rands" ``` +### Usage + +```go +s, err := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g== +s, err := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ +s, err := rands.Hex(16) // => 956e2ec9e7f19ddd58bb935826926531 +s, err := rands.Alphanumeric(16) // => Fvk1PkrmG5crgOjT +s, err := rands.Alphabetic(16) // => XEJIzcZufHkuUmRM +s, err := rands.Upper(16) // => UMAGAFPPNDRGLUPZ +s, err := rands.UpperNumeric(16) // => DF0CQS0TK9CPUO3E +s, err := rands.Lower(16) // => ocsmggykzrxzfwgt +s, err := rands.LowerNumeric(16) // => rwlv7a1p7klqffs5 +s, err := rands.Numeric(16) // => 9403373143598295 + +s, err := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb +s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三 + +s, err := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h +s, err := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785 + +n, err := rands.Int(2147483647) // => 1334400235 +n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 + +b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] +``` + +## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) package + +`randsmust` is specifically intended as an alternative to `rands` for use in +tests. All functions return a single value, and panic in the event of an error. +This makes them easy to use when building structs in test cases that need random +data. + +For production code, make sure to use the `rands` package and check returned +errors. + +### Import + +``` +import "github.com/jimeh/rands/randsmust" +``` + +### Usage + +```go +s := randsmust.Base64(16) // => d1wm/wS6AQGduO3uaey1Cg== +s := randsmust.Base64URL(16) // => 4pHWVcddXsL_45vhOfCdng +s := randsmust.Hex(16) // => b5552558bc009264d129c422a666fe56 +s := randsmust.Alphanumeric(16) // => j5WkpNKmW8K701XF +s := randsmust.Alphabetic(16) // => OXxsqfFjNLvmZqDb +s := randsmust.Upper(16) // => AOTLYQRCVNMEPRCX +s := randsmust.UpperNumeric(16) // => 1NTY6KATDVAXBTY2 +s := randsmust.Lower(16) // => xmftrwvurrritqfu +s := randsmust.LowerNumeric(16) // => yszg56fzeql7pjpl +s := randsmust.Numeric(16) // => 0761782105447226 + +s := randsmust.String(16, "abcdefABCDEF") // => dfAbBfaDDdDFDaEa +s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 十十千口三十十下九上千口七夕土口 + +s := randsmust.DNSLabel(16) // => pu31o0gqyk76x35f +s := randsmust.UUID() // => d616c873-f3dd-4690-bcd6-ed307eec1105 + +n := randsmust.Int(2147483647) // => 1293388115 +n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239 + +b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51] +``` + ## Documentation -Please see the -[Go Reference](https://pkg.go.dev/github.com/jimeh/rands#section-documentation) -for documentation and examples. +Please see the Go Reference for documentation and examples: + +- [`rands`](https://pkg.go.dev/github.com/jimeh/rands) +- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) ## Benchmarks @@ -79,4 +132,4 @@ https://jimeh.me/rands/dev/bench/ ## License -[MIT](https://github.com/jimeh/rands/blob/master/LICENSE) +[MIT](https://github.com/jimeh/rands/blob/main/LICENSE) diff --git a/bytes_example_test.go b/bytes_example_test.go index b0b8c90..83434f7 100644 --- a/bytes_example_test.go +++ b/bytes_example_test.go @@ -2,11 +2,16 @@ package rands_test import ( "fmt" + "log" "github.com/jimeh/rands" ) func ExampleBytes() { - b, _ := rands.Bytes(8) - fmt.Printf("%+v\n", b) // => [0 220 137 243 135 204 34 63] + b, err := rands.Bytes(8) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%+v\n", b) // => [181 153 143 235 241 20 208 173] } diff --git a/bytes_test.go b/bytes_test.go index f02769a..8996b8e 100644 --- a/bytes_test.go +++ b/bytes_test.go @@ -7,6 +7,8 @@ import ( ) func TestBytes(t *testing.T) { + t.Parallel() + for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { got, _ := Bytes(tt.n) diff --git a/ints.go b/ints.go index 02925e5..2d2369a 100644 --- a/ints.go +++ b/ints.go @@ -6,12 +6,12 @@ import ( "math/big" ) -var errInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", errBase) +var ErrInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", Err) // Int generates a random int ranging between 0 and max. func Int(max int) (int, error) { if max < 1 { - return 0, errInvalidMaxInt + return 0, ErrInvalidMaxInt } r, err := rand.Int(rand.Reader, big.NewInt(int64(max))) @@ -25,7 +25,7 @@ func Int(max int) (int, error) { // Int64 generates a random int64 ranging between 0 and max. func Int64(max int64) (int64, error) { if max < 1 { - return 0, errInvalidMaxInt + return 0, ErrInvalidMaxInt } r, err := rand.Int(rand.Reader, big.NewInt(max)) diff --git a/ints_example_test.go b/ints_example_test.go index 78b43dd..f6faf67 100644 --- a/ints_example_test.go +++ b/ints_example_test.go @@ -2,16 +2,25 @@ package rands_test import ( "fmt" + "log" "github.com/jimeh/rands" ) func ExampleInt() { - n, _ := rands.Int(2147483647) - fmt.Printf("%d\n", n) // => 1334400235 + n, err := rands.Int(2147483647) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%d\n", n) // => 1908357440 } func ExampleInt64() { - n, _ := rands.Int64(int64(9223372036854775807)) - fmt.Printf("%d\n", n) // => 8256935979116161233 + n, err := rands.Int64(int64(9223372036854775807)) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%d\n", n) // => 6530460062499341591 } diff --git a/ints_test.go b/ints_test.go index 25d9f52..295b3f5 100644 --- a/ints_test.go +++ b/ints_test.go @@ -1,7 +1,6 @@ package rands import ( - "errors" "testing" "github.com/stretchr/testify/assert" @@ -16,67 +15,67 @@ var testIntCases = []struct { { name: "n=-2394345", max: -2394345, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-409600", max: -409600, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-1024", max: -1024, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-128", max: -128, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-32", max: -32, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-16", max: -16, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-8", max: -8, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-7", max: -7, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-2", max: -2, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=-1", max: -1, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, { name: "n=0", max: 0, - errIs: errInvalidMaxInt, + errIs: ErrInvalidMaxInt, errStr: "rands: max cannot be less than 1", }, {name: "n=1", max: 1}, @@ -92,6 +91,8 @@ var testIntCases = []struct { } func TestInt(t *testing.T) { + t.Parallel() + for _, tt := range testIntCases { t.Run(tt.name, func(t *testing.T) { got, err := Int(tt.max) @@ -102,7 +103,7 @@ func TestInt(t *testing.T) { } if tt.errIs != nil { - assert.True(t, errors.Is(err, errInvalidMaxInt)) + assert.ErrorIs(t, err, tt.errIs) } if tt.errStr != "" { @@ -123,6 +124,8 @@ func BenchmarkInt(b *testing.B) { } func TestInt64(t *testing.T) { + t.Parallel() + for _, tt := range testIntCases { t.Run(tt.name, func(t *testing.T) { got, err := Int64(int64(tt.max)) @@ -133,7 +136,7 @@ func TestInt64(t *testing.T) { } if tt.errIs != nil { - assert.True(t, errors.Is(err, errInvalidMaxInt)) + assert.ErrorIs(t, err, tt.errIs) } if tt.errStr != "" { diff --git a/rands.go b/rands.go index 882a2fa..a21dd3e 100644 --- a/rands.go +++ b/rands.go @@ -4,8 +4,15 @@ // // All functions which produce strings from a alphabet of characters uses // rand.Int() to ensure a uniform distribution of all possible values. +// +// rands is intended for use in production code where random data generation is +// required. All functions have a error return value, which should be +// checked. +// +// For tests there is the randsmust package, which has all the same functions +// but with single return values, and they panic in the event of an error. package rands import "errors" -var errBase = errors.New("rands") +var Err = errors.New("rands") diff --git a/randsmust/bytes.go b/randsmust/bytes.go new file mode 100644 index 0000000..cfe163c --- /dev/null +++ b/randsmust/bytes.go @@ -0,0 +1,13 @@ +package randsmust + +import "github.com/jimeh/rands" + +// Bytes generates a byte slice of n number of random bytes. +func Bytes(n int) []byte { + r, err := rands.Bytes(n) + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/bytes_example_test.go b/randsmust/bytes_example_test.go new file mode 100644 index 0000000..edba62d --- /dev/null +++ b/randsmust/bytes_example_test.go @@ -0,0 +1,12 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleBytes() { + b := randsmust.Bytes(8) + fmt.Printf("%+v\n", b) // => [6 99 106 54 163 188 28 152] +} diff --git a/randsmust/bytes_test.go b/randsmust/bytes_test.go new file mode 100644 index 0000000..983ca56 --- /dev/null +++ b/randsmust/bytes_test.go @@ -0,0 +1,19 @@ +package randsmust + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBytes(t *testing.T) { + t.Parallel() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Bytes(tt.n) + + assert.Len(t, got, tt.n) + }) + } +} diff --git a/randsmust/ints.go b/randsmust/ints.go new file mode 100644 index 0000000..83ffcac --- /dev/null +++ b/randsmust/ints.go @@ -0,0 +1,23 @@ +package randsmust + +import "github.com/jimeh/rands" + +// Int generates a random int ranging between 0 and max. +func Int(max int) int { + r, err := rands.Int(max) + if err != nil { + panic(err) + } + + return r +} + +// Int64 generates a random int64 ranging between 0 and max. +func Int64(max int64) int64 { + r, err := rands.Int64(max) + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/ints_example_test.go b/randsmust/ints_example_test.go new file mode 100644 index 0000000..6f7b572 --- /dev/null +++ b/randsmust/ints_example_test.go @@ -0,0 +1,17 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleInt() { + n := randsmust.Int(2147483647) + fmt.Printf("%d\n", n) // => 1616989970 +} + +func ExampleInt64() { + n := randsmust.Int64(int64(9223372036854775807)) + fmt.Printf("%d\n", n) // => 1599573251306894157 +} diff --git a/randsmust/ints_test.go b/randsmust/ints_test.go new file mode 100644 index 0000000..ecc410a --- /dev/null +++ b/randsmust/ints_test.go @@ -0,0 +1,144 @@ +package randsmust + +import ( + "testing" + + "github.com/jimeh/rands" + "github.com/stretchr/testify/assert" +) + +var testIntCases = []struct { + name string + max int + panicErrIs error + panicStr string +}{ + { + name: "n=-2394345", + max: -2394345, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-409600", + max: -409600, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-1024", + max: -1024, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-128", + max: -128, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-32", + max: -32, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-16", + max: -16, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-8", + max: -8, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-7", + max: -7, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-2", + max: -2, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-1", + max: -1, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=0", + max: 0, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + {name: "n=1", max: 1}, + {name: "n=2", max: 2}, + {name: "n=7", max: 7}, + {name: "n=8", max: 8}, + {name: "n=16", max: 16}, + {name: "n=32", max: 32}, + {name: "n=128", max: 128}, + {name: "n=1024", max: 1024}, + {name: "n=409600", max: 409600}, + {name: "n=2394345", max: 2394345}, +} + +func TestInt(t *testing.T) { + t.Parallel() + + for _, tt := range testIntCases { + t.Run(tt.name, func(t *testing.T) { + var got int + p := recoverPanic(func() { + got = Int(tt.max) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.GreaterOrEqual(t, got, 0) + assert.LessOrEqual(t, got, tt.max) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} + +func TestInt64(t *testing.T) { + t.Parallel() + + for _, tt := range testIntCases { + t.Run(tt.name, func(t *testing.T) { + var got int64 + p := recoverPanic(func() { + got = Int64(int64(tt.max)) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.GreaterOrEqual(t, got, int64(0)) + assert.LessOrEqual(t, got, int64(tt.max)) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} diff --git a/randsmust/randsmust.go b/randsmust/randsmust.go new file mode 100644 index 0000000..66bb288 --- /dev/null +++ b/randsmust/randsmust.go @@ -0,0 +1,15 @@ +// Package randsmust provides a suite of functions that use crypto/rand to +// generate cryptographically secure random strings in various formats, as well +// as ints and bytes. +// +// All functions which produce strings from a alphabet of characters uses +// rand.Int() to ensure a uniform distribution of all possible values. +// +// randsmust is specifically intended as an alternative to rands for use in +// tests. All functions return a single value, and panic in the event of an +// error. This makes them easy to use when building structs in test cases that +// need random data. +// +// For production code, make sure to use the rands package and check returned +// errors. +package randsmust diff --git a/randsmust/randsmust_test.go b/randsmust/randsmust_test.go new file mode 100644 index 0000000..e486934 --- /dev/null +++ b/randsmust/randsmust_test.go @@ -0,0 +1,30 @@ +package randsmust + +var testCases = []struct { + name string + n int +}{ + {name: "n=0", n: 0}, + {name: "n=1", n: 1}, + {name: "n=2", n: 2}, + {name: "n=7", n: 7}, + {name: "n=8", n: 8}, + {name: "n=16", n: 16}, + {name: "n=32", n: 32}, + {name: "n=128", n: 128}, + {name: "n=1024", n: 1024}, + {name: "n=409600", n: 409600}, + {name: "n=2394345", n: 2394345}, +} + +func recoverPanic(f func()) (p interface{}) { + defer func() { + if r := recover(); r != nil { + p = r + } + }() + + f() + + return +} diff --git a/randsmust/strings.go b/randsmust/strings.go new file mode 100644 index 0000000..9ba687f --- /dev/null +++ b/randsmust/strings.go @@ -0,0 +1,192 @@ +package randsmust + +import ( + "github.com/jimeh/rands" +) + +// Base64 generates a random base64 encoded string of n number of bytes. +// +// Length of the returned string is about one third greater than the value of n, +// and it may contain characters A-Z, a-z, 0-9, "+", "/", and "=". +func Base64(n int) string { + r, err := rands.Base64(n) + if err != nil { + panic(err) + } + + return r +} + +// Base64URL generates a URL-safe un-padded random base64 encoded string of n +// number of bytes. +// +// Length of the returned string is about one third greater than the value of n, +// and it may contain characters A-Z, a-z, 0-9, "-", and "_". +func Base64URL(n int) string { + r, err := rands.Base64URL(n) + if err != nil { + panic(err) + } + + return r +} + +// Hex generates a random hexadecimal encoded string of n number of bytes. +// +// Length of the returned string is twice the value of n, and it may contain +// characters 0-9 and a-f. +func Hex(n int) string { + r, err := rands.Hex(n) + if err != nil { + panic(err) + } + + return r +} + +// Alphanumeric generates a random alphanumeric string of n length. +// +// The returned string may contain A-Z, a-z, and 0-9. +func Alphanumeric(n int) string { + r, err := rands.Alphanumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Alphabetic generates a random alphabetic string of n length. +// +// The returned string may contain A-Z, and a-z. +func Alphabetic(n int) string { + r, err := rands.Alphabetic(n) + if err != nil { + panic(err) + } + + return r +} + +// Numeric generates a random numeric string of n length. +// +// The returned string may contain 0-9. +func Numeric(n int) string { + r, err := rands.Numeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Upper generates a random uppercase alphabetic string of n length. +// +// The returned string may contain A-Z. +func Upper(n int) string { + r, err := rands.Upper(n) + if err != nil { + panic(err) + } + + return r +} + +// UpperNumeric generates a random uppercase alphanumeric string of n length. +// +// The returned string may contain A-Z and 0-9. +func UpperNumeric(n int) string { + r, err := rands.UpperNumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Lower generates a random lowercase alphabetic string of n length. +// +// The returned string may contain a-z. +func Lower(n int) string { + r, err := rands.Lower(n) + if err != nil { + panic(err) + } + + return r +} + +// LowerNumeric generates a random lowercase alphanumeric string of n length. +// +// The returned string may contain A-Z and 0-9. +func LowerNumeric(n int) string { + r, err := rands.LowerNumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// String generates a random string of n length using the given ASCII alphabet. +// +// The specified alphabet determines what characters are used in the returned +// random string. The alphabet can only contain ASCII characters, use +// UnicodeString() if you need a alphabet with Unicode characters. +func String(n int, alphabet string) string { + r, err := rands.String(n, alphabet) + if err != nil { + panic(err) + } + + return r +} + +// UnicodeString generates a random string of n length using the given Unicode +// alphabet. +// +// The specified alphabet determines what characters are used in the returned +// random string. The length of the returned string will be n or greater +// depending on the byte-length of characters which were randomly selected from +// the alphabet. +func UnicodeString(n int, alphabet []rune) string { + r, err := rands.UnicodeString(n, alphabet) + if err != nil { + panic(err) + } + + return r +} + +// DNSLabel returns a random string of n length in a DNS label compliant format +// as defined in RFC 1035, section 2.3.1. +// +// It also adheres to RFC 5891, section 4.2.3.1. +// +// In summary, the generated random string will: +// +// - be between 1 and 63 characters in length, other n values returns a error +// - first character will be one of a-z +// - last character will be one of a-z or 0-9 +// - in-between first and last characters consist of a-z, 0-9, or "-" +// - potentially contain two or more consecutive "-", except the 3rd and 4th +// characters, as that would violate RFC 5891, section 4.2.3.1. +func DNSLabel(n int) string { + r, err := rands.DNSLabel(n) + if err != nil { + panic(err) + } + + return r +} + +// UUID returns a random UUID v4 in string format as defined by RFC 4122, +// section 4.4. +func UUID() string { + r, err := rands.UUID() + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/strings_example_test.go b/randsmust/strings_example_test.go new file mode 100644 index 0000000..01e78fd --- /dev/null +++ b/randsmust/strings_example_test.go @@ -0,0 +1,77 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleBase64() { + s := randsmust.Base64(16) + fmt.Println(s) // => rGnZOxJunCd5h+piBpOfDA== +} + +func ExampleBase64URL() { + s := randsmust.Base64URL(16) + fmt.Println(s) // => NlXKmutou2knLU8q7Hlp5Q +} + +func ExampleHex() { + s := randsmust.Hex(16) + fmt.Println(s) // => 1013ec67a802be177d3e37f46951e97f +} + +func ExampleAlphanumeric() { + s := randsmust.Alphanumeric(16) + fmt.Println(s) // => mjT119HdPslVfvUE +} + +func ExampleAlphabetic() { + s := randsmust.Alphabetic(16) + fmt.Println(s) // => RLaRaTVqcrxvNkiz +} + +func ExampleUpper() { + s := randsmust.Upper(16) + fmt.Println(s) // => CANJDLMHANPQNXUE +} + +func ExampleUpperNumeric() { + s := randsmust.UpperNumeric(16) + fmt.Println(s) // => EERZHC96KOIRU9DM +} + +func ExampleLower() { + s := randsmust.Lower(16) + fmt.Println(s) // => aoybqdwigyezucjy +} + +func ExampleLowerNumeric() { + s := randsmust.LowerNumeric(16) + fmt.Println(s) // => hs8l2l0750med3g2 +} + +func ExampleNumeric() { + s := randsmust.Numeric(16) + fmt.Println(s) // => 3126402104379869 +} + +func ExampleString() { + s := randsmust.String(16, "abcdefABCDEF") + fmt.Println(s) // => cbdCAbABECaADcaB +} + +func ExampleUnicodeString() { + s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) + fmt.Println(s) // => 下夕七下千千力入八三力夕千三土七 +} + +func ExampleDNSLabel() { + s := randsmust.DNSLabel(16) + fmt.Println(s) // => urqkt-remuwz5083 +} + +func ExampleUUID() { + s := randsmust.UUID() + fmt.Println(s) // => 5baa35a6-9a46-49b4-91d0-9530173e118d +} diff --git a/randsmust/strings_test.go b/randsmust/strings_test.go new file mode 100644 index 0000000..cebaafe --- /dev/null +++ b/randsmust/strings_test.go @@ -0,0 +1,558 @@ +package randsmust + +import ( + "encoding/base64" + "encoding/hex" + "regexp" + "strings" + "testing" + + "github.com/jimeh/rands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHex(t *testing.T) { + t.Parallel() + + allowed := "0123456789abcdef" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Hex(tt.n) + + assert.Len(t, got, tt.n*2) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestBase64(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789+/=" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Base64(tt.n) + + b, err := base64.StdEncoding.DecodeString(got) + require.NoError(t, err) + + assert.Len(t, b, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestBase64URL(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789-_" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Base64URL(tt.n) + + b, err := base64.RawURLEncoding.DecodeString(got) + require.NoError(t, err) + + assert.Len(t, b, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestAlphanumeric(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Alphanumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestAlphabetic(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Alphabetic(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestNumeric(t *testing.T) { + t.Parallel() + + allowed := "0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Numeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestUpper(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Upper(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestUpperNumeric(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := UpperNumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestLower(t *testing.T) { + t.Parallel() + + allowed := "abcdefghijklmnopqrstuvwxyz" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Lower(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestLowerNumeric(t *testing.T) { + t.Parallel() + + allowed := "abcdefghijklmnopqrstuvwxyz0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := LowerNumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +var stringTestCases = []struct { + name string + n int + alphabet string + panicErrIs error + panicStr string +}{ + { + name: "greek", + n: 32, + alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "chinese", + n: 32, + alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + + "时道出而要于就下得可你年生", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "japanese", + n: 32, + alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + + "手文日月木水火犬王正出本右四", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "n=0", + n: 0, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=1", + n: 1, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=2", + n: 2, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=7", + n: 7, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=8", + n: 8, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=16", + n: 16, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=32", + n: 32, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=128", + n: 128, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=1024", + n: 1024, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=409600", + n: 409600, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=2394345", + n: 2394345, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "uppercase", + n: 16, + alphabet: "ABCDEFGHJKMNPRSTUVWXYZ", + }, + { + name: "lowercase", + n: 16, + alphabet: "abcdefghjkmnprstuvwxyz", + }, +} + +func TestString(t *testing.T) { + t.Parallel() + + for _, tt := range stringTestCases { + t.Run(tt.name, func(t *testing.T) { + var got string + p := recoverPanic(func() { + got = String(tt.n, tt.alphabet) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.Len(t, []rune(got), tt.n) + assertAllowedChars(t, tt.alphabet, got) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} + +var unicodeStringTestCases = []struct { + name string + n int + alphabet string +}{ + { + name: "n=0", + n: 0, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=1", + n: 1, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=2", + n: 2, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=7", + n: 7, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=8", + n: 8, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=16", + n: 16, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=32", + n: 32, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=128", + n: 128, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=1024", + n: 1024, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=409600", + n: 409600, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=2394345", + n: 2394345, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "latin", + n: 32, + alphabet: "ABCDEFGHJKMNPRSTUVWXYZabcdefghjkmnprstuvwxyz", + }, + { + name: "greek", + n: 32, + alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", + }, + { + name: "chinese", + n: 32, + alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + + "时道出而要于就下得可你年生", + }, + { + name: "japanese", + n: 32, + alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + + "手文日月木水火犬王正出本右四", + }, +} + +func TestUnicodeString(t *testing.T) { + t.Parallel() + + for _, tt := range unicodeStringTestCases { + t.Run(tt.name, func(t *testing.T) { + got := UnicodeString(tt.n, []rune(tt.alphabet)) + + assert.Len(t, []rune(got), tt.n) + assertAllowedChars(t, tt.alphabet, got) + }) + } +} + +var dnsLabelTestCases = []struct { + name string + n int + panicErrIs error + panicStr string +}{ + { + name: "n=-128", + n: -128, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + { + name: "n=0", + n: 0, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + {name: "n=1", n: 1}, + {name: "n=2", n: 2}, + {name: "n=3", n: 3}, + {name: "n=4", n: 4}, + {name: "n=5", n: 5}, + {name: "n=6", n: 6}, + {name: "n=7", n: 7}, + {name: "n=8", n: 8}, + {name: "n=16", n: 16}, + {name: "n=32", n: 32}, + {name: "n=63", n: 63}, + { + name: "n=64", + n: 64, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + { + name: "n=128", + n: 128, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, +} + +func TestDNSLabel(t *testing.T) { + t.Parallel() + + for _, tt := range dnsLabelTestCases { + t.Run(tt.name, func(t *testing.T) { + // generate lots of labels to increase the chances of catching any + // obscure bugs + for i := 0; i < 10000; i++ { + var got string + p := recoverPanic(func() { + got = DNSLabel(tt.n) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + require.Len(t, got, tt.n) + asserDNSLabel(t, got) + } + + if tt.panicErrIs != nil { + require.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + require.EqualError(t, p.(error), tt.panicStr) + } + } + }) + } +} + +func TestUUID(t *testing.T) { + t.Parallel() + + m := regexp.MustCompile( + `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`, + ) + + for i := 0; i < 10000; i++ { + got := UUID() + require.Regexp(t, m, got) + + raw := strings.ReplaceAll(got, "-", "") + b := make([]byte, 16) + _, err := hex.Decode(b, []byte(raw)) + require.NoError(t, err) + + require.Equal(t, 4, int(b[6]>>4), "version is not 4") + require.Equal(t, byte(0x80), b[8]&0xc0, + "variant is not RFC 4122", + ) + } +} + +// +// Helpers +// + +var ( + dnsLabelHeadRx = regexp.MustCompile(`^[a-z]$`) + dnsLabelBodyRx = regexp.MustCompile(`^[a-z0-9-]+$`) + dnsLabelTailRx = regexp.MustCompile(`^[a-z0-9]$`) +) + +func asserDNSLabel(t *testing.T, label string) { + require.LessOrEqualf(t, len(label), 63, + `DNS label "%s" is longer than 63 characters`, label, + ) + + require.GreaterOrEqualf(t, len(label), 1, + `DNS label "%s" is shorter than 1 character`, label, + ) + + if len(label) >= 1 { + require.Regexpf(t, dnsLabelHeadRx, string(label[0]), + `DNS label "%s" must start with a-z`, label, + ) + } + if len(label) >= 2 { + require.Regexpf(t, dnsLabelTailRx, string(label[len(label)-1]), + `DNS label "%s" must end with a-z0-9`, label, + ) + } + if len(label) >= 3 { + require.Regexpf(t, dnsLabelBodyRx, label[1:len(label)-1], + `DNS label "%s" body must only contain a-z0-9-`, label) + } + if len(label) >= 4 { + require.NotEqualf(t, "--", label[2:4], + `DNS label "%s" cannot contain "--" as 3rd and 4th char`, label, + ) + } +} + +func assertAllowedChars(t *testing.T, allowed string, s string) { + invalid := "" + for _, c := range s { + if !strings.Contains(allowed, string(c)) && + !strings.Contains(invalid, string(c)) { + invalid += string(c) + } + } + + assert.Truef( + t, len(invalid) == 0, "string contains invalid chars: %s", invalid, + ) +} diff --git a/strings.go b/strings.go index f3152d3..96f5af2 100644 --- a/strings.go +++ b/strings.go @@ -22,12 +22,12 @@ const ( ) var ( - errNonASCIIAlphabet = fmt.Errorf( - "%w: alphabet contains non-ASCII characters", errBase, + ErrNonASCIIAlphabet = fmt.Errorf( + "%w: alphabet contains non-ASCII characters", Err, ) - errDNSLabelLength = fmt.Errorf( - "%w: DNS labels must be between 1 and 63 characters in length", errBase, + ErrDNSLabelLength = fmt.Errorf( + "%w: DNS labels must be between 1 and 63 characters in length", Err, ) ) @@ -127,7 +127,7 @@ func LowerNumeric(n int) (string, error) { // UnicodeString() if you need a alphabet with Unicode characters. func String(n int, alphabet string) (string, error) { if !isASCII(alphabet) { - return "", errNonASCIIAlphabet + return "", ErrNonASCIIAlphabet } l := big.NewInt(int64(len(alphabet))) @@ -180,7 +180,7 @@ func UnicodeString(n int, alphabet []rune) (string, error) { func DNSLabel(n int) (string, error) { switch { case n < 1 || n > 63: - return "", errDNSLabelLength + return "", ErrDNSLabelLength case n == 1: return String(1, lowerChars) default: diff --git a/strings_example_test.go b/strings_example_test.go index e59a97a..110186f 100644 --- a/strings_example_test.go +++ b/strings_example_test.go @@ -2,76 +2,133 @@ package rands_test import ( "fmt" + "log" "github.com/jimeh/rands" ) func ExampleBase64() { - s, _ := rands.Base64(16) - fmt.Println(s) // => CYxqEdUB1Rzno3SyZu2g/g== + s, err := rands.Base64(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => nYQLhIYTqh8oH/W4hZuXMQ== } func ExampleBase64URL() { - s, _ := rands.Base64URL(16) - fmt.Println(s) // => zlqw9aFqcFggbk2asn3_aQ + s, err := rands.Base64URL(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => zI_zrc1l0uPT4MxncR6e5w } func ExampleHex() { - s, _ := rands.Hex(16) - fmt.Println(s) // => 956e2ec9e7f19ddd58bb935826926531 + s, err := rands.Hex(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => b59e8977a13f3c030bd2ea1002ec8081 } func ExampleAlphanumeric() { - s, _ := rands.Alphanumeric(16) - fmt.Println(s) // => Fvk1PkrmG5crgOjT + s, err := rands.Alphanumeric(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => EgPieCBO7MuWhHtj } func ExampleAlphabetic() { - s, _ := rands.Alphabetic(16) - fmt.Println(s) // => XEJIzcZufHkuUmRM + s, err := rands.Alphabetic(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => VzcovEqvMRBWUtQC } func ExampleUpper() { - s, _ := rands.Upper(16) - fmt.Println(s) // => UMAGAFPPNDRGLUPZ + s, err := rands.Upper(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => MCZEGPWGYKNUEDCK } func ExampleUpperNumeric() { - s, _ := rands.UpperNumeric(16) - fmt.Println(s) // => DF0CQS0TK9CPUO3E + s, err := rands.UpperNumeric(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => 6LLPBBUW77B26X2X } func ExampleLower() { - s, _ := rands.Lower(16) - fmt.Println(s) // => ocsmggykzrxzfwgt + s, err := rands.Lower(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => dhoqhrqljadsztaa } func ExampleLowerNumeric() { - s, _ := rands.LowerNumeric(16) - fmt.Println(s) // => rwlv7a1p7klqffs5 + s, err := rands.LowerNumeric(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => th1z1b1d24l5h8pu } func ExampleNumeric() { - s, _ := rands.Numeric(16) - fmt.Println(s) // => 9403373143598295 + s, err := rands.Numeric(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => 3378802228987741 } func ExampleString() { - s, _ := rands.String(16, "abcdefABCDEF") - fmt.Println(s) // => adCDCaDEdeffeDeb + s, err := rands.String(16, "abcdefABCDEF") + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => BAFffADaadeeacfa } func ExampleUnicodeString() { - s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) - fmt.Println(s) // => 下下口九力下土夕下土八上二夕大三 + s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => 八三口上土土七入力夕人力下三上力 } func ExampleDNSLabel() { - s, _ := rands.DNSLabel(16) - fmt.Println(s) // => z0ij9o8qkbs0ru-h + s, err := rands.DNSLabel(16) + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => ab-sbh5q0gfb6sqo } func ExampleUUID() { - s, _ := rands.UUID() - fmt.Println(s) // => a62b8712-f238-43ba-a47e-333f5fffe785 + s, err := rands.UUID() + if err != nil { + log.Fatal(err) + } + + fmt.Println(s) // => 6a1c4f65-d5d6-4a28-aa51-eaa94fa7ad4a } diff --git a/strings_test.go b/strings_test.go index 1df898e..64651dd 100644 --- a/strings_test.go +++ b/strings_test.go @@ -3,7 +3,6 @@ package rands import ( "encoding/base64" "encoding/hex" - "errors" "regexp" "strings" "testing" @@ -13,6 +12,8 @@ import ( ) func TestHex(t *testing.T) { + t.Parallel() + allowed := "0123456789abcdef" for _, tt := range testCases { @@ -36,6 +37,8 @@ func BenchmarkHex(b *testing.B) { } func TestBase64(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/=" @@ -63,6 +66,8 @@ func BenchmarkBase64(b *testing.B) { } func TestBase64URL(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-_" @@ -90,6 +95,8 @@ func BenchmarkBase64URL(b *testing.B) { } func TestAlphanumeric(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for _, tt := range testCases { @@ -113,6 +120,8 @@ func BenchmarkAlphanumeric(b *testing.B) { } func TestAlphabetic(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" for _, tt := range testCases { @@ -136,6 +145,8 @@ func BenchmarkAlphabetic(b *testing.B) { } func TestNumeric(t *testing.T) { + t.Parallel() + allowed := "0123456789" for _, tt := range testCases { @@ -159,6 +170,8 @@ func BenchmarkNumeric(b *testing.B) { } func TestUpper(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for _, tt := range testCases { @@ -182,6 +195,8 @@ func BenchmarkUpper(b *testing.B) { } func TestUpperNumeric(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" for _, tt := range testCases { @@ -205,6 +220,8 @@ func BenchmarkUpperNumeric(b *testing.B) { } func TestLower(t *testing.T) { + t.Parallel() + allowed := "abcdefghijklmnopqrstuvwxyz" for _, tt := range testCases { @@ -228,6 +245,8 @@ func BenchmarkLower(b *testing.B) { } func TestLowerNumeric(t *testing.T) { + t.Parallel() + allowed := "abcdefghijklmnopqrstuvwxyz0123456789" for _, tt := range testCases { @@ -262,7 +281,7 @@ var stringTestCases = []struct { n: 32, alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -270,7 +289,7 @@ var stringTestCases = []struct { n: 32, alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + "时道出而要于就下得可你年生", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -278,7 +297,7 @@ var stringTestCases = []struct { n: 32, alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + "手文日月木水火犬王正出本右四", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -349,6 +368,8 @@ var stringTestCases = []struct { } func TestString(t *testing.T) { + t.Parallel() + for _, tt := range stringTestCases { t.Run(tt.name, func(t *testing.T) { got, err := String(tt.n, tt.alphabet) @@ -359,7 +380,7 @@ func TestString(t *testing.T) { } if tt.errIs != nil { - assert.True(t, errors.Is(err, errNonASCIIAlphabet)) + assert.ErrorIs(t, err, tt.errIs) } if tt.errStr != "" { @@ -476,6 +497,8 @@ var unicodeStringTestCases = []struct { } func TestUnicodeString(t *testing.T) { + t.Parallel() + for _, tt := range unicodeStringTestCases { t.Run(tt.name, func(t *testing.T) { got, _ := UnicodeString(tt.n, []rune(tt.alphabet)) @@ -507,14 +530,14 @@ var dnsLabelTestCases = []struct { { name: "n=-128", n: -128, - errIs: errDNSLabelLength, + errIs: ErrDNSLabelLength, errStr: "rands: DNS labels must be between 1 and 63 characters " + "in length", }, { name: "n=0", n: 0, - errIs: errDNSLabelLength, + errIs: ErrDNSLabelLength, errStr: "rands: DNS labels must be between 1 and 63 characters " + "in length", }, @@ -532,20 +555,22 @@ var dnsLabelTestCases = []struct { { name: "n=64", n: 64, - errIs: errDNSLabelLength, + errIs: ErrDNSLabelLength, errStr: "rands: DNS labels must be between 1 and 63 characters " + "in length", }, { name: "n=128", n: 128, - errIs: errDNSLabelLength, + errIs: ErrDNSLabelLength, errStr: "rands: DNS labels must be between 1 and 63 characters " + "in length", }, } func TestDNSLabel(t *testing.T) { + t.Parallel() + for _, tt := range dnsLabelTestCases { t.Run(tt.name, func(t *testing.T) { // generate lots of labels to increase the chances of catching any @@ -559,7 +584,7 @@ func TestDNSLabel(t *testing.T) { } if tt.errIs != nil { - require.True(t, errors.Is(err, errDNSLabelLength)) + require.ErrorIs(t, err, tt.errIs) } if tt.errStr != "" { @@ -581,6 +606,8 @@ func BenchmarkDNSLabel(b *testing.B) { } func TestUUID(t *testing.T) { + t.Parallel() + m := regexp.MustCompile( `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`, )