init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro recompilation), paid by every process that builds a ctx from source — each jpm-test file, embedders, workers. init-cached marshals the built ctx to a disk image (same root-env dicts as snapshot/fork) and later processes unmarshal it in ~5 ms, any process: nothing from the baking process is needed at load. The cache key fingerprints the embedded .clj stdlib (which covers jolt-core: analyzer, IR, core tiers), the .janet seed sources next to the module, the janet version, the init opts, and the env knobs that shape a ctx (JOLT_PATH/ MUTABLE/AOT_CORE/FEATURES) — any change rebuilds. Corrupt or non-ctx images fall back to a rebuild (unmarshal of garbage can 'succeed' with a scalar, so the shape is checked, not just the throw). Writes are atomic (tmp + rename) so racing cold starts never publish a torn image. JOLT_NO_IMAGE_CACHE=1 opts out; JOLT_IMAGE_CACHE_DIR overrides the location (default TMPDIR). Test consumers switch to init-cached (harness, suite-worker, conformance, the behavioral unit/integration tests); tests that validate the bootstrap itself (bootstrap-fixpoint, staged-bootstrap, aot round-trip, direct-linking) and the deps tests (tmp-dir :paths would fragment the key) keep real init. Full jpm test: 2:46 -> 1:58 (~29%). New ctx-image-test covers cold/warm, cross-process load (subprocess runs defn/redef/macros/protocols/multimethods off the baked image), per-opts keying, and corrupt-image fallback.
99 lines
4 KiB
Text
99 lines
4 KiB
Text
# 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")
|
|
|
|
# Watchdog: never let a hang stall CI — bail out after 30s.
|
|
(ev/spawn (ev/sleep 30) (eprint "nrepl-test: watchdog fired (possible hang)") (os/exit 1))
|
|
|
|
(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-cached))
|
|
(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")
|
|
|
|
# a def's value renders as #'ns/name (pr-str loops on a var's cyclic ns refs)
|
|
(check "def renders as #'ns/name"
|
|
"(some #(get % \"value\") (jolt.nrepl/client-eval c \"(def yy 21)\" s))" "#'user/yy")
|
|
|
|
# defs persist across evals in the session
|
|
(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")
|
|
|
|
# in-ns switches the session ns, and it persists to the next eval
|
|
(check "in-ns switches ns"
|
|
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(in-ns (quote foo.bar))\" s))" "foo.bar")
|
|
(check "ns persists across evals"
|
|
"(some #(get % \"ns\") (jolt.nrepl/client-eval c \"(+ 2 2)\" s))" "foo.bar")
|
|
# explicit :ns on the message overrides
|
|
(ev "(jolt.nrepl/request c {\"op\" \"eval\" \"code\" \"(+ 1 1)\" \"session\" s \"ns\" \"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)
|
|
(do (eprint "nrepl-test: " fails " failing check(s)") (os/exit 1))
|
|
(do (print "\nAll nREPL tests passed!") (os/exit 0)))
|