diff --git a/sci b/sci index ccdc8668..0d416b62 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit ccdc8668ad20a8fb3431f370b1e782fb6e0ff0e9 +Subproject commit 0d416b62a3e21f8757db1452c2e41854bf4bebcd diff --git a/script/test b/script/test index 24ddf291..5e7fec10 100755 --- a/script/test +++ b/script/test @@ -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 diff --git a/src/babashka/impl/error_handler.clj b/src/babashka/impl/error_handler.clj new file mode 100644 index 00000000..b11218ba --- /dev/null +++ b/src/babashka/impl/error_handler.clj @@ -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 + "" (:expression opts) + "" (: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]))))) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 8d854144..aa4146da 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -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 - "" (:expression opts) - "" (: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)})))) diff --git a/test/babashka/error_test.clj b/test/babashka/error_test.clj index e4df4ba3..3a0bfa91 100644 --- a/test/babashka/error_test.clj +++ b/test/babashka/error_test.clj @@ -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 - :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// - 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: :1:1 + +----- Context ------------------------------------------------------------------ +1: File/x + ^--- No matching field found: x for class java.io.File + +----- Stack trace -------------------------------------------------------------- +user - :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: :1:1 + +----- Context ------------------------------------------------------------------ +1: (File/x) + ^--- No matching method x found taking 0 args + +----- Stack trace -------------------------------------------------------------- +user - :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: :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 - +user/foo - :1:19 +user/foo - :1:1 +user - :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: :1:47 + +----- Context ------------------------------------------------------------------ +1: (defmacro foo [x & xs] `(do (subs nil 1) ~x)) (foo 1) + ^--- + +----- Stack trace -------------------------------------------------------------- +clojure.core/subs - ")) + (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: :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 - +user/quux - :1:15 +user/quux - :1:1 +user/bar - :1:69 +user - :1:91")))) diff --git a/test/babashka/pod_test.clj b/test/babashka/pod_test.clj index d1dfcd58..0b20ff58 100644 --- a/test/babashka/pod_test.clj +++ b/test/babashka/pod_test.clj @@ -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.")))