czan / stateful-check

Stateful generative testing in clojure
MIT License
117 stars 11 forks source link

Error in report step: `class java.io.StringWriter cannot be cast to class java.io.PrintWriter` #12

Closed danielSbastos closed 4 years ago

danielSbastos commented 4 years ago

Hey there! First and foremost, thank you for this amazing lib, it has been a blast using it πŸŽ‰

So, I've been having this error at the end of the executions and I'm not sure how to solve it, would you mind taking a look? Maybe it is something on my test configuration that's causing it, but I haven't been able to figure out 😬

OS info: MacOS Catalina - 10.15.3

ERROR in () (core.clj:200)
expected: (specification-correct? conn-spec {:report {:first-case? true, :stacktrace? true}})
  actual: java.lang.ClassCastException: class java.io.StringWriter cannot be cast to class java.io.PrintWriter (java.io.StringWriter and java.io.PrintWriter are in module java.base of loader 'bootstrap')
 at stateful_check.core$report_result$fn__12635.invoke (core.clj:200)
    stateful_check.core$report_result.invokeStatic (core.clj:193)
    stateful_check.core$report_result.invoke (core.clj:182)
    pocket_conn_pool.core_test$eval12667.invokeStatic (form-init869914851329692435.clj:1)
    pocket_conn_pool.core_test$eval12667.invoke (form-init869914851329692435.clj:1)
    clojure.lang.Compiler.eval (Compiler.java:7177)
    clojure.lang.Compiler.eval (Compiler.java:7132)
    clojure.core$eval.invokeStatic (core.clj:3214)
    clojure.core$eval.invoke (core.clj:3210)
    clojure.main$repl$read_eval_print__9086$fn__9089.invoke (main.clj:437)
    clojure.main$repl$read_eval_print__9086.invoke (main.clj:437)
    clojure.main$repl$fn__9095.invoke (main.clj:458)
    clojure.main$repl.invokeStatic (main.clj:458)
    clojure.main$repl.doInvoke (main.clj:368)
    clojure.lang.RestFn.invoke (RestFn.java:1523)
    nrepl.middleware.interruptible_eval$evaluate.invokeStatic (interruptible_eval.clj:79)
    nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:55)
    nrepl.middleware.interruptible_eval$interruptible_eval$fn__10569$fn__10573.invoke (interruptible_eval.clj:142)
    clojure.lang.AFn.run (AFn.java:22)
    nrepl.middleware.session$session_exec$main_loop__10670$fn__10674.invoke (session.clj:171)
    nrepl.middleware.session$session_exec$main_loop__10670.invoke (session.clj:170)
    clojure.lang.AFn.run (AFn.java:22)
    java.lang.Thread.run (Thread.java:834)
czan commented 4 years ago

Hmm, that's strange... Out of interest, which test runner are you using? Can you provide a minimal example that I can use to reproduce the failure?

danielSbastos commented 4 years ago

I am using the default one given by leiningen with lein test.

I think it is a mistake by me, I have narrowed down the issue to a postcondition that checks if a promise has realized and coded this minimum example:

(ns core-test
  (:require [clojure.test :refer [is]]
            [stateful-check.core :refer [specification-correct?]]))

(defn bla []
  (promise))

(def bla-specification
  {:command #'bla
   :next-state (fn [state _ result]
                 (conj state result))
   :postcondition (fn [prev-state next-state _ result]
                    (= (false (realized? result))))})

(def bla-spec
  {:commands {:bla #'bla-specification}})

(is (specification-correct? bla-spec))

if I change the postcondition to, for example,

(def bla-specification
  {:command #'bla
   :next-state (fn [state _ result]
                 (conj state result))
   :postcondition (fn [prev-state next-state _ result]
                    false)})

is does not raise an error.

I thought that maybe it is because result may be a symbolic value and therefore I can't check that it is realized, however, as it's written here,

Everything provided to the :postcondition function is a β€œreal” value. All symbolic values will be replaced before the :postcondition function is called.

I should be able to do this πŸ€” , or not?

czan commented 4 years ago

You definitely should be able to do this. I'm pleased to report that you've found a bug!

It looks I made a mistake and assumed that with-out-str bound a java.io.PrintWriter to *out*, but it actually bound a java.io.StringWriter. It turns out this assumption is only used when an exception is thrown in a postcondition.

Given the way postconditions are run, this takes out the entire test run and makes it hard for us to extract the details of the run, so we can't easily attach the postcondition failure to the step that failed. We can print out the details of the postcondition exception, though, by wrapping *out* in a java.io.PrintWriter.

If you use 0.4.2-SNAPSHOT (which I just deployed) and re-run your example you should see this output:

FAIL in () (core.clj:192)
java.lang.ClassCastException: java.lang.Boolean cannot be cast to clojure.lang.IFn
    at core_test$fn__7309.invokeStatic(core_test.clj:13)
    at core_test$fn__7309.invoke(core_test.clj:12)
    ... lots more lines ...

Seed: 1592609637031

expected: all executions to match specification
  actual: the above execution did not match the specification

This points to (= (false (realized? result))), which is the line throwing an exception, because you are attempting to call false as a function. Changing this to (= false (realized? result)) causes the test to pass.

Let me know how this goes in your real-world code, and if things are working I'll push out a proper release.

danielSbastos commented 4 years ago

That's it! It worked perfectly and now I can see my error

The fact that I had the extra parenthesis passed unnoticed, thank you for noticing that! I guess my mistake made the bug evident. Glad to be able to help in any way, even if not intended πŸ˜†

Thanks for the help and time taken to solve this :)

czan commented 4 years ago

Excellent! I've just deployed a release for 0.4.2. :)