clojuretest.check

How to generate UUIDs that can work with test.check in Clojure


Generative testing seems interesting, but I needed to generate random UUIDs as part of the testing. java.util.UUID/newRandom doesn't play nice with test.check shrinking.

The java code looks like:

public static UUID randomUUID()
{
  long lsb = r.nextLong();
  long msb = r.nextLong();

  lsb &= 0x3FFFFFFFFFFFFFFFL;
  lsb |= 0x8000000000000000L; // set top two bits to variant 2

  msb &= 0xFFFFFFFFFFFF0FFFL;
  msb |= 0x4000; // Version 4;

  return new UUID( msb, lsb );
}

Which is trickier to translate to Clojure than it would seem.

How do I write a random UUID function in Clojure that can be successfully shrunk?


Solution

  • A fn that takes two longs and generates a proper type 4 UUID is:

    (defn make-uuid [[msb lsb]]
      (java.util.UUID. (-> msb
                       (bit-clear 15)
                       (bit-set   14)
                       (bit-clear 13)
                       (bit-clear 12))
                   (-> lsb
                       (bit-set   63)
                       (bit-clear 62))))
    

    You can use a regex to check the result, (need to convert it to a string first).

    (def uuid-v4-regex
         #"(?i)[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[98ab][a-f0-9]{3}-[a-f0-9]{12}")
    

    Then you can test it as following:

    (def uuids (gen/fmap make-uuid (gen/tuple (gen/choose 0 Long/MAX_VALUE)
                                              (gen/choose 0 Long/MAX_VALUE))))
    
    (defspec check-random-uuid 100000
      (for-all [uuid uuids]
           (re-find uuid-v4-regex (str uuid))))
    

    And the tests look like:

    (check-random-uuid)
    => {:result true, :num-tests 100000, :seed 1422050154338}
    

    Just for fun, I removed one of the valid characters (9) for the second field, and this is what a failing test looks like, so you can see how shrinking from :fail to :smallest can help.

    (pp/pprint (check-random-uuid))
    {:result nil,
     :seed 1422050276824,
     :failing-size 2,
     :num-tests 3,
     :fail [#uuid "2c6d1442-eec3-4800-972e-02905c1b3c00"],
     :shrunk
     {:total-nodes-visited 932,
      :depth 29,
      :result nil,
      :smallest [#uuid "00000000-0000-4000-9000-000000000000"]}}
    

    Which shows you how much noise shrinking can remove from your test case.