Merge pull request #95 from jolt-lang/migratus-fixes

Migratus fixes
This commit is contained in:
Dmitri Sotnikov 2026-06-13 23:13:43 +00:00 committed by GitHub
commit 52bea0b620
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1214 additions and 114 deletions

View file

@ -462,6 +462,58 @@
["not-any?" "true" "(not-any? even? [1 3 5])"]
["take-last" "[3 4]" "(take-last 2 [1 2 3 4])"]
["replace nil val" "[1 nil 3]" "(replace {2 nil} [1 2 3])"]
### ---- migratus enablement: def/defmacro/defmulti forms, assoc, try, ns ----
# (def name docstring value): the value is the 3rd form, not the docstring
# (jolt-6ym — the analyzer used to bind the docstring as the value).
["def 3-arg docstring" "42" "(do (def dd \"the doc\" 42) dd)"]
["def docstring value type" "true" "(do (def ds \"doc\" [1 2]) (vector? ds))"]
# ^{:map} metadata on a def/defn name reads as a with-meta form (jolt-8w2);
# def/defn/defmacro unwrap it instead of choking.
["def ^{:map} name" "5" "(do (def ^{:private true} mmv 5) mmv)"]
["defn ^{:map} name" "25" "(do (defn ^{:private true} sqf [x] (* x x)) (sqf 5))"]
# defmacro arity-clause form (jolt-whp) and a leading docstring (with-store shape)
["defmacro arity-clause" "10" "(do (defmacro m2c ([x] (list (quote *) x 2))) (m2c 5))"]
["defmacro doc + arity" "30" "(do (defmacro m3c \"doc\" ([x] (list (quote *) x 3))) (* (m3c 5) 2))"]
# defmulti drops a leading docstring (jolt-es4 — it used to be the dispatch fn)
["defmulti docstring" "\"A\"" "(do (defmulti gmm \"the doc\" identity) (defmethod gmm :a [_] \"A\") (gmm :a))"]
# (assoc nil k v) yields a real map (jolt-w4s); assoc-in nests real maps
["assoc nil is a map" "1" "(count (assoc nil :a 1))"]
["assoc-in nested is a map" "1" "(count (:a (assoc-in {} [:a :b] 1)))"]
["assoc-in deep get" "9" "(get-in (assoc-in {} [:a :b :c] 9) [:a :b :c])"]
# try: a multi-form body and finally that runs on the SUCCESS path with a
# catch present (jolt-0z9 — body forms past the first were dropped and
# finally was skipped on success).
["try multi-body last" "3" "(try 1 2 3 (catch :default e 0))"]
["try finally on ok+catch" "9" "(let [a (atom 0)] (try 1 2 (catch :default e :c) (finally (reset! a 9))) @a)"]
["try finally on throw" "9" "(let [a (atom 0)] (try (throw (ex-info \"x\" {})) (catch :default e nil) (finally (reset! a 9))) @a)"]
# current-ns is restored after a caught throw (jolt-96m): the alias/ns seen by
# code after the catch is the one at the catch site, not the thrower's.
["ns restored after catch" "\"user\""
"(do (ns cf.boom) (defn bz [] (throw (Exception. \"e\"))) (in-ns (quote user)) (try (cf.boom/bz) (catch :default e nil)) (str *ns*))"]
# methods sees cross-ns defmethods through a bare multifn ref in its defining
# ns (jolt-9pu — it used to see an empty table).
["cross-ns methods visible" "[:sql]"
"(do (ns cf.mm) (defmulti ext identity) (defmethod ext :default [_] :d) (defn allk [] (vec (for [[k v] (methods ext) :when (not= k :default)] k))) (ns cf.mmi) (defmethod cf.mm/ext :sql [_] :s) (in-ns (quote user)) (cf.mm/allk))"]
### ---- defmacro surface + syntax-quote/ns (enabling real clojure libs) ----
# multi-arity defmacro (clojure.tools.logging/log has 4 arities)
["defmacro multi-arity" "[6 5 6]"
"(do (defmacro mar ([a] (list (quote +) a 1)) ([a b] (list (quote +) a b)) ([a b c] (list (quote +) a b c))) [(mar 5) (mar 2 3) (mar 1 2 3)])"]
# defmacro with docstring AND attr-map before the params (every tools.logging
# level macro is shaped this way)
["defmacro doc + attr-map" "10"
"(do (defmacro mam \"doc\" {:arglists (quote ([x]))} [x] (list (quote inc) x)) (mam 9))"]
# syntax-quote resolves a namespace ALIAS to its target ns, so a macro's
# template resolves at the USE site (jolt-9av)
["syntax-quote resolves alias" "\"HI\""
"(do (ns sq.lib (:require [clojure.string :as s])) (defmacro up [x] `(s/upper-case ~x)) (in-ns (quote user)) (sq.lib/up \"hi\"))"]
# ^{:map} metadata on an ns name (jolt-8w2): the ns name is the bare symbol
["ns name with ^{:map} meta" "5"
"(do (ns ^{:author \"a\" :doc \"d\"} nm.meta) (def q 5) (in-ns (quote user)) nm.meta/q)"]
# ~*ns* splices the live namespace object into a template (it self-evaluates)
["unquote *ns* in template" "true"
"(do (defmacro cur-ns [] `(str ~*ns*)) (string? (cur-ns)))"]
])
# Run every case under a given context factory and return the failures. The same

View file

@ -67,7 +67,10 @@
"(refer (quote clojure.string))"
# Stage 2 tier 6c: dispatch-table ops + misc compile as macros/plain invokes
"(prefer-method mf :a :b)" "(remove-method mf :a)" "(remove-all-methods mf)"
"(get-method mf :a)" "(methods mf)"
# get-method/methods take the multimethod VALUE (Clojure semantics), so the
# arg must resolve — use a real multifn (print-method) rather than the
# never-defined mf the isolated analyzer can't resolve.
"(get-method print-method :default)" "(methods print-method)"
"(satisfies? P 5)" "(instance? String \"x\")" "(locking :x 1)"
"(defonce fz-once 1)" "(read-string \"[1 2]\")"
"(macroexpand-1 (quote (when true 1)))"])

View file

@ -223,3 +223,70 @@
"(do (def prev (__reader-features)) (__reader-features-set! [\"clj\" \"jolt\" \"default\"]) (def r (contains? (set (__reader-features)) \"clj\")) (__reader-features-set! prev) r)"]
["restore returns to default" "false"
"(do (def prev (__reader-features)) (__reader-features-set! [\"clj\"]) (__reader-features-set! prev) (contains? (set (__reader-features)) \"clj\"))"])
# JVM class shims migratus relies on. Exception constructors resolve as bare
# class symbols (jolt-6xk) and carry a message; Character/Thread/Long statics;
# java.sql.Timestamp is the millis number; SimpleDateFormat formats UTC.
(defspec "host-interop / migratus class shims"
["Exception. message" "\"boom\""
"(try (throw (Exception. \"boom\")) (catch Throwable e (.getMessage e)))"]
["IllegalArgumentException." "\"bad\""
"(try (throw (IllegalArgumentException. \"bad\")) (catch Exception e (.getMessage e)))"]
["InterruptedException." "\"stop\""
"(try (throw (InterruptedException. \"stop\")) (catch Throwable e (.getMessage e)))"]
["Character/isUpperCase" "true" "(Character/isUpperCase \\A)"]
["Character/isLowerCase" "true" "(Character/isLowerCase \\a)"]
["Character/isUpperCase neg" "false" "(Character/isUpperCase \\a)"]
["Thread/interrupted" "false" "(Thread/interrupted)"]
["Long/valueOf" "42" "(Long/valueOf \"42\")"]
["Timestamp is millis" "1000" "(.getTime (java.util.Date. (java.sql.Timestamp. 1000)))"]
["SimpleDateFormat UTC" "\"19700101000000\""
"(let [f (doto (java.text.SimpleDateFormat. \"yyyyMMddHHmmss\") (.setTimeZone (java.util.TimeZone/getTimeZone \"UTC\")))] (.format f (java.util.Date. 0)))"])
# java.io.File model (jolt-hjw): io/file builds a value that answers
# (instance? File _) so migratus's File-vs-jar branch takes the filesystem path;
# the method surface and a File-aware file-seq back it; str/slurp coerce to path.
(defspec "host-interop / java.io.File"
["instance? File" "true" "(do (require '[clojure.java.io :as io]) (instance? java.io.File (io/file \"/a/b\")))"]
["str is the path" "\"/a/b\"" "(do (require '[clojure.java.io :as io]) (str (io/file \"/a/b\")))"]
["getName" "\"c.txt\"" "(do (require '[clojure.java.io :as io]) (.getName (io/file \"/a/b/c.txt\")))"]
["getPath joins" "\"/a/b\"" "(do (require '[clojure.java.io :as io]) (.getPath (io/file \"/a\" \"b\")))"]
["isDirectory of repo dir" "true" "(do (require '[clojure.java.io :as io]) (.isDirectory (io/file \"docs\")))"]
["isFile of repo file" "true" "(do (require '[clojure.java.io :as io]) (.isFile (io/file \"project.janet\")))"]
["exists is false off-disk" "false" "(do (require '[clojure.java.io :as io]) (.exists (io/file \"/no/such/path/xyz\")))"]
["file-seq yields File values" "true"
"(do (require '[clojure.java.io :as io]) (every? (fn [f] (instance? java.io.File f)) (file-seq (io/file \"docs\"))))"]
["file-seq finds files" "true"
"(do (require '[clojure.java.io :as io]) (pos? (count (filter (fn [f] (.isFile f)) (file-seq (io/file \"docs\"))))))"])
# The REAL clojure.tools.logging (vendored verbatim) runs on jolt: a jolt
# clojure.tools.logging.impl backend (stderr LoggerFactory) plus host shims
# (agent/send-off, clojure.lang.LockingTransaction/isRunning, a clojure.pprint
# subset, clojure.string/trim-newline) and the defmacro/syntax-quote/ns fixes.
# These cases assert the public API behaves; the level rows return nil because
# the real level macros return the (nil) result of log* when enabled.
(defspec "host-interop / clojure.tools.logging"
["info returns nil" "nil" "(do (require '[clojure.tools.logging :as log]) (log/info \"hi\" 42))"]
["debug suppressed" "nil" "(do (require '[clojure.tools.logging :as log]) (log/debug \"x\"))"]
["debug skips args" "0" "(do (require '[clojure.tools.logging :as log]) (let [a (atom 0)] (log/debug (reset! a 9)) @a))"]
["enabled? info" "true" "(do (require '[clojure.tools.logging :as log]) (log/enabled? :info))"]
["enabled? debug" "false" "(do (require '[clojure.tools.logging :as log]) (log/enabled? :debug))"]
["spy returns value" "3" "(do (require '[clojure.tools.logging :as log]) (log/spy :info (+ 1 2)))"]
["impl factory name" "\"jolt/stderr\""
"(do (require '[clojure.tools.logging.impl :as impl]) (impl/name (impl/find-factory)))"]
# vars that exist only in the real library (not in a hand-rolled shim)
["real lib *tx-agent-levels*" "#{:info :warn}"
"(do (require '[clojure.tools.logging :as log]) log/*tx-agent-levels*)"]
["real lib log-capture! present" "true"
"(do (require '[clojure.tools.logging :as log]) (fn? log/log-capture!))"]
["real lib spyf present" "true"
"(do (require '[clojure.tools.logging :as log]) (boolean (resolve 'log/spyf)))"])
# Host shims that let the real clojure.tools.logging load: no STM (a transaction
# is never running) and a minimal clojure.pprint (used by spy).
(defspec "host-interop / logging host shims"
["LockingTransaction/isRunning" "false" "(clojure.lang.LockingTransaction/isRunning)"]
["pprint writes value" "\"[1 2 3]\\n\""
"(do (require '[clojure.pprint :as pp]) (with-out-str (pp/pprint [1 2 3])))"]
["with-pprint-dispatch runs body" "42"
"(do (require '[clojure.pprint :as pp]) (pp/with-pprint-dispatch pp/code-dispatch 42))"])

View file

@ -85,3 +85,24 @@
["with-redefs-fn" "42" "(do (defn wr-i [] 1) (with-redefs-fn {(var wr-i) (fn [] 42)} (fn [] (wr-i))))"]
["macroexpand full" "true" "(let [e (macroexpand (quote (when-not false 1)))] (= (quote if) (first e)))"]
["macroexpand non-macro" "[1 2]" "(macroexpand (quote [1 2]))"])
# defmacro accepts the arity-clause form (defmacro name ([params] body...)), a
# leading docstring, and ^{:map} metadata on the name (jolt-whp, jolt-8w2).
(defspec "macros / defmacro arity-clause & name metadata"
["arity-clause form" "10" "(do (defmacro tw ([x] (list (quote *) x 2))) (tw 5))"]
["docstring + arity" "15" "(do (defmacro th \"triple\" ([x] (list (quote *) x 3))) (th 5))"]
["^{:map} name meta" "7" "(do (defmacro ^{:private true} pm [] 7) (pm))"]
["multi-form body" "6" "(do (defmacro mb ([a b] (list (quote +) a b))) (mb 2 4))"])
# Multi-arity defmacro (dispatch on arg count) and the docstring + attr-map +
# params form — both needed to run real Clojure macros, e.g.
# clojure.tools.logging/log (4 arities) and its level macros (jolt-q8l, jolt-qnr).
(defspec "macros / defmacro multi-arity & attr-map"
["multi-arity 1" "6" "(do (defmacro ma ([a] (list (quote +) a 1)) ([a b] (list (quote +) a b))) (ma 5))"]
["multi-arity 2" "5" "(do (defmacro ma ([a] (list (quote +) a 1)) ([a b] (list (quote +) a b))) (ma 2 3))"]
["arity delegates" "[:d nil 9]"
"(do (defmacro lg ([m] `(lg :d nil ~m)) ([l t m] (list (quote vector) l t m))) (lg 9))"]
["doc + attr-map + params" "10"
"(do (defmacro am \"doc\" {:arglists (quote ([x]))} [x] (list (quote inc) x)) (am 9))"]
["doc + attr-map + variadic" "6"
"(do (defmacro vg \"d\" {:arglists (quote ([& a]))} [& xs] `(+ ~@xs)) (vg 1 2 3))"])

View file

@ -177,3 +177,13 @@
["empty? lazy nil elem" "false" "(empty? (cons nil nil))"]
["empty? sorted" "[true false]" "[(empty? (sorted-map)) (empty? (sorted-set 1))]"]
["empty? number throws" :throws "(empty? 5)"])
# (assoc nil k v) yields a real immutable map, not a raw host table, so
# assoc-in into absent keys nests countable/seqable maps (jolt-w4s).
(defspec "map / assoc on nil"
["assoc nil is a map" "{:a 1}" "(assoc nil :a 1)"]
["count of assoc nil" "1" "(count (assoc nil :a 1))"]
["assoc-in nested countable" "1" "(count (:a (assoc-in {} [:a :b] 1)))"]
["assoc-in deep get" "9" "(get-in (assoc-in {} [:a :b :c] 9) [:a :b :c])"]
["seq over assoc-nil map" ":a" "(ffirst (seq (assoc nil :a 1)))"]
["keys of assoc-nil map" "[:a]" "(vec (keys (assoc nil :a 1)))"])

View file

@ -54,3 +54,17 @@
"(do (defmulti pm5 identity) (defmethod pm5 :a [x] 1) (defmethod pm5 :b [x] 2) (prefer-method pm5 :a :b) (contains? (get (prefers pm5) :a) :b))"]
["exact match needs no preference" ":exact"
"(do (derive :t/sq :t/rect) (defmulti pm6 identity) (defmethod pm6 :t/sq [x] :exact) (defmethod pm6 :t/rect [x] :parent) (pm6 :t/sq))"])
# defmulti drops a leading docstring/attr-map (jolt-es4 — it used to be taken as
# the dispatch fn). methods/get-method take the multimethod VALUE and recover
# the table, so a bare multifn ref works even when defmethods live elsewhere
# (jolt-9pu).
(defspec "multimethods / docstring & value-based table ops"
["defmulti docstring" "\"A\""
"(do (defmulti gd \"the dispatcher\" identity) (defmethod gd :a [_] \"A\") (gd :a))"]
["defmulti doc+default" "\"d\""
"(do (defmulti gx \"doc\" identity) (defmethod gx :default [_] \"d\") (gx :anything))"]
["methods on value" "2"
"(do (defmulti gm identity) (defmethod gm 1 [_] :one) (defmethod gm 2 [_] :two) (count (methods gm)))"]
["get-method on value" "true"
"(do (defmulti gg identity) (defmethod gg :a [_] :x) (fn? (get-method gg :a)))"])

View file

@ -47,3 +47,18 @@
["p{Ps}/p{Pe}" "\"(x)\"" `(re-matches #"^\p{Ps}x\p{Pe}$" "(x)")`]
["(?u) accepted" "\"hi\"" `(re-matches #"(?u)^hi$" "hi")`]
["unknown class throws" :throws `(re-pattern "\p{Greek}")`])
# java.util.regex.Pattern statics (jolt-47b): compile returns jolt's native
# regex value (so .split / str-ops accept it), MULTILINE maps to (?m), quote
# escapes metachars. Plus the regex String methods migratus uses.
(defspec "regex / Pattern statics & String regex methods"
["Pattern/compile is a regex" "true" "(regex? (Pattern/compile \"a.c\"))"]
["compiled .split" "[\"a\" \"b\" \"c\"]" "(.split (Pattern/compile \",\") \"a,b,c\")"]
["str/replace w/ Pattern" "\"ab\"" "(do (require '[clojure.string :as s]) (s/replace \"a1b2\" (Pattern/compile \"[0-9]\") \"\"))"]
["Pattern/MULTILINE ^" "true" "(boolean (re-find (Pattern/compile \"^x\" Pattern/MULTILINE) \"y\\nx\"))"]
["Pattern/quote literal" "true" "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"za.cy\"))"]
["Pattern/quote not meta" "false" "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"zabcy\"))"]
[".matches whole string" "true" "(.matches \"abc\" \"a.c\")"]
[".matches partial -> false" "false" "(.matches \"abcd\" \"a.c\")"]
[".replaceAll regex" "\"a-b-c\"" "(.replaceAll \"a_b_c\" \"_\" \"-\")"]
[".replaceFirst regex" "\"a-b_c\"" "(.replaceFirst \"a_b_c\" \"_\" \"-\")"])

View file

@ -37,3 +37,12 @@
(defspec "state / promises"
["promise deliver" "5" "(let [p (promise)] (deliver p 5) @p)"]
["promise undelivered" "nil" "(let [p (promise)] @p)"])
# Minimal synchronous agent shim (jolt has no thread pool/STM) — enough for
# libraries that hold an agent without depending on async dispatch.
(defspec "state / agents (synchronous shim)"
["agent deref" "0" "(deref (agent 0))"]
["agent with opts" "0" "(deref (agent 0 :error-mode :continue))"]
["send-off applies" "5" "(let [a (agent 0)] (send-off a + 5) (deref a))"]
["send applies" "7" "(let [a (agent 1)] (send a + 6) (deref a))"]
["agent-error nil" "nil" "(agent-error (agent 0))"])

View file

@ -62,3 +62,11 @@
["get out of range nil" "nil" "(get \"abc\" 9)"]
["get negative nil" "nil" "(get \"abc\" -1)"]
["get default honored" ":none" "(get \"abc\" 9 :none)"])
# clojure.string/trim-newline (ported from clojure.string): strips trailing \n/\r.
(defspec "clojure.string / trim-newline"
["trailing newline" "\"x\"" "(do (require (quote [clojure.string :as s])) (s/trim-newline \"x\\n\"))"]
["trailing \\r\\n" "\"x\"" "(do (require (quote [clojure.string :as s])) (s/trim-newline \"x\\r\\n\"))"]
["no trailing" "\"ab\"" "(do (require (quote [clojure.string :as s])) (s/trim-newline \"ab\"))"]
["only newlines" "\"\"" "(do (require (quote [clojure.string :as s])) (s/trim-newline \"\\n\\n\"))"]
["interior kept" "\"a\\nb\"" "(do (require (quote [clojure.string :as s])) (s/trim-newline \"a\\nb\\n\"))"])