[#543] Stacktrace improvements
This commit is contained in:
parent
2f04a63c8c
commit
7cbaa36652
6 changed files with 258 additions and 140 deletions
2
sci
2
sci
|
|
@ -1 +1 @@
|
|||
Subproject commit ccdc8668ad20a8fb3431f370b1e782fb6e0ff0e9
|
||||
Subproject commit 0d416b62a3e21f8757db1452c2e41854bf4bebcd
|
||||
|
|
@ -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
|
||||
|
|
|
|||
127
src/babashka/impl/error_handler.clj
Normal file
127
src/babashka/impl/error_handler.clj
Normal 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])))))
|
||||
|
|
@ -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)}))))
|
||||
|
|
|
|||
|
|
@ -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"))))
|
||||
|
|
|
|||
|
|
@ -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.")))
|
||||
|
|
|
|||
Loading…
Reference in a new issue