Skip to content

Commit

Permalink
Add UUID version 7 (#54834)
Browse files Browse the repository at this point in the history
This PR does two things:

 * Add `uuid7()` & tests
* Document the tests better, so that future people have an easier time
figuring out what is being tested

---------

Co-authored-by: Sukera <[email protected]>
  • Loading branch information
Seelengrab and Seelengrab committed Jun 18, 2024
1 parent a9b4869 commit 0d30be8
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 42 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ New library functions
* `logrange(start, stop; length)` makes a range of constant ratio, instead of constant step ([#39071])
* The new `isfull(c::Channel)` function can be used to check if `put!(c, some_value)` will block. ([#53159])
* `waitany(tasks; throw=false)` and `waitall(tasks; failfast=false, throw=false)` which wait multiple tasks at once ([#53341]).
* `uuid7()` creates an RFC 9652 compliant UUID with version 7 ([#54834]).

New library features
--------------------
Expand Down
41 changes: 40 additions & 1 deletion stdlib/UUIDs/src/UUIDs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ using Random

import SHA

export UUID, uuid1, uuid4, uuid5, uuid_version
export UUID, uuid1, uuid4, uuid5, uuid7, uuid_version

import Base: UUID

Expand Down Expand Up @@ -157,4 +157,43 @@ function uuid5(ns::UUID, name::String)
return UUID(v)
end

"""
uuid7([rng::AbstractRNG]) -> UUID
Generates a version 7 (random or pseudo-random) universally unique identifier (UUID),
as specified by RFC 9652.
The default rng used by `uuid7` is not `Random.default_rng()` and every invocation of `uuid7()` without
an argument should be expected to return a unique identifier. Importantly, the outputs of
`uuid7` do not repeat even when `Random.seed!(seed)` is called. Currently (as of Julia 1.12),
`uuid7` uses `Random.RandomDevice` as the default rng. However, this is an implementation
detail that may change in the future.
!!! compat "Julia 1.12"
`uuid7()` is available as of Julia 1.12.
# Examples
```jldoctest; filter = r"[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}"
julia> using Random
julia> rng = Xoshiro(123);
julia> uuid7(rng)
UUID("019026ca-e086-772a-9638-f7b8557cd282")
```
"""
function uuid7(rng::AbstractRNG=Random.RandomDevice())
bytes = rand(rng, UInt128)
# make space for the timestamp
bytes &= 0x0000000000000fff3fffffffffffffff
# version & variant
bytes |= 0x00000000000070008000000000000000

# current time in ms, rounded to an Integer
timestamp = round(UInt128, time() * 1e3)
bytes |= timestamp << UInt128(80)

return UUID(bytes)
end

end
116 changes: 75 additions & 41 deletions stdlib/UUIDs/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,6 @@

using Test, UUIDs, Random

u1 = uuid1()
u4 = uuid4()
u5 = uuid5(u1, "julia")
@test uuid_version(u1) == 1
@test uuid_version(u4) == 4
@test uuid_version(u5) == 5
@test u1 == UUID(string(u1)) == UUID(GenericString(string(u1)))
@test u4 == UUID(string(u4)) == UUID(GenericString(string(u4)))
@test u5 == UUID(string(u5)) == UUID(GenericString(string(u5)))
@test u1 == UUID(UInt128(u1))
@test u4 == UUID(UInt128(u4))
@test u5 == UUID(UInt128(u5))
@test uuid4(MersenneTwister(0)) == uuid4(MersenneTwister(0))
@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-446655440000")
@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-44665544000098")
@test_throws ArgumentError UUID("z50e8400-e29b-41d4-a716-446655440000")

# results similar to Python builtin uuid
# To reproduce the sequence
Expand All @@ -37,11 +21,6 @@ const following_uuids = [
UUID("d8cc6298-75d5-57e0-996c-279259ab365c"),
]

for (idx, init_uuid) in enumerate(following_uuids[1:end-1])
next_id = uuid5(init_uuid, "julia")
@test next_id == following_uuids[idx+1]
end

# Python-generated UUID following each of the standard namespaces
const standard_namespace_uuids = [
(UUIDs.namespace_dns, UUID("00ca23ad-40ef-500c-a910-157de3950d07")),
Expand All @@ -50,30 +29,85 @@ const standard_namespace_uuids = [
(UUIDs.namespace_x500, UUID("993c6684-82e7-5cdb-bd46-9bff0362e6a9")),
]

for (init_uuid, next_uuid) in standard_namespace_uuids
result = uuid5(init_uuid, "julia")
@test next_uuid == result
end

# Issue 35860
Random.seed!(Random.default_rng(), 10)
@testset "UUIDs" begin
u1 = uuid1()
u4 = uuid4()
Random.seed!(Random.default_rng(), 10)
@test u1 != uuid1()
@test u4 != uuid4()

@test_throws ArgumentError UUID("22b4a8a1ae548-4eeb-9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548a4eeb-9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548-4eeba9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548-4eeb-9270a60426d66a48e")
str = "22b4a8a1-e548-4eeb-9270-60426d66a48e"
@test UUID(uppercase(str)) == UUID(str)

for r in rand(UInt128, 10^3)
@test UUID(r) == UUID(string(UUID(r)))
u5 = uuid5(u1, "julia")
u7 = uuid7()

@testset "Extraction of version numbers" begin
@test uuid_version(u1) == 1
@test uuid_version(u4) == 4
@test uuid_version(u5) == 5
@test uuid_version(u7) == 7
end

@testset "Parsing from string" begin
@test u1 == UUID(string(u1)) == UUID(GenericString(string(u1)))
@test u4 == UUID(string(u4)) == UUID(GenericString(string(u4)))
@test u5 == UUID(string(u5)) == UUID(GenericString(string(u5)))
@test u7 == UUID(string(u7)) == UUID(GenericString(string(u7)))
end

@testset "UInt128 conversion" begin
@test u1 == UUID(UInt128(u1))
@test u4 == UUID(UInt128(u4))
@test u5 == UUID(UInt128(u5))
@test u7 == UUID(UInt128(u7))
end

@testset "uuid4 & uuid7 RNG stability" begin
@test uuid4(Xoshiro(0)) == uuid4(Xoshiro(0))
@test uuid7(Xoshiro(0)) == uuid7(Xoshiro(0))
end

@testset "Rejection of invalid UUID strings" begin
@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-446655440000")
@test_throws ArgumentError UUID("550e8400e29b-41d4-a716-44665544000098")
@test_throws ArgumentError UUID("z50e8400-e29b-41d4-a716-446655440000")
@test_throws ArgumentError UUID("22b4a8a1ae548-4eeb-9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548a4eeb-9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548-4eeba9270-60426d66a48e")
@test_throws ArgumentError UUID("22b4a8a1-e548-4eeb-9270a60426d66a48e")
end

@testset "UUID sequence" begin
for (idx, init_uuid) in enumerate(following_uuids[1:end-1])
next_id = uuid5(init_uuid, "julia")
@test next_id == following_uuids[idx+1]
end
end

@testset "Standard namespace UUIDs" begin
for (init_uuid, next_uuid) in standard_namespace_uuids
result = uuid5(init_uuid, "julia")
@test next_uuid == result
end
end

@testset "Use of Random.RandomDevice (#35860)" begin
Random.seed!(Random.default_rng(), 10)
u1 = uuid1()
u4 = uuid4()
u7 = uuid7()
Random.seed!(Random.default_rng(), 10)
@test u1 != uuid1()
@test u4 != uuid4()
@test u7 != uuid7()
end

@testset "case invariance" begin
str = "22b4a8a1-e548-4eeb-9270-60426d66a48e"
@test UUID(uppercase(str)) == UUID(str)
end

@testset "Equality of string parsing & direct UInt128 passing" begin
for r in rand(UInt128, 10^3)
@test UUID(r) == UUID(string(UUID(r)))
end
end

@testset "Docstrings" begin
@test isempty(Docs.undocumented_names(UUIDs))
end
end

0 comments on commit 0d30be8

Please sign in to comment.