diff --git a/CHANGELOG/CHANGELOG-1.x.md b/CHANGELOG/CHANGELOG-1.x.md index c5ab5d4..937c66d 100644 --- a/CHANGELOG/CHANGELOG-1.x.md +++ b/CHANGELOG/CHANGELOG-1.x.md @@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Security +--- +## [1.4.0] - 2024-OCT-26 + +### Added +- **FEATURE:**: Added concurrent benchmark tests. +### Changed +- **DEBT:** Maintained Safety with Linter Suppression: Added `// nolint:gosec` with justification for safe conversions. +- **DEBT:** Refactored Slice Initialization: Initialized `idRunes` with zero length and pre-allocated capacity, using append to build the slice. +- **DEBT:** Ensured Comprehensive Testing: Reviewed and updated tests to handle all edge cases and ensure no runtime errors. +### Deprecated +### Removed +- **FEATURE:** Removed Unicode support for custom dictionaries. +### Fixed +- **DEFECT:** Fixed Operator Precedence: Changed `bits.Len(uint(alphabetLen - 1))` to `bits.Len(uint(alphabetLen) - 1)` to ensure safe conversion. +### Security + --- ## [1.3.0] - 2024-OCT-26 @@ -35,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated ### Removed ### Fixed -- **DFECT:** Fixed version compare links in CHANGELOG. +- **DEFECT:** Fixed version compare links in CHANGELOG. ### Security --- @@ -49,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Security -[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.3.0...HEAD +[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.4.0...HEAD +[1.4.0]: https://github.com/sixafter/nanoid/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/sixafter/nanoid/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/sixafter/nanoid/compare/v1.0.0...v1.2.0 [1.0.0]: https://github.com/sixafter/nanoid/compare/a6a1eb74b61e518fd0216a17dfe5c9b4c432e6e8...v1.0.0 diff --git a/Makefile b/Makefile index f24b0fd..9fdd2ea 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ test: ## Execute unit tests .PHONY: bench bench: ## Execute benchmark tests - $(GO_TEST) -bench=. + $(GO_TEST) -bench=. -benchmem ./... .PHONY: clean clean: ## Remove previous build diff --git a/README.md b/README.md index 861ccec..e038f89 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,18 @@ A simple, fast, and efficient Go implementation of [NanoID](https://github.com/a ## Features -- **Secure**: Uses `crypto/rand` for cryptographically secure random number generation. -- **Fast**: Optimized for performance with efficient algorithms. -- **Thread-Safe**: Safe for concurrent use in multi-threaded applications. -- **Customizable**: Specify custom ID lengths and alphabets. -- **Easy to Use**: Simple API with sensible defaults. +* **Stateless Design**: Each function operates independently without relying on global state or caches, eliminating the need for synchronization primitives like mutexes. This design ensures predictable behavior and simplifies usage in various contexts. +* **Cryptographically Secure**: Utilizes Go's crypto/rand package for generating cryptographically secure random numbers. This guarantees that the generated IDs are both unpredictable and suitable for security-sensitive applications. +* **High Performance**: Optimized algorithms and efficient memory management techniques ensure rapid ID generation. Whether you're generating a few IDs or millions, the library maintains consistent speed and responsiveness. +* **Memory Efficient**: Implements sync.Pool to reuse byte slices, minimizing memory allocations and reducing garbage collection overhead. This approach significantly enhances performance, especially in high-throughput scenarios. +* **Thread-Safe**: Designed for safe concurrent use in multi-threaded applications. Multiple goroutines can generate IDs simultaneously without causing race conditions or requiring additional synchronization. +* **Customizable**: Offers flexibility to specify custom ID lengths and alphabets. Whether you need short, compact IDs or longer, more complex ones, the library can accommodate your specific requirements. +* **User-Friendly API**: Provides a simple and intuitive API with sensible defaults, making integration straightforward. Developers can start generating IDs with minimal configuration and customize as needed. +* **Zero External Dependencies**: Relies solely on Go's standard library, ensuring ease of use, compatibility, and minimal footprint within your projects. +* **Comprehensive Testing**: Includes a robust suite of unit tests and concurrency tests to ensure reliability, correctness, and thread safety. This commitment to quality guarantees consistent performance across different use cases. +* **Detailed Documentation**: Accompanied by clear and thorough documentation, including examples and usage guidelines. New users can quickly understand how to implement and customize the library to fit their needs. +* **Efficient Error Handling**: Employs predefined errors to avoid unnecessary allocations, enhancing both performance and clarity in error management. +* **Optimized for Low Allocations**: Carefully structured to minimize heap allocations, reducing memory overhead and improving cache locality. This optimization is crucial for applications where performance and resource usage are critical. ## Installation @@ -61,27 +68,14 @@ fmt.Println("Generated Nano ID of size 32:", id) Generate a Nano ID using a custom alphabet: ```go -customAlphabet := "abcdef123456" -id, err := nanoid.NewCustom(16, customAlphabet) +alphabet := "abcdef123456" +id, err := nanoid.NewCustom(16, alphabet) if err != nil { log.Fatal(err) } fmt.Println("Generated Nano ID with custom alphabet:", id) ``` -### Generate a Nano ID with Unicode Alphabet - -Generate a Nano ID using a Unicode alphabet: - -```go -unicodeAlphabet := "あいうえお漢字🙂🚀" -id, err := nanoid.NewCustom(10, unicodeAlphabet) -if err != nil { - log.Fatal(err) -} -fmt.Println("Generated Nano ID with Unicode alphabet:", id) -``` - ### Generate a Nano ID with Custom Random Source Generate a Nano ID using a custom random source that implements io.Reader: @@ -148,10 +142,6 @@ func main() { * `DefaultAlphabet`: The default alphabet used for ID generation: `-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz` * `DefaultSize`: The default size of the generated ID: `21` -## Unicode Support - -This implementation fully supports custom alphabets containing Unicode characters, including emojis and characters from various languages. By using []rune internally, it correctly handles multi-byte Unicode characters. - ## Performance The package is optimized for performance and low memory consumption: @@ -159,6 +149,57 @@ The package is optimized for performance and low memory consumption: * **Avoids `math/big`**: Does not use `math/big`, relying on built-in integer types for calculations. * **Minimized System Calls**: Reads random bytes in batches to reduce the number of system calls. +## Execute Benchmarks: + +Run the benchmarks using the go test command with the `bench` make target: + +```shell +make bench +``` + +### Interpreting Results: + +Sample output might look like this: + +```shell +go test -bench=. -benchmem ./... +goos: darwin +goarch: arm64 +pkg: github.com/sixafter/nanoid +cpu: Apple M3 Max +BenchmarkNew-16 6329498 189.2 ns/op 40 B/op 3 allocs/op +BenchmarkNewSize/Size10-16 11600679 102.4 ns/op 24 B/op 2 allocs/op +BenchmarkNewSize/Size21-16 6384469 186.7 ns/op 40 B/op 3 allocs/op +BenchmarkNewSize/Size50-16 2680179 448.2 ns/op 104 B/op 6 allocs/op +BenchmarkNewSize/Size100-16 1387914 863.3 ns/op 192 B/op 11 allocs/op +BenchmarkNewCustom/Size10_CustomASCIIAlphabet-16 9306187 128.8 ns/op 24 B/op 2 allocs/op +BenchmarkNewCustom/Size21_CustomASCIIAlphabet-16 5062975 239.4 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom/Size50_CustomASCIIAlphabet-16 2322037 515.3 ns/op 101 B/op 5 allocs/op +BenchmarkNewCustom/Size100_CustomASCIIAlphabet-16 1235755 972.0 ns/op 182 B/op 9 allocs/op +BenchmarkNew_Concurrent/Concurrency1-16 2368245 513.1 ns/op 40 B/op 3 allocs/op +BenchmarkNew_Concurrent/Concurrency2-16 1940826 609.5 ns/op 40 B/op 3 allocs/op +BenchmarkNew_Concurrent/Concurrency4-16 1986049 585.6 ns/op 40 B/op 3 allocs/op +BenchmarkNew_Concurrent/Concurrency8-16 1999959 602.2 ns/op 40 B/op 3 allocs/op +BenchmarkNew_Concurrent/Concurrency16-16 2018793 595.6 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom_Concurrent/Concurrency1-16 1960315 611.7 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom_Concurrent/Concurrency2-16 1790460 673.7 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom_Concurrent/Concurrency4-16 1766841 670.7 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom_Concurrent/Concurrency8-16 1768189 677.4 ns/op 40 B/op 3 allocs/op +BenchmarkNewCustom_Concurrent/Concurrency16-16 1765303 689.5 ns/op 40 B/op 3 allocs/op +PASS +ok github.com/sixafter/nanoid 33.279s +``` + +* `ns/op` (Nanoseconds per Operation): + * Indicates the average time taken per operation. + * Lower values signify better CPU performance. +* `B/op` (Bytes Allocated per Operation): + * Shows the average number of bytes allocated per operation. + * `0 B/op` indicates no heap allocations, which is optimal. +* `allocs/op` (Allocations per Operation): + * Represents the average number of memory allocations per operation. + * `0 allocs/op` is ideal as it indicates no heap allocations. + ## Contributing Contributions are welcome! Please feel free to submit issues or pull requests. diff --git a/nanoid.go b/nanoid.go index 04ea9ff..33e3778 100644 --- a/nanoid.go +++ b/nanoid.go @@ -2,107 +2,118 @@ // // This source code is licensed under the MIT License found in the // LICENSE file in the root directory of this source tree. + +// nanoid.go package nanoid import ( "crypto/rand" "errors" - "io" "math/bits" - "strings" + "sync" ) +// Constants for default settings. const ( DefaultAlphabet = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" DefaultSize = 21 + MaxUintSize = 1024 // Adjust as needed +) + +// Predefined errors to avoid allocations on each call. +var ( + ErrInvalidSize = errors.New("size must be greater than zero") + ErrSizeExceedsMaxUint = errors.New("size exceeds maximum allowed value") + ErrEmptyAlphabet = errors.New("alphabet must not be empty") + ErrRandomSourceNoData = errors.New("random source returned no data") ) -// New generates a NanoID with the default size and alphabet using crypto/rand as the random source. +// Byte pool to reuse byte slices and minimize allocations. +var bytePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, MaxUintSize) // Non-zero length and capacity + + return &b + }, +} + +// New generates a Nano ID with the default size and alphabet using crypto/rand as the random source. func New() (string, error) { return NewSize(DefaultSize) } -// NewSize generates a NanoID with a specified size and the default alphabet using crypto/rand as the random source. +// NewSize generates a Nano ID with a specified size and the default alphabet using crypto/rand as the random source. func NewSize(size int) (string, error) { return NewCustom(size, DefaultAlphabet) } -// NewCustom generates a NanoID with a specified size and custom alphabet using crypto/rand as the random source. +// NewCustom generates a Nano ID with a specified size and custom ASCII alphabet using crypto/rand as the random source. func NewCustom(size int, alphabet string) (string, error) { - return NewCustomReader(size, alphabet, cryptoRandReader) -} - -// NewCustomReader generates a NanoID with a specified size, custom alphabet, and custom random source. -func NewCustomReader(size int, alphabet string, rnd io.Reader) (string, error) { - if rnd == nil { - return "", errors.New("random source cannot be nil") - } if size <= 0 { - return "", errors.New("size must be greater than zero") + return "", ErrInvalidSize } - - // Convert alphabet to []rune to support Unicode characters - alphabetRunes := []rune(alphabet) - alphabetLen := len(alphabetRunes) - if alphabetLen == 0 { - return "", errors.New("alphabet must not be empty") + if size > MaxUintSize { + return "", ErrSizeExceedsMaxUint } - - // Handle special case when alphabet length is 1 - if alphabetLen == 1 { - return strings.Repeat(string(alphabetRunes[0]), size), nil + if len(alphabet) == 0 { + return "", ErrEmptyAlphabet } - // Calculate the number of bits needed to represent the alphabet indices - bitsPerChar := bits.Len(uint(alphabetLen - 1)) + return generateASCIIID(size, alphabet) +} + +// generateASCIIID generates an ID using a byte-based (ASCII) alphabet. +func generateASCIIID(size int, alphabet string) (string, error) { + //nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking + bitsPerChar := bits.Len(uint(len(alphabet) - 1)) if bitsPerChar == 0 { bitsPerChar = 1 } - idRunes := make([]rune, size) + // Acquire a pointer to a byte slice from the pool + bufPtr, ok := bytePool.Get().(*[]byte) + if !ok { + panic("bytePool.Get() did not return a *[]byte") + } + buf := *bufPtr + buf = buf[:size] // Slice to desired size + + defer func() { + // Reset the slice back to MaxUintSize before putting it back + *bufPtr = (*bufPtr)[:MaxUintSize] + bytePool.Put(bufPtr) + }() + var bitBuffer uint64 var bitsInBuffer int - i := 0 - for i < size { - // If we don't have enough bits, read more random bytes + for i := 0; i < size; { if bitsInBuffer < bitsPerChar { - var b [8]byte // Read up to 8 bytes at once for efficiency - n, err := rnd.Read(b[:]) + var b [8]byte + n, err := rand.Read(b[:]) if err != nil { return "", err } if n == 0 { - return "", errors.New("random source returned no data") + return "", ErrRandomSourceNoData } - // Append the new random bytes to the bit buffer for j := 0; j < n; j++ { bitBuffer |= uint64(b[j]) << bitsInBuffer bitsInBuffer += 8 } } - // Extract bitsPerChar bits to get the index - idx := int(bitBuffer & ((1 << bitsPerChar) - 1)) + mask := uint64((1 << bitsPerChar) - 1) + idx := bitBuffer & mask bitBuffer >>= bitsPerChar bitsInBuffer -= bitsPerChar - // Use the index if it's within the alphabet range - if idx < alphabetLen { - idRunes[i] = alphabetRunes[idx] + //nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking + if int(idx) < len(alphabet) { + buf[i] = alphabet[idx] i++ } - // Else discard and continue } - return string(idRunes), nil -} - -// cryptoRandReader is a wrapper around crypto/rand.Reader to match io.Reader interface. -var cryptoRandReader io.Reader = cryptoRandReaderType{} - -type cryptoRandReaderType struct{} - -func (cryptoRandReaderType) Read(p []byte) (int, error) { - return rand.Read(p) + return string(buf), nil } diff --git a/nanoid_benchmark_test.go b/nanoid_benchmark_test.go index 4270ee1..08b9ded 100644 --- a/nanoid_benchmark_test.go +++ b/nanoid_benchmark_test.go @@ -3,30 +3,33 @@ // This source code is licensed under the MIT License found in the // LICENSE file in the root directory of this source tree. -package nanoid +package nanoid_test import ( - "bytes" "fmt" - "io" + "github.com/sixafter/nanoid" + "runtime" "testing" ) -func BenchmarkGenerate(b *testing.B) { +// BenchmarkNew benchmarks the New function with default settings. +func BenchmarkNew(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := New() + _, err := nanoid.New() if err != nil { b.Fatal(err) } } } -func BenchmarkGenerateSize(b *testing.B) { +// BenchmarkNewSize benchmarks the NewSize function with various sizes. +func BenchmarkNewSize(b *testing.B) { sizes := []int{10, 21, 50, 100} for _, size := range sizes { + size := size // Capture range variable b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := NewSize(size) + _, err := nanoid.NewSize(size) if err != nil { b.Fatal(err) } @@ -35,13 +38,15 @@ func BenchmarkGenerateSize(b *testing.B) { } } -func BenchmarkGenerateCustom(b *testing.B) { +// BenchmarkNewCustom benchmarks the NewCustom function with a custom ASCII alphabet and various sizes. +func BenchmarkNewCustom(b *testing.B) { sizes := []int{10, 21, 50, 100} - customAlphabet := "abcdef123456" + customASCIIAlphabet := "abcdef123456" for _, size := range sizes { - b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { + size := size // Capture range variable + b.Run(fmt.Sprintf("Size%d_CustomASCIIAlphabet", size), func(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := NewCustom(size, customAlphabet) + _, err := nanoid.NewCustom(size, customASCIIAlphabet) if err != nil { b.Fatal(err) } @@ -50,36 +55,43 @@ func BenchmarkGenerateCustom(b *testing.B) { } } -func BenchmarkGenerateCustomUnicodeAlphabet(b *testing.B) { - sizes := []int{10, 21, 50, 100} - unicodeAlphabet := "あいうえお漢字🙂🚀" - for _, size := range sizes { - b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := NewCustom(size, unicodeAlphabet) - if err != nil { - b.Fatal(err) +// BenchmarkNew_Concurrent benchmarks the New function under concurrent load. +func BenchmarkNew_Concurrent(b *testing.B) { + concurrencyLevels := []int{1, 2, 4, 8, runtime.NumCPU()} + + for _, concurrency := range concurrencyLevels { + concurrency := concurrency // Capture range variable + b.Run(fmt.Sprintf("Concurrency%d", concurrency), func(b *testing.B) { + b.SetParallelism(concurrency) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := nanoid.New() + if err != nil { + b.Fatal(err) + } } - } + }) }) } } -func BenchmarkGenerateCustomReader(b *testing.B) { - size := 21 - customAlphabet := "abcdef123456" - randomData := make([]byte, 1024) - for i := 0; i < len(randomData); i++ { - randomData[i] = byte(i % 256) - } - rnd := bytes.NewReader(randomData) +// BenchmarkNewCustom_Concurrent benchmarks the NewCustom function with a custom ASCII alphabet under concurrent load. +func BenchmarkNewCustom_Concurrent(b *testing.B) { + concurrencyLevels := []int{1, 2, 4, 8, runtime.NumCPU()} + customASCIIAlphabet := "abcdef123456" - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := NewCustomReader(size, customAlphabet, rnd) - if err != nil { - b.Fatal(err) - } - rnd.Seek(0, io.SeekStart) // Reset the reader for the next iteration + for _, concurrency := range concurrencyLevels { + concurrency := concurrency // Capture range variable + b.Run(fmt.Sprintf("Concurrency%d", concurrency), func(b *testing.B) { + b.SetParallelism(concurrency) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := nanoid.NewCustom(21, customASCIIAlphabet) + if err != nil { + b.Fatal(err) + } + } + }) + }) } } diff --git a/nanoid_test.go b/nanoid_test.go index 99d3c61..58ae094 100644 --- a/nanoid_test.go +++ b/nanoid_test.go @@ -1,162 +1,224 @@ -package nanoid +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the MIT License found in the +// LICENSE file in the root directory of this source tree. + +// nanoid_test.go +package nanoid_test import ( - "bytes" - "strings" + "fmt" + "github.com/sixafter/nanoid" + "github.com/stretchr/testify/assert" "sync" "testing" - "unicode/utf8" - - "github.com/stretchr/testify/assert" ) +// TestNew verifies that the New function generates an ID of the default size and alphabet. func TestNew(t *testing.T) { t.Parallel() + is := assert.New(t) - id, err := New() - is.NoError(err) - is.Equal(DefaultSize, utf8.RuneCountInString(id)) + id, err := nanoid.New() + is.NoError(err, "New() should not return an error") + is.Equal(nanoid.DefaultSize, len(id), "ID length should match the default size") + + // Check that all characters are within the default alphabet + for _, char := range id { + is.Contains(nanoid.DefaultAlphabet, string(char), "Character '%c' should be in the default alphabet", char) + } } +// TestNewSize verifies that the NewSize function generates IDs of specified sizes. func TestNewSize(t *testing.T) { t.Parallel() - is := assert.New(t) - - size := 30 - id, err := NewSize(size) - is.NoError(err) - is.Equal(size, utf8.RuneCountInString(id)) -} -func TestNewCustom(t *testing.T) { - t.Parallel() is := assert.New(t) - size := 15 - customAlphabet := "abcdef123456" - id, err := NewCustom(size, customAlphabet) - is.NoError(err) - is.Equal(size, utf8.RuneCountInString(id)) - - // Ensure that the ID contains only characters from the custom alphabet - for _, r := range id { - is.Contains(customAlphabet, string(r)) + testCases := []struct { + name string + size int + }{ + {"Size1", 1}, + {"Size10", 10}, + {"Size21", 21}, + {"Size50", 50}, + {"Size100", 100}, } -} -func TestNewCustomWithUnicodeAlphabet(t *testing.T) { - t.Parallel() - is := assert.New(t) + for _, tc := range testCases { + tc := tc // Capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - size := 10 - unicodeAlphabet := "あいうえお漢字🙂🚀" - id, err := NewCustom(size, unicodeAlphabet) - is.NoError(err) - is.Equal(size, utf8.RuneCountInString(id)) + id, err := nanoid.NewSize(tc.size) + is.NoError(err, "NewSize(%d) should not return an error", tc.size) + is.Equal(tc.size, len(id), "ID length should match the specified size") - // Ensure that the ID contains only characters from the Unicode alphabet - for _, r := range id { - is.Contains(unicodeAlphabet, string(r)) + // Check that all characters are within the default alphabet + for _, char := range id { + is.Contains(nanoid.DefaultAlphabet, string(char), "Character '%c' should be in the default alphabet", char) + } + }) } } -func TestNewCustomReader(t *testing.T) { +// TestNewCustom verifies that the NewCustom function generates IDs using a custom ASCII alphabet. +func TestNewCustom(t *testing.T) { t.Parallel() - is := assert.New(t) - size := 10 - customAlphabet := "abcd" - // Use a deterministic random source for testing - randomData := bytes.Repeat([]byte{0xFF}, 10) // Simulate random bytes - rnd := bytes.NewReader(randomData) - - id, err := NewCustomReader(size, customAlphabet, rnd) - is.NoError(err) - is.Equal(size, utf8.RuneCountInString(id)) + is := assert.New(t) - // Since we used 0xFF, the index will be masked accordingly - for _, r := range id { - is.Contains(customAlphabet, string(r)) + customASCIIAlphabet := "abcdef123456" + testCases := []struct { + name string + size int + alphabet string + }{ + {"Size10_CustomASCIIAlphabet", 10, customASCIIAlphabet}, + {"Size21_CustomASCIIAlphabet", 21, customASCIIAlphabet}, + {"Size50_CustomASCIIAlphabet", 50, customASCIIAlphabet}, + {"Size100_CustomASCIIAlphabet", 100, customASCIIAlphabet}, + {"Size10_SingleCharacter", 10, "x"}, // Single-character alphabet + {"Size5_SingleCharacter", 5, "A"}, // Single-character alphabet } -} -func TestNewCustomReaderNil(t *testing.T) { - t.Parallel() - is := assert.New(t) + for _, tc := range testCases { + tc := tc // Capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - size := 10 - customAlphabet := DefaultAlphabet + id, err := nanoid.NewCustom(tc.size, tc.alphabet) + is.NoError(err, "NewCustom(%d, %s) should not return an error", tc.size, tc.alphabet) + is.Equal(tc.size, len(id), "ID length should match the specified size") - _, err := NewCustomReader(size, customAlphabet, nil) - is.Error(err) - is.EqualError(err, "random source cannot be nil") + // Check that all characters are within the custom alphabet + for _, char := range id { + is.Contains(tc.alphabet, string(char), "Character '%c' should be in the custom alphabet", char) + } + }) + } } -func TestNewCustomEmptyAlphabet(t *testing.T) { +// TestErrorHandling verifies that functions return errors for invalid inputs. +func TestErrorHandling(t *testing.T) { t.Parallel() - is := assert.New(t) - size := 10 - customAlphabet := "" - - _, err := NewCustom(size, customAlphabet) - is.Error(err) - is.EqualError(err, "alphabet must not be empty") -} - -func TestNewCustomNegativeSize(t *testing.T) { - t.Parallel() is := assert.New(t) - size := -5 - customAlphabet := DefaultAlphabet + testCases := []struct { + name string + function func() (string, error) + }{ + { + name: "NewSize with zero size", + function: func() (string, error) { + return nanoid.NewSize(0) + }, + }, + { + name: "NewSize with negative size", + function: func() (string, error) { + return nanoid.NewSize(-10) + }, + }, + { + name: "NewCustom with empty alphabet", + function: func() (string, error) { + return nanoid.NewCustom(10, "") + }, + }, + { + name: "NewCustom with size exceeding MaxUintSize", + function: func() (string, error) { + return nanoid.NewCustom(nanoid.MaxUintSize+1, "abcdef") + }, + }, + } - _, err := NewCustom(size, customAlphabet) - is.Error(err) - is.EqualError(err, "size must be greater than zero") + for _, tc := range testCases { + tc := tc // Capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + id, err := tc.function() + is.Error(err, "Expected an error for test case '%s'", tc.name) + is.Empty(id, "Expected empty ID for test case '%s'", tc.name) + }) + } } -func TestNewCustomSingleCharacterAlphabet(t *testing.T) { +// TestUniqueness verifies that multiple generated IDs are unique. +func TestUniqueness(t *testing.T) { + // Note: Due to the high memory consumption and execution time, + // this test can be marked as skipped unless specifically needed. + t.Skip("Skipping TestUniqueness to save resources during regular test runs") + t.Parallel() + is := assert.New(t) - size := 10 - customAlphabet := "X" + const sampleSize = 100000 + ids := make(map[string]struct{}, sampleSize) + + for i := 0; i < sampleSize; i++ { + id, err := nanoid.New() + is.NoError(err, "New() should not return an error") - id, err := NewCustom(size, customAlphabet) - is.NoError(err) - is.Equal(strings.Repeat(customAlphabet, size), id) + if _, exists := ids[id]; exists { + is.FailNow(fmt.Sprintf("Duplicate ID found: %s", id)) + } + ids[id] = struct{}{} + } } -func TestThreadSafety(t *testing.T) { +// TestConcurrencySafety verifies that concurrent ID generation does not produce errors or duplicates. +func TestConcurrencySafety(t *testing.T) { t.Parallel() + is := assert.New(t) - const numGoroutines = 100 - const idSize = 21 + const ( + concurrency = 100 + perGoroutine = 1000 + totalSample = concurrency * perGoroutine + ) + ids := make(chan string, totalSample) + errs := make(chan error, totalSample) - ids := make(chan string, numGoroutines) var wg sync.WaitGroup + wg.Add(concurrency) - for i := 0; i < numGoroutines; i++ { - wg.Add(1) + for i := 0; i < concurrency; i++ { go func() { defer wg.Done() - id, err := New() - is.NoError(err) - is.Equal(idSize, utf8.RuneCountInString(id)) - ids <- id + for j := 0; j < perGoroutine; j++ { + id, err := nanoid.New() + if err != nil { + errs <- err + continue + } + ids <- id + } }() } wg.Wait() close(ids) + close(errs) + + // Check for errors + for err := range errs { + is.NoError(err, "New() should not return an error in concurrent execution") + } - idSet := make(map[string]struct{}) + // Check for duplicates + uniqueIDs := make(map[string]struct{}, totalSample) for id := range ids { - // Ensure uniqueness - is.NotContains(idSet, id) - idSet[id] = struct{}{} + if _, exists := uniqueIDs[id]; exists { + is.FailNow(fmt.Sprintf("Duplicate ID found: %s", id)) + } + uniqueIDs[id] = struct{}{} } }