From 164ccc497ad9880d43b22b74d6a83c5e68d79334 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Thu, 16 Dec 2021 20:15:17 +0000 Subject: [PATCH 1/6] feat(error): export error variables --- ints.go | 6 +++--- ints_test.go | 27 +++++++++++++-------------- rands.go | 2 +- strings.go | 12 ++++++------ strings_test.go | 19 +++++++++---------- 5 files changed, 32 insertions(+), 34 deletions(-) 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_test.go b/ints_test.go index 25d9f52..6db37c2 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}, @@ -102,7 +101,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 != "" { @@ -133,7 +132,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..514a4b9 100644 --- a/rands.go +++ b/rands.go @@ -8,4 +8,4 @@ package rands import "errors" -var errBase = errors.New("rands") +var Err = errors.New("rands") 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_test.go b/strings_test.go index 1df898e..22ca63e 100644 --- a/strings_test.go +++ b/strings_test.go @@ -3,7 +3,6 @@ package rands import ( "encoding/base64" "encoding/hex" - "errors" "regexp" "strings" "testing" @@ -262,7 +261,7 @@ var stringTestCases = []struct { n: 32, alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -270,7 +269,7 @@ var stringTestCases = []struct { n: 32, alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + "时道出而要于就下得可你年生", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -278,7 +277,7 @@ var stringTestCases = []struct { n: 32, alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + "手文日月木水火犬王正出本右四", - errIs: errNonASCIIAlphabet, + errIs: ErrNonASCIIAlphabet, errStr: "rands: alphabet contains non-ASCII characters", }, { @@ -359,7 +358,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 != "" { @@ -507,14 +506,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,14 +531,14 @@ 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", }, @@ -559,7 +558,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 != "" { From b59d42132298e313a951a26d3f8d7333557d332a Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Thu, 16 Dec 2021 20:16:03 +0000 Subject: [PATCH 2/6] chore(test): enable parallel test execution Seems to cut overall total test time down to one third the time. --- bytes_test.go | 2 ++ ints_test.go | 4 ++++ strings_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) 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_test.go b/ints_test.go index 6db37c2..295b3f5 100644 --- a/ints_test.go +++ b/ints_test.go @@ -91,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) @@ -122,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)) diff --git a/strings_test.go b/strings_test.go index 22ca63e..64651dd 100644 --- a/strings_test.go +++ b/strings_test.go @@ -12,6 +12,8 @@ import ( ) func TestHex(t *testing.T) { + t.Parallel() + allowed := "0123456789abcdef" for _, tt := range testCases { @@ -35,6 +37,8 @@ func BenchmarkHex(b *testing.B) { } func TestBase64(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/=" @@ -62,6 +66,8 @@ func BenchmarkBase64(b *testing.B) { } func TestBase64URL(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-_" @@ -89,6 +95,8 @@ func BenchmarkBase64URL(b *testing.B) { } func TestAlphanumeric(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for _, tt := range testCases { @@ -112,6 +120,8 @@ func BenchmarkAlphanumeric(b *testing.B) { } func TestAlphabetic(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" for _, tt := range testCases { @@ -135,6 +145,8 @@ func BenchmarkAlphabetic(b *testing.B) { } func TestNumeric(t *testing.T) { + t.Parallel() + allowed := "0123456789" for _, tt := range testCases { @@ -158,6 +170,8 @@ func BenchmarkNumeric(b *testing.B) { } func TestUpper(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for _, tt := range testCases { @@ -181,6 +195,8 @@ func BenchmarkUpper(b *testing.B) { } func TestUpperNumeric(t *testing.T) { + t.Parallel() + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" for _, tt := range testCases { @@ -204,6 +220,8 @@ func BenchmarkUpperNumeric(b *testing.B) { } func TestLower(t *testing.T) { + t.Parallel() + allowed := "abcdefghijklmnopqrstuvwxyz" for _, tt := range testCases { @@ -227,6 +245,8 @@ func BenchmarkLower(b *testing.B) { } func TestLowerNumeric(t *testing.T) { + t.Parallel() + allowed := "abcdefghijklmnopqrstuvwxyz0123456789" for _, tt := range testCases { @@ -348,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) @@ -475,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)) @@ -545,6 +569,8 @@ var dnsLabelTestCases = []struct { } 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 @@ -580,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}$`, ) From 0a42d1e112d7e2c642485daf45d75d2286c67047 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Dec 2021 00:10:50 +0000 Subject: [PATCH 3/6] chore(deps): update golangci-lint to 1.43 --- .github/workflows/ci.yml | 2 +- .golangci.yml | 29 +++++++++++++++++++++++------ Makefile | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) 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..5ffa451 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ 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,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43)) $(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) .PHONY: tools From a86282e34d084df7bd5acc476a2d390e9b4025b8 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Dec 2021 00:17:40 +0000 Subject: [PATCH 4/6] chore(makefile): various minor updates and tweaks --- Makefile | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 5ffa451..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,15 +42,13 @@ $(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,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)) @@ -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)) From 74dd8fb7e99982a0efbc4e5a53ef676aa7a1b1f4 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Dec 2021 01:25:25 +0000 Subject: [PATCH 5/6] docs(examples): add error handling to examples --- README.md | 34 ++++++------ bytes_example_test.go | 9 +++- ints_example_test.go | 17 ++++-- rands.go | 4 ++ strings_example_test.go | 113 ++++++++++++++++++++++++++++++---------- 5 files changed, 126 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index efbb4aa..d2f4726 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,27 @@ 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 +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, _ := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb -s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三 +s, err := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb +s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三 -s, _ := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h -s, _ := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785 +s, err := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h +s, err := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785 -n, _ := rands.Int(2147483647) // => 1334400235 -n, _ := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 +n, err := rands.Int(2147483647) // => 1334400235 +n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 -b, _ := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] +b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] ``` ## Import 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/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/rands.go b/rands.go index 514a4b9..beb394e 100644 --- a/rands.go +++ b/rands.go @@ -4,6 +4,10 @@ // // 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. package rands import "errors" 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 } From 22fe517baa8b6939503c0c804dd71628f7d473a3 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Dec 2021 01:52:07 +0000 Subject: [PATCH 6/6] feat(randsmust): add randsmust 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. Internally the package simply calls the equivalent function from the rands package, and panics if a error is returned. --- README.md | 65 +++- rands.go | 3 + randsmust/bytes.go | 13 + randsmust/bytes_example_test.go | 12 + randsmust/bytes_test.go | 19 + randsmust/ints.go | 23 ++ randsmust/ints_example_test.go | 17 + randsmust/ints_test.go | 144 ++++++++ randsmust/randsmust.go | 15 + randsmust/randsmust_test.go | 30 ++ randsmust/strings.go | 192 ++++++++++ randsmust/strings_example_test.go | 77 +++++ randsmust/strings_test.go | 558 ++++++++++++++++++++++++++++++ 13 files changed, 1162 insertions(+), 6 deletions(-) create mode 100644 randsmust/bytes.go create mode 100644 randsmust/bytes_example_test.go create mode 100644 randsmust/bytes_test.go create mode 100644 randsmust/ints.go create mode 100644 randsmust/ints_example_test.go create mode 100644 randsmust/ints_test.go create mode 100644 randsmust/randsmust.go create mode 100644 randsmust/randsmust_test.go create mode 100644 randsmust/strings.go create mode 100644 randsmust/strings_example_test.go create mode 100644 randsmust/strings_test.go diff --git a/README.md b/README.md index d2f4726..365760d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,22 @@ alt="GitHub issues">

+## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package + +`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. + +### Import + +``` +import "github.com/jimeh/rands" +``` + +### Usage + ```go s, err := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g== s, err := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ @@ -60,17 +76,54 @@ n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] ``` -## Import +## [`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" +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/rands.go b/rands.go index beb394e..a21dd3e 100644 --- a/rands.go +++ b/rands.go @@ -8,6 +8,9 @@ // 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" 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, + ) +}