[#543] Stacktrace improvements

This commit is contained in:
Michiel Borkent 2020-08-26 18:16:11 +02:00 committed by GitHub
parent 2f04a63c8c
commit 7cbaa36652
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 258 additions and 140 deletions

2
sci

@ -1 +1 @@
Subproject commit ccdc8668ad20a8fb3431f370b1e782fb6e0ff0e9
Subproject commit 0d416b62a3e21f8757db1452c2e41854bf4bebcd

View file

@ -31,3 +31,6 @@ export BABASHKA_CLASSPATH_TEST=true
export BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env"
echo "running tests part 4"
lein test :only babashka.classpath-test/classpath-env-test
export BABASHKA_POD_TEST=true
lein test :only babashka.pod-test

View file

@ -0,0 +1,127 @@
(ns babashka.impl.error-handler
(:refer-clojure :exclude [error-handler])
(:require [babashka.impl.classpath :as cp]
[clojure.java.io :as io]
[clojure.stacktrace :refer [print-stack-trace]]
[clojure.string :as str]
[sci.impl.callstack :as cs]))
(defn ruler [title]
(println (apply str "----- " title " " (repeat (- 80 7 (count title)) \-))))
(defn split-stacktrace [stacktrace verbose?]
(if verbose? [stacktrace]
(let [stack-count (count stacktrace)]
(if (<= stack-count 10)
[stacktrace]
[(take 5 stacktrace)
(drop (- stack-count 5) stacktrace)]))))
(defn print-stacktrace
[stacktrace {:keys [:verbose?]}]
(let [stacktrace (cs/format-stacktrace stacktrace)
segments (split-stacktrace stacktrace verbose?)
[fst snd] segments]
(run! println fst)
(when snd
(println "...")
(run! println snd))))
(defn error-context [ex opts]
(let [{:keys [:file :line :column]} (ex-data ex)]
(when file
(when-let [content (case file
"<expr>" (:expression opts)
"<preloads>" (:preloads opts)
(let [f (io/file file)]
(or (when (.exists f) (slurp f))
(and (not (.isAbsolute f))
(when-let [loader (:loader opts)]
(:source (cp/getResource loader [file] nil)))))))]
(let [matching-line (dec line)
start-line (max (- matching-line 4) 0)
end-line (+ matching-line 6)
[before after] (->>
(str/split-lines content)
(map-indexed list)
(drop start-line)
(take (- end-line start-line))
(split-at (inc (- matching-line start-line))))
snippet-lines (concat before
[[nil (str (str/join "" (repeat (dec column) " "))
(str "^--- " (ex-message ex)))]]
after)
indices (map first snippet-lines)
max-size (reduce max 0 (map (comp count str) indices))
snippet-lines (map (fn [[idx line]]
(if idx
(let [line-number (inc idx)]
(str (format (str "%" max-size "d: ") line-number) line))
(str (str/join (repeat (+ max-size 2) " ")) line)))
snippet-lines)]
(clojure.string/join "\n" snippet-lines))))))
(defn right-pad [s n]
(let [n (- n (count s))]
(str s (str/join (repeat n " ")))))
(defn print-locals [locals]
(let [max-name-length (reduce max 0 (map (comp count str)
(keys locals)))
max-name-length (+ max-name-length 2)]
(binding [*print-length* 10
*print-level* 2]
(doseq [[k v] locals]
(print (str (right-pad (str k ": ") max-name-length)))
;; print nil as nil
(prn v)))))
(defn error-handler [^Exception e opts]
(binding [*out* *err*]
(let [d (ex-data e)
exit-code (:bb/exit-code d)
sci-error? (isa? (:type d) :sci/error)
ex-name (when sci-error?
(some-> ^Throwable (ex-cause e)
.getClass .getName))
stacktrace (some->
d :callstack
cs/stacktrace)]
(if exit-code [nil exit-code]
(do
(ruler "Error")
(println "Type: " (or
ex-name
(.. e getClass getName)))
(when-let [m (.getMessage e)]
(println (str "Message: " m)))
(let [{:keys [:file :line :column]} d]
(when line
(println (str "Location: "
(when file (str file ":"))
line ":" column""))))
(when-let [phase (cs/phase e stacktrace)]
(println "Phase: " phase))
(println)
(when-let [ec (when sci-error?
(error-context e opts))]
(ruler "Context")
(println ec)
(println))
(when-let [locals (not-empty (:locals d))]
(ruler "Locals")
(print-locals locals)
(println))
(when sci-error?
(when-let
[st (let [st (with-out-str
(when stacktrace
(print-stacktrace stacktrace opts)))]
(when-not (str/blank? st) st))]
(ruler "Stack trace")
(println st)))
(when (:verbose? opts)
(ruler "Exception")
(print-stack-trace e))
(flush)
[nil 1])))))

View file

@ -1,5 +1,6 @@
(ns babashka.main
{:no-doc true}
(:refer-clojure :exclude [error-handler])
(:require
[babashka.impl.bencode :refer [bencode-namespace]]
[babashka.impl.cheshire :refer [cheshire-core-namespace]]
@ -17,6 +18,7 @@
[babashka.impl.curl :refer [curl-namespace]]
[babashka.impl.data :as data]
[babashka.impl.datafy :refer [datafy-namespace]]
[babashka.impl.error-handler :refer [error-handler]]
[babashka.impl.features :as features]
[babashka.impl.pods :as pods]
[babashka.impl.protocols :refer [protocols-namespace]]
@ -28,12 +30,10 @@
[babashka.wait :as wait]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.stacktrace :refer [print-stack-trace]]
[clojure.string :as str]
[hf.depstar.uberjar :as uberjar]
[sci.addons :as addons]
[sci.core :as sci]
[sci.impl.callstack :as cs]
[sci.impl.namespaces :as sci-namespaces]
[sci.impl.unrestrict :refer [*unrestricted*]]
[sci.impl.vars :as vars])
@ -403,123 +403,6 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that
{'java.lang.System/exit exit ;; override exit, so we have more control
'System/exit exit})
(defn ruler [title]
(println (apply str "----- " title " " (repeat (- 80 7 (count title)) \-))))
(defn split-stacktrace [stacktrace verbose?]
(if verbose? [stacktrace]
(let [stack-count (count stacktrace)]
(if (<= stack-count 10)
[stacktrace]
[(take 5 stacktrace)
(drop (- stack-count 5) stacktrace)]))))
(defn print-stacktrace
[stacktrace {:keys [:verbose?]}]
(let [stacktrace (cs/format-stacktrace stacktrace)
segments (split-stacktrace stacktrace verbose?)
[fst snd] segments]
(run! println fst)
(when snd
(println "...")
(run! println snd))))
(defn error-context [ex opts]
(let [{:keys [:file :line :column]} (ex-data ex)]
(when file
(when-let [content (case file
"<expr>" (:expression opts)
"<preloads>" (:preloads opts)
(let [f (io/file file)]
(or (when (.exists f) (slurp f))
(and (not (.isAbsolute f))
(when-let [loader (:loader opts)]
(:source (cp/getResource loader [file] nil)))))))]
(let [matching-line (dec line)
start-line (max (- matching-line 4) 0)
end-line (+ matching-line 6)
[before after] (->>
(clojure.string/split-lines content)
(map-indexed list)
(drop start-line)
(take (- end-line start-line))
(split-at (inc (- matching-line start-line))))
snippet-lines (concat before
[[nil (str (clojure.string/join "" (repeat (dec column) " "))
(str "^--- " (ex-message ex)))]]
after)
indices (map first snippet-lines)
max-size (reduce max 0 (map (comp count str) indices))
snippet-lines (map (fn [[idx line]]
(if idx
(let [line-number (inc idx)]
(str (format (str "%" max-size "d: ") line-number) line))
(str (clojure.string/join (repeat (+ max-size 2) " ")) line)))
snippet-lines)]
(clojure.string/join "\n" snippet-lines))))))
(defn right-pad [s n]
(let [n (- n (count s))]
(str s (str/join (repeat n " ")))))
(defn print-locals [locals]
(let [max-name-length (reduce max 0 (map (comp count str)
(keys locals)))
max-name-length (+ max-name-length 2)]
(binding [*print-length* 10
*print-level* 2]
(doseq [[k v] locals]
(println (str (right-pad (str k ": ") max-name-length) v))))))
(defn error-handler* [^Exception e opts]
(binding [*out* *err*]
(let [d (ex-data e)
exit-code (:bb/exit-code d)
sci-error? (isa? (:type d) :sci/error)
ex-name (when sci-error?
(some-> ^Throwable (ex-cause e)
.getClass .getName))]
(if exit-code [nil exit-code]
(do
(ruler "Error")
(println "Type: " (or
ex-name
(.. e getClass getName))
(when-let [t (:type d)]
(str " / " t)))
(when-let [m (.getMessage e)]
(println (str "Message: " m)))
(let [{:keys [:file :line :column]} d]
(when line
(println (str "Location: "
(when file (str file ":"))
line ":" column""))))
(println)
(when-let [ec (when sci-error?
(error-context e opts))]
(ruler "Context")
(println ec)
(println))
(when-let [locals (not-empty (:locals d))]
(ruler "Locals")
(print-locals locals)
(println))
(when sci-error?
(when-let
[st (let [st (with-out-str
(some->
d :callstack
cs/stacktrace
(print-stacktrace opts)))]
(when-not (str/blank? st) st))]
(ruler "Stack trace")
(println st)))
(when (:verbose? opts)
(ruler "Exception")
(print-stack-trace e))
(flush)
[nil 1])))))
(def imports
'{ArithmeticException java.lang.ArithmeticException
AssertionError java.lang.AssertionError
@ -659,7 +542,7 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that
main)] nil]
file (try [[(read-file file)] nil]
(catch Exception e
(error-handler* e {:expression expressions
(error-handler e {:expression expressions
:verbose? verbose?
:preloads preloads
:loader (:loader @cp-state)}))))
@ -672,7 +555,7 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that
(try
(sci/eval-string* sci-ctx preloads)
(catch Throwable e
(error-handler* e {:expression expression
(error-handler e {:expression expression
:verbose? verbose?
:preloads preloads
:loader (:loader @cp-state)})))))
@ -714,7 +597,7 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that
(recur)
res)))))
(catch Throwable e
(error-handler* e {:expression expression
(error-handler e {:expression expression
:verbose? verbose?
:preloads preloads
:loader (:loader @cp-state)}))))

View file

@ -3,7 +3,22 @@
[babashka.test-utils :as tu]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.test :as t :refer [deftest is]]))
[clojure.test :as t :refer [deftest is testing]]))
(defn multiline-equals [s1 s2]
(let [lines-s1 (str/split-lines s1)
lines-s2 (str/split-lines s2)
max-lines (max (count lines-s1) (count lines-s2))]
(run! (fn [i]
(let [l1 (get lines-s1 i)
l2 (get lines-s2 i)]
(if (and l1 l2)
(is (= l1 l2)
(format "Lines did not match.\nLine: %s\nLeft: %s\nRight: %s"
i (pr-str l1) (pr-str l2)))
(is false (format "Out of lines at line: %s.\nLeft: %s\nRight: %s"
i (pr-str l1) (pr-str l2))))))
(range max-lines))))
(deftest stacktrace-from-script-test
(try (tu/bb nil (.getPath (io/file "test" "babashka" "scripts" "divide_by_zero.bb")))
@ -53,7 +68,7 @@ user - <expr>:3:1"))))
(let [output (try (tu/bb nil "-cp" (.getPath (io/file "test-resources" "divide_by_zero.jar")) "-e" "(require 'foo)")
(catch Exception e (ex-message e)))]
(is (str/includes? output "----- Error --------------------------------------------------------------------
Type: java.lang.ArithmeticException / :sci/error
Type: java.lang.ArithmeticException
Message: Divide by zero
Location: foo.clj:1:10
@ -64,3 +79,91 @@ Location: foo.clj:1:10
----- Stack trace --------------------------------------------------------------
clojure.core// - <built-in>
foo - foo.clj:1:10"))))
(deftest static-call-test
(let [output (try (tu/bb nil "-e" "File/x")
(catch Exception e (ex-message e)))]
(is (str/includes? output
"----- Error --------------------------------------------------------------------
Type: java.lang.IllegalArgumentException
Message: No matching field found: x for class java.io.File
Location: <expr>:1:1
----- Context ------------------------------------------------------------------
1: File/x
^--- No matching field found: x for class java.io.File
----- Stack trace --------------------------------------------------------------
user - <expr>:1:1"))
(let [output (try (tu/bb nil "-e" "(File/x)")
(catch Exception e (ex-message e)))]
(is (str/includes? output
"----- Error --------------------------------------------------------------------
Type: java.lang.IllegalArgumentException
Message: No matching method x found taking 0 args
Location: <expr>:1:1
----- Context ------------------------------------------------------------------
1: (File/x)
^--- No matching method x found taking 0 args
----- Stack trace --------------------------------------------------------------
user - <expr>:1:1")))))
(deftest error-while-macroexpanding-test
(let [output (try (tu/bb nil "-e" "(defmacro foo [x] (subs nil 1) `(do ~x ~x)) (foo 1)")
(catch Exception e (ex-message e)))]
(multiline-equals output
"----- Error --------------------------------------------------------------------
Type: java.lang.NullPointerException
Location: <expr>:1:19
Phase: macroexpand
----- Context ------------------------------------------------------------------
1: (defmacro foo [x] (subs nil 1) `(do ~x ~x)) (foo 1)
^---
----- Locals -------------------------------------------------------------------
&form: (foo 1)
&env: {}
x: 1
----- Stack trace --------------------------------------------------------------
clojure.core/subs - <built-in>
user/foo - <expr>:1:19
user/foo - <expr>:1:1
user - <expr>:1:45")))
(deftest error-in-macroexpansion-test
(let [output (try (tu/bb nil "-e" "(defmacro foo [x & xs] `(do (subs nil 1) ~x)) (foo 1)")
(catch Exception e (ex-message e)))]
(multiline-equals output
"----- Error --------------------------------------------------------------------
Type: java.lang.NullPointerException
Location: <expr>:1:47
----- Context ------------------------------------------------------------------
1: (defmacro foo [x & xs] `(do (subs nil 1) ~x)) (foo 1)
^---
----- Stack trace --------------------------------------------------------------
clojure.core/subs - <built-in>"))
(testing "calling a var inside macroexpansion"
(let [output (try (tu/bb nil "-e" "(defn quux [] (subs nil 1)) (defmacro foo [x & xs] `(do (quux) ~x)) (defn bar [] (foo 1)) (bar)")
(catch Exception e (ex-message e)))]
(multiline-equals output
"----- Error --------------------------------------------------------------------
Type: java.lang.NullPointerException
Location: <expr>:1:15
----- Context ------------------------------------------------------------------
1: (defn quux [] (subs nil 1)) (defmacro foo [x & xs] `(do (quux) ~x)) (defn bar [] (foo 1)) (bar)
^---
----- Stack trace --------------------------------------------------------------
clojure.core/subs - <built-in>
user/quux - <expr>:1:15
user/quux - <expr>:1:1
user/bar - <expr>:1:69
user - <expr>:1:91"))))

View file

@ -4,18 +4,20 @@
[clojure.test :as t :refer [deftest is]]))
(deftest pod-test
(let [native? tu/native?
sw (java.io.StringWriter.)
res (apply tu/bb {:err sw}
(cond-> ["-f" "test-resources/pod.clj"]
native?
(conj "--native")))
err (str sw)]
(is (= "6\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\"Illegal arguments / {:args (1 2 3)}\"\n(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\ntrue\n" res))
(when-not tu/native?
(is (= "(\"hello\" \"print\" \"this\" \"error\")\n" err)))
(is (= {:a 1 :b 2}
(edn/read-string
(apply tu/bb nil (cond-> ["-f" "test-resources/pod.clj" "--json"]
native?
(conj "--native"))))))))
(if (= "true" (System/getenv "BABASHKA_POD_TEST"))
(let [native? tu/native?
sw (java.io.StringWriter.)
res (apply tu/bb {:err sw}
(cond-> ["-f" "test-resources/pod.clj"]
native?
(conj "--native")))
err (str sw)]
(is (= "6\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\"Illegal arguments / {:args (1 2 3)}\"\n(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\ntrue\n" res))
(when-not tu/native?
(is (= "(\"hello\" \"print\" \"this\" \"error\")\n" err)))
(is (= {:a 1 :b 2}
(edn/read-string
(apply tu/bb nil (cond-> ["-f" "test-resources/pod.clj" "--json"]
native?
(conj "--native")))))))
(println "Skipping pod test because BABASHKA_POD_TEST isn't set to true.")))