mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 11:26:38 +00:00
The UUID v7 format is a time-ordered random UUID. It uses a timestamp with millisecond precision in the most significant bits, followed by random data. This provides both uniqueness and chronological ordering, making it ideal for database primary keys and situations where sorting by creation time is desired. References: - https://uuid7.com/ - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
476 lines
8.9 KiB
Go
476 lines
8.9 KiB
Go
package uuid
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUUID_String(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
uuid UUID
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Zero UUID",
|
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
expected: "00000000-0000-0000-0000-000000000000",
|
|
},
|
|
{
|
|
name: "Random UUID",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xde, 0xf0,
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
expected: "12345678-9abc-def0-1234-56789abcdef0",
|
|
},
|
|
{
|
|
name: "UUID with max values",
|
|
uuid: UUID{
|
|
0xff, 0xff, 0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
},
|
|
expected: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.uuid.String()
|
|
assert.Equal(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkUUID_String(b *testing.B) {
|
|
u := UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xde, 0xf0,
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = u.String()
|
|
}
|
|
}
|
|
|
|
func TestFromBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
bytes []byte
|
|
want UUID
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "Valid 16 bytes",
|
|
bytes: []byte{
|
|
0x12,
|
|
0x34,
|
|
0x56,
|
|
0x78,
|
|
0x9a,
|
|
0xbc,
|
|
0xde,
|
|
0xf0,
|
|
0x12,
|
|
0x34,
|
|
0x56,
|
|
0x78,
|
|
0x9a,
|
|
0xbc,
|
|
0xde,
|
|
0xf0,
|
|
},
|
|
want: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xde, 0xf0,
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "Empty bytes",
|
|
bytes: []byte{},
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
{
|
|
name: "Too few bytes",
|
|
bytes: []byte{0x12, 0x34, 0x56, 0x78, 0x9a},
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
{
|
|
name: "Too many bytes",
|
|
bytes: []byte{
|
|
0x12,
|
|
0x34,
|
|
0x56,
|
|
0x78,
|
|
0x9a,
|
|
0xbc,
|
|
0xde,
|
|
0xf0,
|
|
0x12,
|
|
0x34,
|
|
0x56,
|
|
0x78,
|
|
0x9a,
|
|
0xbc,
|
|
0xde,
|
|
0xf0,
|
|
0xab,
|
|
},
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := FromBytes(tt.bytes)
|
|
|
|
if tt.wantErr != nil {
|
|
assert.EqualError(t, err, tt.wantErr.Error())
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFromBytes(b *testing.B) {
|
|
bytes := []byte{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xde, 0xf0,
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = FromBytes(bytes)
|
|
}
|
|
}
|
|
|
|
func TestFromString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
str string
|
|
want UUID
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "Valid UUID string",
|
|
str: "12345678-9abc-def0-1234-56789abcdef0",
|
|
want: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xde, 0xf0,
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "Empty string",
|
|
str: "",
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
{
|
|
name: "Too short string",
|
|
str: "12345678-9abc-def0-1234-56789abcde",
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
{
|
|
name: "Too long string",
|
|
str: "12345678-9abc-def0-1234-56789abcdef0a",
|
|
want: UUID{},
|
|
wantErr: ErrInvalidLength,
|
|
},
|
|
{
|
|
name: "Invalid characters",
|
|
str: "12345678-9abc-defg-1234-56789abcdef0",
|
|
want: UUID{},
|
|
wantErr: errors.New("encoding/hex: invalid byte: U+0067 'g'"),
|
|
},
|
|
{
|
|
name: "Zero UUID",
|
|
str: "00000000-0000-0000-0000-000000000000",
|
|
want: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "Max value UUID",
|
|
str: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
|
want: UUID{
|
|
0xff, 0xff, 0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff,
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := FromString(tt.str)
|
|
|
|
if tt.wantErr != nil {
|
|
assert.EqualError(t, err, tt.wantErr.Error())
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFromString(b *testing.B) {
|
|
uuidStr := "12345678-9abc-def0-1234-56789abcdef0"
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = FromString(uuidStr)
|
|
}
|
|
}
|
|
|
|
func TestUUID_Time(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Define a reference time for testing.
|
|
refTime := time.Date(2023, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
// The timestamp in milliseconds (Unix timestamp * 1000 in 6 bytes).
|
|
timestampMillis := refTime.UnixMilli()
|
|
|
|
// Create bytes for the timestamp (first 6 bytes of UUID).
|
|
timestampBytes := []byte{
|
|
byte(timestampMillis >> 40),
|
|
byte(timestampMillis >> 32),
|
|
byte(timestampMillis >> 24),
|
|
byte(timestampMillis >> 16),
|
|
byte(timestampMillis >> 8),
|
|
byte(timestampMillis),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
uuid UUID
|
|
wantTime time.Time
|
|
wantOk bool
|
|
}{
|
|
{
|
|
name: "Version 7 UUID",
|
|
uuid: func() UUID {
|
|
var u UUID
|
|
// Set first 6 bytes to timestamp.
|
|
copy(u[:6], timestampBytes)
|
|
// Set version to 7 (0111 as the high nibble of byte 6).
|
|
u[6] = (u[6] & 0x0F) | 0x70
|
|
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
|
u[8] = (u[8] & 0x3F) | 0x80
|
|
|
|
return u
|
|
}(),
|
|
wantTime: refTime,
|
|
wantOk: true,
|
|
},
|
|
{
|
|
name: "Version 4 UUID (not time-based)",
|
|
uuid: func() UUID {
|
|
var u UUID
|
|
// Set first 6 bytes to same timestamp to verify it's ignored.
|
|
copy(u[:6], timestampBytes)
|
|
// Set version to 4 (0100 as the high nibble of byte 6).
|
|
u[6] = (u[6] & 0x0F) | 0x40
|
|
|
|
return u
|
|
}(),
|
|
wantTime: time.Time{}, // Zero time for non-V7 UUIDs
|
|
wantOk: false,
|
|
},
|
|
{
|
|
name: "Zero UUID",
|
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
wantTime: time.Time{}, // Zero time for version 0
|
|
wantOk: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotTime, gotOk := tt.uuid.Time()
|
|
|
|
assert.Equal(t, tt.wantOk, gotOk)
|
|
|
|
if tt.wantTime.IsZero() {
|
|
assert.True(t, gotTime.IsZero())
|
|
} else {
|
|
// Compare time at millisecond precision.
|
|
assert.Equal(t, tt.wantTime.UnixMilli(), gotTime.UnixMilli())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkUUID_Time(b *testing.B) {
|
|
uuid, err := NewV7()
|
|
require.NoError(b, err)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = uuid.Time()
|
|
}
|
|
}
|
|
|
|
func TestUUID_Version(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
uuid UUID
|
|
want int
|
|
}{
|
|
{
|
|
name: "Version 0 (invalid/nil UUID)",
|
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "Version 1 (time-based)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x10, 0xf0, // 0x10 = version 1 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "Version 2 (DCE Security)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x20, 0xf0, // 0x20 = version 2 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 2,
|
|
},
|
|
{
|
|
name: "Version 3 (name-based, MD5)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x30, 0xf0, // 0x30 = version 3 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "Version 4 (random)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x40, 0xf0, // 0x40 = version 4 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 4,
|
|
},
|
|
{
|
|
name: "Version 5 (name-based, SHA-1)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x50, 0xf0, // 0x50 = version 5 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 5,
|
|
},
|
|
{
|
|
name: "Version 7 (time-ordered)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x70, 0xf0, // 0x70 = version 7 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 7,
|
|
},
|
|
{
|
|
name: "Version 8 (custom)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0x80, 0xf0, // 0x80 = version 8 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 8,
|
|
},
|
|
{
|
|
name: "Version 15 (theoretical max)",
|
|
uuid: UUID{
|
|
0x12, 0x34, 0x56, 0x78,
|
|
0x9a, 0xbc,
|
|
0xf0, 0xf0, // 0xf0 = version 15 in top nibble
|
|
0x12, 0x34,
|
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
|
},
|
|
want: 15,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.uuid.Version()
|
|
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkUUID_Version(b *testing.B) {
|
|
uuid, err := NewV7()
|
|
require.NoError(b, err)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = uuid.Version()
|
|
}
|
|
}
|