Skip to content

Commit 67a9ad8

Browse files
CopilotDaanV2
andauthored
Add comprehensive RFC4122/RFC9562 compliance tests and fix V7 epoch bug (#46)
* Initial plan * Add comprehensive RFC4122 and RFC9562 compliance tests Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com> * Fix V7 UUID to use Unix milliseconds per RFC9562 instead of FileTime Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com> * Address code review feedback: improve performance and test precision Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com> * Fix macOS test failures for time-based UUID uniqueness tests Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com> * Use batch functions for time-based UUID uniqueness tests per feedback Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com>
1 parent ce106aa commit 67a9ad8

6 files changed

Lines changed: 608 additions & 13 deletions

File tree

Library/Static Classes/V7/V7 - Const.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ public static partial class V7 {
1010

1111
private static readonly Vector128<Byte> _VersionMask = Format.VersionVariantMaskNot(V7.Version, V7.Variant);
1212
private static readonly Vector128<Byte> _VersionOverlay = Format.VersionVariantOverlayer(V7.Version, V7.Variant);
13+
14+
/// <summary>Unix epoch: January 1, 1970 00:00:00 UTC</summary>
15+
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
16+
17+
/// <summary>Maximum value that can fit in 48 bits (for RFC9562 V7 timestamp)</summary>
18+
private const UInt64 Max48BitValue = 0xFFFFFFFFFFFF;
1319
}
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
namespace DaanV2.UUID;
22

33
public static partial class V7 {
4-
/// <summary>Extracts the UTC from the UUID</summary>
5-
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
6-
/// <returns>The UTC value</returns>
4+
/// <summary>Extracts the Unix milliseconds timestamp from the UUID</summary>
5+
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
6+
/// <returns>Unix timestamp in milliseconds</returns>
77
public static UInt64 ExtractUtc(UUID uuid) {
88
(UInt64 bits48, _, _) = Format.Extract(uuid);
99

1010
return bits48;
1111
}
1212

1313
/// <summary>Extracts the datetime from the UUID</summary>
14-
/// <param name="uuid">The <see cref="UUID"/> to extract the UTC from</param>
14+
/// <param name="uuid">The <see cref="UUID"/> to extract the timestamp from</param>
1515
/// <returns>The <see cref="DateTime"/></returns>
1616
public static DateTime Extract(UUID uuid) {
17-
UInt64 fileUTC = ExtractUtc(uuid);
17+
UInt64 unixMs = ExtractUtc(uuid);
1818

19-
return DateTime.FromFileTimeUtc((Int64)fileUTC);
19+
return UnixMillisecondsToDateTime(unixMs);
20+
}
21+
22+
/// <summary>Converts Unix milliseconds to DateTime</summary>
23+
/// <param name="unixMs">Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</param>
24+
/// <returns>The corresponding DateTime in UTC</returns>
25+
private static DateTime UnixMillisecondsToDateTime(UInt64 unixMs) {
26+
try {
27+
return UnixEpoch.AddMilliseconds(unixMs);
28+
}
29+
catch (ArgumentOutOfRangeException) {
30+
// If the milliseconds value is too large, return MaxValue
31+
return DateTime.MaxValue;
32+
}
2033
}
2134
}

Library/Static Classes/V7/V7 - Generate.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,32 @@ public static UUID Generate(UInt64 utc, ReadOnlySpan<Byte> bytes10) {
6565

6666
/// <inheritdoc cref="Generate()"/>
6767
public static UUID Generate(DateTime timestamp, UInt16 randomA, UInt64 randomB) {
68-
UInt64 t = (UInt64)timestamp.ToFileTimeUtc();
68+
// RFC9562: V7 uses Unix epoch timestamp in milliseconds (48 bits)
69+
UInt64 unixMs = DateTimeToUnixMilliseconds(timestamp);
6970

70-
return Generate(t, randomA, randomB);
71+
return Generate(unixMs, randomA, randomB);
7172
}
7273

7374
/// <inheritdoc cref="Generate()"/>
74-
public static UUID Generate(UInt64 utc, UInt16 randomA, UInt64 randomB) {
75-
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, utc, randomA, randomB);
75+
/// <param name="unixMs">Unix timestamp in milliseconds (48-bit value)</param>
76+
/// <param name="randomA">12 bits of random data</param>
77+
/// <param name="randomB">62 bits of random data</param>
78+
public static UUID Generate(UInt64 unixMs, UInt16 randomA, UInt64 randomB) {
79+
Vector128<Byte> u = Format.Create(V7.Version, V7.Variant, unixMs, randomA, randomB);
7680

7781
return new UUID(u);
7882
}
83+
84+
/// <summary>Converts a DateTime to Unix milliseconds</summary>
85+
/// <param name="timestamp">The DateTime to convert</param>
86+
/// <returns>Milliseconds since Unix epoch (1970-01-01 00:00:00 UTC)</returns>
87+
private static UInt64 DateTimeToUnixMilliseconds(DateTime timestamp) {
88+
var milliseconds = (timestamp.ToUniversalTime() - UnixEpoch).TotalMilliseconds;
89+
90+
// Ensure non-negative and within 48-bit range
91+
if (milliseconds < 0) milliseconds = 0;
92+
if (milliseconds > Max48BitValue) milliseconds = Max48BitValue;
93+
94+
return (UInt64)milliseconds;
95+
}
7996
}

Tests/Generation/V7Tests.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ public sealed partial class V7Tests {
55
// From https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-example-of-a-uuidv7-value
66
[Fact(DisplayName = "Given a known example, will generate the expected UUID")]
77
public void TestVector() {
8-
var timestamp = DateTime.FromFileTimeUtc(1645557742000);
8+
// RFC9562 test vector: UUID 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
9+
// First 48 bits are Unix milliseconds: 0x017f22e279b0 = 1645557742000
10+
// This is 2022-02-22 19:22:22 UTC
11+
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
12+
var timestamp = unixEpoch.AddMilliseconds(1645557742000);
13+
914
UInt16 randA = (UInt16)0xCC3;
1015
UInt64 randB = (UInt64)0x18C4DC0C0C07398F;
1116

1217
UUID u = V7.Generate(timestamp, randA, randB);
1318

1419
Assert.Equal("017f22e2-79b0-7cc3-98c4-dc0c0c07398f", u.ToString());
1520

16-
Int64 extractedUTC = (Int64)V7.ExtractUtc(u);
17-
Assert.Equal(timestamp.ToFileTimeUtc(), extractedUTC);
21+
UInt64 extractedMs = V7.ExtractUtc(u);
22+
Assert.Equal(1645557742000UL, extractedMs);
1823

1924
DateTime extracted = V7.Extract(u);
2025
Assert.Equal(timestamp, extracted);

Tests/RFC/RFC4122.cs

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using DaanV2.UUID;
2+
using System.Text;
23

34
namespace Tests.RFC;
45

@@ -12,4 +13,253 @@ public void RFC4122_AppendixB_V3_MD5_DNS_ExampleCom() {
1213
Utility.ValidateUUID(uuid, V3.Version, V3.Variant);
1314
}
1415

16+
// ========== Bit-Level Validation Tests ==========
17+
18+
[Fact(DisplayName = "RFC4122 Section 4.1.3 - Version bits must be in correct position")]
19+
public void RFC4122_VersionBits_CorrectPosition() {
20+
// Version field is in octet 6, bits 12-15 of time_hi_and_version
21+
var v1uuid = V1.Generate();
22+
var v3uuid = V3.Generate("test");
23+
var v4uuid = V4.Generate();
24+
var v5uuid = V5.Generate("test");
25+
26+
// Version should be readable from the UUID
27+
Assert.Equal(DaanV2.UUID.Version.V1, v1uuid.Version);
28+
Assert.Equal(DaanV2.UUID.Version.V3, v3uuid.Version);
29+
Assert.Equal(DaanV2.UUID.Version.V4, v4uuid.Version);
30+
Assert.Equal(DaanV2.UUID.Version.V5, v5uuid.Version);
31+
}
32+
33+
[Fact(DisplayName = "RFC4122 Section 4.1.1 - Variant bits must be 10x for RFC4122 UUIDs")]
34+
public void RFC4122_VariantBits_RFC4122Compliant() {
35+
// Variant field is in octet 8, bits 6-7 of clock_seq_hi_and_reserved
36+
// Must be 10xxxxxx (0x80-0xBF) for RFC4122 compliant UUIDs
37+
var v1uuid = V1.Generate();
38+
var v3uuid = V3.Generate("test");
39+
var v4uuid = V4.Generate();
40+
var v5uuid = V5.Generate("test");
41+
42+
// All should have variant V1 (RFC4122)
43+
Assert.Equal(Variant.V1, v1uuid.Variant);
44+
Assert.Equal(Variant.V1, v3uuid.Variant);
45+
Assert.Equal(Variant.V1, v4uuid.Variant);
46+
Assert.Equal(Variant.V1, v5uuid.Variant);
47+
}
48+
49+
// ========== Nil and Max UUID Tests (RFC4122 Section 4.1.7) ==========
50+
51+
[Fact(DisplayName = "RFC4122 Section 4.1.7 - Nil UUID is all zeros")]
52+
public void RFC4122_NilUUID_AllZeros() {
53+
var nilUuid = UUID.Zero;
54+
Assert.Equal("00000000-0000-0000-0000-000000000000", nilUuid.ToString().ToLower());
55+
}
56+
57+
[Fact(DisplayName = "RFC4122 - Max UUID is all ones")]
58+
public void RFC4122_MaxUUID_AllOnes() {
59+
var maxUuid = UUID.Max;
60+
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", maxUuid.ToString().ToLower());
61+
}
62+
63+
// ========== Determinism Tests for Name-Based UUIDs ==========
64+
65+
[Fact(DisplayName = "RFC4122 Section 4.3 - V3 (MD5) is deterministic")]
66+
public void RFC4122_V3_Deterministic() {
67+
// Same input should always produce the same UUID
68+
var input = "www.example.com";
69+
var uuid1 = V3.Generate(input);
70+
var uuid2 = V3.Generate(input);
71+
var uuid3 = V3.Generate(input);
72+
73+
Assert.Equal(uuid1, uuid2);
74+
Assert.Equal(uuid2, uuid3);
75+
Assert.Equal(uuid1.ToString(), uuid2.ToString());
76+
}
77+
78+
[Fact(DisplayName = "RFC4122 Section 4.3 - V5 (SHA1) is deterministic")]
79+
public void RFC4122_V5_Deterministic() {
80+
// Same input should always produce the same UUID
81+
var input = "www.example.com";
82+
var uuid1 = V5.Generate(input);
83+
var uuid2 = V5.Generate(input);
84+
var uuid3 = V5.Generate(input);
85+
86+
Assert.Equal(uuid1, uuid2);
87+
Assert.Equal(uuid2, uuid3);
88+
Assert.Equal(uuid1.ToString(), uuid2.ToString());
89+
}
90+
91+
[Fact(DisplayName = "RFC4122 - V3 different inputs produce different UUIDs")]
92+
public void RFC4122_V3_DifferentInputs_DifferentUUIDs() {
93+
var uuid1 = V3.Generate("input1");
94+
var uuid2 = V3.Generate("input2");
95+
var uuid3 = V3.Generate("input3");
96+
97+
Assert.NotEqual(uuid1, uuid2);
98+
Assert.NotEqual(uuid2, uuid3);
99+
Assert.NotEqual(uuid1, uuid3);
100+
}
101+
102+
[Fact(DisplayName = "RFC4122 - V5 different inputs produce different UUIDs")]
103+
public void RFC4122_V5_DifferentInputs_DifferentUUIDs() {
104+
var uuid1 = V5.Generate("input1");
105+
var uuid2 = V5.Generate("input2");
106+
var uuid3 = V5.Generate("input3");
107+
108+
Assert.NotEqual(uuid1, uuid2);
109+
Assert.NotEqual(uuid2, uuid3);
110+
Assert.NotEqual(uuid1, uuid3);
111+
}
112+
113+
// ========== Round-Trip Tests ==========
114+
115+
[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V1")]
116+
public void RFC4122_V1_RoundTrip() {
117+
var original = V1.Generate();
118+
var str = original.ToString();
119+
var parsed = new UUID(str);
120+
121+
Assert.Equal(original, parsed);
122+
Assert.Equal(original.Version, parsed.Version);
123+
Assert.Equal(original.Variant, parsed.Variant);
124+
}
125+
126+
[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V3")]
127+
public void RFC4122_V3_RoundTrip() {
128+
var original = V3.Generate("test");
129+
var str = original.ToString();
130+
var parsed = new UUID(str);
131+
132+
Assert.Equal(original, parsed);
133+
Assert.Equal(original.Version, parsed.Version);
134+
Assert.Equal(original.Variant, parsed.Variant);
135+
}
136+
137+
[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V4")]
138+
public void RFC4122_V4_RoundTrip() {
139+
var original = V4.Generate();
140+
var str = original.ToString();
141+
var parsed = new UUID(str);
142+
143+
Assert.Equal(original, parsed);
144+
Assert.Equal(original.Version, parsed.Version);
145+
Assert.Equal(original.Variant, parsed.Variant);
146+
}
147+
148+
[Fact(DisplayName = "RFC4122 - Parse and ToString round-trip for V5")]
149+
public void RFC4122_V5_RoundTrip() {
150+
var original = V5.Generate("test");
151+
var str = original.ToString();
152+
var parsed = new UUID(str);
153+
154+
Assert.Equal(original, parsed);
155+
Assert.Equal(original.Version, parsed.Version);
156+
Assert.Equal(original.Variant, parsed.Variant);
157+
}
158+
159+
// ========== Format Tests ==========
160+
161+
[Fact(DisplayName = "RFC4122 Section 3 - UUID format is 8-4-4-4-12 hexadecimal")]
162+
public void RFC4122_Format_Correct() {
163+
var uuid = V4.Generate();
164+
var str = uuid.ToString();
165+
166+
// Check format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
167+
Assert.Equal(36, str.Length);
168+
Assert.Equal('-', str[8]);
169+
Assert.Equal('-', str[13]);
170+
Assert.Equal('-', str[18]);
171+
Assert.Equal('-', str[23]);
172+
173+
// All other characters should be valid hex
174+
var hexParts = str.Split('-');
175+
Assert.Equal(5, hexParts.Length);
176+
Assert.Equal(8, hexParts[0].Length);
177+
Assert.Equal(4, hexParts[1].Length);
178+
Assert.Equal(4, hexParts[2].Length);
179+
Assert.Equal(4, hexParts[3].Length);
180+
Assert.Equal(12, hexParts[4].Length);
181+
}
182+
183+
// ========== V1 Time-Based Tests ==========
184+
185+
[Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains timestamp")]
186+
public void RFC4122_V1_ContainsTimestamp() {
187+
var beforeGen = DateTime.UtcNow;
188+
var uuid = V1.Generate();
189+
var afterGen = DateTime.UtcNow;
190+
191+
var info = V1.Extract(uuid);
192+
193+
// Extracted timestamp should be within reasonable range
194+
Assert.True(info.Timestamp >= beforeGen.AddSeconds(-1));
195+
Assert.True(info.Timestamp <= afterGen.AddSeconds(1));
196+
}
197+
198+
[Fact(DisplayName = "RFC4122 Section 4.2.1 - V1 contains MAC address")]
199+
public void RFC4122_V1_ContainsMacAddress() {
200+
var uuid = V1.Generate();
201+
var info = V1.Extract(uuid);
202+
203+
// MAC address should be 6 bytes
204+
Assert.NotNull(info.MacAddress);
205+
Assert.Equal(6, info.MacAddress.Length);
206+
}
207+
208+
// ========== Uniqueness Tests ==========
209+
210+
[Fact(DisplayName = "RFC4122 Section 4.4 - V4 generates unique UUIDs")]
211+
public void RFC4122_V4_Uniqueness() {
212+
var uuids = new HashSet<UUID>();
213+
const int count = 10000;
214+
215+
for (int i = 0; i < count; i++) {
216+
var uuid = V4.Generate();
217+
Assert.True(uuids.Add(uuid), $"Duplicate UUID generated: {uuid}");
218+
}
219+
220+
Assert.Equal(count, uuids.Count);
221+
}
222+
223+
[Fact(DisplayName = "RFC4122 - V1 generates unique UUIDs")]
224+
public void RFC4122_V1_Uniqueness() {
225+
const int count = 1000;
226+
227+
// Use batch function which properly handles timestamp incrementing
228+
var uuids = V1.Batch(count);
229+
var uniqueUuids = new HashSet<UUID>(uuids);
230+
231+
// All UUIDs should be unique when using batch generation
232+
Assert.Equal(count, uniqueUuids.Count);
233+
}
234+
235+
// ========== Encoding Tests ==========
236+
237+
[Fact(DisplayName = "RFC4122 - V3 with different encodings")]
238+
public void RFC4122_V3_DifferentEncodings() {
239+
var input = "test string";
240+
var uuidUtf8 = V3.Generate(input, Encoding.UTF8);
241+
var uuidAscii = V3.Generate(input, Encoding.ASCII);
242+
243+
// Same string with same encoding should produce same UUID
244+
var uuidUtf8_2 = V3.Generate(input, Encoding.UTF8);
245+
Assert.Equal(uuidUtf8, uuidUtf8_2);
246+
247+
// For ASCII-compatible strings, UTF8 and ASCII should be the same
248+
Assert.Equal(uuidUtf8, uuidAscii);
249+
}
250+
251+
[Fact(DisplayName = "RFC4122 - V5 with different encodings")]
252+
public void RFC4122_V5_DifferentEncodings() {
253+
var input = "test string";
254+
var uuidUtf8 = V5.Generate(input, Encoding.UTF8);
255+
var uuidAscii = V5.Generate(input, Encoding.ASCII);
256+
257+
// Same string with same encoding should produce same UUID
258+
var uuidUtf8_2 = V5.Generate(input, Encoding.UTF8);
259+
Assert.Equal(uuidUtf8, uuidUtf8_2);
260+
261+
// For ASCII-compatible strings, UTF8 and ASCII should be the same
262+
Assert.Equal(uuidUtf8, uuidAscii);
263+
}
264+
15265
}

0 commit comments

Comments
 (0)