bhb / expound

Human-optimized error messages for clojure.spec
Eclipse Public License 1.0
924 stars 24 forks source link

Crash bug when printing, if a datomic db is present and the spec fails #209

Closed avocade closed 3 years ago

avocade commented 3 years ago

There seems to still be a crash issue when a spec fails and a datomic db is involved.

This time it's the highlighted-value fn (in expound.printer), which fails in a similar way (datomic.core.db.Datum cannot be cast to class java.lang.Number) as the previous issue that was fixed in:

https://github.com/bhb/expound/issues/205

The printer uses walk, which was also implicated in the issue before.

Here is a stacktrace, starting from expound.printer$highlighted_value:

BUG: Internal error in expound or clojure spec.
nclass datomic.core.db.Datum cannot be cast to class java.lang.Number (datomic.core.db.Datum is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
java.lang.ClassCastException: class datomic.core.db.Datum cannot be cast to class java.lang.Number (datomic.core.db.Datum is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
 at datomic.dev_local.tx$datom_lookup_valfn.invokeStatic (tx.clj:384)
    datomic.dev_local.tx$datom_lookup_valfn.invoke (tx.clj:384)
    datomic.dev_local.local_log.LocalLog.valAt (local_log.clj:56)
    clojure.lang.RT.get (RT.java:760)
    datomic.dev_local.btindex.BTIndex.cons (btindex.clj:281)
    clojure.lang.RT.conj (RT.java:677)
    clojure.core$conj__5408.invokeStatic (core.clj:87)
    clojure.core/conj (core.clj:84)
    clojure.core.protocols$fn__8183.invokeStatic (protocols.clj:168)
    clojure.core.protocols/fn (protocols.clj:124)
    clojure.core.protocols$fn__8138$G__8133__8147.invoke (protocols.clj:19)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:31)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk$fn__9580.invoke (walk.clj:49)
    clojure.core.protocols$iter_reduce.invokeStatic (protocols.clj:49)
    clojure.core.protocols$fn__8164.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.walk$walk.invokeStatic (walk.clj:49)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk$fn__9580.invoke (walk.clj:49)
    clojure.core.protocols$iter_reduce.invokeStatic (protocols.clj:49)
    clojure.core.protocols$fn__8164.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.walk$walk.invokeStatic (walk.clj:49)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.core$map$fn__5885.invoke (core.clj:2759)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.Cons.next (Cons.java:39)
    clojure.lang.RT.next (RT.java:713)
    clojure.core$next__5404.invokeStatic (core.clj:64)
    clojure.core.protocols$fn__8183.invokeStatic (protocols.clj:169)
    clojure.core.protocols/fn (protocols.clj:124)
    clojure.core.protocols$fn__8138$G__8133__8147.invoke (protocols.clj:19)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:31)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.core$map$fn__5885.invoke (core.clj:2757)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.RT.seq (RT.java:535)
    clojure.core$seq__5420.invokeStatic (core.clj:139)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:24)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk_replace.invokeStatic (walk.clj:110)
    clojure.walk$prewalk_replace.invoke (walk.clj:110)
    expound.printer$highlighted_value$fn__29218.invoke (printer.cljc:423)
    expound.printer$highlighted_value.invokeStatic (printer.cljc:423)
    expound.printer$highlighted_value.invoke (printer.cljc:414)
    expound.alpha$value_in_context.invokeStatic (alpha.cljc:102)
    expound.alpha$value_in_context.invoke (alpha.cljc:82)
    clojure.lang.AFn.applyToHelper (AFn.java:171)
    clojure.lang.AFn.applyTo (AFn.java:144)
    clojure.core$apply.invokeStatic (core.clj:675)
    clojure.core$partial$fn__5858.doInvoke (core.clj:2626)
    clojure.lang.RestFn.invoke (RestFn.java:467)
    expound.alpha$value_PLUS_conformed_value.invokeStatic (alpha.cljc:307)
    expound.alpha$value_PLUS_conformed_value.invoke (alpha.cljc:295)
    expound.alpha$eval29338$fn__29339.invoke (alpha.cljc:313)
    clojure.lang.MultiFn.invoke (MultiFn.java:261)
    expound.alpha$value_str_STAR_.invokeStatic (alpha.cljc:283)
    expound.alpha$value_str_STAR_.invoke (alpha.cljc:279)
    expound.alpha$eval29367$fn__29368$iter__29369__29373$fn__29374$fn__29375.invoke (alpha.cljc:379)
    expound.alpha$eval29367$fn__29368$iter__29369__29373$fn__29374.invoke (alpha.cljc:376)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.LazySeq.first (LazySeq.java:73)
    clojure.lang.RT.first (RT.java:692)
    clojure.core$first__5402.invokeStatic (core.clj:55)
    clojure.string$join.invokeStatic (string.clj:180)
    clojure.string$join.invoke (string.clj:180)
    expound.alpha$eval29367$fn__29368.invoke (alpha.cljc:374)
    clojure.lang.MultiFn.invoke (MultiFn.java:261)
    expound.alpha$eval29385$fn__29386.invoke (alpha.cljc:387)
    clojure.lang.MultiFn.invoke (MultiFn.java:261)
    expound.alpha$print_explain_data$iter__29633__29637$fn__29638$fn__29639.invoke (alpha.cljc:867)
    expound.alpha$print_explain_data$iter__29633__29637$fn__29638.invoke (alpha.cljc:865)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.RT.seq (RT.java:535)
    clojure.core$seq__5420.invokeStatic (core.clj:139)
    clojure.core$apply.invokeStatic (core.clj:662)
    clojure.core$apply.invoke (core.clj:662)
    expound.alpha$print_explain_data.invokeStatic (alpha.cljc:864)
    expound.alpha$print_explain_data.invoke (alpha.cljc:850)
    expound.alpha$printer_str.invokeStatic (alpha.cljc:1005)
    expound.alpha$printer_str.invoke (alpha.cljc:987)
    expound.alpha$expound_str.invokeStatic (alpha.cljc:1065)
    expound.alpha$expound_str.invoke (alpha.cljc:1060)
    com.fulcrologic.guardrails.core$run_check$fn__29821.invoke (core.cljc:68)
    com.fulcrologic.guardrails.core$run_check.invokeStatic (core.cljc:66)
    com.fulcrologic.guardrails.core$run_check.invoke (core.cljc:56)
    poc.db.attribute.query$declaration__GT_id$fn__53402$state_machine__6558__auto____53415$fn__53417$inst_53397__53419.invoke (query.clj:56)
    com.fulcrologic.guardrails.core$eval29739$fn__29772$state_machine__6558__auto____29775$fn__29778.invoke (core.cljc:34)
    com.fulcrologic.guardrails.core$eval29739$fn__29772$state_machine__6558__auto____29775.invoke (core.cljc:34)
    clojure.core.async.impl.ioc_macros$run_state_machine.invokeStatic (ioc_macros.clj:978)
    clojure.core.async.impl.ioc_macros$run_state_machine.invoke (ioc_macros.clj:977)
    clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invokeStatic (ioc_macros.clj:982)
    clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invoke (ioc_macros.clj:980)
    clojure.core.async.impl.ioc_macros$take_BANG_$fn__6576.invoke (ioc_macros.clj:991)
    clojure.core.async.impl.channels.ManyToManyChannel$fn__461$fn__462.invoke (channels.clj:95)
    clojure.lang.AFn.run (AFn.java:22)
    java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1128)
    java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:628)
    clojure.core.async.impl.concurrent$counted_thread_factory$reify__330$fn__331.invoke (concurrent.clj:29)
    clojure.lang.AFn.run (AFn.java:22)
    java.lang.Thread.run (Thread.java:834)

WARNING: poc/db/attribute/query.clj:56 declaration->id's argument specs took 340ms to run.
BUG: Internal error in expound or clojure spec.
nclass datomic.core.db.Datum cannot be cast to class java.lang.Number (datomic.core.db.Datum is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
java.lang.ClassCastException: class datomic.core.db.Datum cannot be cast to class java.lang.Number (datomic.core.db.Datum is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
 at datomic.dev_local.tx$datom_lookup_valfn.invokeStatic (tx.clj:384)
    datomic.dev_local.tx$datom_lookup_valfn.invoke (tx.clj:384)
    datomic.dev_local.local_log.LocalLog.valAt (local_log.clj:56)
    clojure.lang.RT.get (RT.java:760)
    datomic.dev_local.btindex.BTIndex.cons (btindex.clj:281)
    clojure.lang.RT.conj (RT.java:677)
    clojure.core$conj__5408.invokeStatic (core.clj:87)
    clojure.core/conj (core.clj:84)
    clojure.core.protocols$fn__8183.invokeStatic (protocols.clj:168)
    clojure.core.protocols/fn (protocols.clj:124)
    clojure.core.protocols$fn__8138$G__8133__8147.invoke (protocols.clj:19)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:31)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk$fn__9580.invoke (walk.clj:49)
    clojure.core.protocols$iter_reduce.invokeStatic (protocols.clj:49)
    clojure.core.protocols$fn__8164.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.walk$walk.invokeStatic (walk.clj:49)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk$fn__9580.invoke (walk.clj:49)
    clojure.core.protocols$iter_reduce.invokeStatic (protocols.clj:49)
    clojure.core.protocols$fn__8164.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.walk$walk.invokeStatic (walk.clj:49)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.walk$walk.invokeStatic (walk.clj:46)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.core$map$fn__5885.invoke (core.clj:2759)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.Cons.next (Cons.java:39)
    clojure.lang.RT.next (RT.java:713)
    clojure.core$next__5404.invokeStatic (core.clj:64)
    clojure.core.protocols$fn__8183.invokeStatic (protocols.clj:169)
    clojure.core.protocols/fn (protocols.clj:124)
    clojure.core.protocols$fn__8138$G__8133__8147.invoke (protocols.clj:19)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:31)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk.invoke (walk.clj:61)
    clojure.core$partial$fn__5858.invoke (core.clj:2628)
    clojure.core$map$fn__5885.invoke (core.clj:2757)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.RT.seq (RT.java:535)
    clojure.core$seq__5420.invokeStatic (core.clj:139)
    clojure.core.protocols$seq_reduce.invokeStatic (protocols.clj:24)
    clojure.core.protocols$fn__8170.invokeStatic (protocols.clj:75)
    clojure.core.protocols/fn (protocols.clj:75)
    clojure.core.protocols$fn__8112$G__8107__8125.invoke (protocols.clj:13)
    clojure.core$reduce.invokeStatic (core.clj:6833)
    clojure.core$into.invokeStatic (core.clj:6900)
    clojure.walk$walk.invokeStatic (walk.clj:50)
    clojure.walk$prewalk.invokeStatic (walk.clj:65)
    clojure.walk$prewalk_replace.invokeStatic (walk.clj:110)
    clojure.walk$prewalk_replace.invoke (walk.clj:110)
    expound.printer$highlighted_value$fn__29218.invoke (printer.cljc:423)
    expound.printer$highlighted_value.invokeStatic (printer.cljc:423)
    expound.printer$highlighted_value.invoke (printer.cljc:414)
    expound.alpha$value_in_context.invokeStatic (alpha.cljc:102)
    expound.alpha$value_in_context.invoke (alpha.cljc:82)
    clojure.lang.AFn.applyToHelper (AFn.java:171)
    clojure.lang.AFn.applyTo (AFn.java:144)
...
eneroth commented 3 years ago

The problem is this line:

https://github.com/bhb/expound/blob/1c0d78570be3865eab8e69c1b568c4e7acee5bd8/src/expound/printer.cljc#L423

If replaced with

(binding [*print-namespace-maps* false] (pprint-str (summary-form show-valid-values? form in)))

… it works fine. I.e., it seems like a similar issue to previously: walk tries to get into the map-like object and do replacements.

avocade commented 3 years ago

Maybe use something similar to the recursive walk used in groups-walk to fix the previous bug:

https://github.com/bhb/expound/blob/ae2a15d4cbae5991ab6fea40163deab6908841a8/src/expound/alpha.cljc#L570

bhb commented 3 years ago

Thanks for reporting this!

bhb commented 3 years ago

Do you happen to have a repro including the spec? I'm having trouble reproducing this.

bhb commented 3 years ago

Also, what options are enabled? https://github.com/bhb/expound#printer-options

bhb commented 3 years ago

Actually, I found a repro. Nonetheless, if you could provide details about your repro and options, that would help ensure I fix the core bug.

My repro:

(s/def ::id int?)
    (is (= ""
           (expound/expound-str
                     (s/cat :db (s/keys
                                 :req-un [::id]))
                     [db]
                     {:show-valid-values? true})))))

Do you have :show-valid-values? set to true? What happens if you set that to false?

bhb commented 3 years ago

In particular, I'm curious what specs you use to validate your Datomic DB.

avocade commented 3 years ago

Hey, sorry we should have posted some repro code. Will try to do that on Monday 👍

bhb commented 3 years ago

@avocade No problem at all, I appreciate you reporting the bug.

I think I have a fix for my simple repro, but I'm curious about how you spec the db value. I'm a little worried my first attempt at a first won't work if someone uses {:show-valid-values? false}, but I'm having trouble reproducing in that case.

eneroth commented 3 years ago

I think I've managed to produce a small failing example. The error it gives is slightly different depending on whether I use a Datomic DB or your fake DB.

(def fake-db (FakeDB. {:a 1}))

(s/def ::value string?)

(expound/expound-str
  (s/keys :req [::value])
  {::value 'not-a-string
   :db fake-db} ;;or (d/db conn)
  {:show-valid-values? true, :print-specs? true})

With fake-db, it gives

Execution error (AbstractMethodError) at poc.db.attribute.query.FakeDB/iterator (REPL:-1).
Method poc/db/attribute/query/FakeDB.iterator()Ljava/util/Iterator; is abstract

With (d/db conn), it gives

Execution error (ClassCastException) at (REPL:1).
null

As previously with walk, it mostly works OK until there's a failing spec. In this case, I can't get it to fail by only giving it the DB and some failing spec, such as nil?. It seems to be that I have to have a failing spec in the vicinity of the DB.

Edit: I just now saw that you have already produced a failing case, sorry 🙂

eneroth commented 3 years ago

Actually, I found a repro. Nonetheless, if you could provide details about your repro and options, that would help ensure I fix the core bug.

My repro:

(s/def ::id int?)
    (is (= ""
           (expound/expound-str
                     (s/cat :db (s/keys
                                 :req-un [::id]))
                     [db]
                     {:show-valid-values? true})))))

Do you have :show-valid-values? set to true? What happens if you set that to false?

Yeah, we encounter these problems via guardrails, which uniformly uses {:show-valid-values? true, :print-specs? true} as args to expound-str.

I can't reproduce the problem with these switches off (or, I think, only :print-specs?). But in general, I think the takeaway is that walk always is an unsafe option when it comes to processing structures where "map-like" objects may occur, due to the excessive deconstruction and reconstruction it does as it walks through a tree.

bhb commented 3 years ago

Thanks for the info! If you're able, do you mind posting the real-world spec you use to spec your db, if any? This will inform my possible solutions here.

But in general, I think the takeaway is that walk always is an unsafe option when it comes to processing structures where "map-like" objects may occur, due to the excessive deconstruction and reconstruction it does as it walks through a tree.

I agree, but doing some sort of traversal and modification of the data is necessary for summarizing failures when :show-valid-values? is set to false. I'd like to preserve that feature, since it can significantly shrink the output. That feature would seem particularly useful for this case, since I'm guessing printing out the db is quite large (although I could be wrong, I don't use Datomic).

One way to make this work is just to skip this step when :show-valid-values? is true. I don't see any downside to doing that, but my concern is this is going to come up again when someone changes that option to false.

I'm wondering what Expound features can reasonably be supported on a Datomic DB. Maybe I can support all features if I take some inspiration from Specter and find a safer way to modify a DB. Or maybe that's not necessary and I can just skip certain features if someone is trying to spec a non-data value, like an atom or a Datomic DB 🤔

eneroth commented 3 years ago

Thanks for the info! If you're able, do you mind posting the real-world spec you use to spec your db, if any? This will inform my possible solutions here.

We create an env map with some information in it (the DB at a revision, the conn, a timestamp, and so on). This we throw around many places, and perhaps most significantly, we merge it into Pathoms env map, meaning that it's subject to any spec failure that might occur on Pathom's side (since any spec failing will cause walk to try to rewrite the entire data structure, even when the DB isn't involved directly in that particular spec).

We have given up on speccing conn and db themselves, since it's very hard to assert what's a correct value here (and Datomic doesn't include any db? or conn? predicates 🙂). So it's always the case that it's not about the specs for the DB itself, but the specs for the values in the map around it, and those can by anything.

I agree, but doing some sort of traversal and modification of the data is necessary for summarizing failures when :show-valid-values? is set to false. I'd like to preserve that feature, since it can significantly shrink the output. That feature would seem particularly useful for this case, since I'm guessing printing out the db is quite large (although I could be wrong, I don't use Datomic).

I don't think one thing has to be sacrificed for the other, rather I think it's about using a more careful algorithm. I.e., ensure that mutation only occurs at the site where the predicate is actually true.

This is partly the reason why I tend to break updates into two steps, like you saw with the Specter function in the previous commit: first collect the sites at which the predicate is true, then surgically alter them exactly where it is needed, as opposed to taking the structure apart and putting it back together while chasing the sites where the predicate is true (which walk secretly seems to do).

I'm wondering what Expound features can reasonably be supported on a Datomic DB. Maybe I can support all features if I take some inspiration from Specter and find a safer way to modify a DB. Or maybe that's not necessary and I can just skip certain features if someone is trying to spec a non-data value, like an atom or a Datomic DB 🤔

I think the answer lies in where your predicates might reasonably return true: :expound.problems/irrelevant, for example seems very, very unlikely to show up in any map-like objects that might occur in the specced data (unless you try to inject it into the map-like object I suppose!). Therefore, you can update those values however you want, given that those updates don't "spill over" and try to disassemble everything else while doing so. 😁

Problems seem to arise when the structure is updated in places you did not intend.

bhb commented 3 years ago

So it's always the case that it's not about the specs for the DB itself, but the specs for the values in the map around it, and those can by anything.

Thanks, that's very helpful!

bhb commented 3 years ago

I think the answer lies in where your predicates might reasonably return true: :expound.problems/irrelevant, for example seems very, very unlikely to show up in any map-like objects that might occur in the specced data (unless you try to inject it into the map-like object I suppose!).

Sorry, I was unclear. In order to support hiding irrelevant data, Expound makes a new version of the specced data structure that will contain many copies of :expound.problems/irrelevant (along with other data that allows underlining the bad value). This works fine in practice if the specced data is plain data, but the problem occurs when we try to spec values like Datomic DBs.

One solution is to rework the algorithm for finding irrelevant data entirely, but that seems potentially out of scope to solve this case. I'll think more on this.

bhb commented 3 years ago

If you have the ability to depend on a SHA version in your project, do you mind trying https://github.com/bhb/expound/pull/210/commits/18c82da80e6bc8f467301dc18750d7013a78f885 and seeing if it fixes the issue for you?

avocade commented 3 years ago

I did a very quick check and it seems to work fine now with https://github.com/bhb/expound/commit/18c82da80e6bc8f467301dc18750d7013a78f885 👍 Having a datomic db value in a failing spec just prints the spec error in the console as we'd expect. Thanks!

bhb commented 3 years ago

Thanks again for reporting this and testing the fix! This has been fixed in 0.8.9

avocade commented 3 years ago

Thanks!