this repo has no description
at main 616 lines 32 kB view raw
1<!DOCTYPE html> 2<html> 3<head> 4 <meta charset="utf-8"> 5 <title>JTW Library Test Runner</title> 6 <style> 7 * { box-sizing: border-box; margin: 0; padding: 0; } 8 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; } 9 h1 { color: #f0f6fc; margin-bottom: 4px; font-size: 24px; } 10 .subtitle { color: #8b949e; margin-bottom: 24px; font-size: 14px; } 11 .summary { display: flex; gap: 16px; margin-bottom: 24px; padding: 16px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; flex-wrap: wrap; } 12 .summary .stat { text-align: center; min-width: 70px; } 13 .summary .stat .num { font-size: 32px; font-weight: 700; } 14 .summary .stat .label { font-size: 12px; color: #8b949e; text-transform: uppercase; } 15 .stat.pass .num { color: #3fb950; } 16 .stat.fail .num { color: #f85149; } 17 .stat.skip .num { color: #d29922; } 18 .stat.run .num { color: #58a6ff; } 19 .progress { height: 4px; background: #21262d; border-radius: 2px; margin-bottom: 24px; overflow: hidden; } 20 .progress-bar { height: 100%; background: #58a6ff; transition: width 0.3s; } 21 22 .group { margin-bottom: 16px; } 23 .group-header { font-size: 14px; font-weight: 600; color: #f0f6fc; padding: 8px 12px; background: #161b22; border: 1px solid #30363d; border-radius: 8px 8px 0 0; cursor: pointer; display: flex; justify-content: space-between; align-items: center; } 24 .group-header:hover { background: #1c2128; } 25 .group-header .arrow { transition: transform 0.2s; } 26 .group-header.collapsed .arrow { transform: rotate(-90deg); } 27 .group-badge { font-size: 11px; padding: 1px 6px; border-radius: 10px; margin-left: 8px; font-weight: 400; } 28 .group-badge.cross { background: #30363d; color: #d2a8ff; } 29 .group-body { border: 1px solid #30363d; border-top: none; border-radius: 0 0 8px 8px; overflow: hidden; } 30 .group-body.hidden { display: none; } 31 32 .code-banner { padding: 6px 12px; background: #0d1117; border-bottom: 1px solid #21262d; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #d2a8ff; } 33 .code-banner code { color: #f0f6fc; } 34 35 .test-row { display: flex; align-items: flex-start; padding: 8px 12px; border-bottom: 1px solid #21262d; font-size: 13px; gap: 8px; } 36 .test-row:last-child { border-bottom: none; } 37 .test-row:hover { background: #161b22; } 38 .test-icon { flex-shrink: 0; width: 20px; text-align: center; font-size: 14px; } 39 .test-name { flex: 1; } 40 .test-name .label { font-family: 'SF Mono', 'Fira Code', monospace; } 41 .test-detail { font-size: 12px; color: #8b949e; margin-top: 2px; } 42 .test-time { color: #8b949e; font-size: 12px; flex-shrink: 0; } 43 44 .test-row.pass .test-icon { color: #3fb950; } 45 .test-row.fail .test-icon { color: #f85149; } 46 .test-row.skip .test-icon { color: #d29922; } 47 .test-row.running .test-icon { color: #58a6ff; } 48 .test-row.pending .test-icon { color: #484f58; } 49 /* "Expected error" pass: still green check but with a visual indicator */ 50 .test-row.pass-neg .test-icon { color: #3fb950; } 51 52 .expect-badge { font-size: 10px; padding: 1px 5px; border-radius: 3px; margin-left: 6px; vertical-align: middle; } 53 .expect-badge.should-pass { background: #23862633; color: #3fb950; } 54 .expect-badge.should-error { background: #f8514933; color: #f85149; } 55 56 .error-detail { background: #1c1215; border: 1px solid #f8514933; border-radius: 4px; padding: 8px; margin-top: 6px; font-family: 'SF Mono', monospace; font-size: 12px; color: #f85149; white-space: pre-wrap; word-break: break-all; } 57 .output-detail { background: #121a16; border: 1px solid #3fb95033; border-radius: 4px; padding: 8px; margin-top: 6px; font-family: 'SF Mono', monospace; font-size: 12px; color: #3fb950; white-space: pre-wrap; word-break: break-all; } 58 .neg-output-detail { background: #1a1520; border: 1px solid #d2a8ff33; border-radius: 4px; padding: 8px; margin-top: 6px; font-family: 'SF Mono', monospace; font-size: 12px; color: #d2a8ff; white-space: pre-wrap; word-break: break-all; } 59 60 .step-transcript { margin-top: 6px; border-radius: 4px; overflow: hidden; border: 1px solid #30363d; } 61 .step-transcript + .step-transcript { margin-top: 4px; } 62 .step-input { padding: 4px 8px; background: #161b22; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #d2a8ff; border-bottom: 1px solid #21262d; } 63 .step-input::before { content: '# '; color: #484f58; } 64 .step-output { padding: 4px 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; } 65 .step-output.out-pass { background: #121a16; color: #3fb950; } 66 .step-output.out-fail { background: #1c1215; color: #f85149; } 67 .step-output.out-neg { background: #1a1520; color: #d2a8ff; } 68 .step-output.out-stdout { background: #161b22; color: #c9d1d9; } 69 70 @keyframes spin { to { transform: rotate(360deg); } } 71 .spinner { display: inline-block; animation: spin 1s linear infinite; } 72 </style> 73</head> 74<body> 75 <h1>JTW Library Test Runner</h1> 76 <p class="subtitle">Testing OCaml libraries across versions via ohc-built JTW output &mdash; including cross-version negative tests</p> 77 78 <div class="summary"> 79 <div class="stat pass"><div class="num" id="pass-count">0</div><div class="label">Passed</div></div> 80 <div class="stat fail"><div class="num" id="fail-count">0</div><div class="label">Unexpected</div></div> 81 <div class="stat skip"><div class="num" id="skip-count">0</div><div class="label">Skipped</div></div> 82 <div class="stat run"><div class="num" id="run-count">0</div><div class="label">Running</div></div> 83 </div> 84 <div class="progress"><div class="progress-bar" id="progress-bar"></div></div> 85 86 <div id="groups"></div> 87 88 <script type="module"> 89 import { OcamlWorker } from '/client/ocaml-worker.js'; 90 91 // ── Universe mapping ────────────────────────────────────────────────── 92 const U = { 93 'fmt.0.9.0': '9901393f978b0a6627c5eab595111f50', 94 'fmt.0.10.0': 'd8140118651d08430f933d410a909e3b', 95 'fmt.0.11.0': '2fc1d989047b6476399007c9b8b69af9', 96 'cmdliner.1.0.4': '0dd34259dc0892e543b03b3afb0a77fa', 97 'cmdliner.1.3.0': '258e7979b874502ea546e90a0742184a', 98 'cmdliner.2.0.0': '91c3d96cea9b89ddd24cf7b78786a5ca', 99 'cmdliner.2.1.0': 'bfc34a228f53ac5ced707eed285a6e5c', 100 'mtime.1.3.0': 'b6735658fd307bba23a7c5f21519b910', 101 'mtime.1.4.0': 'ebccfc43716c6da0ca4a065e60d0f875', 102 'mtime.2.1.0': '7db699c334606d6f66e65c8b515d298d', 103 'logs.0.7.0': '2c014cfbbee1d278b162002eae03eaa8', 104 'logs.0.10.0': '07a565e7588ce100ffd7c8eb8b52df07', 105 'uucp.14.0.0': '60e1409eb30c0650c4d4cbcf3c453e65', 106 'uucp.15.0.0': '6a96a3f145249f110bf14739c78e758c', 107 'uucp.16.0.0': '2bf0fbf12aa05c8f99989a759d2dc8cf', 108 'uucp.17.0.0': '58b9c48e9528ce99586b138d8f4778c2', 109 'uunf.14.0.0': 'cac36534f1bf353fd2192efd015dd0e6', 110 'uunf.17.0.0': '96704cd9810ea1ed504e4ed71cde82b0', 111 'astring.0.8.5': '1cdbe76f0ec91a6eb12bd0279a394492', 112 'jsonm.1.0.2': 'ac28e00ecd46c9464f5575c461b5d48f', 113 'xmlm.1.4.0': 'c4c22d0db3ea01343c1a868bab35e1b4', 114 'ptime.1.2.0': 'd57c69f3dd88b91454622c1841971354', 115 'react.1.2.2': 'f438ba61693a5448718c73116b228f3c', 116 'hmap.0.8.1': '753d7c421afb866e7ffe07ddea3b8349', 117 'gg.1.0.0': '02a9bababc92d6639cdbaf20233597ba', 118 'note.0.0.3': '2545f914c274aa806d29749eb96836fa', 119 'otfm.0.4.0': '4f870a70ee71e41dff878af7123b2cd6', 120 'vg.0.9.5': '0e2e71cfd8fe2e81bff124849421f662', 121 'bos.0.2.1': '2fc1d989047b6476399007c9b8b69af9', 122 'fpath.0.7.3': '2fc1d989047b6476399007c9b8b69af9', 123 'uutf.1.0.4': '4f870a70ee71e41dff878af7123b2cd6', 124 'b0.0.0.6': 'bfc34a228f53ac5ced707eed285a6e5c', 125 }; 126 127 // ── Test definitions ────────────────────────────────────────────────── 128 // 129 // Regular tests: 130 // { group, name, universe, require, steps: [{code, expect}] } 131 // 132 // Cross-version (negative) tests: 133 // { group, crossVersion: true, code: "...", 134 // cases: [{ label, universe, require, 135 // shouldPass: true/false, expect?, expectError? }] } 136 137 const tests = [ 138 139 // ══════════════════════════════════════════════════════════════════ 140 // CROSS-VERSION NEGATIVE TESTS 141 // ══════════════════════════════════════════════════════════════════ 142 143 // ── Cmdliner.Cmd module: introduced in ~1.1, absent in 1.0.4 ── 144 { group: 'Cmdliner: Cmd module boundary', crossVersion: true, 145 code: 'Cmdliner.Cmd.info;;', 146 description: 'Cmdliner.Cmd was introduced after 1.0.x. Same code errors in 1.0.4 but works in 1.3.0+.', 147 cases: [ 148 { label: 'cmdliner 1.0.4', universe: U['cmdliner.1.0.4'], require: ['cmdliner'], 149 shouldPass: false, expectError: 'Unbound module' }, 150 { label: 'cmdliner 1.3.0', universe: U['cmdliner.1.3.0'], require: ['cmdliner'], 151 shouldPass: true, expect: 'Cmdliner.Cmd' }, 152 { label: 'cmdliner 2.1.0', universe: U['cmdliner.2.1.0'], require: ['cmdliner'], 153 shouldPass: true, expect: 'Cmdliner.Cmd' }, 154 ] }, 155 156 // ── Cmdliner.Term.eval: removed in 2.0 ── 157 { group: 'Cmdliner: Term.eval removal', crossVersion: true, 158 code: 'Cmdliner.Term.eval;;', 159 description: 'Cmdliner.Term.eval was removed in 2.0. Present in 1.0.4 and 1.3.0 (transitional), gone in 2.x.', 160 cases: [ 161 { label: 'cmdliner 1.0.4', universe: U['cmdliner.1.0.4'], require: ['cmdliner'], 162 shouldPass: true, expect: 'Cmdliner.Term' }, 163 { label: 'cmdliner 1.3.0', universe: U['cmdliner.1.3.0'], require: ['cmdliner'], 164 shouldPass: true, expect: 'Cmdliner.Term' }, 165 { label: 'cmdliner 2.0.0', universe: U['cmdliner.2.0.0'], require: ['cmdliner'], 166 shouldPass: false, expectError: 'Unbound' }, 167 { label: 'cmdliner 2.1.0', universe: U['cmdliner.2.1.0'], require: ['cmdliner'], 168 shouldPass: false, expectError: 'Unbound' }, 169 ] }, 170 171 // ── Uucp.unicode_version value changes ── 172 { group: 'Uucp: unicode_version value', crossVersion: true, 173 code: 'Uucp.unicode_version;;', 174 description: 'Each Uucp release tracks a specific Unicode version. The returned string differs per version.', 175 cases: [ 176 { label: 'uucp 14.0.0', universe: U['uucp.14.0.0'], require: ['uucp'], 177 shouldPass: true, expect: '"14.0.0"' }, 178 { label: 'uucp 15.0.0', universe: U['uucp.15.0.0'], require: ['uucp'], 179 shouldPass: true, expect: '"15.0.0"' }, 180 { label: 'uucp 16.0.0', universe: U['uucp.16.0.0'], require: ['uucp'], 181 shouldPass: true, expect: '"16.0.0"' }, 182 { label: 'uucp 17.0.0', universe: U['uucp.17.0.0'], require: ['uucp'], 183 shouldPass: true, expect: '"17.0.0"' }, 184 ] }, 185 186 // ── Uucp: "17.0.0" only in uucp 17 ── 187 { group: 'Uucp: version string mismatch', crossVersion: true, 188 code: 'assert (Uucp.unicode_version = "17.0.0");;', 189 description: 'Asserting unicode_version = "17.0.0" passes only in uucp 17, fails in 14.', 190 cases: [ 191 { label: 'uucp 14.0.0', universe: U['uucp.14.0.0'], require: ['uucp'], 192 shouldPass: false, expectError: 'Assert_failure' }, 193 { label: 'uucp 17.0.0', universe: U['uucp.17.0.0'], require: ['uucp'], 194 shouldPass: true, expect: '' }, 195 ] }, 196 197 // ── Uunf version boundary ── 198 { group: 'Uunf: version boundary', crossVersion: true, 199 code: 'assert (Uunf.unicode_version = "17.0.0");;', 200 description: 'Uunf 14.0.0 reports Unicode 14, so asserting "17.0.0" fails. Passes in uunf 17.', 201 cases: [ 202 { label: 'uunf 14.0.0', universe: U['uunf.14.0.0'], require: ['uunf'], 203 shouldPass: false, expectError: 'Assert_failure' }, 204 { label: 'uunf 17.0.0', universe: U['uunf.17.0.0'], require: ['uunf'], 205 shouldPass: true, expect: '' }, 206 ] }, 207 208 // ── Mtime.Span.of_float_ns: added in 2.x ── 209 { group: 'Mtime: Span.of_float_ns boundary', crossVersion: true, 210 code: 'Mtime.Span.of_float_ns;;', 211 description: 'Mtime.Span.of_float_ns was added in mtime 2.0. Not present in 1.x.', 212 cases: [ 213 { label: 'mtime 1.3.0', universe: U['mtime.1.3.0'], require: ['mtime'], 214 shouldPass: false, expectError: 'Unbound' }, 215 { label: 'mtime 1.4.0', universe: U['mtime.1.4.0'], require: ['mtime'], 216 shouldPass: false, expectError: 'Unbound' }, 217 { label: 'mtime 2.1.0', universe: U['mtime.2.1.0'], require: ['mtime'], 218 shouldPass: true, expect: 'Mtime.span option' }, 219 ] }, 220 221 // ══════════════════════════════════════════════════════════════════ 222 // POSITIVE TESTS (functionality verification) 223 // ══════════════════════════════════════════════════════════════════ 224 225 // ── Fmt ── 226 ...['0.9.0', '0.10.0', '0.11.0'].map(v => ({ 227 group: 'Fmt', 228 name: `${v}: Fmt.str formats integers`, 229 universe: U[`fmt.${v}`], 230 require: ['fmt'], 231 steps: [{ code: 'Fmt.str "%d" 42;;', expect: '"42"' }], 232 })), 233 ...['0.9.0', '0.10.0', '0.11.0'].map(v => ({ 234 group: 'Fmt', 235 name: `${v}: Fmt.pr writes to stdout`, 236 universe: U[`fmt.${v}`], 237 require: ['fmt'], 238 steps: [{ code: 'Fmt.pr "hello %s" "world";;', expectStdout: 'hello world' }], 239 })), 240 { group: 'Fmt', name: '0.11.0: completions for Fmt.s*', universe: U['fmt.0.11.0'], require: ['fmt'], 241 steps: [{ complete: { source: 'Fmt.s', pos: 5 }, expectEntries: ['str'] }] }, 242 243 // ── Cmdliner (positive) ── 244 { group: 'Cmdliner', name: '2.1.0: Arg combinators', universe: U['cmdliner.2.1.0'], require: ['cmdliner'], 245 steps: [{ code: 'let name = Cmdliner.Arg.(required & pos 0 (some string) None & info []);;', expect: 'Cmdliner.Term' }] }, 246 247 // ── Uucp (positive) ── 248 { group: 'Uucp', name: '17.0.0: general category A = Lu', universe: U['uucp.17.0.0'], require: ['uucp'], 249 steps: [{ code: 'Uucp.Gc.general_category (Uchar.of_int 0x0041);;', expect: '`Lu' }] }, 250 251 // ── Mtime (positive) ── 252 { group: 'Mtime', name: '1.4.0: Span.to_uint64_ns', universe: U['mtime.1.4.0'], require: ['mtime'], 253 steps: [{ code: 'Mtime.Span.to_uint64_ns;;', expect: '-> int64' }] }, 254 { group: 'Mtime', name: '2.1.0: Span.of_uint64_ns', universe: U['mtime.2.1.0'], require: ['mtime'], 255 steps: [{ code: 'Mtime.Span.of_uint64_ns 1_000_000_000L;;', expect: 'Mtime.span' }] }, 256 257 // ── Logs ── 258 { group: 'Logs', name: '0.10.0: Src.create and Src.name', universe: U['logs.0.10.0'], require: ['logs'], 259 steps: [ 260 { code: 'let src = Logs.Src.create "test" ~doc:"A test source";;', expect: 'Logs.src' }, 261 { code: 'Logs.Src.name src;;', expect: '"test"' }, 262 ] }, 263 264 // ── Astring ── 265 { group: 'Astring', name: '0.8.5: String.cuts', universe: U['astring.0.8.5'], require: ['astring'], 266 steps: [{ code: 'Astring.String.cuts ~sep:"," "a,b,c";;', expect: '["a"; "b"; "c"]' }] }, 267 { group: 'Astring', name: '0.8.5: String.concat', universe: U['astring.0.8.5'], require: ['astring'], 268 steps: [{ code: 'Astring.String.concat ~sep:"-" ["x"; "y"; "z"];;', expect: '"x-y-z"' }] }, 269 { group: 'Astring', name: '0.8.5: String.Sub', universe: U['astring.0.8.5'], require: ['astring'], 270 steps: [{ code: 'Astring.String.Sub.(to_string (v "hello world" ~start:6));;', expect: '"world"' }] }, 271 272 // ── Jsonm ── 273 { group: 'Jsonm', name: '1.0.2: decode JSON', universe: U['jsonm.1.0.2'], require: ['jsonm'], 274 steps: [{ code: 'let d = Jsonm.decoder (`String "42") in Jsonm.decode d;;', expect: '`Lexeme' }] }, 275 276 // ── Xmlm ── 277 { group: 'Xmlm', name: '1.4.0: parse XML', universe: U['xmlm.1.4.0'], require: ['xmlm'], 278 steps: [{ code: 'let i = Xmlm.make_input (`String (0, "<root/>")) in Xmlm.input i;;', expect: '`Dtd' }] }, 279 280 // ── Ptime ── 281 { group: 'Ptime', name: '1.2.0: epoch', universe: U['ptime.1.2.0'], require: ['ptime'], 282 steps: [{ code: 'Ptime.epoch;;', expect: 'Ptime.t' }] }, 283 { group: 'Ptime', name: '1.2.0: date creation', universe: U['ptime.1.2.0'], require: ['ptime'], 284 steps: [{ code: 'Ptime.of_date_time ((2024, 1, 1), ((0, 0, 0), 0));;', expect: 'Some' }] }, 285 { group: 'Ptime', name: '1.2.0: Span round-trip', universe: U['ptime.1.2.0'], require: ['ptime'], 286 steps: [{ code: 'Ptime.Span.of_int_s 3600 |> Ptime.Span.to_int_s;;', expect: '3600' }] }, 287 288 // ── React ── 289 { group: 'React', name: '1.2.2: signal create/read/update', universe: U['react.1.2.2'], require: ['react'], 290 steps: [ 291 { code: 'let s, set_s = React.S.create 0;;', expect: 'React.signal' }, 292 { code: 'React.S.value s;;', expect: '0' }, 293 { code: 'set_s 42;;', expect: '' }, 294 { code: 'React.S.value s;;', expect: '42' }, 295 ] }, 296 297 // ── Hmap ── 298 { group: 'Hmap', name: '0.8.1: heterogeneous keys + lookup', universe: U['hmap.0.8.1'], require: ['hmap'], 299 steps: [ 300 { code: 'let k_int : int Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key' }, 301 { code: 'let k_str : string Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key' }, 302 { code: 'let m = Hmap.empty |> Hmap.add k_int 42 |> Hmap.add k_str "hello";;', expect: 'Hmap.t' }, 303 { code: 'Hmap.find k_int m;;', expect: 'Some 42' }, 304 { code: 'Hmap.find k_str m;;', expect: 'Some "hello"' }, 305 ] }, 306 307 // ── Gg ── 308 { group: 'Gg', name: '1.0.0: V2 vectors + addition', universe: U['gg.1.0.0'], require: ['gg'], 309 steps: [ 310 { code: 'Gg.V2.v 1.0 2.0;;', expect: 'Gg.v2' }, 311 { code: 'let r = Gg.V2.add (Gg.V2.v 1.0 2.0) (Gg.V2.v 3.0 4.0) in Gg.V2.x r;;', expect: '4.' }, 312 ] }, 313 { group: 'Gg', name: '1.0.0: colors', universe: U['gg.1.0.0'], require: ['gg'], 314 steps: [{ code: 'Gg.Color.red;;', expect: 'Gg.color' }] }, 315 316 // ── Vg ── 317 { group: 'Vg', name: '0.9.5: paths and images', universe: U['vg.0.9.5'], require: ['vg', 'gg'], 318 steps: [ 319 { code: 'let p = Vg.P.empty |> Vg.P.line (Gg.V2.v 1.0 1.0);;', expect: 'Vg.path' }, 320 { code: 'let img = Vg.I.cut p (Vg.I.const Gg.Color.red);;', expect: 'Vg.image' }, 321 ] }, 322 323 // ── Note ── 324 { group: 'Note', name: '0.0.3: const signal', universe: U['note.0.0.3'], require: ['note'], 325 steps: [ 326 { code: 'let s = Note.S.const 42;;', expect: 'Note.signal' }, 327 { code: 'Note.S.value s;;', expect: '42' }, 328 ] }, 329 330 // ── Otfm ── 331 { group: 'Otfm', name: '0.4.0: decoder type', universe: U['otfm.0.4.0'], require: ['otfm'], 332 steps: [{ code: 'Otfm.decoder;;', expect: '-> Otfm.decoder' }] }, 333 334 // ── Fpath ── 335 { group: 'Fpath', name: '0.7.3: path ops', universe: U['fpath.0.7.3'], require: ['fpath'], 336 steps: [ 337 { code: 'Fpath.v "/usr/local/bin" |> Fpath.to_string;;', expect: '"/usr/local/bin"' }, 338 { code: 'Fpath.(v "/usr" / "local" / "bin") |> Fpath.to_string;;', expect: '"/usr/local/bin"' }, 339 { code: 'Fpath.v "/usr/local/bin" |> Fpath.parent |> Fpath.to_string;;', expect: '"/usr/local/"' }, 340 { code: 'Fpath.v "/usr/local/bin" |> Fpath.basename;;', expect: '"bin"' }, 341 ] }, 342 343 // ── Uutf ── 344 { group: 'Uutf', name: '1.0.4: UTF-8 decoder', universe: U['uutf.1.0.4'], require: ['uutf'], 345 steps: [ 346 { code: 'let d = Uutf.decoder ~encoding:`UTF_8 (`String "ABC");;', expect: 'Uutf.decoder' }, 347 { code: 'Uutf.decode d;;', expect: '`Uchar' }, 348 ] }, 349 350 // ── B0 ── 351 { group: 'B0', name: '0.0.6: B0_std.Fpath', universe: U['b0.0.0.6'], require: ['b0.std'], 352 steps: [{ code: 'B0_std.Fpath.v "/tmp";;', expect: 'B0_std.Fpath.t' }] }, 353 354 // ── Bos ── 355 { group: 'Bos (cross-library)', name: '0.2.1: Cmd construction', universe: U['bos.0.2.1'], require: ['bos'], 356 steps: [{ code: 'Bos.Cmd.(v "echo" % "hello");;', expect: 'Bos.Cmd' }] }, 357 ]; 358 359 // ── Flatten cross-version tests into individual test items ───────── 360 const flatTests = []; 361 for (const t of tests) { 362 if (t.crossVersion) { 363 for (const c of t.cases) { 364 flatTests.push({ 365 group: t.group, 366 name: c.label, 367 universe: c.universe, 368 require: c.require || [], 369 crossVersion: true, 370 crossCode: t.code, 371 crossDescription: t.description, 372 shouldPass: c.shouldPass, 373 steps: c.shouldPass 374 ? [{ code: t.code, expect: c.expect || '' }] 375 : [{ code: t.code, expectError: c.expectError || 'Error' }], 376 }); 377 } 378 } else { 379 flatTests.push(t); 380 } 381 } 382 383 // ── Runner ──────────────────────────────────────────────────────────── 384 385 let passed = 0, failed = 0, skipped = 0, running = 0; 386 const total = flatTests.length; 387 const groupsEl = document.getElementById('groups'); 388 const groupEls = {}; 389 const testEls = []; 390 391 function updateSummary() { 392 document.getElementById('pass-count').textContent = passed; 393 document.getElementById('fail-count').textContent = failed; 394 document.getElementById('skip-count').textContent = skipped; 395 document.getElementById('run-count').textContent = running; 396 const done = passed + failed + skipped; 397 document.getElementById('progress-bar').style.width = `${(done / total) * 100}%`; 398 } 399 400 function escHtml(s) { 401 return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); 402 } 403 404 // Track which cross-version code banners we've shown per group 405 const shownBanners = {}; 406 407 // Build DOM 408 for (let i = 0; i < flatTests.length; i++) { 409 const t = flatTests[i]; 410 if (!groupEls[t.group]) { 411 const g = document.createElement('div'); 412 g.className = 'group'; 413 const header = document.createElement('div'); 414 header.className = 'group-header'; 415 const isCross = t.crossVersion; 416 header.innerHTML = `<span>${t.group}${isCross ? '<span class="group-badge cross">cross-version</span>' : ''}</span><span class="arrow">\u25BC</span>`; 417 const body = document.createElement('div'); 418 body.className = 'group-body'; 419 header.onclick = () => { 420 header.classList.toggle('collapsed'); 421 body.classList.toggle('hidden'); 422 }; 423 g.appendChild(header); 424 g.appendChild(body); 425 groupsEl.appendChild(g); 426 groupEls[t.group] = { el: g, header, body, tests: [] }; 427 } 428 429 // For cross-version tests, show the code banner once per group 430 if (t.crossVersion && !shownBanners[t.group]) { 431 shownBanners[t.group] = true; 432 const banner = document.createElement('div'); 433 banner.className = 'code-banner'; 434 banner.innerHTML = `<code>${escHtml(t.crossCode)}</code>${t.crossDescription ? ` &mdash; <em>${escHtml(t.crossDescription)}</em>` : ''}`; 435 groupEls[t.group].body.appendChild(banner); 436 } 437 438 const badgeHtml = t.crossVersion 439 ? (t.shouldPass 440 ? '<span class="expect-badge should-pass">expect: pass</span>' 441 : '<span class="expect-badge should-error">expect: error</span>') 442 : ''; 443 444 const row = document.createElement('div'); 445 row.className = 'test-row pending'; 446 row.innerHTML = ` 447 <span class="test-icon">\u25CB</span> 448 <div class="test-name"><span class="label">${escHtml(t.name)}</span>${badgeHtml}<div class="test-detail"></div></div> 449 <span class="test-time"></span> 450 `; 451 groupEls[t.group].body.appendChild(row); 452 groupEls[t.group].tests.push(row); 453 testEls.push(row); 454 } 455 456 // transcript: array of { code, output, outputClass } 457 function setTestState(row, state, transcript, time) { 458 row.className = `test-row ${state}`; 459 const icons = { pass: '\u2714', 'pass-neg': '\u2714', fail: '\u2718', skip: '\u25CB', running: '<span class="spinner">\u25E0</span>', pending: '\u25CB' }; 460 row.querySelector('.test-icon').innerHTML = icons[state] || '\u25CB'; 461 if (transcript && transcript.length > 0) { 462 const detailEl = row.querySelector('.test-detail'); 463 let html = ''; 464 for (const step of transcript) { 465 html += '<div class="step-transcript">'; 466 if (step.code) { 467 html += `<div class="step-input">${escHtml(step.code)}</div>`; 468 } 469 if (step.output) { 470 const cls = step.outputClass || 'out-pass'; 471 html += `<div class="step-output ${cls}">${escHtml(step.output)}</div>`; 472 } 473 html += '</div>'; 474 } 475 detailEl.innerHTML = html; 476 } 477 if (time !== undefined) { 478 row.querySelector('.test-time').textContent = `${time}ms`; 479 } 480 } 481 482 // Worker cache 483 const workerCache = new Map(); 484 485 async function getWorker(universe) { 486 if (workerCache.has(universe)) return workerCache.get(universe); 487 const indexUrl = `/jtw-output/u/${universe}/findlib_index.json`; 488 const { worker: w, stdlib_dcs, findlib_index } = await OcamlWorker.fromIndex( 489 indexUrl, '/jtw-output', { timeout: 120000 }); 490 await w.init({ 491 findlib_requires: [], 492 stdlib_dcs: stdlib_dcs, 493 findlib_index: findlib_index, 494 }); 495 workerCache.set(universe, w); 496 return w; 497 } 498 499 async function runTest(t, idx) { 500 const row = testEls[idx]; 501 setTestState(row, 'running'); 502 running++; 503 updateSummary(); 504 const start = performance.now(); 505 506 try { 507 const worker = await getWorker(t.universe); 508 509 for (const pkg of (t.require || [])) { 510 await worker.eval(`#require "${pkg}";;`); 511 } 512 513 const transcript = []; 514 for (const step of t.steps) { 515 if (step.complete) { 516 const result = await worker.complete(step.complete.source, step.complete.pos); 517 const entries = result.completions?.entries?.map(e => e.name) || []; 518 if (step.expectEntries) { 519 for (const exp of step.expectEntries) { 520 if (!entries.includes(exp)) 521 throw new Error(`Expected completion "${exp}" not found in [${entries.join(', ')}]`); 522 } 523 } 524 transcript.push({ code: step.complete.source, output: `completions: [${entries.slice(0, 5).join(', ')}...]`, outputClass: 'out-pass' }); 525 526 } else if (step.expectError) { 527 // NEGATIVE TEST: we expect this to produce an error 528 const r = await worker.eval(step.code); 529 const ppf = r.caml_ppf || ''; 530 const stderr = r.stderr || ''; 531 const combined = ppf + stderr; 532 if (combined.includes(step.expectError)) { 533 transcript.push({ code: step.code, output: combined.trim(), outputClass: 'out-neg' }); 534 } else if (combined === '' && ppf === '') { 535 transcript.push({ code: step.code, output: '(empty output — error swallowed)', outputClass: 'out-neg' }); 536 } else { 537 transcript.push({ code: step.code, output: `Expected error "${step.expectError}" but got:\n${ppf}${stderr}`, outputClass: 'out-fail' }); 538 throw new Error(`Expected error containing "${step.expectError}" but got success:\nppf: "${ppf}"\nstderr: "${stderr}"`); 539 } 540 541 } else { 542 const r = await worker.eval(step.code); 543 const ppf = r.caml_ppf || ''; 544 const stdout = r.stdout || ''; 545 const stderr = r.stderr || ''; 546 547 if (step.expect && step.expect !== '') { 548 if (!ppf.includes(step.expect)) { 549 transcript.push({ code: step.code, output: ppf || stderr || '(no output)', outputClass: 'out-fail' }); 550 throw new Error(`Expected caml_ppf to contain "${step.expect}"\nGot: "${ppf}"\nstderr: "${stderr}"`); 551 } 552 } 553 if (step.expectStdout) { 554 if (!stdout.includes(step.expectStdout)) { 555 transcript.push({ code: step.code, output: `stdout: "${stdout}"`, outputClass: 'out-fail' }); 556 throw new Error(`Expected stdout to contain "${step.expectStdout}"\nGot: "${stdout}"`); 557 } 558 } 559 if (step.expectNotError) { 560 if ((ppf + stderr).includes(step.expectNotError)) { 561 transcript.push({ code: step.code, output: ppf + stderr, outputClass: 'out-fail' }); 562 throw new Error(`Output contains unexpected "${step.expectNotError}"\nppf: "${ppf}"\nstderr: "${stderr}"`); 563 } 564 } 565 // Build output line: ppf first, then stdout if present 566 let out = ppf ? ppf.trim() : ''; 567 if (stdout) out += (out ? '\n' : '') + '(stdout) ' + stdout.trim(); 568 transcript.push({ code: step.code, output: out || '(unit)', outputClass: 'out-pass' }); 569 } 570 } 571 572 const elapsed = Math.round(performance.now() - start); 573 running--; 574 passed++; 575 576 // Use pass-neg state for negative tests that correctly errored 577 const isNegPass = t.crossVersion && !t.shouldPass; 578 setTestState(row, isNegPass ? 'pass-neg' : 'pass', transcript, elapsed); 579 580 } catch (e) { 581 const elapsed = Math.round(performance.now() - start); 582 running--; 583 failed++; 584 // If transcript has partial results from before the error, show them 585 if (transcript.length === 0) { 586 transcript.push({ code: '', output: e.message, outputClass: 'out-fail' }); 587 } 588 setTestState(row, 'fail', transcript, elapsed); 589 } 590 updateSummary(); 591 } 592 593 // Run tests: parallel across universes, sequential within each universe 594 async function runAll() { 595 const byUniverse = new Map(); 596 flatTests.forEach((t, i) => { 597 if (!byUniverse.has(t.universe)) byUniverse.set(t.universe, []); 598 byUniverse.get(t.universe).push({ test: t, idx: i }); 599 }); 600 601 const promises = []; 602 for (const [, items] of byUniverse) { 603 promises.push((async () => { 604 for (const { test, idx } of items) { 605 await runTest(test, idx); 606 } 607 })()); 608 } 609 await Promise.all(promises); 610 } 611 612 updateSummary(); 613 runAll(); 614 </script> 615</body> 616</html>