Fix the host gaps found running libraries' own test harnesses - #528
Conversation
Replaying third-party suites through each library's real runner (cognitect test-runner, kaocha's fallbacks, lein's paths) instead of the bespoke conformance-run harness surfaced a batch of host bugs the old recipes could not reach, because they hand-listed namespaces and supplied their own dep sets. deps resolution: - accept the sha an annotated tag carries as well as the commit it peels to. git ls-remote prints the tag object for refs/tags/X, so that is what a coordinate written from its output pins (cognitect-labs/test-runner v0.5.0); tools.deps takes either. The tag cache is now two tokens and a one-token file from an older jolt is re-resolved rather than trusted. - accept the legacy :sha / :tag spellings alongside :git/sha / :git/tag, which tools.deps has always taken. malli pins spec-alpha2 that way. host classes: - iconv: issue the POSIX reset call so a stateful charset emits its closing escape. (.getBytes "い" "ISO-2022-JP") dropped the trailing ESC ( B. - give LineNumberingPushbackReader its own jhost tag so it reports its own class; tools.reader extends IndexingReader onto that class and the extension never dispatched. Every literal tag test now goes through one predicate. - io/resource answers an absolute file: URL. Source roots are relative, and resolving a name against "file:./test/x" throws MalformedURLException. - add Character/codePointAt, and Runtime's memory API over Chez's own heap accounting (totalMemory/freeMemory/maxMemory/gc) plus runFinalization. - (. Foo bar x) on an imported simple name is a static call, so route a method the class token does not answer through host-static-ref — which is where the on-demand class autoload lives. The slash form always had it; this form only worked when an earlier slash-form call had already loaded the provider. stdlib: - add clojure.stacktrace, which Clojure ships and jolt did not. Frame lists are empty here (tail calls); the throwable line, ex-data and cause chain match. diagnostics: - a provider that is on the source roots but fails to load is no longer reported as a dependency the caller forgot to declare. Gates: 7 corpus rows, an 8-assertion clojure.stacktrace acceptance test wired into smoke.sh, and 8 deps-alias checks. known-divergences gains the classloader, URLStreamHandler, JMX and spec.alpha-transitivity entries.
Two more gaps from running libraries' own harnesses. org.clojure/clojure is intrinsic — jolt IS Clojure, and putting the artifact's source on the roots would shadow core — but that does not make its DEPENDENCIES intrinsic. The artifact depends on spec.alpha and core.specs.alpha, neither of which is part of core on either host, so on the JVM a project declaring only Clojure still gets clojure.spec.alpha; libraries lean on that and require spec without naming it (kaocha, spec.alpha's own suite). Dropping the coordinate whole took its children with it. Substitute them instead, reading the versions off the declared Clojure's own POM so they match what tools.deps resolves — 1.12.4 gives spec.alpha 0.5.238, 1.9.0 gives 0.1.143, as on the JVM. A pinned pair covers an unreadable POM, and that lookup is quiet: it is not degrading to a jar's pom.xml the way the warning says, since jolt never loads Clojure's jar. host-new already tried the class autoload, but jt-library-names is hand- maintained and had drifted from what jolt-lang/time registers. DateTimeFormatterBuilder was missing, so (java.time.format.DateTimeFormatterBuilder.) autoloaded while an imported (DateTimeFormatterBuilder.) did not — a fully-qualified miss reaches the autoload through java-time-prefixed? anyway, so only the simple form, which is how libraries write it, was affected. It kept malli.transform from loading. The grenadine test that asserted Clojure is pruned now asserts what it was actually protecting: Clojure itself absent, its spec children present.
|
Pushed two more fixes from continuing the sweep (a7bc78f). spec.alpha is transitive from Versions come from the declared Clojure's own POM, so they match what tools.deps resolves:
A pinned pair covers an unreadable POM, and that lookup is quiet — it is not degrading to a jar's A library class autoloads by imported simple name. The grenadine test that asserted "Clojure is pruned" now asserts what it was actually protecting: Clojure itself absent, its spec children present.
Paired with jolt-lang/time#5, which fills in the builder itself, malli goes from not loading at all to 199 tests / 1865 assertions. |
…me extend Four gaps found running orchard, malli, schema and clj-rss through their own test harnesses. clojure.lang.Murmur3 and Util/hashCombine are now reachable as interop. The port already lived in hasheq.ss; this only names it, for a library that folds several hashes together by hand (malli's regex parser combines a function hashCode with a position and a register map). All eight values are certified against 1.12.5. clojure.java.javadoc and clojure.java.browse were missing from the stdlib, so orchard could not load at all. browse drops Clojure's java.awt.Desktop and Swing fallbacks — there is no AWT here — and is the open-url script path alone, which is what covers macOS and xdg-open in practice anyway. extend-protocol to a record named by its FULLY-QUALIFIED name, from a third namespace, filed the impl under a tag no value carries: register-method prefixed the name with the EXTENDING namespace, but a fully-qualified name is already the tag format. schema does exactly this and aborted on load. ClassLoader.getParent answers nil rather than erroring, which terminates the usual (take-while identity (iterate #(.getParent %) loader)) walk. Also records an open bug, jolt-ewmt: protocol method tables are keyed by the protocol's SIMPLE name, so two protocols of the same name in different namespaces silently share one table and the later extend wins. That is a wrong answer, not an error, and it accounts for 594 of malli's 783 errors when its suites run together. test/chez/proto-collision-app pins the current behaviour so the fix cannot land unnoticed.
Protocol dispatch walks a value's class tags in order, and a throwable, an atom and a namespace all reported nothing but "Object" — so an extend-protocol to Throwable or IRef never fired and the Object default won instead. They now report their own class and its ancestry, which is what (class x) already said for all three. Atom joins the graph under IRef rather than IDeref directly, as it does on the JVM via ARef. clojure.datafy was missing, and clojure.core.protocols declared Datafiable and Navigable without Clojure's own nil/Object arms, so "default identity" in those docstrings was a dispatch miss. Both are needed by anything reaching for datafy; aws-api is what surfaced it. The java.lang.Class arm of datafy is absent — it is clojure.reflect, which reads bytecode. test/chez/datafy-test.clj runs unchanged on reference Clojure and prints the same DATAFY OK there. It is a smoke case rather than corpus rows because a qualified reference in the same form as its require compiles before the require runs.
Two protocols with the same name in different namespaces shared one dispatch table, so the later extend silently replaced the earlier one. The JVM keys by the generated interface's FQN, so they stay distinct. Same collision through instance?: a record in one namespace answered true for a same-named protocol in another. defprotocol now bakes "<ns>/<Name>" into the protocol value, its method shims and the devirt registry; deftype/defrecord/reify/extend-type resolve the protocol symbol (through :refer and :as) and key their impls by that. A symbol naming no protocol -- Object, java.util.Map, an :import-ed clojure.lang.ILookup -- keeps its bare name, which is how the tags a value reports are spelled. instance?/satisfies? compare a key against a class name as the interface "<ns>.<Name>". Found via malli: requiring malli.generator-ast evals a renamed copy of malli.generator, defining a second protocol named Generator extended to Object, which replaced the real -generator. That one collision accounted for 594 of malli's 783 errors. Fixes jolt-ewmt.
Calling one already worked -- jolt-invoke dispatches to the declared invoke method -- but both predicates read false, so malli's :ifn schema rejected a value it should accept. Two causes: the IFn instance? arm decided #f for anything that isn't a procedure/keyword/symbol/coll instead of letting the reify's own declared interfaces answer, and jrec-method? only looked at deftypes. Reference Clojure gives ifn? true, fn? false, instance? true; jolt gives the same now, fn? included (reify does not implement Fn). Corpus row certified against 1.12.5.
Plain java.lang.System surface jolt did not have; data.codec's test helper copies byte-array slices through it. Specified to behave as if the source range were copied to a temporary, so an overlapping copy within one array reads pre-copy values -- walk backwards when the ranges overlap forwards. Raises the JVM's ArrayIndexOutOfBoundsException / ArrayStoreException / NullPointerException. Corpus rows cover the overlap and all three classes.
data.codec checks its own base64 against org.apache.commons.codec, a JVM jar, so 11 of its 14 tests died on Base64/encodeBase64 and the suite recorded pass=1. The shim registers the class with an independent RFC 4648 implementation -- deriving it from data.codec's own code would make the test tautological -- on a jolt-owned path ahead of the library's sources, so the checkout stays unmodified. pass=1 fail=2 error=11 -> pass=12 fail=2 error=0. The two remaining failures are instance? against clojure.lang.IFn$OLLOL, a JVM primitive-signature function interface jolt has no model for.
…cipe Keying protocol dispatch by defining namespace let four suites get further: rewrite-clj 162/3380 -> 163/3381, hiccup's one failure to none, ring-core 151/405 -> 152/408, and tick 41/620 with a load failure -> 67/723 clean. honeysql read WORSE for an unrelated reason: honey.sql-alphanumeric-test is a defspec needing test.check, test.chuck's generators and instaparse, none of which were on the recipe, so it load-failed while its passing count went up. With those on :deps the namespace runs -- 175/840 -> 177/2842, no load failures. Property-based, so it gets a tolerance like the other generative suites.
The autourl tests unescape markdown's own HTML output through org.apache.commons.lang.StringEscapeUtils, a JVM jar that does not exist here. A preloaded shim registers unescapeHtml over named and numeric (decimal and hex) entities, implemented independently of markdown's escaping. The two errors collapse to zero; Selmer's suite never references StringEscapeUtils, so it gets no shim and its single :custom-resource-path URL error stays. Tally 94/180/0/2 -> 94/182/0/0.
parse-radix returned (reduced nil) for a bad digit, but reduced only means anything to reduce -- inside a loop it is a Reduced object, and a Reduced is truthy, so the caller took it for a code point and (char x) threw. "&#xZZ;" now comes back unchanged, which is what commons-lang does with an entity it cannot parse.
The byte-array multipart store drains uploads through org.apache.commons.io.IOUtils/toByteArray and the resource-response test writes a temp file through FileUtils/writeStringToFile, a JVM jar that does not exist here. A preloaded shim registers both statics: toByteArray is the stream's readAllBytes, writeStringToFile is spit. Both error tests move: the byte-array store passes fully, and the resource test gets past FileUtils to the java.net.URLClassLoader ctor (a separate JDK gap, recorded in the needs file, not shimmed). Tally 152/408/15/18/2 -> 152/411/15/17/2.
The jolt.time preload does not clear the four malli.experimental.time namespaces: they reach a static field through (. Class FIELD), which jolt answers with nil, and (. Class -FIELD), which it rejects outright. That is a separate gap from the class seams; 12 is the measured number.
The entry still said 8/2/4, measured before arraycopy landed in the java layer. Measured now: 12 pass, 2 fail, no errors -- the whole suite runs and only the IFn$O..O primitive-interface checks remain.
Three gaps the library sweep surfaced, each measured against 1.12.5. java.io.File compared by identity under =, so two Files built from the same path were unequal -- .equals and hash already agreed. That is five of ring-core's failures, which read as (not (= #object[java.io.File "x"] #object[java.io.File "x"])). The two-arg File constructor joined with a bare "/", so a parent ending in a separator produced "a//c". All four corners now match the JVM, including an empty child yielding the parent alone. (char n) accepted only 0-0xFFFF while jolt's strings are code-point indexed, so it could not rebuild a char (first s) had just handed out -- and data.json errored writing an astral character. The range is now the Unicode scalar values, a deliberate superset of the JVM's 16-bit char; surrogates stay rejected because they are not scalar values. Two corpus rows pinned the old limit: the checked-narrow probe moves to a value out of range on both sides, and the astral round trip moves to unit.edn with a known-divergences entry, since the JVM throws there.
ring-core 411 pass / 15 fail -> 419 / 7: File equality by pathname settles the resource and file-response body comparisons. data.json 320 pass / 2 error -> 322 / 0: the :escape-unicode false path no longer throws out of (char cp). Both residues are now stated as what they are -- ring-core's on commons-fileupload and JDK classloader surface, data.json's purely the codepoint string model.
A failure printed "FAIL: (= 1 2) expected: ... actual: ..." on one line; the
reference prints a blank line, "FAIL in (test-name) (file:line)", the testing
context and message, then expected:/ actual: with the two-space alignment.
Every editor integration, CI parser and third-party reporter keys off that
shape, and test.check's own suite asserts on it directly.
The position comes from the reader rather than a stack walk: `is` stashes
(meta &form) plus *file* in *report-pos* and do-report merges it into :fail
and :error maps. Verified byte-identical to 1.12.5 for a failure; an :error
differs only in the frames underneath, which tail calls leave nothing to
report -- already documented.
*report-counters* is now incremented, including :test per test var, so a suite
that binds it and reads it back sees {:test 1 :pass 0 :fail 0 :error 1} as on
the JVM. bump-counters! moves that ref and inc-pass!/fail!/err! move jolt's
process-wide atom; keeping them separate is what stops a count landing twice,
and inc-report-counter keeps its existing contract of moving the atom when no
ref is bound.
Also: the registry runner never bound *testing-vars*, so a failure in a test
run through it had an empty name in the header; and both crash paths still
passed err! a string after it started taking a report map.
clojure.core-test.char's "4+ byte characters" assertion is host-dependent in
the suite itself: it expects a throw on the JVM, but the character back on
jank and basilisp, which index strings by code point as jolt does. jolt has
no branch there so it takes :default and fails -- (= (first "\u{10127}")
(char 65895)) holds here exactly as the :lpy branch asserts.
test.check 230 pass / 15 fail / 3 error -> 236 / 10 / 2, test.chuck 98 / 22 / 3 -> 110 / 17 / 2. Both suites assert on the "FAIL in (name) (file:line)" header and on *report-counters*, which is what the reference-shaped reporter now gives them. Also fixes what the first measurement caught: run-one bound *testing-vars* to a stand-in map, and test.check's reporter reads that stack expecting vars -- 56 errors' worth of "cannot be cast to Named". It resolves the real var now, and leaves the stack alone when a test's var no longer resolves.
(. Class MEMBER) with no arguments always emitted a static CALL, so a static field's value was applied as a zero-arg procedure and came back nil -- a wrong answer, not an error. (. Class -MEMBER), the explicit field spelling, was rejected outright. The no-argument form is ambiguous on the JVM too: a static field if one exists, else a no-arg static method. jolt keeps one registry for both, so jolt.host/static-member decides at runtime on what is registered -- a procedure is a method to call, anything else is a field value. The dash spelling is unambiguous and reads the value directly. With arguments nothing changes. Four malli.experimental.time namespaces load-failed on LocalDate/-MIN. Corpus row covers both field spellings and both call forms, so the fix cannot regress the calls. Adjacent to jolt-5hnv, which fixed the static METHOD case in July; the field case survived it. Fixes jolt-c0ri.
load-fail 12 -> 8: the four malli.experimental.time namespaces blocked on LocalDate/-MIN now load. tests 202 -> 209, pass 11847 -> 11934; fail and error rise because those namespaces run at all now. What is left is fipp reaching Clojure's private mk-am (7) and jsonista (1), both JVM-internal. Note for the record: an earlier expectation in this file guessed load-fail 8 and attributed it to the jolt.time preload. The number was right and the reason was wrong -- the preload does nothing here; the dot form was the blocker.
proxy desugared to reify, which has no base, so a proxy over a concrete class answered only the methods its body declared -- every inherited method threw -- and proxy-super threw unconditionally. clojure.tools.logging's log-stream needs both: it proxies ByteArrayOutputStream, overrides flush, and calls proxy-super flush and reset from inside it. jolt generates no classes, so a proxy now EXTENDS BY DELEGATION: make-proxy constructs a real base instance from the first super when that names a constructible class, answers what the body declares, and forwards the rest to that instance through the full dispatcher. proxy-super calls the base's own implementation. A proxy is an instance of its base and reports the base's class and host tags, so instance?, class and extend-protocol all see through it. A super naming an interface has nothing to construct and stays exactly the reify it was. Delegation is not subclassing in one respect: the base holds no reference back, so a base method calling an overridden method runs the base's version where the JVM re-enters the override. Recorded in known-divergences with the return-value one reify already had. Three things had to follow for the whole path to work: PrintStream, which was absent -- the comment where System/out lives said setOut was being withheld until proxy-over-a-host-class existed, so setOut and setErr land here too, in mutable static cells. OutputStreamWriter now encodes through a port that hands each block to the stream's own write, and its flush reaches the stream underneath. It used to transcode the stream's port directly, which under R6RS CLOSES that port -- so (.toString baos) after wrapping one failed on a closed port. That was broken before this change. println and prn flush when *flush-on-newline*, as on the JVM. The var existed and defaulted true but nothing read it, so text sat in a writer's buffer and a println through an OutputStreamWriter never reached the stream. clojure.core/ flush also only flushed the Chez port, ignoring *out*; it now dispatches to the writer *out* holds, mirroring how jolt-write already routed a write. Costs 1.02-1.07x on a 200k-line println microbenchmark (1350/1413ms -> 1444/1442ms). tools.logging 219 pass / 4 error -> 226 / 0. The unit row pinning proxy-super's old throw moves to the corpus, since jolt and 1.12.5 now agree on it.
A new jolt.host def-var! needs its manifest line; this one was missing, so manifestcheck failed on CI. My local make test had aborted at unit before reaching that target, and the targets I re-ran afterwards did not include it.
make stops at the first failing target, so everything after it never runs. Re-running a hand-picked subset afterwards prints per-target success that looks exactly like the whole gate's, and that is how the missing host-manifest line reached CI: the gate had aborted at unit, and the targets re-run afterwards did not include manifestcheck. The gate now runs as a sub-make behind a wrapper, so a verdict line is printed either way and the log can never end on some passing target's output -- which also covers `make test | tail`, where the pipe takes tail's exit status. The exit code is preserved. On a complete pass the wrapper writes target/gate-receipt naming the gate and a hash of every tracked and untracked-but-not-ignored file. `make gate-status` answers whether THIS working tree is covered by a full run: a subset run leaves no receipt, and any edit since changes the hash. That turns "is this gated?" from something to remember into something to ask. -i and -k are refused for gate targets, at parse time rather than in the recipe: -i ignores a recipe line's failure including a guard's own, so a guard inside the recipe is itself ignored and the gate runs anyway. Confirmed both ways. The target list moved into CI-GATES/TEST-GATES so the banner, the receipt and the status check cannot drift from what actually ran.
Without the + prefix the sub-make runs -j1, so CI's -j$(nproc) silently
serialized: 13m28s against about 7m before. make warns about it
('jobserver unavailable: using -j1') but the gate still passes, so only the
clock shows it.
The + line's comment sat inside the recipe, so make echoed it into the gate log twice per run. Moved above the define. Handing the jobserver down also handed it to devboot-smoke.sh, which invokes make itself and cannot claim it, so it warned twice. MAKEFLAGS is cleared for that recipe; the inner make was serial before this either way.
java.net.URL took only (URL. spec), so (URL. nil spec handler) passed nil as the spec and raised "no protocol: ". The constructors are now told apart by argument type the way the JVM's overloads are -- (URL. spec), (URL. context spec [handler]), (URL. protocol host file) -- and a relative spec resolves against the context. A URL built with a handler reads through it whatever its protocol: openConnection is the handler's, openStream is that connection's getInputStream, and url-content routes slurp and io/reader the same way. That is how a caller serves content from a URL space jolt has no protocol for -- Selmer's :url-stream-handler option, which needs the proxy-over-concrete-class support to express the handler at all. Two bugs fixed alongside, both pre-existing: getPath/getFile returned the spec minus a "file:" prefix, so every other URL reported its whole spec as its path -- (.getFile (URL. "http://h/p")) was "http://h/p" where the JVM says "/p". A unit row asserted the wrong value; it moves to the corpus now that jolt agrees with 1.12.5. getHost was absent. io/reader on a URL read the spec as a local file path, so a non-file URL failed as a missing file rather than saying the protocol has no input. Selmer 525/1 error -> 526/0: its suite now passes completely.
Upstream keeps test-isolated/ as its own kaocha profile because the isolation is the assertion: the test checks a coercion works when rewrite-clj.zip is the only namespace required, so requiring any other test namespace pulls in rewrite-clj.node and it passes for the wrong reason. The harness runs one process per manifest entry, so a second entry over the same checkout is exactly that isolation. 1/1, and the main entry is unchanged at 163/3381.
malli's regex engine allocates through (Array/newInstance Object capacity),
where its own :bb and :cljs branches write (object-array capacity) -- so this
is array allocation with a concrete component type, not the reflection API
tools.namespace needs. newInstance/getLength/get/set over jolt's arrays, with
the JVM's ArrayIndexOutOfBounds and IllegalArgumentException. Like make-array,
the component type selects nothing: jolt's arrays are object-kinded unless
built by a typed constructor.
Void was the one primitive class token missing of the nine. sci maps every
primitive name to its token in one map literal and stopped at the first absent
one, so the whole of sci.core failed to load on that alone.
malli 11934 pass / 134 error -> 12059 / 76: the 58 "Unknown class Array" errors
are gone.
Its residue is characterised in the manifest now rather than left as a number.
72 errors are sci, which does not fully load here for reasons beyond Void: it
reaches PRIVATE clojure.core internals by var (imap-cons, then system-newline)
and pulls further deps behind them. Putting jolt's vendored sci on malli's path
does not move the count, so it stays off. 104 of the 149 failures are
parser-info-test alone, which round-trips values drawn by (mg/sample s {:seed
0}); a fixed seed draws different values here than on the JVM, so the test runs
over different data. :every parsing and registry references agree with 1.12.5
exactly on literal inputs -- checked directly rather than assumed.
The URL divergence entry said jolt has no URLStreamHandler dispatch. It was written before the handler support landed and is now simply false, so it goes. certify does not catch this: a :documented entry is prose, not a row it checks. println flushing on newline, and OutputStreamWriter.flush reaching the stream beneath it, had no regression at all -- only a row reading *flush-on-newline*'s default value, which says nothing about the behaviour. Both are load-bearing for tools.logging, and libconformance is not part of make test, so reverting either would have passed the gate silently. One row now pins the contract: the first write lands, the second does not until the explicit flush.
ring's multipart middleware is written directly against commons-fileupload2: it proxies AbstractFileUpload, reifies RequestContext and ProgressListener, iterates FileItemInput and catches FileUploadException. None of that is parsing. jolt.shim.commons-fileupload registers exactly that class surface over jolt-lang/multipart's RFC 7578 parser, so ring's own code paths stay its own -- the same relationship commons-fileupload has to ring on the JVM, which is what makes running the suite worth anything. The shim is glue; it decides nothing about multipart syntax. Expressing it needs the proxy-over-a-concrete-class support from earlier in this branch: (proxy [AbstractFileUpload] []) has to reach a real base instance or .setMaxFileSize has nothing to call. One general jolt gap came out of it. A deftype or reify declaring java.lang.Iterable or java.util.Iterator is now seqable, as on the JVM: ring hands `sequence` its item iterator wrapped in (reify Iterable (iterator [_] ...)), which failed "Don't know how to create ISeq from: ...$reify__0". The walk is lazy, one element per forced cell, since an iterator is a cursor over something being produced. Seqable wins over Iterable for a type declaring both, matching RT.seqFrom -- without that check these arms would have shadowed the existing coll-interface arm, because arms are consulted newest-first. ring-core 419 pass / 7 fail / 17 error -> 446 / 5 / 5. Every multipart test passes; what is left is classloader and jar: URL surface, already recorded.
The fingerprint concatenated file contents in `git ls-files -c -o` order, which groups untracked separately — so a file merely going from untracked to tracked reordered the input and changed the hash. Committing, which changes nothing, therefore read as "not gated", the opposite of what the receipt is for. Now it hashes each file's name and content over a sorted list, and folds in submodule status so a submodule bump invalidates it too (the submodule paths list as directories the per-file hash cannot read).
Replaying third-party suites through each library's real runner instead of the bespoke
conformance-runharness surfaced a batch of host bugs the old recipes structurally could not reach — they hand-listed namespaces and supplied their own dep sets, so whole code paths never ran.The largest single find is the protocol identity bug; everything else is a gap one of the suites walked into.
Protocol identity
Protocol dispatch keyed a method table by the protocol's SIMPLE name, so two protocols named alike in different namespaces shared one table and the later
extendsilently replaced the earlier one — a wrong answer, not a crash. Dispatch now keys by"<defining-ns>/<Name>", resolved through:referand:asby a newjolt.host/protocol-key-of. A symbol naming no protocol (Object,java.util.Map, an importedclojure.lang.ILookup) keeps its bare name, because that is howvalue-host-tagsspells the tags a value reports.This is what was holding malli down: 1,874 → 16,280 assertions in its own suite, ClassCastException 594 → 1. No dispatch cost —
dispatch69.9/66.9/69.9 ms A/B/A.Host classes and core
java.io.Filecompares by pathname under=..equalsandhashalready agreed, so two Files built from one path were unequal only through=, which is how ring-core's resource tests read them. The two-arg constructor also joins with exactly one separator; a parent ending in/produceda//c.(char n)spans the Unicode scalar values. jolt's strings are code-point indexed, so it could not rebuild a char(first s)had just handed out, and data.json errored writing an astral character. This is a deliberate superset of the JVM's 16-bit char — the clojure-test-suite already treats that assertion as host-dependent (jank and basilisp behave as jolt now does), so it lands as a cts baseline line plus a:string-modeldivergence entry, not as parity.(. Class MEMBER)with no arguments reads a static FIELD when one is registered. It always emitted a static call, so a field's value was applied as a zero-arg procedure and came back nil — a silent wrong answer.(. Class -MEMBER)was rejected outright. Four malli namespaces load-failed onLocalDate/-MIN. (bead jolt-c0ri)System/arraycopy, with the JVM's overlapping-copy semantics.reifydeclaringIFnisifn?and an instance of it.clojure.datafy,Murmur3/hashCombine,clojure.java.javadoc; fixed cross-namespace FQ-nameextend.(.getBytes "い" "ISO-2022-JP")was 5 bytes where the JVM gives 8 — RFC 1468 requires the text to end in ASCII.LineNumberingPushbackReadergets its own jhost tag so it reports its own class. tools.reader does(extend LineNumberingPushbackReader IndexingReader …), which never dispatched.io/resourceanswers an absolutefile:URL. Source roots are relative, and resolving a name againstfile:./test/xthrowsMalformedURLException: no protocol.Character/codePointAtand Runtime's memory API over Chez's heap accounting.(. Foo bar x)on an imported simple name is a static call, so a method the class token does not answer routes throughhost-static-ref, where the on-demand autoload lives. (bead jolt-5hnv)clojure.test reports like reference Clojure
A failure printed one line. The reference prints a blank line,
FAIL in (test-name) (file:line), the testing context and message, thenexpected:/actual:— the shape every editor integration, CI parser and third-party reporter keys off, and which test.check's own suite asserts on directly. Verified byte-identical to 1.12.5 for a failure; an:errordiffers only in the frames underneath, which tail calls leave nothing to report.Position comes from the reader —
(meta &form)plus*file*— not a stack walk.*report-counters*is incremented too, including:testper test var.Also fixed here: the registry runner never bound
*testing-vars*, so a failure in a registry-run test had an empty name in the header.deps resolution
git ls-remoteprints the tag object forrefs/tags/X, so that is what a coordinate written from its output pins (cognitect-labs/test-runner v0.5.0 is one); tools.deps takes either. Verified against the real CLI. The tag cache is now two tokens, and a one-token file from an older jolt is re-resolved rather than trusted.:sha/:tagspellings alongside the namespaced keys. malli pins spec-alpha2 that way, which blocked its whole test alias.stdlib
clojure.stacktrace, which Clojure ships and jolt did not. Frame lists are empty here (tail calls, already a recorded divergence); the throwable line, ex-data and cause chain match exactly.Conformance shims
Three suites test against Java libraries jolt has no equivalent for. Each shim lives on a jolt-owned path under
test/conformance/libs/shims/and goes on the recipe — never written into an upstream checkout, where the edit would be invisible once its.gitis gone and the recorded tally would silently measure our own source: commons-codec Base64 for data.codec (RFC 4648, as the suite's oracle), commons-langStringEscapeUtilsfor markdown-clj, commons-ioIOUtils/FileUtilsfor ring-core.Also fixed markdown-clj's own finding: an unparseable numeric entity now leaves its token alone.
Effect on library tallies
Passing assertions, and what is left failing. Every figure is a
make libconformancemeasurement, not an estimate.malli's
fail/erroralso rise, because seven namespaces that used toload-fail now run at all. rewrite-clj was already complete before this branch —
163 tests / 3,381 assertions is the whole suite, the same counts reference
Clojure runs for it, and the one assertion is a real fix rather than newly
reached coverage.
proxy extends a concrete class by delegation
proxydesugared toreify, which has no base, so a proxy over a concrete class answered only the methods its body declared — every inherited method threw — andproxy-superthrew unconditionally. Since jolt generates no classes, a proxy now constructs a real base instance from the first super when that names a constructible class, answers what the body declares, and forwards the rest through the full dispatcher.proxy-supercalls the base's own implementation. A proxy is an instance of its base and reports the base's class and host tags, soinstance?,classandextend-protocolsee through it. A super naming an interface has nothing to construct and stays the reify it was.Delegation is not subclassing in one respect, recorded in
known-divergences: the base holds no reference back, so a base method calling an overridden method runs the base's version where the JVM re-enters the override. Overriding a leaf method — the common case — is identical.Three things had to follow, and two were bugs that predate this work.
PrintStreamdid not exist (the comment besideSystem/outsaidsetOutwas being withheld until proxy-over-a-host-class existed, sosetOut/setErrland here too).OutputStreamWritertranscoded the wrapped stream's port, and R6RStranscoded-portcloses it, so(.toString baos)after wrapping one failed on a closed port. Andprintln/prnnever flushed although*flush-on-newline*existed and defaulted true with nothing reading it, whileclojure.core/flushignored*out*entirely — both now match Clojure, at 1.02–1.07x on a 200k-line println microbenchmark (1350/1413ms → 1444/1442ms).A URLStreamHandler decides what its URL reads
java.net.URLtook only(URL. spec), so(URL. nil spec handler)passed nil as the spec and raisedno protocol:. The constructors are now told apart by argument type the way the JVM's overloads are, and a URL built with a handler reads through it whatever its protocol —openConnectionis the handler's,openStreamis that connection'sgetInputStream, andslurp/io/readerroute the same way.Two pre-existing bugs alongside:
getPath/getFilestripped only afile:prefix, so every other URL reported its whole spec as its path ((.getFile (URL. "http://h/p"))was"http://h/p", the JVM says"/p"); andio/readeron a URL read the spec as a local file path, so a non-file URL failed as a missing file rather than saying the protocol has no input.An incomplete gate run can no longer read as a pass
makestops at the first failing target, so everything after it never runs, and re-running a hand-picked subset prints per-target success that looks exactly like the whole gate's. That is how a missing host-manifest line reached CI in this branch.The gate now runs as a sub-make behind a wrapper: a verdict line is printed either way, so the log can never end on some passing target's output — which also covers
make test | tail, where the pipe takes tail's exit status. On a complete pass it writes a receipt naming the gate and a hash of every tracked and untracked-but-not-ignored file, andmake gate-statusanswers whether this working tree is covered.-iand-kare refused for gate targets, at parse time, because-iignores a recipe line's failure including a guard's own.Documented rather than fixed
ring-core's 12 remaining errors need an RFC 7578 multipart parser — library-sized, and shimming it would have ring's own multipart tests exercising our parser. data.json's 2 remaining failures are the code-point vs UTF-16 string model. tools.namespace's 1 error is
java.lang.reflect. lasertag's 12 are the class model. Each is stated in the manifest comment for the library it affects; the model-level ones are inknown-divergences.edn.Coverage
rewrite-clj keeps a
test-isolated/tree that upstream runs as its own kaocha profile, and the isolation is the assertion: it checks a coercion works whenrewrite-clj.zipis the ONLY namespace required, so requiring any other test namespace pulls inrewrite-clj.nodeand it passes for the wrong reason. The harness runs one process per manifest entry, sorewrite-clj-isolatedis exactly that isolation. 1/1; rewrite-clj's own 163/3381 is unchanged, and matches what reference Clojure runs for the same suite.Gates
Corpus rows for every fix that is a single expression, certified against real JVM Clojure 1.12.5. A
clojure.stacktraceacceptance test and aclojure.testoutput-shape regression, both wired into smoke.sh. 8 deps-alias checks.make testgreen: selfhost fixpoint holds, corpus 0 new divergences, unit 1226/1226, cts at baseline, smoke 121/0, certify 0 NEW / 0 stale.