@@ -871,6 +871,206 @@ def test_uuid6_test_vectors(self):
871
871
equal ((u .int >> 80 ) & 0xffff , 0x232a )
872
872
equal ((u .int >> 96 ) & 0xffff_ffff , 0x1ec9_414c )
873
873
874
+ def test_uuid7 (self ):
875
+ equal = self .assertEqual
876
+ u = self .uuid .uuid7 ()
877
+ equal (u .variant , self .uuid .RFC_4122 )
878
+ equal (u .version , 7 )
879
+
880
+ # 1 Jan 2023 12:34:56.123_456_789
881
+ timestamp_ns = 1672533296_123_456_789 # ns precision
882
+ timestamp_ms , _ = divmod (timestamp_ns , 1_000_000 )
883
+
884
+ for _ in range (100 ):
885
+ counter_hi = random .getrandbits (11 )
886
+ counter_lo = random .getrandbits (30 )
887
+ counter = (counter_hi << 30 ) | counter_lo
888
+
889
+ tail = random .getrandbits (32 )
890
+ # effective number of bits is 32 + 30 + 11 = 73
891
+ random_bits = counter << 32 | tail
892
+
893
+ # set all remaining MSB of fake random bits to 1 to ensure that
894
+ # the implementation correctly removes them
895
+ random_bits = (((1 << 7 ) - 1 ) << 73 ) | random_bits
896
+ random_data = random_bits .to_bytes (10 )
897
+
898
+ with (
899
+ mock .patch .multiple (
900
+ self .uuid ,
901
+ _last_timestamp_v7 = None ,
902
+ _last_counter_v7 = 0 ,
903
+ ),
904
+ mock .patch ('time.time_ns' , return_value = timestamp_ns ),
905
+ mock .patch ('os.urandom' , return_value = random_data ) as urand
906
+ ):
907
+ u = self .uuid .uuid7 ()
908
+ urand .assert_called_once_with (10 )
909
+ equal (u .variant , self .uuid .RFC_4122 )
910
+ equal (u .version , 7 )
911
+
912
+ equal (self .uuid ._last_timestamp_v7 , timestamp_ms )
913
+ equal (self .uuid ._last_counter_v7 , counter )
914
+
915
+ unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff
916
+ equal ((u .int >> 80 ) & 0xffff_ffff_ffff , unix_ts_ms )
917
+
918
+ equal ((u .int >> 75 ) & 1 , 0 ) # check that the MSB is 0
919
+ equal ((u .int >> 64 ) & 0xfff , counter_hi )
920
+ equal ((u .int >> 32 ) & 0x3fff_ffff , counter_lo )
921
+ equal (u .int & 0xffff_ffff , tail )
922
+
923
+ def test_uuid7_uniqueness (self ):
924
+ # Test that UUIDv7-generated values are unique.
925
+ #
926
+ # While UUIDv8 has an entropy of 122 bits, those 122 bits may not
927
+ # necessarily be sampled from a PRNG. On the other hand, UUIDv7
928
+ # uses os.urandom() as a PRNG which features better randomness.
929
+ N = 1000
930
+ uuids = {self .uuid .uuid7 () for _ in range (N )}
931
+ self .assertEqual (len (uuids ), N )
932
+
933
+ versions = {u .version for u in uuids }
934
+ self .assertSetEqual (versions , {7 })
935
+
936
+ def test_uuid7_monotonicity (self ):
937
+ equal = self .assertEqual
938
+
939
+ us = [self .uuid .uuid7 () for _ in range (10_000 )]
940
+ equal (us , sorted (us ))
941
+
942
+ with mock .patch .multiple (
943
+ self .uuid ,
944
+ _last_timestamp_v7 = 0 ,
945
+ _last_counter_v7 = 0 ,
946
+ ):
947
+ # 1 Jan 2023 12:34:56.123_456_789
948
+ timestamp_ns = 1672533296_123_456_789 # ns precision
949
+ timestamp_ms , _ = divmod (timestamp_ns , 1_000_000 )
950
+
951
+ # counter_{hi,lo} are chosen so that "counter + 1" does not overflow
952
+ counter_hi = random .getrandbits (11 )
953
+ counter_lo = random .getrandbits (29 )
954
+ counter = (counter_hi << 30 ) | counter_lo
955
+ self .assertLess (counter + 1 , 0x3ff_ffff_ffff )
956
+
957
+ tail = random .getrandbits (32 )
958
+ random_bits = counter << 32 | tail
959
+ random_data = random_bits .to_bytes (10 )
960
+
961
+ with (
962
+ mock .patch ('time.time_ns' , return_value = timestamp_ns ),
963
+ mock .patch ('os.urandom' , return_value = random_data ) as urand
964
+ ):
965
+ u1 = self .uuid .uuid7 ()
966
+ urand .assert_called_once_with (10 )
967
+ equal (self .uuid ._last_timestamp_v7 , timestamp_ms )
968
+ equal (self .uuid ._last_counter_v7 , counter )
969
+ equal ((u1 .int >> 64 ) & 0xfff , counter_hi )
970
+ equal ((u1 .int >> 32 ) & 0x3fff_ffff , counter_lo )
971
+ equal (u1 .int & 0xffff_ffff , tail )
972
+
973
+ # 1 Jan 2023 12:34:56.123_457_032 (same millisecond but not same ns)
974
+ next_timestamp_ns = 1672533296_123_457_032
975
+ next_timestamp_ms , _ = divmod (timestamp_ns , 1_000_000 )
976
+ equal (timestamp_ms , next_timestamp_ms )
977
+
978
+ next_tail_bytes = os .urandom (4 )
979
+ next_fail = int .from_bytes (next_tail_bytes )
980
+
981
+ with (
982
+ mock .patch ('time.time_ns' , return_value = next_timestamp_ns ),
983
+ mock .patch ('os.urandom' , return_value = next_tail_bytes ) as urand
984
+ ):
985
+ u2 = self .uuid .uuid7 ()
986
+ urand .assert_called_once_with (4 )
987
+ # same milli-second
988
+ equal (self .uuid ._last_timestamp_v7 , timestamp_ms )
989
+ # 42-bit counter advanced by 1
990
+ equal (self .uuid ._last_counter_v7 , counter + 1 )
991
+ equal ((u2 .int >> 64 ) & 0xfff , counter_hi )
992
+ equal ((u2 .int >> 32 ) & 0x3fff_ffff , counter_lo + 1 )
993
+ equal (u2 .int & 0xffff_ffff , next_fail )
994
+
995
+ self .assertLess (u1 , u2 )
996
+
997
+ def test_uuid7_timestamp_backwards (self ):
998
+ equal = self .assertEqual
999
+ # 1 Jan 2023 12:34:56.123_456_789
1000
+ timestamp_ns = 1672533296_123_456_789 # ns precision
1001
+ timestamp_ms , _ = divmod (timestamp_ns , 1_000_000 )
1002
+ fake_last_timestamp_v7 = timestamp_ms + 1
1003
+
1004
+ # counter_{hi,lo} are chosen so that "counter + 1" does not overflow
1005
+ counter_hi = random .getrandbits (11 )
1006
+ counter_lo = random .getrandbits (29 )
1007
+ counter = (counter_hi << 30 ) | counter_lo
1008
+ self .assertLess (counter + 1 , 0x3ff_ffff_ffff )
1009
+
1010
+ tail_bytes = os .urandom (4 )
1011
+ tail = int .from_bytes (tail_bytes )
1012
+
1013
+ with (
1014
+ mock .patch .multiple (
1015
+ self .uuid ,
1016
+ _last_timestamp_v7 = fake_last_timestamp_v7 ,
1017
+ _last_counter_v7 = counter ,
1018
+ ),
1019
+ mock .patch ('time.time_ns' , return_value = timestamp_ns ),
1020
+ mock .patch ('os.urandom' , return_value = tail_bytes ) as urand
1021
+ ):
1022
+ u = self .uuid .uuid7 ()
1023
+ urand .assert_called_once_with (4 )
1024
+ equal (u .variant , self .uuid .RFC_4122 )
1025
+ equal (u .version , 7 )
1026
+ equal (self .uuid ._last_timestamp_v7 , fake_last_timestamp_v7 + 1 )
1027
+ unix_ts_ms = (fake_last_timestamp_v7 + 1 ) & 0xffff_ffff_ffff
1028
+ equal ((u .int >> 80 ) & 0xffff_ffff_ffff , unix_ts_ms )
1029
+ # 42-bit counter advanced by 1
1030
+ equal (self .uuid ._last_counter_v7 , counter + 1 )
1031
+ equal ((u .int >> 64 ) & 0xfff , counter_hi )
1032
+ # 42-bit counter advanced by 1 (counter_hi is untouched)
1033
+ equal ((u .int >> 32 ) & 0x3fff_ffff , counter_lo + 1 )
1034
+ equal (u .int & 0xffff_ffff , tail )
1035
+
1036
+ def test_uuid7_overflow_counter (self ):
1037
+ equal = self .assertEqual
1038
+ # 1 Jan 2023 12:34:56.123_456_789
1039
+ timestamp_ns = 1672533296_123_456_789 # ns precision
1040
+ timestamp_ms , _ = divmod (timestamp_ns , 1_000_000 )
1041
+
1042
+ new_counter_hi = random .getrandbits (11 )
1043
+ new_counter_lo = random .getrandbits (30 )
1044
+ new_counter = (new_counter_hi << 30 ) | new_counter_lo
1045
+
1046
+ tail = random .getrandbits (32 )
1047
+ random_bits = (new_counter << 32 ) | tail
1048
+ random_data = random_bits .to_bytes (10 )
1049
+
1050
+ with (
1051
+ mock .patch .multiple (
1052
+ self .uuid ,
1053
+ _last_timestamp_v7 = timestamp_ms ,
1054
+ # same timestamp, but force an overflow on the counter
1055
+ _last_counter_v7 = 0x3ff_ffff_ffff ,
1056
+ ),
1057
+ mock .patch ('time.time_ns' , return_value = timestamp_ns ),
1058
+ mock .patch ('os.urandom' , return_value = random_data ) as urand
1059
+ ):
1060
+ u = self .uuid .uuid7 ()
1061
+ urand .assert_called_with (10 )
1062
+ equal (u .variant , self .uuid .RFC_4122 )
1063
+ equal (u .version , 7 )
1064
+ # timestamp advanced due to overflow
1065
+ equal (self .uuid ._last_timestamp_v7 , timestamp_ms + 1 )
1066
+ unix_ts_ms = (timestamp_ms + 1 ) & 0xffff_ffff_ffff
1067
+ equal ((u .int >> 80 ) & 0xffff_ffff_ffff , unix_ts_ms )
1068
+ # counter overflowed, so we picked a new one
1069
+ equal (self .uuid ._last_counter_v7 , new_counter )
1070
+ equal ((u .int >> 64 ) & 0xfff , new_counter_hi )
1071
+ equal ((u .int >> 32 ) & 0x3fff_ffff , new_counter_lo )
1072
+ equal (u .int & 0xffff_ffff , tail )
1073
+
874
1074
def test_uuid8 (self ):
875
1075
equal = self .assertEqual
876
1076
u = self .uuid .uuid8 ()
0 commit comments