feat(nrepl): nREPL server + client in Clojure on a Janet interop bridge
Add a general Janet interop bridge and an nREPL implemented on top of it. Interop bridge (evaluator): - A qualified symbol whose namespace is `janet` or `janet.<module>` resolves against Janet's environment: `janet/<name>` -> root binding (janet/slurp), `janet.<module>/<name>` -> module binding (janet.net/server, janet.os/clock). The explicit `janet` segment marks every crossing into host code (where Clojure semantics, e.g. collection representation, no longer hold). This makes the whole Janet stdlib — networking included — reachable from Clojure. jolt.nrepl (Clojure, src/jolt/jolt/nrepl.clj): - bencode codec (encode + streaming decode), ported from nrepl.bencode. - server: accept loop via janet.net/accept + janet.ev/call (Janet's built-in handler arity-checks Jolt closures, so we drive accept ourselves); ops clone/ describe/eval/load-file/close/ls-sessions/interrupt/eldoc following babashka.nrepl response shapes. eval captures *out* by rebinding Janet's :out, reports ns, streams out, and isolates the eval namespace (current-ns is global ctx state) restoring it afterward. Vars are rendered as #'ns/name (pr-str loops on a var's cyclic ns refs). - client: connect / request / client-eval / client-clone / client-close. CLI: `jolt nrepl [port]` starts the server and writes .nrepl-port; the Clojure source is embedded at build time so the binary is self-contained from any cwd. Tests: test/spec/nrepl-spec.janet (bencode), test/integration/nrepl-test.janet (server+client over a real TCP/bencode wire, server in a subprocess).
This commit is contained in:
parent
0f12598b06
commit
8cbc695f99
6 changed files with 457 additions and 7 deletions
85
test/integration/nrepl-test.janet
Normal file
85
test/integration/nrepl-test.janet
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Integration test: jolt.nrepl server + client over a real TCP/bencode wire.
|
||||
#
|
||||
# The server runs in a subprocess (`jolt nrepl PORT`) so the client (this
|
||||
# process) isn't affected by the server's accept-loop fiber, which leaves the
|
||||
# shared ctx's current-ns pointing at jolt.nrepl. The client uses the jolt.nrepl
|
||||
# Clojure API, exercising both halves of the implementation.
|
||||
|
||||
(use ../../src/jolt/api)
|
||||
(use ../../src/jolt/types)
|
||||
|
||||
(def port "17888")
|
||||
|
||||
(print "Starting jolt.nrepl server subprocess on port " port " ...")
|
||||
(def proc (os/spawn ["janet" "src/jolt/main.janet" "nrepl" port] :p {:out :pipe :err :pipe}))
|
||||
|
||||
# Wait until the server accepts connections (poll up to ~5s).
|
||||
(var ready false)
|
||||
(var tries 0)
|
||||
(while (and (not ready) (< tries 50))
|
||||
(let [r (protect (net/connect "127.0.0.1" port))]
|
||||
(if (r 0) (do (:close (r 1)) (set ready true))
|
||||
(do (ev/sleep 0.1) (++ tries)))))
|
||||
(assert ready "nREPL server did not start")
|
||||
|
||||
(def ctx (init))
|
||||
(ctx-set-current-ns ctx "user")
|
||||
(load-string ctx "(require '[jolt.nrepl])")
|
||||
(load-string ctx (string "(def c (jolt.nrepl/connect {:port " port "}))"))
|
||||
|
||||
(defn ev [e] (eval-string ctx e))
|
||||
(var fails 0)
|
||||
(defn check [label expr expected]
|
||||
(let [got (ev expr)]
|
||||
(if (= got expected)
|
||||
(print " ok " label)
|
||||
(do (++ fails) (printf " FAIL %s: want %q, got %q" label expected got)))))
|
||||
|
||||
# describe advertises ops
|
||||
(check "describe has ops"
|
||||
"(boolean (get (first (jolt.nrepl/request c {\"op\" \"describe\"})) \"ops\"))" true)
|
||||
|
||||
# clone yields a session id
|
||||
(ev "(def s (jolt.nrepl/client-clone c))")
|
||||
(check "clone session is string" "(string? s)" true)
|
||||
|
||||
# eval returns a value
|
||||
(check "eval (+ 1 2)" "(some #(get % \"value\") (jolt.nrepl/client-eval c \"(+ 1 2)\" s))" "3")
|
||||
|
||||
# defs persist across evals in the session
|
||||
(ev "(jolt.nrepl/client-eval c \"(def yy 21)\" s)")
|
||||
(check "def then use" "(some #(get % \"value\") (jolt.nrepl/client-eval c \"(* yy 2)\" s))" "42")
|
||||
|
||||
# stdout is captured and streamed as an out message
|
||||
(check "println captured as out"
|
||||
"(some #(get % \"out\") (jolt.nrepl/client-eval c \"(do (println \\\"hi\\\") 9)\" s))" "hi\n")
|
||||
|
||||
# the response carries the current ns
|
||||
(check "ns field reported"
|
||||
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(+ 1 1)\" s))" "user")
|
||||
|
||||
# eval error -> eval-error status, and the connection keeps working afterward
|
||||
(check "eval error status"
|
||||
"(boolean (some #(let [st (get % \"status\")] (and (sequential? st) (some (fn [x] (= \"eval-error\" x)) st))) (jolt.nrepl/client-eval c \"(/ 1 :z)\" s)))"
|
||||
true)
|
||||
(check "still alive after error"
|
||||
"(some #(get % \"value\") (jolt.nrepl/client-eval c \"(+ 5 5)\" s))" "10")
|
||||
|
||||
# multiple forms in one eval -> a value per form (values arrive as strings)
|
||||
(check "multiple forms"
|
||||
"(= [\"2\" \"4\"] (mapv #(get % \"value\") (filter #(get % \"value\") (jolt.nrepl/client-eval c \"(+ 1 1) (+ 2 2)\" s))))"
|
||||
true)
|
||||
|
||||
# unknown op -> error/unknown-op/done
|
||||
(check "unknown op status"
|
||||
"(let [st (get (first (jolt.nrepl/request c {\"op\" \"nope\"})) \"status\")] (and (some #(= \"unknown-op\" %) st) true))"
|
||||
true)
|
||||
|
||||
# clean up
|
||||
(ev "(jolt.nrepl/client-close c)")
|
||||
(os/proc-kill proc true)
|
||||
(when (os/stat ".nrepl-port") (os/rm ".nrepl-port"))
|
||||
|
||||
(if (> fails 0)
|
||||
(error (string "nrepl-test: " fails " failing check(s)"))
|
||||
(print "\nAll nREPL tests passed!"))
|
||||
27
test/spec/nrepl-spec.janet
Normal file
27
test/spec/nrepl-spec.janet
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Specification: jolt.nrepl bencode codec (pure, no networking).
|
||||
# The server/client wire behavior is covered by test/integration/nrepl-test.janet.
|
||||
(use ../support/harness)
|
||||
|
||||
(defn- b [body]
|
||||
(string "(do (require '[jolt.nrepl :as nr]) " body ")"))
|
||||
(defn- rt [body]
|
||||
# round-trip a value through encode -> decode
|
||||
(b (string "(nr/decode (nr/reader nil (nr/encode " body ")))")))
|
||||
|
||||
(defspec "jolt.nrepl / bencode round-trip"
|
||||
["integer" "42" (rt "42")]
|
||||
["negative" "-7" (rt "-7")]
|
||||
["string" "\"hello\"" (rt "\"hello\"")]
|
||||
["empty string" "\"\"" (rt "\"\"")]
|
||||
["list" "[\"a\" 1 \"b\"]" (rt "[\"a\" 1 \"b\"]")]
|
||||
["nested list" "[1 [2 3]]" (rt "[1 [2 3]]")]
|
||||
["dict" "{\"op\" \"eval\" \"id\" \"7\"}" (rt "{\"op\" \"eval\" \"id\" \"7\"}")]
|
||||
["dict with list" "{\"status\" [\"done\"]}" (rt "{\"status\" [\"done\"]}")]
|
||||
["nested dict" "{\"a\" {\"b\" 1}}" (rt "{\"a\" {\"b\" 1}}")])
|
||||
|
||||
(defspec "jolt.nrepl / bencode encode shape"
|
||||
["int" "\"i42e\"" (b "(nr/encode 42)")]
|
||||
["string" "\"5:hello\"" (b "(nr/encode \"hello\")")]
|
||||
["list" "\"li1ei2ee\"" (b "(nr/encode [1 2])")]
|
||||
# dict keys are sorted lexicographically
|
||||
["dict sorted keys" "\"d1:ai1e1:bi2ee\"" (b "(nr/encode {\"b\" 2 \"a\" 1})")])
|
||||
Loading…
Add table
Add a link
Reference in a new issue