Skip to content

Commit 1261aed

Browse files
committed
CLJS-2845: [spec] generate random subsets of or'd required keys in map specs
1 parent 7187f8f commit 1261aed

File tree

3 files changed

+95
-15
lines changed

3 files changed

+95
-15
lines changed

src/main/cljs/cljs/spec/alpha.cljs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,35 @@
378378
(explain* pred path (if-let [name (spec-name pred)] (conj via name) via) in v)
379379
[{:path path :pred form :val v :via via :in in}])))
380380

381+
(declare ^{:arglists '([s] [min-count s])} or-k-gen
382+
^{:arglists '([s])} and-k-gen)
383+
384+
(defn- k-gen
385+
"returns a generator for form f, which can be a keyword or a list
386+
starting with 'or or 'and."
387+
[f]
388+
(cond
389+
(keyword? f) (gen/return f)
390+
(= 'or (first f)) (or-k-gen 1 (rest f))
391+
(= 'and (first f)) (and-k-gen (rest f))))
392+
393+
(defn- or-k-gen
394+
"returns a tuple generator made up of generators for a random subset
395+
of min-count (default 0) to all elements in s."
396+
([s] (or-k-gen 0 s))
397+
([min-count s]
398+
(gen/bind (gen/tuple
399+
(gen/choose min-count (count s))
400+
(gen/shuffle (map k-gen s)))
401+
(fn [[n gens]]
402+
(apply gen/tuple (take n gens))))))
403+
404+
(defn- and-k-gen
405+
"returns a tuple generator made up of generators for every element
406+
in s."
407+
[s]
408+
(apply gen/tuple (map k-gen s)))
409+
381410
(defn ^:skip-wiki map-spec-impl
382411
"Do not call this directly, use 'spec' with a map argument"
383412
[{:keys [req-un opt-un keys-pred pred-exprs opt-keys req-specs req req-keys opt-specs pred-forms opt gfn]
@@ -438,21 +467,26 @@
438467
(if gfn
439468
(gfn)
440469
(let [rmap (inck rmap id)
441-
gen (fn [k s] (gensub s overrides (conj path k) rmap k))
470+
rgen (fn [k s] [k (gensub s overrides (conj path k) rmap k)])
442471
ogen (fn [k s]
443472
(when-not (recur-limit? rmap id path k)
444473
[k (gen/delay (gensub s overrides (conj path k) rmap k))]))
445-
req-gens (map gen req-keys req-specs)
446-
opt-gens (remove nil? (map ogen opt-keys opt-specs))]
447-
(when (every? identity (concat req-gens opt-gens))
448-
(let [reqs (zipmap req-keys req-gens)
449-
opts (into {} opt-gens)]
450-
(gen/bind (gen/choose 0 (count opts))
451-
#(let [args (concat (seq reqs) (when (seq opts) (shuffle (seq opts))))]
452-
(->> args
453-
(take (c/+ % (count reqs)))
454-
(apply concat)
455-
(apply gen/hash-map)))))))))
474+
reqs (map rgen req-keys req-specs)
475+
opts (remove nil? (map ogen opt-keys opt-specs))]
476+
(when (every? identity (concat (map second reqs) (map second opts)))
477+
(gen/bind
478+
(gen/tuple
479+
(and-k-gen req)
480+
(or-k-gen opt)
481+
(and-k-gen req-un)
482+
(or-k-gen opt-un))
483+
(fn [[req-ks opt-ks req-un-ks opt-un-ks]]
484+
(let [qks (flatten (concat req-ks opt-ks))
485+
unqks (map (comp keyword name) (flatten (concat req-un-ks opt-un-ks)))]
486+
(->> (into reqs opts)
487+
(filter #((set (concat qks unqks)) (first %)))
488+
(apply concat)
489+
(apply gen/hash-map)))))))))
456490
(with-gen* [_ gfn] (map-spec-impl (assoc argm :gfn gfn)))
457491
(describe* [_] (cons `keys
458492
(cond-> []

src/main/cljs/cljs/spec/gen/alpha.cljs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
(ns cljs.spec.gen.alpha
1010
(:refer-clojure :exclude [boolean cat hash-map list map not-empty set vector
11-
char double int keyword symbol string uuid delay])
11+
char double int keyword symbol string uuid delay shuffle])
1212
(:require-macros [cljs.core :as c]
1313
[cljs.spec.gen.alpha :as gen :refer [dynaload lazy-combinators lazy-prims]])
1414
(:require [cljs.core :as c])
@@ -69,7 +69,7 @@
6969

7070
(lazy-combinators hash-map list map not-empty set vector vector-distinct fmap elements
7171
bind choose one-of such-that tuple sample return
72-
large-integer* double* frequency)
72+
large-integer* double* frequency shuffle)
7373

7474
(lazy-prims any any-printable boolean char char-alpha char-alphanumeric char-ascii double
7575
int keyword keyword-ns large-integer ratio simple-type simple-type-printable
@@ -160,7 +160,7 @@ gen-builtins
160160
:b (gen-for-pred keyword?)}
161161
opts {:c (gen-for-pred string?)}]
162162
(generate (bind (choose 0 (count opts))
163-
#(let [args (concat (seq reqs) (shuffle (seq opts)))]
163+
#(let [args (concat (seq reqs) (c/shuffle (seq opts)))]
164164
(->> args
165165
(take (+ % (count reqs)))
166166
(mapcat identity)

src/test/cljs/cljs/spec_test.cljs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,52 @@
312312
(is (= ::ABC (s/def ::ABC nil)))
313313
(is (nil? (s/get-spec ::ABC))))
314314

315+
;; TODO replace this with a generative test once we have specs for s/keys
316+
(deftest map-spec-generators
317+
(s/def ::a nat-int?)
318+
(s/def ::b boolean?)
319+
(s/def ::c keyword?)
320+
(s/def ::d double?)
321+
(s/def ::e inst?)
322+
323+
(is (= #{[::a]
324+
[::a ::b]
325+
[::a ::b ::c]
326+
[::a ::c]}
327+
(->> (s/exercise (s/keys :req [::a] :opt [::b ::c]) 100)
328+
(map (comp sort keys first))
329+
(into #{}))))
330+
331+
(is (= #{[:a]
332+
[:a :b]
333+
[:a :b :c]
334+
[:a :c]}
335+
(->> (s/exercise (s/keys :req-un [::a] :opt-un [::b ::c]) 100)
336+
(map (comp sort keys first))
337+
(into #{}))))
338+
339+
(is (= #{[::a ::b]
340+
[::a ::b ::c ::d]
341+
[::a ::b ::c ::d ::e]
342+
[::a ::b ::c ::e]
343+
[::a ::c ::d]
344+
[::a ::c ::d ::e]
345+
[::a ::c ::e]}
346+
(->> (s/exercise (s/keys :req [::a (or ::b (and ::c (or ::d ::e)))]) 200)
347+
(map (comp vec sort keys first))
348+
(into #{}))))
349+
350+
(is (= #{[:a :b]
351+
[:a :b :c :d]
352+
[:a :b :c :d :e]
353+
[:a :b :c :e]
354+
[:a :c :d]
355+
[:a :c :d :e]
356+
[:a :c :e]}
357+
(->> (s/exercise (s/keys :req-un [::a (or ::b (and ::c (or ::d ::e)))]) 200)
358+
(map (comp vec sort keys first))
359+
(into #{})))))
360+
315361
(s/fdef foo.bar/cljs-2275
316362
:args (s/cat :k keyword?)
317363
:ret string?)

0 commit comments

Comments
 (0)