Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions bb/tasks.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@
((requiring-resolve 'clojure.pprint/pprint)
parsed)))))

(defn compile-test-runtime
"Pre-compile the cljs.test runtime from its .cljs source so it ships in
the npm package and is available to local tests."
[]
(shell "node" "node_cli.js" "compile" "src/squint/test.cljs"))

(defn build-squint-npm-package []
(fs/create-dirs ".work")
(fs/delete-tree "lib")
(fs/delete-tree ".shadow-cljs")
(bump-core-vars)
(spit ".work/config-merge.edn" "{}")
(shell "npx shadow-cljs --config-merge .work/config-merge.edn release squint"))
(shell "npx shadow-cljs --config-merge .work/config-merge.edn release squint")
(compile-test-runtime))

(defn publish []
(build-squint-npm-package)
Expand Down Expand Up @@ -74,15 +81,26 @@
out (:out (shell {:dir dir :out :string} (fs/which "npx") "squint" "run" "script.cljs"))]
(assert (str/includes? out "dude"))))

(defn test-cljs-test [_]
(let [src "test-resources/cljs_test_smoke.cljs"
out-file "test-resources/cljs_test_smoke.mjs"]
(shell "node" "node_cli.js" "compile" src)
(let [out (:out (shell {:out :string} "node" out-file))]
(fs/delete out-file)
(assert (str/includes? out "Ran 8 tests containing 17 assertions") out)
(assert (str/includes? out "1 failures, 0 errors") out))))

(defn test-squint []
(fs/create-dirs ".work")
(spit ".work/config-merge.edn" (shadow-extra-test-config))
(bump-core-vars)
(shell "npx shadow-cljs --config-merge .work/config-merge.edn compile squint")
(compile-test-runtime)
(shell "node lib/squint_tests.js")
(node-repl-tests/run-tests {})
(test-project {})
(test-run {}))
(test-run {})
(test-cljs-test {}))

(defn clojure-mode-test []
(let [dir "libtests"]
Expand Down
121 changes: 121 additions & 0 deletions doc/cljs-test-todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# cljs.test / clojure.test: known gaps in squint's implementation

This list captures behavior the squint (and cherry) cljs.test built-ins
don't yet match against the canonical `clojure.test` / `cljs.test`. The
ordering is rough priority — high-impact correctness gaps first, then
extensibility gaps, then polish.

## Correctness — likely to bite real users

### 1. Fixtures are global, not per-namespace ✅ DONE
~~`set-each-fixtures!` / `set-once-fixtures!` write into the single global
env. Real `clojure.test` keys fixtures by namespace (`(alter-meta! ns ...)`).
Today, two test namespaces each calling `(use-fixtures :each ...)` will
clobber each other; whichever loaded last wins for *every* test.~~

Fixed: `:each-fixtures` and `:once-fixtures` are now `{ns-str → vec}`
maps; `core-deftest` stashes `:ns` in the test fn's meta; `test-var`
and `run-tests` look fixtures up by that ns. The 1-arg legacy setters
target a `nil`-keyed bucket for back-compat. Regression tests in both
the squint smoke suite and cherry's `cross_platform_test`.

### 2. run-tests doesn't reset counters per call ✅ DONE
~~`(run-tests)` auto-inits the env only when `*current-env*` is `nil`. A
second `(run-tests)` in the same module reuses (and inflates) the
existing counters. Tests that themselves invoke `run-tests` (squint and
cherry's own runtime tests do this) emit summaries showing cumulative
counts, not per-run counts.~~

Fixed: `run-tests` saves the caller's `:report-counters`, runs against
fresh ones, restores on return. The returned summary map reflects only
that run. Inner `run-tests` calls now show their own per-call summary
instead of polluted cumulative totals. Regression tests in both repos.

### 3. Quoted-symbol emission breaks `is` reporting
The shared macro `(:name '~name)` would expand to `cljs.core.symbol(...)`
under squint, which doesn't exist at runtime. We worked around it by
storing `:name` as a string (and `assert-expr` uses `(pr-str form)`).
Result: `(:name (meta test-fn))` is a string, not a symbol — diverges
from `clojure.test`'s expectations and breaks code that calls
`namespace`/`name` on it.

**Fix sketch:** fix squint's quote emission to route through
`squint_core.symbol` (already a real fn) instead of the literal
`cljs.core.symbol`. Then revert macros to symbol form for parity with
cherry/cljs.test.

## Extensibility — currently impossible to extend

### 4. `assert-expr` is hardcoded
Real `clojure.test/assert-expr` is a multimethod keyed on the head of
the form inside `(is ...)`. Users add new assertions by `defmethod`-ing
it. Ours is a `case` over `=`, `thrown?`, `thrown-with-msg?`. No
extension point.

**Fix sketch:** make `assert-expr` a multimethod (or a registry of
`{op-sym (fn [msg form] ...)}`) that the macro consults. Squint can ship
the existing four cases as default methods.

### 5. `report` is hardcoded too
`clojure.test/report` is a multimethod keyed on `:type`; users add
methods to extend reporting (e.g. cljs-test-display). Ours is a single
`case` defn.

**Fix sketch:** convert to a multimethod (works in squint's runtime; we
already use `(use-fixtures ...)` etc.).

### 6. `test-var` is a plain fn
`clojure.test/test-var` is a multimethod (`:default` impl is what
everyone uses, but it's overridable). Same idea as `report`.

## Coverage gaps — features that exist but are partial

### 7. `:begin-test-ns` / `:end-test-ns` are never emitted
We support the report types in `report`'s `case`, but `run-tests` never
fires them. Real `clojure.test` brackets each ns it processes with these
events; reporters use them to group output.

### 8. No `(t/async done body)` form
Real `cljs.test` async tests use `(async done (do ... (done)))`. We use
`^:async` on the deftest plus a Promise-returning body. Functionally
equivalent but different surface — code copied from a CLJS project
won't run.

### 9. Test discovery is registration-based, not metadata-based
`clojure.test/run-tests` walks `ns-publics` and filters by
`(:test (meta var))`. We use a separate `test-registry` because squint
has no var metadata at runtime. That works for tests defined via our
`deftest`, but a user who manually attaches `:test true` to a defn
won't be picked up.

**Note:** unclear this is fixable without a var registry — probably
accept the divergence and document.

### 10. ~~`successful?` reads global counters~~ — non-issue
On re-read: `successful?` takes a counters map and reads `:fail`/`:error`
from it. No global state involved. Removed.

## Polish — cosmetic but worth fixing

### 11. Inner `run-tests` calls produce double summary output ✅ DONE
~~Two of cherry's `cross_platform_test` cases call `run-tests` inside test
bodies (testing the runtime itself). Each emits its own `:summary` line,
on top of the outer test run's summary.~~

Side-effect of #2: each inner `run-tests` now reports its own per-call
summary with accurate counts (instead of cumulative pollution). Still
multiple summary lines, but the lines are correct and informative —
not noise. Closing.

### 12. `:report-counters :summary` increments needlessly
`report` calls `(inc-report-counter! :type)` unconditionally, including
for `:type :summary`. Adds a meaningless counter to the env map.

### 13. No `*test-out*` redirection
Output goes hard-wired to `js/console.log`/`js/console.error`. Real
`cljs.test` lets users rebind via `*report-out*` or per-method.

### 14. Cherry runtime and squint runtime drifted in subtle ways
Both implement the same surface but are separate `.cljs` files. Easy to
fix one and forget the other. Could share via a `.cljc` if we factor
out the few JS-vs-CLJS differences.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"src/squint/set.js",
"src/squint/html.js",
"src/squint/math.js",
"src/squint/test.mjs",
"lib",
"node_cli.js",
"index.js",
Expand Down Expand Up @@ -59,6 +60,7 @@
"./src/squint/math.js": "./src/squint/math.js",
"./src/squint/string.js": "./src/squint/string.js",
"./node-api.js": "./node-api.js",
"./src/squint/html.js": "./src/squint/html.js"
"./src/squint/html.js": "./src/squint/html.js",
"./src/squint/test.mjs": "./src/squint/test.mjs"
}
}
2 changes: 1 addition & 1 deletion playground/bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
(fs/create-sym-link "public/js/squint-local" (fs/parent (fs/parent (fs/absolutize ".")))))
(let [squint-prod (fs/file "public" "public" "src" "squint")]
(fs/create-dirs squint-prod)
(doseq [source-file ["core.js" "string.js" "set.js" "html.js" "math.js"]]
(doseq [source-file ["core.js" "string.js" "set.js" "html.js" "math.js" "test.mjs"]]
(fs/copy (fs/file ".." "src" "squint" source-file)
squint-prod
{:replace-existing true}))))}
Expand Down
18 changes: 13 additions & 5 deletions playground/public/js/main_js.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,22 @@ let evalCode = async (code) => {
let js = globalThis.compilerState.javascript;
if (dev) {
console.log("Loading local squint libs");
js = js.replaceAll("'squint-cljs/", "'./squint-local/");
// Rewrite to absolute URLs: Blob URLs used for non-REPL eval have an
// opaque origin that can't resolve relative specifiers. Anchor on the
// page origin — import.meta.url can point to an unexpected bundle path
// under Vite.
const localBase = `${window.location.origin}/js/squint-local/`;
js = js.replaceAll("'squint-cljs/", `'${localBase}`);
}
JSEditor(js);
if (!repl) {
const encodedJs = encodeURIComponent(js);
const dataUri =
'data:text/javascript;charset=utf-8;eval=' + Date.now() + ',' + encodedJs;
let result = await import(/* @vite-ignore */dataUri);
const blob = new Blob([js], { type: 'text/javascript' });
const blobUrl = URL.createObjectURL(blob);
try {
let result = await import(/* @vite-ignore */blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
} else {
let result = await eval(`(async function() { ${js} })()`);
if (result && result?.constructor?.name === 'LazyIterable') {
Expand Down
31 changes: 28 additions & 3 deletions src/squint/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
[squint.internal.fn :refer [core-defmacro core-defn core-defn- core-fn]]
[squint.internal.loop :as loop]
[squint.internal.macros :as macros]
[squint.internal.protocols :as protocols])
[squint.internal.protocols :as protocols]
[squint.internal.test :as test])
#?(:cljs (:require-macros [squint.resource :refer [edn-resource]])))


Expand Down Expand Up @@ -105,6 +106,16 @@
'vswap! macros/vswap!}
cc/common-macros))

(def built-in-macro-nss
;; Keyed by every plausible form of the macro namespace so lookups can
;; succeed whether the compiler sees the original symbol, the post-
;; resolve-ns libname string, or the macro-ns returned by resolve-macro-ns.
(let [m test/core-test-macros]
{'squint.test m
'cljs.test m
'clojure.test m
"squint-cljs/src/squint/test.mjs" m}))

(def core-config {:vars (edn-resource "squint/core.edn")})

(def core-vars (conj (:vars core-config) 'goog_typeOf))
Expand Down Expand Up @@ -242,10 +253,24 @@
(some (fn [[alias-sym alias-lib]]
(when (= (str alias-lib) resolved-ns)
(get-in ns-state [:macros alias-sym nms])))
(:aliases current-ns-state)))))))
(:aliases current-ns-state)))
;; Built-in fallback. Only consult if the user actually
;; required this ns — otherwise (cljs.test/foo) with no
;; require would silently work. Cheaper to first see if
;; there's even a built-in candidate.
(when-let [m (or (get-in built-in-macro-nss [macro-ns nms])
(get-in built-in-macro-nss [resolved-ns nms]))]
(when (or (contains? (:aliases current-ns-state) nss)
(contains? (:aliases current-ns-state)
(symbol (cc/alias-munge (str nss)))))
m))))))
(let [refers (:refers current-ns-state)]
(when-let [macro-ns (get refers nms)]
(get-in ns-state [:macros macro-ns nms]))))
(or (get-in ns-state [:macros macro-ns nms])
(let [resolved (cc/resolve-macro-ns macro-ns)]
(or (get-in ns-state [:macros resolved nms])
(get-in built-in-macro-nss [macro-ns nms])
(get-in built-in-macro-nss [resolved nms])))))))
;; lazy resolve: ask SCI for transitive macro deps
(when-let [resolve-macro (:resolve-macro ns-state)]
(resolve-macro (or (some-> ns symbol) nms) nms))))))]
Expand Down
Loading
Loading