| /* | |
| * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. | |
| * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. | |
| */ | |
| package test.uuid | |
| import kotlin.comparisons.naturalOrder | |
| import kotlin.random.Random | |
| import kotlin.test.* | |
| import kotlin.time.Clock | |
| import kotlin.time.Duration.Companion.milliseconds | |
| import kotlin.time.Instant | |
| import kotlin.uuid.Uuid | |
| class UuidTest { | |
| private val mostSignificantBits = 0x550e8400e29b41d4uL | |
| private val leastSignificantBits = 0xa716446655440000uL | |
| private val uuidByteArray = byteArrayOf( | |
| 0x55, 0x0e, 0x84.toByte(), 0x00, 0xe2.toByte(), 0x9b.toByte(), 0x41, 0xd4.toByte(), | |
| 0xa7.toByte(), 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00 | |
| ) | |
| private val uuid = Uuid.fromULongs(mostSignificantBits, leastSignificantBits) | |
| private val uuidString = "550e8400-e29b-41d4-a716-446655440000" | |
| private val uuidHexString = "550e8400e29b41d4a716446655440000" | |
| private val uuidStringNil = "00000000-0000-0000-0000-000000000000" | |
| private val uuidHexStringNil = "00000000000000000000000000000000" | |
| private val uuidMax = Uuid.fromLongs(-1, -1) | |
| private val uuidStringMax = "ffffffff-ffff-ffff-ffff-ffffffffffff" | |
| private val uuidHexStringMax = "ffffffffffffffffffffffffffffffff" | |
| private val Uuid.isIetfVariant: Boolean | |
| get() = toULongs { _, leastSignificantBits -> (leastSignificantBits shr 62) == 2uL } | |
| private val Uuid.version: Int | |
| get() = toULongs { mostSignificantBits, _ -> ((mostSignificantBits shr 12) and 0xFuL).toInt() } | |
| @Test | |
| fun fromLongs() { | |
| assertEquals( | |
| uuidString, | |
| Uuid.fromLongs(mostSignificantBits.toLong(), leastSignificantBits.toLong()).toString() | |
| ) | |
| assertSame( | |
| Uuid.NIL, | |
| Uuid.fromLongs(0, 0) | |
| ) | |
| assertEquals( | |
| uuidStringMax, | |
| Uuid.fromLongs(-1, -1).toString() | |
| ) | |
| } | |
| @Test | |
| fun fromULongs() { | |
| assertEquals( | |
| uuidString, | |
| Uuid.fromULongs(mostSignificantBits, leastSignificantBits).toString() | |
| ) | |
| assertSame( | |
| Uuid.NIL, | |
| Uuid.fromULongs(0uL, 0uL) | |
| ) | |
| assertEquals( | |
| uuidStringMax, | |
| Uuid.fromULongs(ULong.MAX_VALUE, ULong.MAX_VALUE).toString() | |
| ) | |
| } | |
| @Test | |
| fun fromByteArray() { | |
| assertEquals( | |
| uuidString, | |
| Uuid.fromByteArray(uuidByteArray).toString() | |
| ) | |
| assertSame( | |
| Uuid.NIL, | |
| Uuid.fromByteArray(ByteArray(16)) | |
| ) | |
| assertEquals( | |
| uuidStringMax, | |
| Uuid.fromByteArray(ByteArray(16) { -1 }).toString() | |
| ) | |
| // Illegal ByteArray size | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromByteArray(ByteArray(0)) } | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromByteArray(ByteArray(15)) } | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromByteArray(ByteArray(17)) } | |
| // Truncating the illegal ByteArray | |
| val byteArray = ByteArray(32) { it.toByte() } | |
| val byteArrayContent = byteArray.joinToString() | |
| assertFailsWith<IllegalArgumentException> { | |
| Uuid.fromByteArray(byteArray) | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was [$byteArrayContent] of size 32") | |
| } | |
| assertFailsWith<IllegalArgumentException> { | |
| Uuid.fromByteArray(byteArray + 32) | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was [$byteArrayContent, ...] of size 33") | |
| } | |
| } | |
| @Test | |
| fun fromUByteArray() { | |
| assertEquals( | |
| uuidString, | |
| Uuid.fromUByteArray(uuidByteArray.asUByteArray()).toString() | |
| ) | |
| assertSame( | |
| Uuid.NIL, | |
| Uuid.fromUByteArray(UByteArray(16)) | |
| ) | |
| assertEquals( | |
| uuidStringMax, | |
| Uuid.fromUByteArray(UByteArray(16) { 0xFFu }).toString() | |
| ) | |
| // Illegal UByteArray size | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromUByteArray(UByteArray(0)) } | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromUByteArray(UByteArray(15)) } | |
| assertFailsWith<IllegalArgumentException> { Uuid.fromUByteArray(UByteArray(17)) } | |
| // Truncating the illegal UByteArray | |
| val ubyteArray = UByteArray(32) { it.toUByte() } | |
| val ubyteArrayContent = ubyteArray.joinToString() | |
| assertFailsWith<IllegalArgumentException> { | |
| Uuid.fromUByteArray(ubyteArray) | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was [$ubyteArrayContent] of size 32") | |
| } | |
| assertFailsWith<IllegalArgumentException> { | |
| Uuid.fromUByteArray(ubyteArray + 32.toUByte()) | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was [$ubyteArrayContent, ...] of size 33") | |
| } | |
| } | |
| private fun String.mixedcase(): String = map { | |
| if (Random.nextBoolean()) it.uppercase() else it.lowercase() | |
| }.joinToString("") | |
| @Test | |
| fun parse() { | |
| fun test(hexDash: String? = null, hex: String? = null, check: (parse: () -> Uuid) -> Unit) { | |
| require(hexDash != null || hex != null) | |
| if (hexDash != null) { | |
| check { Uuid.parseHexDash(hexDash) } | |
| check { Uuid.parse(hexDash) } | |
| check { Uuid.parseHexDashOrNull(hexDash)!! } | |
| check { Uuid.parseOrNull(hexDash)!! } | |
| } | |
| if (hex != null) { | |
| check { Uuid.parseHex(hex) } | |
| check { Uuid.parse(hex) } | |
| check { Uuid.parseHexOrNull(hex)!! } | |
| check { Uuid.parseOrNull(hex)!! } | |
| } | |
| } | |
| // lower case | |
| test(uuidString, uuidHexString) { assertEquals(uuid, it()) } | |
| test(uuidStringNil, uuidHexStringNil) { assertSame(Uuid.NIL, it()) } | |
| test(uuidStringMax, uuidHexStringMax) { assertEquals(uuidMax, it()) } | |
| // upper case | |
| test(uuidString.uppercase(), uuidHexString.uppercase()) { assertEquals(uuid, it()) } | |
| test(uuidStringMax.uppercase(), uuidHexStringMax.uppercase()) { assertEquals(uuidMax, it()) } | |
| // mixed case | |
| test(uuidString.mixedcase(), uuidHexString.mixedcase()) { assertEquals(uuid, it()) } | |
| test(uuidStringMax.mixedcase(), uuidHexStringMax.mixedcase()) { assertEquals(uuidMax, it()) } | |
| } | |
| @Test | |
| fun parseInvalid() { | |
| fun test(hexDash: String? = null, hex: String? = null, check: (parse: () -> Uuid) -> Unit) { | |
| require(hexDash != null || hex != null) | |
| if (hexDash != null) { | |
| check { Uuid.parseHexDash(hexDash) } | |
| check { Uuid.parse(hexDash) } | |
| assertNull(Uuid.parseHexDashOrNull(hexDash)) | |
| assertNull(Uuid.parseOrNull(hexDash)) | |
| } | |
| if (hex != null) { | |
| check { Uuid.parseHex(hex) } | |
| check { Uuid.parse(hex) } | |
| assertNull(Uuid.parseHexOrNull(hex)) | |
| assertNull(Uuid.parseOrNull(hex)) | |
| } | |
| } | |
| // Illegal String format | |
| assertFailsWith<IllegalArgumentException> { Uuid.parseHexDash(uuidHexString) } | |
| assertNull(Uuid.parseHexDashOrNull(uuidHexString)) | |
| assertFailsWith<IllegalArgumentException> { Uuid.parseHex(uuidString) } | |
| assertNull(Uuid.parseHexOrNull(uuidString)) | |
| test("$uuidString-", "$uuidHexString-") { assertFailsWith<IllegalArgumentException> { it() } } | |
| test("-$uuidString", "-$uuidHexString") { assertFailsWith<IllegalArgumentException> { it() } } | |
| test("${uuidString}0", "${uuidHexString}0") { assertFailsWith<IllegalArgumentException> { it() } } | |
| test("0${uuidString}", "0${uuidHexString}") { assertFailsWith<IllegalArgumentException> { it() } } | |
| test(uuidString.replace("d", "g"), uuidHexString.replace("d", "g")) { assertFailsWith<IllegalArgumentException> { it() } } | |
| test(uuidString.drop(1), uuidHexString.drop(1)) { assertFailsWith<IllegalArgumentException> { it() } } | |
| test(uuidString.dropLast(1), uuidHexString.dropLast(1)) { assertFailsWith<IllegalArgumentException> { it() } } | |
| for (i in uuidString.indices) { | |
| if (uuidString[i] == '-') { | |
| test(hexDash = uuidString.substring(0..<i) + "+" + uuidString.substring(i + 1)) { | |
| assertFailsWith<IllegalArgumentException> { | |
| it() | |
| }.also { exception -> | |
| assertEquals("Expected '-' (hyphen) at index $i, but was '+'", exception.message) | |
| } | |
| } | |
| test(hexDash = uuidString.substring(0..<i) + "0" + uuidString.substring(i + 1)) { | |
| assertFailsWith<IllegalArgumentException> { | |
| it() | |
| }.also { exception -> | |
| assertEquals("Expected '-' (hyphen) at index $i, but was '0'", exception.message) | |
| } | |
| } | |
| } | |
| } | |
| // Truncating the illegal String | |
| val longString = "x".repeat(64) | |
| test(longString, longString) { | |
| assertFailsWith<IllegalArgumentException> { | |
| it() | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was \"$longString\" of length 64") | |
| } | |
| } | |
| test(longString + "0", longString + "0") { | |
| assertFailsWith<IllegalArgumentException> { | |
| it() | |
| }.also { exception -> | |
| assertContains(exception.message!!, "but was \"$longString...\" of length 65") | |
| } | |
| } | |
| } | |
| @Test | |
| fun random() { | |
| val set = hashSetOf<Uuid>() | |
| repeat(10) { | |
| val randomUuid = Uuid.random() | |
| assertTrue(randomUuid.isIetfVariant) | |
| assertEquals(4, randomUuid.version) | |
| assertFalse(set.contains(randomUuid)) | |
| set.add(randomUuid) | |
| } | |
| } | |
| @Test | |
| fun lexicalOrder() { | |
| val maxUuid = Uuid.parse(uuidStringMax) | |
| // TODO: Once LEXICAL_ORDER is dropped, test Uuid.compareTo directly instead | |
| @Suppress("DEPRECATION_ERROR") | |
| for (lexicalOrder in listOf(Uuid.LEXICAL_ORDER, naturalOrder())) { | |
| for (id in listOf(uuid, Uuid.NIL, maxUuid)) { | |
| assertEquals(0, lexicalOrder.compare(id, id), id.toString()) | |
| } | |
| assertTrue(lexicalOrder.compare(Uuid.NIL, uuid) < 0) | |
| assertTrue(lexicalOrder.compare(Uuid.NIL, maxUuid) < 0) | |
| assertTrue(lexicalOrder.compare(uuid, Uuid.NIL) > 0) | |
| assertTrue(lexicalOrder.compare(uuid, maxUuid) < 0) | |
| assertTrue(lexicalOrder.compare(maxUuid, Uuid.NIL) > 0) | |
| assertTrue(lexicalOrder.compare(maxUuid, uuid) > 0) | |
| } | |
| } | |
| @Test | |
| fun toLongs() { | |
| val onceInPlace: Boolean | |
| val version = uuid.toLongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(this.mostSignificantBits.toLong(), mostSignificantBits) | |
| assertEquals(this.leastSignificantBits.toLong(), leastSignificantBits) | |
| onceInPlace = true | |
| ((mostSignificantBits shr 12) and 0xF).toInt() | |
| } | |
| assertTrue(onceInPlace) | |
| assertEquals(uuid.version, version) | |
| Uuid.NIL.toLongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(0, mostSignificantBits) | |
| assertEquals(0, leastSignificantBits) | |
| } | |
| Uuid.parse(uuidStringMax).toLongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(-1, mostSignificantBits) | |
| assertEquals(-1, leastSignificantBits) | |
| } | |
| } | |
| @Test | |
| fun toULongs() { | |
| val onceInPlace: Boolean | |
| val isIetfVariant = uuid.toULongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(this.mostSignificantBits, mostSignificantBits) | |
| assertEquals(this.leastSignificantBits, leastSignificantBits) | |
| onceInPlace = true | |
| (leastSignificantBits shr 62) == 2uL | |
| } | |
| assertTrue(onceInPlace) | |
| assertEquals(uuid.isIetfVariant, isIetfVariant) | |
| Uuid.NIL.toULongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(0uL, mostSignificantBits) | |
| assertEquals(0uL, leastSignificantBits) | |
| } | |
| Uuid.parse(uuidStringMax).toULongs { mostSignificantBits, leastSignificantBits -> | |
| assertEquals(ULong.MAX_VALUE, mostSignificantBits) | |
| assertEquals(ULong.MAX_VALUE, leastSignificantBits) | |
| } | |
| } | |
| @Test | |
| fun toStringTest() { | |
| assertEquals(uuidString, uuid.toString()) | |
| assertEquals(uuidStringNil, Uuid.NIL.toString()) | |
| assertEquals(uuidStringMax, Uuid.parse(uuidStringMax).toString()) | |
| } | |
| @Test | |
| fun toHexDashString() { | |
| assertEquals(uuidString, uuid.toHexDashString()) | |
| assertEquals(uuidStringNil, Uuid.NIL.toHexDashString()) | |
| assertEquals(uuidStringMax, Uuid.parse(uuidStringMax).toHexDashString()) | |
| } | |
| @Test | |
| fun toHexString() { | |
| assertEquals(uuidHexString, uuid.toHexString()) | |
| assertEquals(uuidHexStringNil, Uuid.NIL.toHexString()) | |
| assertEquals(uuidHexStringMax, Uuid.parse(uuidStringMax).toHexString()) | |
| } | |
| @Test | |
| fun toByteArray() { | |
| assertContentEquals(uuidByteArray, uuid.toByteArray()) | |
| assertContentEquals(ByteArray(16), Uuid.NIL.toByteArray()) | |
| assertContentEquals(ByteArray(16) { -1 }, Uuid.parse(uuidStringMax).toByteArray()) | |
| } | |
| @Test | |
| fun toUByteArray() { | |
| assertContentEquals(uuidByteArray.asUByteArray(), uuid.toUByteArray()) | |
| assertContentEquals(UByteArray(16), Uuid.NIL.toUByteArray()) | |
| assertContentEquals(UByteArray(16) { 0xFFu }, Uuid.parse(uuidStringMax).toUByteArray()) | |
| } | |
| @Test | |
| fun testEquals() { | |
| assertEquals(uuid, Uuid.parse(uuidString)) | |
| assertEquals(Uuid.NIL, Uuid.parse(uuidStringNil)) | |
| assertEquals(Uuid.fromLongs(-1, -1), Uuid.parse(uuidStringMax)) | |
| } | |
| @Test | |
| fun testHashCode() { | |
| assertEquals(uuid.hashCode(), Uuid.parse(uuidString).hashCode()) | |
| assertEquals(Uuid.NIL.hashCode(), Uuid.parse(uuidStringNil).hashCode()) | |
| assertEquals(Uuid.fromLongs(-1, -1).hashCode(), Uuid.parse(uuidStringMax).hashCode()) | |
| } | |
| @Test | |
| fun testV7UuidStructure() { | |
| val uuid = Uuid.generateV7() | |
| assertEquals(7, uuid.version, "Generated Uuid has a wrong version: ${uuid.toHexDashString()}") | |
| assertTrue(uuid.isIetfVariant, "Generated Uuid has a wrong variant: ${uuid.toHexDashString()}") | |
| } | |
| @Test | |
| fun testConsequentV7Uuids() { | |
| val uuids = Array(100) { Uuid.generateV7() } | |
| for (idx in 1 until uuids.size) { | |
| val prev = uuids[idx - 1] | |
| val curr = uuids[idx] | |
| assertTrue( | |
| prev < curr, | |
| "Uuids has to be monotonic, but following two are not: ${prev.toHexDashString()} and ${curr.toHexDashString()}" | |
| ) | |
| } | |
| } | |
| @Test | |
| fun testV7UuidGenerationForNonMonotonicClock() { | |
| var clock: NonMonotonicClock | |
| var baseUuid: Uuid | |
| // wait until we get a counter value small enough for the test | |
| while (true) { | |
| clock = NonMonotonicClock() | |
| baseUuid = Uuid.generateV7(clock) | |
| if (baseUuid.mostSignificantBits.and(0x03FFL) <= 0x03FAL) { | |
| break | |
| } | |
| } | |
| fun checkUuidsMonotonicity(prev: Uuid, curr: Uuid) { | |
| assertTrue( | |
| prev < curr, | |
| "Uuids should be monotonic, but they are not: ${prev.toHexDashString()}, ${curr.toHexDashString()}" | |
| ) | |
| assertTrue(prev.leastSignificantBits != curr.leastSignificantBits) | |
| assertEquals(1, curr.mostSignificantBits - prev.mostSignificantBits) | |
| } | |
| // clocks are frozen, they will return the same timestamp and Uuids MSB should differ by 1 | |
| val nextUuid = Uuid.generateV7(clock) | |
| checkUuidsMonotonicity(baseUuid, nextUuid) | |
| // tick one ms back | |
| // clocks return a timestamp from the past, so we have to increment the counter field (rand_a) again | |
| clock.inc(-1L) | |
| val uuidFromPast = Uuid.generateV7(clock) | |
| checkUuidsMonotonicity(nextUuid, uuidFromPast) | |
| // unfreeze the clocks | |
| // clocks are no longer frozen, so timestamps should be different, as well as counter field's (rand_a) values | |
| clock.inc(42L) | |
| val uuidFromTheFuture = Uuid.generateV7(clock) | |
| assertTrue( | |
| uuidFromPast < uuidFromTheFuture, | |
| "Uuids should be monotonic, but they are not: ${uuidFromPast.toHexDashString()}, ${uuidFromTheFuture.toHexDashString()}" | |
| ) | |
| assertTrue( | |
| uuidFromPast.mostSignificantBits.and(0x03FFL.inv()) < uuidFromTheFuture.mostSignificantBits.and(0x03FFL.inv()), | |
| "Uuids should have different timestamps, but they do not: ${uuidFromPast.toHexDashString()}, ${uuidFromTheFuture.toHexDashString()}" | |
| ) | |
| // While it is significantly low, | |
| // the probability of a collision when generating rand_a part is non-zero (it should be 1 / (2^12) = 1 / 4096). | |
| // Thus, the check may occasionally fail. | |
| // Instead, we check that rand_a differs for a set of Uuids generated with different timestamps. | |
| // | |
| // assertTrue( | |
| // uuidFromPast.mostSignificantBits.and(0x03FFL) != uuidFromTheFuture.mostSignificantBits.and(0x03FFL), | |
| // "Uuids should have different rand_a values, but they do not: ${uuidFromPast.toHexDashString()}, ${uuidFromTheFuture.toHexDashString()}" | |
| // ) | |
| val randAValues = mutableSetOf<Int>() | |
| repeat(1000) { | |
| clock.inc(1) | |
| val uuid = Uuid.generateV7(clock) | |
| randAValues += uuid.mostSignificantBits.and(0x03FFL).toInt() | |
| } | |
| assertTrue(randAValues.size > 1, "All rand_a values are equal: ${randAValues.first()}") | |
| // now let's check that generation won't stuck if clocks will stuck forever | |
| var previousUuid = Uuid.generateV7(clock) | |
| // we're using 12-bit wide counter, so let's generate more uuids | |
| repeat(1.shl(14)) { | |
| val uuid = Uuid.generateV7(clock) | |
| assertTrue( | |
| previousUuid < uuid, | |
| "Uuids should be monotonic, but they are not: ${previousUuid.toHexDashString()}, ${uuid.toHexDashString()}" | |
| ) | |
| previousUuid = uuid | |
| } | |
| } | |
| @Test | |
| fun testV7GenerateUnordered() { | |
| val uuid = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(0L)) | |
| assertEquals(7, uuid.version, "Generated Uuid has a wrong version: ${uuid.toHexDashString()}") | |
| assertTrue(uuid.isIetfVariant, "Generated Uuid has a wrong variant: ${uuid.toHexDashString()}") | |
| uuid.toLongs { msb, _ -> | |
| assertEquals(msb.shr(16), 0L, "Timestamp field has a wrong value") | |
| } | |
| val anotherUuid = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(0L)) | |
| assertNotEquals(uuid, anotherUuid, "Two uuids generated for the same timestamp should not be equal") | |
| val tsValue = 1757440583123L | |
| val nonZeroTs = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(tsValue)) | |
| nonZeroTs.toLongs { msb, _ -> | |
| assertEquals(msb.shr(16), tsValue, "Timestamp field has a wrong value") | |
| } | |
| } | |
| private class NonMonotonicClock : Clock { | |
| private var currentTime = Clock.System.now() | |
| override fun now(): Instant = currentTime | |
| fun inc(millis: Long = 1L) { | |
| currentTime = currentTime + millis.milliseconds | |
| } | |
| } | |
| } |