From b0fc196de41b056fd19c5aca623477d90e7b03fc Mon Sep 17 00:00:00 2001 From: mitch-kyle Date: Wed, 10 Jul 2024 20:42:02 -0300 Subject: [PATCH] Add uuids v6, v7, and v8 draft - Add support for max, v6, v7, v8 uuids - Add helper functions for comparing the ordinality of multiple uuids Draft RFC4122 ammendment https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html --- README.md | 56 ++++++++++ src/clj_uuid.clj | 195 +++++++++++++++++++++++++++++------ src/clj_uuid/clock.clj | 25 ++++- src/clj_uuid/constants.clj | 2 + src/clj_uuid/random.clj | 26 +++++ src/clj_uuid/util.clj | 16 +-- test/clj_uuid/api_test.clj | 95 ++++++++++++++++- test/clj_uuid/clock_test.clj | 53 ++++++---- test/clj_uuid/v1_test.clj | 48 ++++++++- test/clj_uuid/v3_test.clj | 5 +- test/clj_uuid/v4_test.clj | 13 ++- test/clj_uuid/v5_test.clj | 4 +- 12 files changed, 461 insertions(+), 77 deletions(-) create mode 100644 src/clj_uuid/random.clj diff --git a/README.md b/README.md index 444f06a..cff665a 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,9 @@ _(function)_ `v0 []` > Return the null UUID, #uuid "00000000-0000-0000-0000-000000000000" +_(function)_ `max []` + +> Return the maximum UUID, #uuid "ffffffff-ffff-ffff-ffff-ffffffffffff" _(function)_ `v1 []` @@ -620,6 +623,30 @@ _(function)_ `v5 [^UUID namespace ^Object local-name]` > * If two v5 UUID's are equal, then there is a high degree of certainty > that they were generated from the same name in the same namespace. +_(function)_ `v6 []` + +> Generate a v6 (time-based), lexically sortable, unique identifier, +> guaranteed to be unique and thread-safe regardless of clock +> precision or degree of concurrency. Creation of v6 UUID's does not +> require any call to a cryptographic generator and can be +> accomplished much more efficiently than v3, v4, v5, or squuid's. A +> v6 UUID reveals both the identity of the computer that generated the +> UUID and the time at which it did so. Its uniqueness across +> computers is guaranteed as long as MAC addresses are not +> duplicated. Used for compatibility with systems that already use v1; +> UUID v7 should be prefered over v1 or v6. + +_(function)_ `v7 []` + +> Generate a v7 unix time-based, lexically sortable UUID with monotonic +> counter and cryptographically secure random portion. The 12 bit +> rand_a is used as counter, rand_b is CSPRNG. Random numbers are +> generated using the JVM default implementation of +> java.security.SecureRandom. + +_(function)_ `v8 [^long msb, ^long lsb]` + +> Generate a v8 custom UUID with up to 122 bits of user data. _(function)_ `squuid []` @@ -631,6 +658,35 @@ _(function)_ `squuid []` > time (seconds since 12:00am January 1, 1970 UTC) with the most > significant 32 bits of the UUID +_(function)_ `= [x]` + +_(function)_ `= [x y]` + +_(function)_ `= [x y & more]` + +> Directly compare two or more UUIDs for = relation based on the +> ordinality semantics defined by [RFC4122:3 RULES FOR LEXICAL +> EQUIVALENCE]. + +_(function)_ `> [x]` + +_(function)_ `> [x y]` + +_(function)_ `> [x y & more]` + +> Directly compare two or more UUIDs for > relation based on the +> ordinality semantics defined by [RFC4122:3 RULES FOR LEXICAL +> EQUIVALENCE]. + +_(function)_ `< [x]` + +_(function)_ `< [x y]` + +_(function)_ `< [x y & more]` + +> Directly compare two or more UUIDs for < relation based on the +> ordinality semantics defined by [RFC4122:3 RULES FOR LEXICAL +> EQUIVALENCE]. _(function)_ `monotonic-time []` diff --git a/src/clj_uuid.clj b/src/clj_uuid.clj index 65618ec..545cc10 100644 --- a/src/clj_uuid.clj +++ b/src/clj_uuid.clj @@ -1,20 +1,22 @@ (ns clj-uuid - (:refer-clojure :exclude [== uuid?]) - (:require [clj-uuid + (:refer-clojure :exclude [== uuid? max < > =]) + (:require [clojure.core :as cc] + [clj-uuid [constants :refer :all] [util :refer :all] [bitmop :as bitmop] [clock :as clock] - [node :as node]]) - (:import [java.security MessageDigest] - [java.io ByteArrayOutputStream + [node :as node] + [random :as random]]) + (:import [java.io ByteArrayOutputStream ObjectOutputStream] - [java.nio ByteBuffer] + [java.lang IllegalArgumentException] [java.net URI URL] + [java.nio ByteBuffer] + [java.security MessageDigest] [java.util UUID - Date] - [java.lang IllegalArgumentException])) + Date])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Leach-Salz UUID Representation [RFC4122:4.1.2 "LAYOUT AND BYTE ORDER"] ;; @@ -121,6 +123,17 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; The MAX UUID [RFC4122:5.4 "MAX UUID"] ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; "The [max] UUID is a special form of UUID that is specified to have +;; all 128 bits set." + +(def ^:const +max+ #uuid "ffffffff-ffff-ffff-ffff-ffffffffffff") + + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Well-Known UUIDs [RFC4122:Appendix-C "SOME NAMESPACE IDs"] ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -193,23 +206,30 @@ specialized hash computation.") (null? [uuid] - "Return `true` only if `uuid` has all 128 bits set ot zero and is + "Return `true` only if `uuid` has all 128 bits set to zero and is therefore equal to the null UUID, 00000000-0000-0000-0000-000000000000.") + (max? [uuid] + "Return `true` only if `uuid` has all 128 bits set and is + therefore equal to the maximum UUID, ffffffff-ffff-ffff-ffff-ffffffffffff.") + (uuid? [x] "Return `true` if `x` implements an RFC4122 unique identifier.") (uuid= [x y] "Directly compare two UUID's for = relation based on the equality - semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE].") + semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE]. + See: `clj-uuid/=`") (uuid< [x y] "Directly compare two UUID's for < relation based on the ordinality - semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE].") + semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE]. + See: `clj-uuid/<`") (uuid> [x y] "Directly compare two UUID's for > relation based on the ordinality - semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE].") + semantics defined by [RFC4122:3 RULES FOR LEXICAL EQUIVALENCE]. + See: `clj-uuid/>`") (get-word-high [uuid] "Return the most significant 64 bits of UUID's 128 bit value.") @@ -292,7 +312,7 @@ For non-time-based (v3, v4, v5, squuid) UUID's, always returns `nil`.") (get-instant ^java.util.Date [uuid] - "For time-based (v1) UUID's, return a java.util.Date object that represents + "For time-based (v1, v6) UUID's, return a java.util.Date object that represents the system time at which this UUID was generated. NOTE: the returned value may not necessarily be temporally unique. For non-time-based (v3, v4, v5, squuid) UUID's, always returns `nil`.") @@ -342,22 +362,22 @@ (uuid? ^boolean [_] true) - (uuid= ^boolean [^UUID x ^UUID y] + (uuid= ^boolean [^UUID x ^UUID y & more] (.equals x y)) (uuid< ^boolean [^UUID x ^UUID y] (let [xh (.getMostSignificantBits x) yh (.getMostSignificantBits y)] - (or (< xh yh) - (and (= xh yh) (< (.getLeastSignificantBits x) - (.getLeastSignificantBits y)))))) + (or (cc/< xh yh) + (and (cc/= xh yh) (cc/< (.getLeastSignificantBits x) + (.getLeastSignificantBits y)))))) (uuid> ^boolean [^UUID x ^UUID y] (let [xh (.getMostSignificantBits x) yh (.getMostSignificantBits y)] - (or (> xh yh) - (and (= xh yh) (> (.getLeastSignificantBits x) - (.getLeastSignificantBits y)))))) + (or (cc/> xh yh) + (and (cc/= xh yh) (cc/> (.getLeastSignificantBits x) + (.getLeastSignificantBits y)))))) (get-word-high ^long [uuid] (.getMostSignificantBits uuid)) @@ -366,7 +386,10 @@ (.getLeastSignificantBits uuid)) (null? ^boolean [uuid] - (= 0 (.getMostSignificantBits uuid) (.getLeastSignificantBits uuid))) + (cc/= 0 (.getMostSignificantBits uuid) (.getLeastSignificantBits uuid))) + + (max? ^boolean [uuid] + (uuid= uuid +max+)) (to-byte-array ^bytes [uuid] (let [arr (byte-array 16)] @@ -396,16 +419,20 @@ (URI/create (to-urn-string uuid))) (get-time-low ^long [uuid] - (bitmop/ldb #=(bitmop/mask 32 0) - (bit-shift-right (.getMostSignificantBits uuid) 32))) + (let [msb (.getMostSignificantBits uuid)] + (if (cc/= 6 (get-version uuid)) + (bitmop/ldb #=(bitmop/mask 16 0) msb) + (bitmop/ldb #=(bitmop/mask 32 0) (bit-shift-right msb 32))))) (get-time-mid ^long [uuid] (bitmop/ldb #=(bitmop/mask 16 16) (.getMostSignificantBits uuid))) (get-time-high ^long [uuid] - (bitmop/ldb #=(bitmop/mask 16 0) - (.getMostSignificantBits uuid))) + (let [msb (.getMostSignificantBits uuid)] + (if (cc/= 6 (get-version uuid)) + (bitmop/ldb #=(bitmop/mask 32 0) (bit-shift-right msb 32)) + (bitmop/ldb #=(bitmop/mask 16 0) msb)))) (get-clk-low ^long [uuid] (bitmop/ldb #=(bitmop/mask 8 0) @@ -416,7 +443,7 @@ (.getLeastSignificantBits uuid))) (get-clk-seq ^short [uuid] - (when (= 1 (.version uuid)) + (when (#{1 6} (.version uuid)) (.clockSequence uuid))) (get-node-id ^long [uuid] @@ -424,8 +451,12 @@ (.getLeastSignificantBits uuid))) (get-timestamp ^long [uuid] - (when (= 1 (.version uuid)) - (.timestamp uuid))) + (case (.version uuid) + 1 (.timestamp uuid) + 6 (bit-or (get-time-low uuid) + (bit-shift-left (get-time-mid uuid) 12) + (bit-shift-left (get-time-high uuid) 28)) + nil)) (get-instant [uuid] (when-let [ts (get-timestamp uuid)] @@ -457,6 +488,16 @@ [] +null+) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; The MAX UUID Constructor [RFC4122:5.4 "MAX UUID"] ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn max + "Generates the maximum UUID, ffffffff-ffff-ffff-ffff-ffffffffffff." + ^java.util.UUID + [] + +max+) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -488,6 +529,28 @@ (bit-shift-left time-mid 16))] (UUID. msb (node/+v1-lsb+)))) +(defn v6 + "Generate a v6 (time-based), lexically sortable, unique identifier, + guaranteed to be unique and thread-safe regardless of clock + precision or degree of concurrency. Creation of v6 UUID's does not + require any call to a cryptographic generator and can be + accomplished much more efficiently than v3, v4, v5, or squuid's. A + v6 UUID reveals both the identity of the computer that generated the + UUID and the time at which it did so. Its uniqueness across + computers is guaranteed as long as MAC addresses are not + duplicated. Used for compatibility with systems that already use v1; + UUID v7 should be prefered over v1 or v6." + ^java.util.UUID + [] + (let [ts (clock/monotonic-time) + time-high (bitmop/ldb #=(bitmop/mask 32 28) ts) + time-mid (bitmop/ldb #=(bitmop/mask 16 12) ts) + time-low (bitmop/dpb #=(bitmop/mask 4 12) + (bitmop/ldb #=(bitmop/mask 12 0) ts) 0x6) + msb (bit-or time-low + (bit-shift-left time-mid 16) + (bit-shift-left time-high 32))] + (UUID. msb (node/+v1-lsb+)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -601,7 +664,7 @@ (defn- build-digested-uuid ^java.util.UUID [^long version ^bytes arr] - {:pre [(or (= version 3) (= version 5))]} + {:pre [(or (cc/= version 3) (cc/= version 5))]} (let [msb (bitmop/bytes->long arr 0) lsb (bitmop/bytes->long arr 8)] (UUID. @@ -666,11 +729,79 @@ (as-byte-array local-part)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Lexically Sortable UUID [RFC4122:5.2: UUID Version 7];; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn v7 + "Generate a v7 unix time-based, lexically sortable UUID with monotonic + counter and cryptographically secure random portion. The 12 bit + rand_a is used as counter, rand_b is CSPRNG. Random numbers are + generated using the JVM default implementation of + java.security.SecureRandom." + ^java.util.UUID + [] + (let [[t counter] (clock/monotonic-unix-time-and-random-counter) + time (bitmop/ldb #=(bitmop/mask 48 0) t) + ver-and-counter (bitmop/dpb #=(bitmop/mask 4 12) counter 0x7) + msb (bit-or ver-and-counter (bit-shift-left time 16)) + lsb (bitmop/dpb #=(bitmop/mask 2 62) (random/long) 0x2)] + (UUID. msb lsb))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Custom UUID [RFC4122:5.2: UUID Version 8];; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn v8 + "Generate a v8 custom UUID with up to 122 bits of user data." + ^java.util.UUID + [^long msb ^long lsb] + (UUID. + (bitmop/dpb #=(bitmop/mask 4 12) msb 0x8) + (bitmop/dpb #=(bitmop/mask 2 62) lsb 0x2))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Predicates ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn- compare-many + [f x y more] + (if (f x y) + (if (next more) + (recur f y (first more) (next more)) + (f y (first more))) + false)) + +(defn = + "Directly compare two or more UUIDs for = relation based on the + equality semantics defined by [RFC4122:3 RULES FOR LEXICAL + EQUIVALENCE]." + ([_] true) + ([x y] + (uuid= x y)) + ([x y & more] + (compare-many uuid= x y more))) + +(defn > + "Directly compare two or more UUIDs for > relation based on the + ordinality semantics defined by [RFC4122:3 RULES FOR LEXICAL + EQUIVALENCE]." + ([_] true) + ([x y] + (uuid> x y)) + ([x y & more] + (compare-many uuid> x y more))) + +(defn < + "Directly compare two or more UUIDs for < relation based on the + ordinality semantics defined by [RFC4122:3 RULES FOR LEXICAL + EQUIVALENCE]." + ([_] true) + ([x y] + (uuid< x y)) + ([x y & more] + (compare-many uuid< x y more))) + (defn uuid-string? [str] (and (string? str) (some? (re-matches uuid-regex str)))) @@ -680,8 +811,8 @@ (some? (re-matches urn-regex str)))) (defn uuid-vec? [v] - (and (= (count v) 16) - (every? #(and (integer? %) (>= -128 %) (<= 127 %)) v))) + (and (cc/= (count v) 16) + (every? #(and (integer? %) (>= -128 % 127)) v))) @@ -708,7 +839,7 @@ (let [bb (ByteBuffer/wrap ba)] (UUID. (.getLong bb) (.getLong bb)))) (uuidable? [^bytes ba] - (= 16 (alength ^bytes ba))) + (cc/= 16 (alength ^bytes ba))) String (uuidable? ^boolean [s] diff --git a/src/clj_uuid/clock.clj b/src/clj_uuid/clock.clj index 36022a3..400fa48 100644 --- a/src/clj_uuid/clock.clj +++ b/src/clj_uuid/clock.clj @@ -1,5 +1,6 @@ -(ns clj-uuid.clock) - +(ns clj-uuid.clock + (:require [clj-uuid.random :as random])) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Lock-Free, Thread-safe Monotonic Clock ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -12,7 +13,7 @@ ;; Universal time is represented as the number of seconds that have ;; elapsed since 00:00 January 1, 1900 GMT. ;; -;; POSIX time is represented as the number of seconds that have +;; POSIX time is represented as the number of seconds that have ;; elaspsed since 00:00 January 1, 1970 UTC ;; ;; Java time is represented as the difference, measured in milliseconds, @@ -56,10 +57,24 @@ (set! *warn-on-reflection* true) -(def +subcounter-resolution+ 9999) +(def ^:const +subcounter-resolution+ 9999) +(def ^:const +random-counter-resolution+ 0xfff) (deftype State [^short seqid ^long millis]) +(let [-state- (atom (->State 0 0))] + (defn monotonic-unix-time-and-random-counter [] + (let [^State new-state + (swap! -state- + (fn [^State current-state] + (loop [time-now (System/currentTimeMillis)] + (if-not (= (.millis current-state) time-now) + (->State (random/long-12bit) time-now) + (let [tt (.seqid current-state)] + (if (< tt +random-counter-resolution+) + (->State (inc tt) time-now) + (recur (System/currentTimeMillis))))))))] + [(.millis new-state) (.seqid new-state)]))) (let [-state- (atom (->State 0 0))] (defn monotonic-time [] @@ -88,5 +103,5 @@ (universal-time (monotonic-time))) ([^long gregorian] (+ (posix-time gregorian) 2208988800))) - + (set! *warn-on-reflection* false) diff --git a/src/clj_uuid/constants.clj b/src/clj_uuid/constants.clj index f071fb3..b3fc6bb 100644 --- a/src/clj_uuid/constants.clj +++ b/src/clj_uuid/constants.clj @@ -23,3 +23,5 @@ (def ^:const +ub8-mask+ 0x00000000000000ff) (def ^:const +ub4-mask+ 0x000000000000000f) (def ^:const +ub1-mask+ 0x0000000000000001) + +(def ^:const +max-counter+ 0xfff) diff --git a/src/clj_uuid/random.clj b/src/clj_uuid/random.clj new file mode 100644 index 0000000..cfd3ba8 --- /dev/null +++ b/src/clj_uuid/random.clj @@ -0,0 +1,26 @@ +(ns clj-uuid.random + (:require [clj-uuid.bitmop :as bitmop]) + (:import (java.security SecureRandom)) + (:refer-clojure :exclude [bytes long])) + +(defonce ^:private secure-random + (delay (SecureRandom.))) + +(defn bytes + "Generate `n` random bytes." + [n] + (let [bs (byte-array n)] + (.nextBytes ^SecureRandom @secure-random bs) + bs)) + +(defn long + "Generate a long value that is hard to guess. limited to the number of bytes." + ([] + (long 8)) + ([n-bytes] + (reduce (fn [n b] (+ (bit-shift-left n 8) b)) 0 (bytes n-bytes)))) + +(defn long-12bit + "Generate a long value between 0 and 512" + [] + (bit-and (long 2) 0xfff)) diff --git a/src/clj_uuid/util.clj b/src/clj_uuid/util.clj index e69e269..2dd0791 100644 --- a/src/clj_uuid/util.clj +++ b/src/clj_uuid/util.clj @@ -14,7 +14,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PROG1 but with more idiomatic clojure name +;; PROG1 but with more idiomatic clojure name ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defmacro returning @@ -35,7 +35,7 @@ (defmacro compile-if "Evaluate `exp` and if it returns logical true and doesn't error, expand to - `then` otherwise expand to `else`. + `then` otherwise expand to `else`. credit: (compile-if (Class/forName \"java.util.concurrent.ForkJoinTask\") @@ -76,13 +76,13 @@ (defmacro wrap-fn [name args & body] `(let [old-fn# (var-get (var ~name)) - new-fn# (fn [& p#] - (let [~args p#] + new-fn# (fn [& p#] + (let [~args p#] (do ~@body))) wrapper# (fn [& params#] (if (= ~(count args) (count params#)) (apply new-fn# params#) - (apply old-fn# params#)))] + (apply old-fn# params#)))] (alter-var-root (var ~name) (constantly wrapper#)))) @@ -110,7 +110,7 @@ ;; Condition Handling ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defmacro exception [& [param & more :as params]] - (if (class? param) - `(throw (new ~param (str ~@(interpose " " more)))) +(defmacro exception [& [param & more :as params]] + (if (class? param) + `(throw (new ~param (str ~@(interpose " " more)))) `(throw (Exception. (str ~@(interpose " " params)))))) diff --git a/test/clj_uuid/api_test.clj b/test/clj_uuid/api_test.clj index 0410bdb..d683859 100644 --- a/test/clj_uuid/api_test.clj +++ b/test/clj_uuid/api_test.clj @@ -1,7 +1,7 @@ (ns clj-uuid.api-test - (:refer-clojure :exclude [uuid?]) + (:refer-clojure :exclude [uuid? max]) (:require [clojure.test :refer :all] - [clj-uuid :refer :all]) + [clj-uuid :refer :all :exclude [= > <]]) (:import (java.lang IllegalArgumentException))) @@ -77,8 +77,7 @@ (is (= (get-timestamp tmpid) nil)))) (testing "v4 uuid protocol..." - (let [tmpid - (java.util.UUID/fromString "3eb1e29a-4747-4a7d-8e40-94e245f57dc0")] + (let [tmpid #uuid "3eb1e29a-4747-4a7d-8e40-94e245f57dc0"] (is (= (get-word-high tmpid) 4517641053478013565)) (is (= (get-word-low tmpid) -8196387622257066560)) (is (= (null? tmpid) false)) @@ -97,9 +96,95 @@ (is (= (get-clk-low tmpid) 142)) (is (= (get-clk-high tmpid) 64)) (is (= (get-node-id tmpid) 163699557236160)) - (is (= (get-timestamp tmpid) nil))))) + (is (= (get-timestamp tmpid) nil)))) + + (testing "max uuid protocol..." + (let [tmpid +max+] + (is (= (get-word-high tmpid) -1)) + (is (= (get-word-low tmpid) -1)) + (is (= (null? tmpid) false)) + (is (= (max? tmpid) true)) + (is (= (seq (to-byte-array tmpid)) [-1 -1 -1 -1 -1 -1 -1 -1 + -1 -1 -1 -1 -1 -1 -1 -1])) + (is (= (hash-code tmpid) 0)) + (is (= (get-version tmpid) 0xf)) + (is (= (to-string tmpid) "ffffffff-ffff-ffff-ffff-ffffffffffff")) + (is (= + (to-urn-string tmpid) + "urn:uuid:ffffffff-ffff-ffff-ffff-ffffffffffff")) + (is (= (get-time-low tmpid) 0xffffffff)) + (is (= (get-time-mid tmpid) 0xffff)) + (is (= (get-time-high tmpid) 0xffff)) + (is (= (get-clk-low tmpid) 0xff)) + (is (= (get-clk-high tmpid) 0xff)) + (is (= (get-node-id tmpid) 0xffffffffffff)) + (is (= (get-timestamp tmpid) nil)))) + (testing "v6 uuid protocol..." + (let [tmpid #uuid "1ef3f06f-16db-6ff0-bb01-1b50e6f39e7f"] + (is (= (get-word-high tmpid) 2230390600394043376)) + (is (= (get-word-low tmpid) -4971662479354257793)) + (is (= (null? tmpid) false)) + (is (= (max? tmpid) false)) + (is (= (seq (to-byte-array tmpid)) [30 -13 -16 111 22 -37 111 -16 + -69 1 27 80 -26 -13 -98 127])) + (is (= (hash-code tmpid) 1440357040)) + (is (= (get-version tmpid) 6)) + (is (= (to-string tmpid) "1ef3f06f-16db-6ff0-bb01-1b50e6f39e7f")) + (is (= + (to-urn-string tmpid) + "urn:uuid:1ef3f06f-16db-6ff0-bb01-1b50e6f39e7f")) + (is (= (get-time-low tmpid) 0x6ff0)) + (is (= (get-time-mid tmpid) 0x16db)) + (is (= (get-time-high tmpid) 0x1ef3f06f)) + (is (= (get-clk-low tmpid) 0xbb)) + (is (= (get-clk-high tmpid) 0x1)) + (is (= (get-node-id tmpid) 0x1b50e6f39e7f)) + (is (= (get-timestamp tmpid) 0x1ef3f06f16dfff0)))) + + (testing "v7 uuid protocol..." + (let [tmpid #uuid "01909eae-4801-753a-bcd5-0889c34ac129"] + (is (= (get-word-high tmpid) 112764462053815610)) + (is (= (get-word-low tmpid) -4839952836759731927)) + (is (= (null? tmpid) false)) + (is (= (max? tmpid) false)) + (is (= (seq (to-byte-array tmpid)) [1 -112 -98 -82 72 1 117 58 + -68 -43 8 -119 -61 74 -63 41])) + (is (= (hash-code tmpid) 906895924)) + (is (= (get-version tmpid) 7)) + (is (= (to-string tmpid) "01909eae-4801-753a-bcd5-0889c34ac129")) + (is (= + (to-urn-string tmpid) + "urn:uuid:01909eae-4801-753a-bcd5-0889c34ac129")) + (is (= (get-time-low tmpid) 0x01909eae)) + (is (= (get-time-mid tmpid) 0x4801)) + (is (= (get-time-high tmpid) 0x753a)) + (is (= (get-clk-low tmpid) 0xbc)) + (is (= (get-clk-high tmpid) 0xd5)) + (is (= (get-node-id tmpid) 0x0889c34ac129)) + (is (= (get-timestamp tmpid) nil)))) + (testing "v8 uuid protocol..." + (let [tmpid #uuid "ffffffff-ffff-8fff-bfff-ffffffffffff"] + (is (= (get-word-high tmpid) -28673)) + (is (= (get-word-low tmpid) -4611686018427387905)) + (is (= (null? tmpid) false)) + (is (= (max? tmpid) false)) + (is (= (seq (to-byte-array tmpid)) [-1 -1 -1 -1 -1 -1 -113 -1 + -65 -1 -1 -1 -1 -1 -1 -1])) + (is (= (hash-code tmpid) 1073770496)) + (is (= (get-version tmpid) 8)) + (is (= (to-string tmpid) "ffffffff-ffff-8fff-bfff-ffffffffffff")) + (is (= + (to-urn-string tmpid) + "urn:uuid:ffffffff-ffff-8fff-bfff-ffffffffffff")) + (is (= (get-time-low tmpid) 0xffffffff)) + (is (= (get-time-mid tmpid) 0xffff)) + (is (= (get-time-high tmpid) 0x8fff)) + (is (= (get-clk-low tmpid) 0xbf)) + (is (= (get-clk-high tmpid) 0xff)) + (is (= (get-node-id tmpid) 0xffffffffffff)) + (is (= (get-timestamp tmpid) nil))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Predicate Tests diff --git a/test/clj_uuid/clock_test.clj b/test/clj_uuid/clock_test.clj index a9abb76..10bffd7 100644 --- a/test/clj_uuid/clock_test.clj +++ b/test/clj_uuid/clock_test.clj @@ -27,21 +27,38 @@ (map #(apply < %) answers))))))) - - - - - - - - - - - - - - - - - - +(deftest check-monotonic-unix-time-and-random-counter + (doseq [concur (range 5 9)] + (let [extent 100000 + agents (map agent (repeat concur nil)) + working (map #(send-off % + (fn [state] + (repeatedly extent + monotonic-unix-time-and-random-counter))) + agents) + _ (apply await working) + answers (map deref working)] + (testing "single-thread timestamp uniqueness..." + (is (= + (* concur extent) + (apply + (map (comp count set) answers))))) + (testing "concurrent timestamp uniqueness..." + (is (= + (* concur extent) + (count (apply clojure.set/union (map set answers)))))) + (testing "concurrent monotonic increasing..." + (doseq [answer answers] + (let [[time counter] (first answer)] + (loop [time time + counter counter + more (rest answer)] + (when-let [[next-time next-counter] (first more)] + (cond + (< next-time time) + (is false "time must be increasing") + + (and (= next-time time) (<= next-counter counter)) + (is false "counter must be increasing") + + :else + (recur next-time next-counter (rest more))))))))))) diff --git a/test/clj_uuid/v1_test.clj b/test/clj_uuid/v1_test.clj index 82457de..628b422 100644 --- a/test/clj_uuid/v1_test.clj +++ b/test/clj_uuid/v1_test.clj @@ -1,8 +1,8 @@ (ns clj-uuid.v1-test + "Time based UUIDs tests" (:require [clojure.test :refer :all] [clojure.set] - [clj-uuid :refer [v1 get-timestamp]])) - + [clj-uuid :as uuid :refer [v7 v6 v1 get-timestamp]])) (deftest check-v1-concurrency (doseq [concur (range 5 9)] @@ -25,3 +25,47 @@ (testing "concurrent v1 monotonic increasing..." (is (every? identity (map #(apply < (map get-timestamp %)) answers))))))) + +(deftest check-v6-concurrency + (doseq [concur (range 5 9)] + (let [extent 100000 + agents (map agent (repeat concur nil)) + working (map #(send-off % + (fn [state] + (repeatedly extent v6))) + agents) + _ (apply await working) + answers (map deref working)] + (testing "single-thread v6 uuid uniqueness..." + (is (= + (* concur extent) + (apply + (map (comp count set) answers))))) + (testing "concurrent v6 uuid uniqueness..." + (is (= + (* concur extent) + (count (apply clojure.set/union (map set answers)))))) + (testing "concurrent v6 monotonic increasing..." + (is (every? identity + (map uuid/< answers))))))) + +(deftest check-v7-concurrency + (doseq [concur (range 5 9)] + (let [extent 100000 + agents (map agent (repeat concur nil)) + working (map #(send-off % + (fn [state] + (repeatedly extent v7))) + agents) + _ (apply await working) + answers (map deref working)] + (testing "single-thread v7 uuid uniqueness..." + (is (= + (* concur extent) + (apply + (map (comp count set) answers))))) + (testing "concurrent v7 uuid uniqueness..." + (is (= + (* concur extent) + (count (apply clojure.set/union (map set answers)))))) + (testing "concurrent v7 monotonic increasing..." + (is (every? identity + (map uuid/< answers))))))) diff --git a/test/clj_uuid/v3_test.clj b/test/clj_uuid/v3_test.clj index 63776fb..0601ce2 100644 --- a/test/clj_uuid/v3_test.clj +++ b/test/clj_uuid/v3_test.clj @@ -1,7 +1,7 @@ (ns clj-uuid.v3-test - (:refer-clojure :exclude [uuid?]) + (:refer-clojure :exclude [uuid? max]) (:require [clojure.test :refer :all] - [clj-uuid :refer :all])) + [clj-uuid :refer :all :exclude [> < =]])) (deftest check-v3-special-cases @@ -336,4 +336,3 @@ (testing "v3 oid-ns case-based correctness..." (doseq [case +v3-oid-ns-cases+] (is (= (second case) (v3 +namespace-oid+ (first case))))))) - diff --git a/test/clj_uuid/v4_test.clj b/test/clj_uuid/v4_test.clj index c7b9ced..578d30b 100644 --- a/test/clj_uuid/v4_test.clj +++ b/test/clj_uuid/v4_test.clj @@ -1,7 +1,8 @@ (ns clj-uuid.v4-test - (:refer-clojure :exclude [uuid?]) + "Custom UUIDs tests" + (:refer-clojure :exclude [uuid? max]) (:require [clojure.test :refer :all] - [clj-uuid :refer :all])) + [clj-uuid :refer :all :exclude [> < =]])) (deftest check-v4-special-cases @@ -11,3 +12,11 @@ (is (= (v4 0 -1) #uuid "00000000-0000-4000-bfff-ffffffffffff")) (is (= (v4 -1 0) #uuid "ffffffff-ffff-4fff-8000-000000000000")) (is (= (v4 -1 -1) #uuid "ffffffff-ffff-4fff-bfff-ffffffffffff")))) + +(deftest check-v8-special-cases + (testing "v8 custom UUID" + (is (= (v8 0 0) #uuid "00000000-0000-8000-8000-000000000000")) + (is (= (v8 0 1) #uuid "00000000-0000-8000-8000-000000000001")) + (is (= (v8 0 -1) #uuid "00000000-0000-8000-bfff-ffffffffffff")) + (is (= (v8 -1 0) #uuid "ffffffff-ffff-8fff-8000-000000000000")) + (is (= (v8 -1 -1) #uuid "ffffffff-ffff-8fff-bfff-ffffffffffff")))) diff --git a/test/clj_uuid/v5_test.clj b/test/clj_uuid/v5_test.clj index bcf7c1c..153fb82 100644 --- a/test/clj_uuid/v5_test.clj +++ b/test/clj_uuid/v5_test.clj @@ -1,7 +1,7 @@ (ns clj-uuid.v5-test - (:refer-clojure :exclude [uuid?]) + (:refer-clojure :exclude [uuid? max]) (:require [clojure.test :refer :all] - [clj-uuid :refer :all])) + [clj-uuid :refer :all :exclude [> < =]]))