jolt/test/integration/deps-resolve-test.janet
Yogthos c2511fee7e deps: make the .cpcache key stable and keep git fetch off stdout
The cache key was built with janet's (hash ...), which is seeded per
process, so it never matched across invocations and every jolt-deps run
re-resolved and re-fetched. Store the raw key material instead.

Run the jpm git calls silenced (:silent) with a one-line progress note
on stderr — the checkout chatter ("HEAD is now at ...") was landing on
stdout and corrupting the documented JOLT_PATH=$(jolt-deps path) capture.

Covered by two new deps-resolve checks: a cross-process cache-hit probe
(sentinel-tampered cache file) and a stdout-cleanliness check against a
local file:// git dep.
2026-06-11 01:31:37 -04:00

130 lines
5.6 KiB
Text

# deps.edn resolution into source roots, then loading a library through them.
# Uses :local/root deps only so it needs no network (the git path is the same
# code, exercised manually against real repos).
(use ../../src/jolt/api)
(use ../../src/jolt/types)
(import ../../src/jolt/deps :as deps)
# Captured before any os/cd: subprocess tests below re-import src/jolt/deps.
(def repo-root (os/cwd))
(def base (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-deps-resolve-" (os/time)))
(defn rmrf [p]
(when (os/stat p)
(if (= :directory (os/stat p :mode))
(do (each e (os/dir p) (rmrf (string p "/" e))) (os/rmdir p))
(os/rm p))))
(rmrf base)
(defn mkdirs [p]
(def abs (string/has-prefix? "/" p))
(var acc nil)
(each seg (filter |(not= "" $) (string/split "/" p))
(set acc (cond (nil? acc) (if abs (string "/" seg) seg) (string acc "/" seg)))
(unless (os/stat acc) (os/mkdir acc))))
# project -> depends on lib-a (local), which depends on lib-b (local, transitive)
(each d ["proj/src" "a/src/liba" "b/src/libb"] (mkdirs (string base "/" d)))
(spit (string base "/proj/deps.edn") `{:paths ["src"] :deps {my/a {:local/root "../a"}}}`)
(spit (string base "/a/deps.edn") `{:paths ["src"] :deps {my/b {:local/root "../b"}}}`)
(spit (string base "/b/deps.edn") `{:paths ["src"]}`)
(spit (string base "/a/src/liba/core.clj") "(ns liba.core (:require [libb.core :as b]))\n(defn val [] (b/n))\n")
(spit (string base "/b/src/libb/core.clj") "(ns libb.core)\n(defn n [] 99)\n")
(var fails 0)
(defn check [label got expected]
(if (= got expected) (print " ok " label)
(do (++ fails) (printf " FAIL %s: want %q, got %q" label expected got))))
(os/cd (string base "/proj"))
(def roots (deps/resolve-deps "deps.edn" (string base "/proj/jpm_tree")))
# roots: proj/src, a/src (transitive: b/src) — at least the two dep srcs present
(check "resolves transitive local dep roots"
(truthy? (and (some |(string/has-suffix? "/a/src" $) roots)
(some |(string/has-suffix? "/b/src" $) roots)))
true)
# load through the resolved roots: liba.core requires libb.core transitively
(def ctx (init {:paths roots}))
(ctx-set-current-ns ctx "user")
(let [r (protect (eval-string ctx "(do (require (quote [liba.core :as a])) (a/val))"))]
(check "require local lib + transitive dep" (if (r 0) (r 1) (string "ERR " (r 1))) 99))
# cached resolution returns the same roots without re-walking
(check "cached resolve matches"
(deep= roots (deps/resolve-deps-cached "deps.edn" (string base "/proj/jpm_tree")))
true)
# The cache must hit across PROCESSES: janet's (hash ...) is seeded per process,
# so a key built with it never matches a cached one and every invocation
# re-resolved (and re-fetched git deps). Detection: plant a sentinel root in the
# cache file — a fresh process that hits the cache returns it; a process that
# misses re-resolves and overwrites it.
(os/cd (string base "/proj"))
(def cache-file ".cpcache/jolt-deps.jdn")
(def cached-now (parse (slurp cache-file)))
(spit cache-file
(string/format "%j" (merge cached-now
{:roots [;(get cached-now :roots) "/SENTINEL"]})))
(defn subprocess-roots []
(def code
(string `(os/cd "` repo-root `") `
`(import ./src/jolt/deps :as deps) `
`(os/cd "` base "/proj" `") `
`(print (string/join (deps/resolve-deps-cached "deps.edn" "` base "/proj/jpm_tree" `") ":"))`))
(def p (os/spawn ["janet" "-e" code] :px {:out :pipe}))
(def out (ev/read (p :out) :all))
(os/proc-wait p)
(string/trim (string out)))
(check "cache hits across processes"
(string/has-suffix? "/SENTINEL" (subprocess-roots))
true)
(os/cd "/")
(rmrf base)
# Git-dep resolution must keep stdout clean: `JOLT_PATH=$(jolt-deps path)` is
# the documented capture, and git's checkout chatter ("HEAD is now at …") was
# corrupting it. Uses a file:// git dep so no network is needed.
(def gbase (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-deps-gitout-" (os/time)))
(rmrf gbase)
(each d ["lib/src/glib" "proj2"] (mkdirs (string gbase "/" d)))
(spit (string gbase "/lib/src/glib/core.clj") "(ns glib.core)\n(defn n [] 7)\n")
(spit (string gbase "/lib/deps.edn") `{:paths ["src"]}`)
(defn sh-out [args &opt cwd]
(def p (os/spawn args :px {:out :pipe :err :pipe :cd (or cwd ".")}))
(def out (ev/read (p :out) :all))
(os/proc-wait p)
(string/trim (string out)))
(def git-ok
(truthy?
(protect
(do (sh-out ["git" "-c" "init.defaultBranch=master" "init"] (string gbase "/lib"))
(sh-out ["git" "add" "-A"] (string gbase "/lib"))
(sh-out ["git" "-c" "user.email=t@t" "-c" "user.name=t" "commit" "-m" "init" "-q"]
(string gbase "/lib"))))))
(if (not git-ok)
(print " skip git-dep stdout test (git unavailable)")
(do
(def sha (sh-out ["git" "rev-parse" "HEAD"] (string gbase "/lib")))
(spit (string gbase "/proj2/deps.edn")
(string `{:paths ["src"] :deps {my/glib {:git/url "file://` gbase `/lib" :git/sha "` sha `"}}}`))
(os/setenv "JOLT_GITLIBS" (string gbase "/gitlibs"))
(def code
(string `(os/cd "` repo-root `") `
`(import ./src/jolt/deps :as deps) `
`(os/cd "` gbase "/proj2" `") `
`(deps/resolve-deps "deps.edn" "` gbase "/proj2/jpm_tree" `") `
`(eprint "done")`))
(def p (os/spawn ["janet" "-e" code] :px {:out :pipe}))
(def out (ev/read (p :out) :all))
(os/proc-wait p)
(os/setenv "JOLT_GITLIBS" nil)
(check "git-dep resolution keeps stdout clean" (string (or out "")) ""))
)
(rmrf gbase)
(if (> fails 0)
(error (string "deps-resolve-test: " fails " failing check(s)"))
(print "\nAll deps-resolve tests passed!"))