test.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # Licensed under the Mozilla Public Licence 2.0.
  2. # https://www.mozilla.org/en-US/MPL/2.0
  3. import uuid
  4. import slugid
  5. def testEncode():
  6. """ Test that we can correctly encode a "non-nice" uuid (with first bit
  7. set) to its known slug. The specific uuid was chosen since it has a slug
  8. which contains both `-` and `_` characters."""
  9. # 10000000010011110011111111001000110111111100101101001011000001101000100111111011101011101111101011010101111000011000011101010100....
  10. # <8 ><0 ><4 ><f ><3 ><f ><c ><8 ><d ><f ><c ><b ><4 ><b ><0 ><6 ><8 ><9 ><f ><b ><a ><e ><f ><a ><d ><5 ><e ><1 ><8 ><7 ><5 ><4 >
  11. # < g >< E >< 8 >< _ >< y >< N >< _ >< L >< S >< w >< a >< J >< - >< 6 >< 7 >< 6 >< 1 >< e >< G >< H >< V >< A >
  12. uuid_ = uuid.UUID('{804f3fc8-dfcb-4b06-89fb-aefad5e18754}')
  13. expectedSlug = 'gE8_yN_LSwaJ-6761eGHVA'
  14. actualSlug = slugid.encode(uuid_)
  15. assert expectedSlug == actualSlug, "UUID not correctly encoded into slug: '" + expectedSlug + "' != '" + actualSlug + "'"
  16. def testDecode():
  17. """ Test that we can decode a "non-nice" slug (first bit of uuid is set)
  18. that begins with `-`"""
  19. # 11111011111011111011111011111011111011111011111001000011111011111011111111111111111111111111111111111111111111111111111111111101....
  20. # <f ><b ><e ><f ><b ><e ><f ><b ><e ><f ><b ><e ><4 ><3 ><e ><f ><b ><f ><f ><f ><f ><f ><f ><f ><f ><f ><f ><f ><f ><f ><f ><d >
  21. # < - >< - >< - >< - >< - >< - >< - >< - >< Q >< - >< - >< - >< _ >< _ >< _ >< _ >< _ >< _ >< _ >< _ >< _ >< Q >
  22. slug = '--------Q--__________Q'
  23. expectedUuid = uuid.UUID('{fbefbefb-efbe-43ef-bfff-fffffffffffd}')
  24. actualUuid = slugid.decode(slug)
  25. assert expectedUuid == actualUuid, "Slug not correctly decoded into uuid: '" + str(expectedUuid) + "' != '" + str(actualUuid) + "'"
  26. def testUuidEncodeDecode():
  27. """ Test that 10000 v4 uuids are unchanged after encoding and then decoding them"""
  28. for i in range(0, 10000):
  29. uuid1 = uuid.uuid4()
  30. slug = slugid.encode(uuid1)
  31. uuid2 = slugid.decode(slug)
  32. assert uuid1 == uuid2, "Encode and decode isn't identity: '" + str(uuid1) + "' != '" + str(uuid2) + "'"
  33. def testSlugDecodeEncode():
  34. """ Test that 10000 v4 slugs are unchanged after decoding and then encoding them."""
  35. for i in range(0, 10000):
  36. slug1 = slugid.v4()
  37. uuid_ = slugid.decode(slug1)
  38. slug2 = slugid.encode(uuid_)
  39. assert slug1 == slug2, "Decode and encode isn't identity"
  40. def testSpreadNice():
  41. """ Make sure that all allowed characters can appear in all allowed
  42. positions within the "nice" slug. In this test we generate over a thousand
  43. slugids, and make sure that every possible allowed character per position
  44. appears at least once in the sample of all slugids generated. We also make
  45. sure that no other characters appear in positions in which they are not
  46. allowed.
  47. base 64 encoding char -> value:
  48. ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
  49. 0 1 2 3 4 5 6
  50. 0123456789012345678901234567890123456789012345678901234567890123
  51. e.g. from this we can see 'j' represents 35 in base64
  52. The following comments show the 128 bits of the v4 uuid in binary, hex and
  53. base 64 encodings. The 6 fixed bits (`0`/`1`) according to RFC 4122, plus
  54. the first (most significant) fixed bit (`0`) are shown among the 121
  55. arbitrary value bits (`.`/`x`). The `x` means the same as `.` but just
  56. highlights which bits are grouped together for the respective encoding.
  57. schema:
  58. <..........time_low............><...time_mid...><time_hi_+_vers><clk_hi><clk_lo><.....................node.....................>
  59. bin: 0xxx............................................0100............10xx............................................................
  60. hex: $A <01><02><03><04><05><06><07><08><09><10><11> 4 <13><14><15> $B <17><18><19><20><21><22><23><24><25><26><27><28><29><30><31>
  61. => $A in {0, 1, 2, 3, 4, 5, 6, 7} (0b0xxx)
  62. => $B in {8, 9, A, B} (0b10xx)
  63. bin: 0xxxxx..........................................0100xx......xxxx10............................................................xx0000
  64. b64: $C < 01 >< 02 >< 03 >< 04 >< 05 >< 06 >< 07 > $D < 09 > $E < 11 >< 12 >< 13 >< 14 >< 15 >< 16 >< 17 >< 18 >< 19 >< 20 > $F
  65. => $C in {A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f} (0b0xxxxx)
  66. => $D in {Q, R, S, T} (0b0100xx)
  67. => $E in {C, G, K, O, S, W, a, e, i, m, q, u, y, 2, 6, -} (0bxxxx10)
  68. => $F in {A, Q, g, w} (0bxx0000)"""
  69. charsAll = ''.join(sorted('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'))
  70. # 0 - 31: 0b0xxxxx
  71. charsC = ''.join(sorted('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef'))
  72. # 16, 17, 18, 19: 0b0100xx
  73. charsD = ''.join(sorted('QRST'))
  74. # 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62: 0bxxxx10
  75. charsE = ''.join(sorted('CGKOSWaeimquy26-'))
  76. # 0, 16, 32, 48: 0bxx0000
  77. charsF = ''.join(sorted('AQgw'))
  78. expected = [charsC, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsD, charsAll, charsE, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsF]
  79. spreadTest(slugid.nice, expected)
  80. def testSpreadV4():
  81. """ This test is the same as niceSpreadTest but for slugid.v4() rather than
  82. slugid.nice(). The only difference is that a v4() slug can start with any of
  83. the base64 characters since the first six bits of the uuid are random."""
  84. charsAll = ''.join(sorted('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'))
  85. # 16, 17, 18, 19: 0b0100xx
  86. charsD = ''.join(sorted('QRST'))
  87. # 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62: 0bxxxx10
  88. charsE = ''.join(sorted('CGKOSWaeimquy26-'))
  89. # 0, 16, 32, 48: 0bxx0000
  90. charsF = ''.join(sorted('AQgw'))
  91. expected = [charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsD, charsAll, charsE, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsAll, charsF]
  92. spreadTest(slugid.v4, expected)
  93. def spreadTest(generator, expected):
  94. """ `spreadTest` runs a test against the `generator` function, to check that
  95. when calling it 64*40 times, the range of characters per string position it
  96. returns matches the array `expected`, where each entry in `expected` is a
  97. string of all possible characters that should appear in that position in the
  98. string, at least once in the sample of 64*40 responses from the `generator`
  99. function"""
  100. # k is an array which stores which characters were found at which
  101. # positions. It has one entry per slugid character, therefore 22 entries.
  102. # Each entry is a dict with a key for each character found, and its value
  103. # as the number of times that character appeared at that position in the
  104. # slugid in the large sample of slugids generated in this test.
  105. k = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
  106. # Generate a large sample of slugids, and record what characters appeared
  107. # where... A monte-carlo test has demonstrated that with 64 * 20
  108. # iterations, no failure occurred in 1000 simulations, so 64 * 40 should be
  109. # suitably large to rule out false positives.
  110. for i in range(0, 64 * 40):
  111. slug = generator()
  112. assert len(slug) == 22
  113. for j in range(0, 22):
  114. if slug[j] in k[j]:
  115. k[j][slug[j]] = k[j][slug[j]] + 1
  116. else:
  117. k[j][slug[j]] = 1
  118. # Compose results into an array `actual`, for comparison with `expected`
  119. actual = []
  120. for j in range(0, len(k)):
  121. actual.append('')
  122. for a in k[j].keys():
  123. if k[j][a] > 0:
  124. actual[j] += a
  125. # sort for easy comparison
  126. actual[j] = ''.join(sorted(actual[j]))
  127. assert arraysEqual(expected, actual), "In a large sample of generated slugids, the range of characters found per character position in the sample did not match expected results.\n\nExpected: " + str(expected) + "\n\nActual: " + str(actual)
  128. def arraysEqual(a, b):
  129. """ returns True if arrays a and b are equal"""
  130. return cmp(a, b) == 0