@@ -871,6 +871,206 @@ def test_uuid6_test_vectors(self):
871871 equal ((u .int >> 80 ) & 0xffff , 0x232a )
872872 equal ((u .int >> 96 ) & 0xffff_ffff , 0x1ec9_414c )
873873
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+
8741074 def test_uuid8 (self ):
8751075 equal = self .assertEqual
8761076 u = self .uuid .uuid8 ()
0 commit comments