From 1c0615f88c37247ec7f72ae48871ec696e07b678 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Thu, 31 Oct 2024 21:10:55 +0000 Subject: [PATCH] backported a few nREPL PRs from Basilisp --- CHANGELOG.md | 6 + README.md | 29 +-- examples/torus_pattern.lpy | 21 +- pyproject.toml | 2 +- src/basilisp_blender/nrepl_server.lpy | 189 ++++++++++-------- .../integration/bpy_utils_test.lpy | 5 +- tests/basilisp_blender/nrepl_server_test.lpy | 148 +++++++++++--- 7 files changed, 249 insertions(+), 151 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f38315d..6d2db7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ ## Unreleased +## 0.2.0 + - Added async server interface support in `start-server!` with a client work abstraction. - Implemented the async Blender nREPL server directly in Basilisp. - Enhanced error handling to return errors at the interface layer. - Introduced the nREPL server control panel UI component. - Upgraded Basilisp to 0.2.4. +- Improved on the nREPL server exception messages by matching that of the REPL user friendly format, backported from [basilisp#973](https://github.com/basilisp-lang/basilisp/pull/973). +- Fix incorrect line numbers for compiler exceptions in nREPL when evaluating forms in loaded files, backported from [basilisp#1038](https://github.com/basilisp-lang/basilisp/pull/1038). +- nREPL server no longer sends ANSI color escape sequences in exception messages to clients, backported from [basilisp#1040](https://github.com/basilisp-lang/basilisp/pull/1040). +- Conform to the `cider-nrepl` `info` ops spec by ensuring result's `:file` is URI, also added missing :column number, backported from [basilisp#1068](https://github.com/basilisp-lang/basilisp/pull/1068). ## 0.1.0 diff --git a/README.md b/README.md index d554f9f..6ad0bba 100644 --- a/README.md +++ b/README.md @@ -122,21 +122,16 @@ Here is an example of Basilisp code to create a torus pattern using the bpy Blen (:import bpy math)) -(def object (.. bpy/ops -object)) -(def materials (.. bpy/data -materials)) -(def mesh (.. bpy/ops -mesh)) - - (defn clear-mesh-objects [] - (.select-all object ** :action "DESELECT") - (.select-by-type object ** :type "MESH") - (.delete object)) + (.select-all bpy.ops/object ** :action "DESELECT") + (.select-by-type bpy.ops/object ** :type "MESH") + (.delete bpy.ops/object)) (clear-mesh-objects) (defn create-random-material [] - (let [mat (.new materials ** :name "RandomMaterial") - _ (set! (.-use-nodes mat) true) + (let [mat (.new bpy.data/materials ** :name "RandomMaterial") + _ (set! (.-use-nodes mat) true) bsdf (aget (.. mat -node-tree -nodes) "Principled BSDF")] (set! (-> bsdf .-inputs (aget "Base Color") .-default-value) @@ -144,15 +139,14 @@ Here is an example of Basilisp code to create a torus pattern using the bpy Blen mat)) (defn create-torus [radius tube-radius location segments] - (.primitive-torus-add mesh ** + (.primitive-torus-add bpy.ops/mesh ** :major-radius radius :minor-radius tube-radius :location location :major-segments segments :minor-segments segments) - (let [obj (.. bpy/context -object) - material (create-random-material)] - (-> obj .-data .-materials (.append material)))) + (let [material (create-random-material)] + (-> bpy.context/object .-data .-materials (.append material)))) #_(create-torus 5, 5, [0 0 0] 48) @@ -242,3 +236,10 @@ $ poetry build # build the package $ poetry run python scripts/bb_package_install.py # install it in Blender ``` +# License + +This project is licensed under the Eclipse Public License 2.0. See the [LICENSE](LICENSE) file for details. + +# Acknowledgments + +The nREPL server is a spin-off of [Basilisp](https://github.com/basilisp-lang/basilisp)'s `basilisp.contrib.nrepl-server` namespace. diff --git a/examples/torus_pattern.lpy b/examples/torus_pattern.lpy index 9a65884..72e1870 100644 --- a/examples/torus_pattern.lpy +++ b/examples/torus_pattern.lpy @@ -3,20 +3,16 @@ (:import bpy math)) -(def object (.. bpy/ops -object)) -(def materials (.. bpy/data -materials)) -(def mesh (.. bpy/ops -mesh)) - (defn clear-mesh-objects [] - (.select-all object ** :action "DESELECT") - (.select-by-type object ** :type "MESH") - (.delete object)) + (.select-all bpy.ops/object ** :action "DESELECT") + (.select-by-type bpy.ops/object ** :type "MESH") + (.delete bpy.ops/object)) (clear-mesh-objects) (defn create-random-material [] - (let [mat (.new materials ** :name "RandomMaterial") - _ (set! (.-use-nodes mat) true) + (let [mat (.new bpy.data/materials ** :name "RandomMaterial") + _ (set! (.-use-nodes mat) true) bsdf (aget (.. mat -node-tree -nodes) "Principled BSDF")] (set! (-> bsdf .-inputs (aget "Base Color") .-default-value) @@ -24,15 +20,14 @@ mat)) (defn create-torus [radius tube-radius location segments] - (.primitive-torus-add mesh ** + (.primitive-torus-add bpy.ops/mesh ** :major-radius radius :minor-radius tube-radius :location location :major-segments segments :minor-segments segments) - (let [obj (.. bpy/context -object) - material (create-random-material)] - (-> obj .-data .-materials (.append material)))) + (let [material (create-random-material)] + (-> bpy.context/object .-data .-materials (.append material)))) #_(create-torus 5, 5, [0 0 0] 48) diff --git a/pyproject.toml b/pyproject.toml index 723f248..757e42f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "basilisp-blender" -version = "0.2.0b1" +version = "0.2.0" description = "" authors = ["ikappaki"] readme = "README.md" diff --git a/src/basilisp_blender/nrepl_server.lpy b/src/basilisp_blender/nrepl_server.lpy index cd8d82f..a883d97 100644 --- a/src/basilisp_blender/nrepl_server.lpy +++ b/src/basilisp_blender/nrepl_server.lpy @@ -10,20 +10,25 @@ (:require [basilisp.contrib.bencode :as bc] [basilisp.string :as str] [basilisp-blender.utils :as u]) - (:import contextlib + (:import basilisp.logconfig + contextlib logging - os.path + os queue socket socketserver sys threading + urllib traceback)) (def logger "The logger for this namespace." (logging/getLogger (namespace ::))) +(defmacro ^:private trace [& values] + `(when (.isEnabledFor logger basilisp.logconfig/TRACE) + (.log logger basilisp.logconfig/TRACE (str/join " " [~@values])))) (defmacro ^:private debug [& values] `(when (.isEnabledFor logger logging/DEBUG) (.debug logger (str/join " " [~@values])))) @@ -64,43 +69,41 @@ (defn- log-request-mw [handler] (fn [request send-fn] - (info :request (dissoc request :client*)) + (debug :request (dissoc request :client*)) (handler request send-fn))) (defn- log-response-mw [handler] (fn [request response] - (info :response response) + (debug :response response) (handler request response))) (declare ops) (defn- handle-describe [request send-fn] - (send-fn request - {"versions" {"basilisp" (let [version basilisp.lang.runtime/BASILISP-VERSION-STRING] - (assoc (zipmap ["major" "minor" "incremental"] - version) - "version-string" (str/join "." version))) - "python" (let [version (get (.split sys/version " ") 0)] - (assoc (zipmap ["major" "minor" "incremental"] - (py->lisp (.split version "."))) - "version-string" (py->lisp sys/version)))} - "ops" (zipmap (map name (keys ops)) (repeat {})) - "status" ["done"]})) + (let [basilisp-version (-> (zipmap ["major" "minor" "incremental"] *basilisp-version*) + (assoc "version-string" (str/join "." *basilisp-version*))) + python-version (-> (zipmap ["major" "minor" "incremental"] *python-version*) + (assoc "version-string" (str/join "." *python-version*)))] + (send-fn request + {"versions" {"basilisp" basilisp-version + "python" python-version} + "ops" (zipmap (map name (keys ops)) (repeat {})) + "status" ["done"]}))) (defn- format-value [_nrepl-pprint _pprint-options value] (pr-str value)) (defn- send-value [request send-fn v] - (let [{:keys [client*]} request - {:keys [*1 *2]} @client* - [v opts] v - ns (:ns opts)] + (let [{:keys [client*]} request + {:keys [*1 *2]} @client* + [v opts] v + ns (:ns opts)] (swap! client* assoc :*1 v :*2 *1 :*3 *2) (let [v (format-value (:nrepl.middleware.print/print request) (:nrepl.middleware.print/options request) v)] (send-fn request {"value" (str v) - "ns" (str ns)})))) + "ns" (str ns)})))) (defn- handle-error [send-fn request e] (let [{:keys [client* ns]} request @@ -108,9 +111,9 @@ message (or (:message data) (str e))] (swap! client* assoc :*e e) (send-fn request {"err" (str message)}) - (send-fn request {"ex" (traceback/format-exc) + (send-fn request {"ex" (traceback/format-exc) "status" ["eval-error"] - "ns" ns}))) + "ns" ns}))) (defn- do-handle-eval "Evaluate the ``request`` ``code`` of ``file`` in the ``ns`` namespace @@ -127,32 +130,38 @@ It binds the `*1`, `*2`, `*3` and `*e` variables for evaluation from the corresponding ones found in ``client*``, and updates the latter according to the result." - [{:keys [client* code ns file _column _line] :as request} send-fn] + [{:keys [client* code ns file column line] :as request} send-fn] (let [{:keys [*1 *2 *3 *e eval-ns]} @client* out-stream (StreamOutFn #(send-fn request {"out" %})) - reader (io/StringIO code) ctx (basilisp.lang.compiler.CompilerContext. (or file "")) eval-ns (if ns (create-ns (symbol ns)) eval-ns)] - (binding [*ns* eval-ns + (binding [*ns* eval-ns *out* out-stream - *1 *1 *2 *2 *3 *3 *e *e] + *1 *1 + *2 *2 + *3 *3 + *e *e] (try - (let [results (for [form (seq (basilisp.lang.reader/read reader - *resolver* - *data-readers*))] + (let [result (last + (for [form (read-seq (cond-> {} + line (assoc :init-line line) + column (assoc :init-column column)) + (io/StringIO code))] (basilisp.lang.compiler/compile-and-exec-form form ctx - *ns*)) - result (last results)] + *ns*)))] (send-value request send-fn [result {:ns (str *ns*)}])) (catch python/Exception e - (info :eval-exception e) - (handle-error send-fn (assoc request :ns (str *ns*)) e)) + (debug :eval-exception e) + (let [msg (->> (basilisp.lang.exception/format_exception e + ** :disable-color true) + (str/join ""))] + (handle-error send-fn (assoc request :ns (str *ns*)) msg))) (finally (swap! client* assoc :eval-ns *ns*) - (send-fn request {"ns" (str *ns*) + (send-fn request {"ns" (str *ns*) "status" ["done"]})))))) (defn- handle-eval [request send-fn] @@ -191,12 +200,10 @@ [resolve-ns symbol-str] (let [reader (io/StringIO symbol-str) {:keys [form error]} (try {:form (binding [*ns* resolve-ns] - (first (seq (basilisp.lang.reader/read reader - *resolver* - *data-readers*))))} - (catch python/Exception e - (info :symbol-identify-reader-error :input symbol-str :exception e) - {:error (str e)}))] + (read reader))} + (catch python/Exception e + (debug :symbol-identify-reader-error :input symbol-str :exception e) + {:error (str e)}))] (cond error @@ -216,12 +223,9 @@ (catch python/Exception e {:error (str e)}))] (cond - var - [:var var] - error - [:error error] - :else - [:other form]))))) + var [:var var] + error [:error error] + :else [:other form]))))) (defn- forms-join [forms] (->> (map pr-str forms) @@ -237,37 +241,45 @@ (let [mapping-type (-> request :op) {:keys [eval-ns]} @client*] (try - (let [lookup-ns (if ns - (create-ns (symbol ns)) - eval-ns) - sym-str (or (:sym request) ;; cider - (:symbol request) ;; calva - ) + (let [lookup-ns (if ns + (create-ns (symbol ns)) + eval-ns) + sym-str (or (:sym request) ;; cider + (:symbol request) ;; calva + ) + [tp var-maybe] (symbol-identify lookup-ns sym-str) var-meta (when (= tp :var) (meta var-maybe)) - {:keys [arglists doc file ns line] symname :name} var-meta - ref (when (= tp :var) (var-get var-maybe)) - response (when symname (case mapping-type - :eldoc (cond-> - {"eldoc" (mapv #(mapv str %) arglists) - "ns" (str ns) - "type" (if (fn? ref) - "function" - "variable") - "name" (str symname) - "status" ["done"]} - doc (assoc "docstring" doc)) - :info {"doc" doc - "ns" (str ns) - "name" (str symname) - "file" file - "line" line - "arglists-str" (forms-join arglists) - "status" ["done"]})) - status (if (and (nil? symname) (= mapping-type :eldoc) ) - ["done" "no-eldoc"] - ["done"])] - (debug :lookup :sym sym-str :doc doc :args arglists) + + {:keys [arglists doc file ns line col] symname :name} var-meta + + ref (when (= tp :var) (var-get var-maybe)) + response (when symname + (case mapping-type + :eldoc (cond-> + {"eldoc" (mapv #(mapv str %) arglists) + "ns" (str ns) + "type" (if (fn? ref) + "function" + "variable") + "name" (str symname) + "status" ["done"]} + doc (assoc "docstring" doc)) + :info {"doc" doc + "ns" (str ns) + "name" (str symname) + "file" (if (os.path/isabs file) + (->> (urllib.parse/quote file) + (urllib.parse/urljoin "file:")) + file) + "line" line + "column" col + "arglists-str" (forms-join arglists) + "status" ["done"]})) + status (if (and (nil? symname) (= mapping-type :eldoc) ) + ["done" "no-eldoc"] + ["done"])] + (trace :lookup :sym sym-str :doc doc :args arglists) (send-fn request (assoc response :status status))) (catch python/Exception e (let [status (cond-> @@ -326,17 +338,18 @@ "status" ["done"]}))) (def ops - "A list of operations supported by the nrepl server." - {:eval handle-eval - :describe handle-describe - :info handle-lookup - :eldoc handle-lookup - :clone handle-clone - :close handle-close + "A map of operations supported by the nREPL server (as keywords) to function + handlers for those operations." + {:eval handle-eval + :describe handle-describe + :info handle-lookup ;; cider-nrepl middleware + :eldoc handle-lookup ;; cider-nrepl middleware + :clone handle-clone + :close handle-close + :load-file handle-load-file + :complete handle-complete ;; :macroexpand handle-macroexpand ;; :classpath handle-classpath - :load-file handle-load-file - :complete handle-complete }) (defn- handle-request [{:keys [op] :as request} send-fn] @@ -353,7 +366,7 @@ (defn- make-send-fn [socket] (fn [_request response] - (debug :sending (:id _request) :response-keys (keys response)) + (trace :sending (:id _request) :response-keys (keys response)) (try (.sendall socket (bc/encode response)) (catch python/TypeError e @@ -412,7 +425,7 @@ (info "Connection accepted" :info client-info) ;; Need to load the `clojure.core` alias because cider uses it ;; to test for availability of features. - (eval (read-string "(ns user (:require clojure.core))")) + (eval '(ns user (:require clojure.core))) (swap! client* assoc :eval-ns *ns*) (loop [data (.recv socket recv-buffer-size)] (if (= data zero-bytes) @@ -424,8 +437,8 @@ b) data) [requests unprocessed] (bc/decode-all data {:keywordize-keys true - :string-fn #(.decode % "utf-8")})] - (debug :requests requests) + :string-fn #(.decode % "utf-8")})] + (trace :requests requests) (when (not (str/blank? unprocessed)) (reset! pending unprocessed)) (doseq [request requests] diff --git a/tests/basilisp_blender/integration/bpy_utils_test.lpy b/tests/basilisp_blender/integration/bpy_utils_test.lpy index a16e1ce..b6898af 100644 --- a/tests/basilisp_blender/integration/bpy_utils_test.lpy +++ b/tests/basilisp_blender/integration/bpy_utils_test.lpy @@ -347,9 +347,8 @@ ;; Test error handling (let [{:keys [err exc]} (with-client-eval! (/ 1 0))] - (is (= "ZeroDivisionError('Fraction(1, 0)')" err)) - (is (str/starts-with? exc "Traceback (most recent call last):") exc)) - ))) + (is (str/includes? err "ZeroDivisionError: Fraction(1, 0)") err) + (is (str/starts-with? exc "Traceback (most recent call last):") exc))))) #_(tu/pp-code (test-with-blender-nrepl-run)) diff --git a/tests/basilisp_blender/nrepl_server_test.lpy b/tests/basilisp_blender/nrepl_server_test.lpy index 37e1240..cf3a109 100644 --- a/tests/basilisp_blender/nrepl_server_test.lpy +++ b/tests/basilisp_blender/nrepl_server_test.lpy @@ -8,13 +8,18 @@ [basilisp.io :as bio] [basilisp.set :as set] [basilisp.string :as str :refer [starts-with?]] - [basilisp.test :refer [deftest are is testing]]) + [basilisp.test :refer [deftest are is testing use-fixtures]] + [basilisp.test.fixtures :as fixtures :refer [*tempdir*]]) (:import [datetime :as dt] os socket tempfile - time)) + threading + time + urllib)) + +(use-fixtures :each fixtures/tempdir) (def ^:dynamic *nrepl-port* "The port the :lpy:py:`with-server` is bound to." @@ -521,7 +526,9 @@ {:id @id* :ns "xyz" :status ["done"]}) (client-send! client {:id (id-inc!) :op "eval" :code "(/ 3 0)"}) - (is (= {:id @id* :err "ZeroDivisionError('Fraction(3, 0)')"} (client-recv! client))) + (let [{:keys [id err]} (client-recv! client)] + (is (= id @id* )) + (is (str/includes? err "ZeroDivisionError: Fraction(3, 0)"))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "xyz" ns)) @@ -533,8 +540,10 @@ (client-send! client {:id (id-inc!) :op "eval" :code "(println :hey)\n(/ 4 0)"}) (are [response] (= response (client-recv! client)) {:id @id* :out ":hey"} - {:id @id* :out os/linesep} - {:id @id* :err "ZeroDivisionError('Fraction(4, 0)')"}) + {:id @id* :out os/linesep}) + (let [{:keys [id err]} (client-recv! client)] + (is (= @id* id)) + (is (str/includes? err "ZeroDivisionError: Fraction(4, 0)"))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "xyz" ns)) @@ -543,15 +552,23 @@ (are [response] (= response (client-recv! client)) {:id @id* :ns "xyz" :status ["done"]}) - (client-send! client {:id (id-inc!) :op "eval" :code "[*1 *2 *3 *e]"}) + (client-send! client {:id (id-inc!) :op "eval" :code "[*1 *2 *3]"}) + (are [response] (= response (client-recv! client)) + {:id @id* :ns "xyz" :value "[6 nil 4]"} + {:id @id* :ns "xyz" :status ["done"]}) + + (client-send! client {:id (id-inc!) :op "eval" :code "*e"}) + (let [{:keys [id value status]} (client-recv! client)] + (is (= @id* id)) + (is (str/includes? value "ZeroDivisionError: Fraction(4, 0)") value)) (are [response] (= response (client-recv! client)) - {:id @id* :ns "xyz" :value "[6 nil 4 ZeroDivisionError('Fraction(4, 0)')]"} {:id @id* :ns "xyz" :status ["done"]}) ;; error with :file (client-send! client {:id (id-inc!) :op "eval" :file "/hey/you.lpy" :code "1\n2\n(/ 5 0)"}) - (are [response] (= response (client-recv! client)) - {:id @id* :err "ZeroDivisionError('Fraction(5, 0)')"}) + (let [{:keys [id err]} (client-recv! client)] + (is (= @id* id)) + (is (str/includes? err "ZeroDivisionError: Fraction(5, 0)"))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "xyz" ns)) @@ -564,7 +581,11 @@ (client-send! client {:id (id-inc!) :op "eval" :code "(xyz"}) (let [{:keys [id err]} (client-recv! client)] (is (= @id* id)) - (is (str/starts-with? err "basilisp.lang.reader.SyntaxError"))) + (is (= ["" + " exception: " + " message: Unexpected EOF in list" + " line: 1:4"] + (str/split-lines err)))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "xyz" ns)) @@ -574,7 +595,15 @@ {:id @id* :ns "xyz" :status ["done"]}) (client-send! client {:id (id-inc!) :op "eval" :code "(+ 3 5)" :ns "not-there"}) - (is (= {:id @id* :err "CompilerException(msg=\"unable to resolve symbol '+' in this context\", phase=, filename='', form=+, lisp_ast=None, py_ast=None)"} (client-recv! client))) + (let [{:keys [id err]} (client-recv! client)] + (is (= id @id*)) + (is (= ["" + " exception: " + " phase: :analyzing" + " message: unable to resolve symbol '+' in this context" + " form: +" + " location: :1"] + (str/split-lines err)))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "not-there" ns)) @@ -598,7 +627,13 @@ ;; bad namespace (client-send! client {:id 3 :op "eval" :code "(+ 3 5)" :ns "#,,"}) - (is (= {:id 3 :err "CompilerException(msg=\"unable to resolve symbol '+' in this context\", phase=, filename='', form=+, lisp_ast=None, py_ast=None)"} (client-recv! client))) + (let [{:keys [id err]} (client-recv! client)] + (is (= 3 id)) + (is (= ["" + " exception: " + " phase: :analyzing" " message: unable to resolve symbol '+' in this context" + " form: +" " location: :1"] + (str/split-lines err)))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= 3 id)) (is (= "#,," ns)) @@ -619,13 +654,17 @@ (let [{:keys [status]} (client-recv! client)] (is (= ["done"] status))) (client-send! client {:id (id-inc!) :op "info" :ns "user" :sym "sort-by"}) - (let [{:keys [file line] :as response} (client-recv! client) - {:keys [doc] - meta-file :file} (meta (resolve 'sort-by))] + (let [{:keys [file line column] :as response} (client-recv! client) + {:keys [doc col] + meta-file :file + meta-line :line} (meta (resolve 'sort-by)) + meta-file-uri (->> (urllib.parse/quote meta-file) + (urllib.parse/urljoin "file:"))] (is (= {:ns "basilisp.core" :status ["done"] :id @id* :arglists-str "[keyfn coll]\n[keyfn cmp coll]" + :line line :column col :doc doc :name "sort-by"} - (select-keys response [:ns :status :id :arglists-str :doc :name]))) - (is (= meta-file file))) + (select-keys response [:ns :status :id :arglists-str :doc :name :line :column]))) + (is (= meta-file-uri file))) ;; test fqdn, aliases and refers (client-send! client {:id (id-inc!) :op "eval" @@ -689,13 +728,25 @@ (with-server-make {} (with-connect client (let [id* (atom 0) - id-inc! #(swap! id* inc)] + id-inc! #(swap! id* inc) + filename (str (bio/path *tempdir* "load-file-test.lpy"))] (client-send! client {:id (id-inc!) :op "clone"}) (let [{:keys [status]} (client-recv! client)] (is (= ["done"] status))) + + (spit filename "(ns abc.xyz (:require [clojure.string :as str]) +(:import [sys :as s])) +(defn afn [] + (str/lower-case \"ABC\")) +(comment + (abc) + (xyz)) + +(afn)") + (client-send! client {:id (id-inc!) :op "load-file" - :ns "user" :file "(ns abc.xyz (:require [clojure.string :as str]) (:import [sys :as s])) (defn afn [] (str/lower-case \"ABC\")) (afn)" - :file-name "xyz.lpy" :file-path "/abc/xyz.lpy"}) + :ns "user" :file (slurp filename) + :file-name "xyz.lpy" :file-path filename}) (are [response] (= response (client-recv! client)) {:id @id* :ns "abc.xyz" :value "\"abc\""} {:id @id* :ns "abc.xyz" :status ["done"]}) @@ -727,9 +778,42 @@ {:id @id* :ns "abc.other" :value "\"abc\""} {:id @id* :ns "abc.other" :status ["done"]}) + (client-send! client {:id (id-inc!) :ns "abc.xyz" :op "eval" :code "(abc)" + :file filename + :line 6 :column 2}) + (let [{:keys [id err]} (client-recv! client)] + (is (= @id* id)) + (is (= ["" + " exception: " + " phase: :analyzing" + " message: unable to resolve symbol 'abc' in this context" + " form: abc" + (str " location: " filename ":6") + " context:" + "" + " 2 | (:import [sys :as s]))" + " 3 | (defn afn []" + " 4 | (str/lower-case \"ABC\"))" + " 5 | (comment" + " 6 > | (abc)" + " 7 | (xyz))" + " 8 | " + " 9 | (afn)"] + (str/split-lines err)))) + (let [{:keys [id ex status ns]} (client-recv! client)] + (is (= @id* id)) + (is (= "abc.xyz" ns)) + (is (= ["eval-error"] status)) + (is (str/starts-with? ex "Traceback (most recent call last):"))) + (are [response] (= response (client-recv! client)) + {:id @id* :ns "abc.xyz" :status ["done"]}) + + (client-send! client {:id (id-inc!) :op "load-file" :ns "user" :file "(ns abc.third)\n\n(/ 3 0)" :file-name "third.lpy" :file-path "/abc/third.lpy"}) - (is (= {:id @id* :err "ZeroDivisionError('Fraction(3, 0)')"} (client-recv! client))) + (let [{:keys [id err]} (client-recv! client)] + (is (= @id* id)) + (is (str/includes? err "ZeroDivisionError: Fraction(3, 0)") (str/split-lines err))) (let [{:keys [id ex status ns]} (client-recv! client)] (is (= @id* id)) (is (= "abc.third" ns)) @@ -737,18 +821,18 @@ (is (not= -1 (.find ex "File \"/abc/third.lpy\", line 3")) ex) (is (str/starts-with? ex "Traceback (most recent call last):"))) (are [response] (= response (client-recv! client)) - {:id @id* :ns "abc.third" :status ["done"]})))) + {:id @id* :ns "abc.third" :status ["done"]}))))) - (testing "no file" - (with-server-make {} - (with-connect client - (client-send! client {:id 1 :op "clone"}) - (let [{:keys [status]} (client-recv! client)] - (is (= ["done"] status))) - (client-send! client {:id 2 :op "load-file" :ns "user"}) - (are [response] (= response (client-recv! client)) - {:id 2 :ns "user" :value "nil"} - {:id 2 :ns "user" :status ["done"]})))))) + (testing "no file" + (with-server-make {} + (with-connect client + (client-send! client {:id 1 :op "clone"}) + (let [{:keys [status]} (client-recv! client)] + (is (= ["done"] status))) + (client-send! client {:id 2 :op "load-file" :ns "user"}) + (are [response] (= response (client-recv! client)) + {:id 2 :ns "user" :value "nil"} + {:id 2 :ns "user" :status ["done"]}))))) (defmacro with-start-server [opts & body]