Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

api-fake.test.mjs

v0.1.0-build.85 Latest Commit ede38f5

Size
11 KB
Lines
240
Language
javascript
SHA-256
ac75e153080a…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Pure-JS tests of the wrapper's marshalling (js/api.mjs) against a fake
2// Emscripten module that mimics the bridge's C contract: malloc'd results +
3// ckw_last_len, NUL-terminated strings, u32/f64 out-parameters, and a heap
4// that GROWS (replacing HEAPU8) so stale-view bugs show up. It also checks
5// that every temporary allocation is freed. The real core is exercised by
6// wasm.test.mjs (CI) and test/native/bridge_test.cpp.
7import test from 'node:test';
8import assert from 'node:assert/strict';
9import { createCKey } from '../js/api.mjs';
10import { loadCKey } from '../js/ckey.mjs';
11
12function fakeModule() {
13 const enc = new TextEncoder();
14 const M = { HEAPU8: new Uint8Array(1024) };
15 let top = 8;
16 const live = new Map(); // ptr -> size
17 const symbols = new Map(); // ptr -> { payload, version, level, layout }
18 const styles = new Map();
19 let lastLen = 0;
20 let errPtr = 0;
21 const tel = { mode: 1, client: null, events: [], batchId: 0, inflight: 0 };
22
23 M._ckw_malloc = (n) => {
24 n = Math.max(1, n);
25 if (top + n > M.HEAPU8.length) {
26 const grown = new Uint8Array(Math.max(M.HEAPU8.length * 2, top + n + 1024));
27 grown.set(M.HEAPU8);
28 M.HEAPU8 = grown; // views on the old buffer are now stale
29 }
30 const p = top;
31 top += (n + 7) & ~7;
32 live.set(p, n);
33 return p;
34 };
35 M._ckw_free = (p) => {
36 if (p) {
37 assert.ok(live.has(p), `free of unknown pointer ${p}`);
38 live.delete(p);
39 }
40 };
41 const putBytes = (bytes) => {
42 const p = M._ckw_malloc(bytes.length);
43 M.HEAPU8.set(bytes, p);
44 lastLen = bytes.length;
45 return p;
46 };
47 const putCString = (s, track = true) => {
48 const b = enc.encode(s);
49 const p = M._ckw_malloc(b.length + 1);
50 M.HEAPU8.set(b, p);
51 M.HEAPU8[p + b.length] = 0;
52 if (track) lastLen = b.length;
53 return p;
54 };
55 const readCString = (p) => {
56 let e = p;
57 while (M.HEAPU8[e]) e++;
58 return new TextDecoder().decode(M.HEAPU8.subarray(p, e));
59 };
60 const setError = (s) => {
61 if (errPtr) M._ckw_free(errPtr);
62 errPtr = putCString(s, false);
63 };
64 setError('');
65 const u32 = (p, i, v) => new DataView(M.HEAPU8.buffer).setUint32(p + 4 * i, v, true);
66 const f64 = (p, i, v) => new DataView(M.HEAPU8.buffer).setFloat64(p + 8 * i, v, true);
67
68 M._ckw_last_error = () => errPtr;
69 M._ckw_last_len = () => lastLen;
70 const versionPtr = putCString('0.1.0-build.63', false);
71 M._ckw_software_version = () => versionPtr;
72 M._ckw_abi_version = () => 4;
73 M._ckw_scalable_capacity = () => 100;
74 M._ckw_scalable_modules_across = (v) => 28 + v;
75 M._ckw_encode = (p, len, version, level, layout) => {
76 if (len > 100) return setError('payload exceeds the selected Profile 0 version and level'), 0;
77 const s = M._ckw_malloc(8192);
78 symbols.set(s, { payload: M.HEAPU8.slice(p, p + len), version: version || 3, level, layout });
79 return s;
80 };
81 M._ckw_symbol_free = (s) => {
82 assert.ok(symbols.delete(s), 'double symbol free');
83 M._ckw_free(s);
84 };
85 M._ckw_symbol_info = (s, out) => {
86 const x = symbols.get(s);
87 [x.version, x.level, x.layout, x.payload.length, 1].forEach((v, i) => u32(out, i, v));
88 };
89 M._ckw_style_new = (preset) => {
90 if (preset > 1) return setError('unknown style preset'), 0;
91 const p = M._ckw_malloc(64);
92 styles.set(p, { preset, rgb: {}, num: {}, logo: null });
93 return p;
94 };
95 M._ckw_style_free = (p) => (styles.delete(p), M._ckw_free(p));
96 M._ckw_style_set_rgb = (p, f, r, g, b) => ((styles.get(p).rgb[f] = [r, g, b]), 1);
97 M._ckw_style_set_num = (p, f, v) => ((styles.get(p).num[f] = v), 1);
98 M._ckw_style_set_logo = (p, s) => ((styles.get(p).logo = readCString(s)), 1);
99 const refused = (ws) => ws && styles.get(ws).rgb[2]?.[0] > 200 && styles.get(ws).num[10] !== 2;
100 M._ckw_style_check = (s, ws) => (refused(ws) ? (setError('ink is 1.0:1; need 3:1'), 1) : (setError('style OK'), 0));
101 M._ckw_render_raster = (s, ws, size, policy, transparent) => {
102 if (refused(ws)) return setError('ink is 1.0:1; need 3:1'), 0;
103 const px = new Uint8Array(size * size * 4).fill(255);
104 if (transparent) for (let i = 3; i < px.length; i += 4) px[i] = 0;
105 px[1] = [...symbols.keys()].indexOf(s); // the fake "symbol" the decoder recognises
106 px[2] = 0xc4;
107 return putBytes(px);
108 };
109 M._ckw_encode_png_rgba = (p, w, h) => putBytes(Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, w, h));
110 M._ckw_render_svg = (s, ws, scale) => (refused(ws) ? (setError('ink is 1.0:1; need 3:1'), 0) : putCString(`<svg width="${31 * scale}"></svg>`));
111 M._ckw_render_pdf = () => putBytes(enc.encode('%PDF-1.4\n'));
112 M._ckw_has_certify = () => 1;
113 M._ckw_certify = (s, ws, transparent, colours, n, px, w, h, ppm, blur, noise, glare, trials, out) => {
114 [transparent ? 0 : 1, 0, 1 | 32, 3, transparent ? 0 : 3, 21, 0.9, 0].forEach((v, i) => f64(out, i, v));
115 setError(transparent && !n ? 'background unknown' : 'certified');
116 return transparent && !n ? 1 : 0;
117 };
118 const decode = (p, w, h, stride, info, channels) => {
119 if (channels === 4 && M.HEAPU8[p + 2] === 0xc4) {
120 const s = [...symbols.keys()][M.HEAPU8[p + 1]];
121 const x = symbols.get(s);
122 [x.layout, x.version, x.layout, 1].forEach((v, i) => u32(info, i, v));
123 return putBytes(x.payload);
124 }
125 setError('no CKey border found');
126 return 0;
127 };
128 M._ckw_decode_rgba = (p, w, h, s, info) => decode(p, w, h, s, info, 4);
129 M._ckw_decode_gray = (p, w, h, s, info) => decode(p, w, h, s, info, 1);
130 M._ckw_tel_set_mode = (m) => (m >= 0 && m <= 2 ? ((tel.mode = m), 0) : 1);
131 M._ckw_tel_get_mode = () => tel.mode;
132 M._ckw_tel_set_client = (a, b, c) => ((tel.client = [readCString(a), readCString(b), readCString(c)]), 0);
133 M._ckw_tel_session_begin = (t) => tel.events.push(['begin', t]);
134 M._ckw_tel_frame = (t, ms) => tel.events.push(['frame', t, ms]);
135 M._ckw_tel_session_success = (t, code) => (tel.events.push(['success', t, code]), 0);
136 M._ckw_tel_session_end = (t) => tel.events.push(['end', t]);
137 M._ckw_tel_should_send = () => (tel.mode === 1 && !tel.inflight ? 1 : 0);
138 M._ckw_tel_take_batch = (day) => {
139 tel.inflight = ++tel.batchId;
140 return putCString(`{"schema":1,"sdk":"web","day":"${readCString(day)}"}`);
141 };
142 M._ckw_tel_last_batch_id = () => tel.batchId;
143 M._ckw_tel_batch_result = (id, status) => (id === tel.inflight ? ((tel.inflight = 0), (tel.status = status), 0) : 1);
144 M._ckw_tel_snapshot = (p) => {
145 for (let i = 0; i < 33; i++) f64(p, i, i);
146 return 0;
147 };
148 M._ckw_tel_reset_for_testing = () => {};
149 return { M, live, symbols, tel, base: () => new Set([errPtr, versionPtr]) };
150}
151
152const leaks = (f) => [...f.live.keys()].filter((p) => !f.base().has(p) && !f.symbols.has(p));
153
154test('encode, render, decode marshalling with a growing heap and no leaks', () => {
155 const f = fakeModule();
156 const ckey = createCKey(f.M, { fetch: null });
157 assert.equal(ckey.version, '0.1.0-build.63');
158 assert.deepEqual(f.tel.client, ['web', '0.1.0-build.63', 'web']);
159 const sym = ckey.encode('héllo', { level: 'Q', layout: 'print' });
160 assert.deepEqual(
161 { v: sym.version, l: sym.level, y: sym.layout, n: sym.payloadLength, c: sym.capacity, m: sym.modules },
162 { v: 3, l: 'Q', y: 'print', n: 6, c: 100, m: 31 },
163 );
164 const img = ckey.renderPixels(sym, { size: 64, transparent: true }); // forces heap growth
165 assert.equal(img.data.length, 64 * 64 * 4);
166 assert.equal(img.data[7], 0);
167 const r = ckey.decodeImageData(img);
168 assert.equal(r.text, 'héllo');
169 assert.equal(r.profile, '0.1');
170 assert.equal(r.layout, 'print');
171 assert.equal(r.version, 3);
172 assert.equal('profileCode' in r, false);
173 assert.equal(ckey.decodeImageData({ width: 2, height: 2, data: new Uint8ClampedArray(16) }), null);
174 assert.equal(ckey.decodeGray({ width: 2, height: 2, data: new Uint8Array(4) }), null);
175 assert.deepEqual([...ckey.renderPNG(sym, { size: 32 }).subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47]);
176 assert.equal(ckey.renderSVG(sym, { scale: 2 }), '<svg width="62"></svg>');
177 assert.equal(new TextDecoder().decode(ckey.renderPDF(sym)).slice(0, 5), '%PDF-');
178 assert.throws(() => ckey.encode('x'.repeat(101)), /payload exceeds/);
179 assert.throws(() => ckey.renderSVG(sym, { style: { ink: '#fafafa' } }), /need 3:1/);
180 assert.ok(ckey.renderSVG(sym, { style: { ink: '#fafafa', policy: 'override' } }));
181 assert.deepEqual(ckey.checkStyle(sym, { preset: 'concept' }), { ok: true, message: 'style OK' });
182 const cert = ckey.certify(sym, { backdrop: { colours: ['#000'] } });
183 assert.equal(cert.certified, true);
184 assert.deepEqual(cert.adjustments, ['ink', 'materials']);
185 assert.equal(cert.policy, 'strict');
186 assert.equal(ckey.certify(sym, { transparent: true }).ok, false);
187 assert.deepEqual(leaks(f).filter((p) => f.live.get(p) !== 64 * 64 * 4), [], 'only the frame scratch buffer stays');
188 sym.free();
189 sym.free();
190 assert.equal(sym.freed, true);
191 assert.throws(() => ckey.renderSVG(sym), /freed/);
192 assert.throws(() => ckey.renderSVG({}), TypeError);
193});
194
195test('telemetry property, counters and scanner wiring', async () => {
196 const f = fakeModule();
197 let clock = 0;
198 const sent = [];
199 const ckey = createCKey(f.M, { fetch: async (u, init) => (sent.push(init.body), { status: 204 }), now: () => (clock += 10), day: () => '2026-01-02' });
200 assert.equal(ckey.telemetry, 1);
201 assert.throws(() => (ckey.telemetry = 5), RangeError);
202 assert.equal(ckey.telemetry, 1);
203 ckey.telemetry = 2;
204 assert.equal(f.tel.mode, 2);
205 ckey.telemetry = 1;
206 const sym = ckey.encode('scan me');
207 const frame = ckey.renderPixels(sym, { size: 16 });
208 const got = [];
209 const sc = ckey.createScanner({ onResult: (r) => got.push(r.text) });
210 sc.processFrame(frame);
211 sc.stop();
212 assert.deepEqual(f.tel.events.map((e) => e[0]), ['begin', 'frame', 'success', 'end']);
213 assert.deepEqual(got, ['scan me']);
214 assert.equal(await ckey.flushTelemetry(), 204);
215 assert.deepEqual(sent, ['{"schema":1,"sdk":"web","day":"2026-01-02"}']);
216 assert.equal(f.tel.status, 204);
217 const c = ckey.telemetryCounters();
218 assert.deepEqual(c.sessions, { started: 0, succeeded: 1, failed: 2 });
219 assert.deepEqual(c.time_to_decode_ms.buckets, [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
220 assert.equal(c.frame_ms.count, 21);
221 ckey._testing.sender.disarm();
222 assert.throws(() => createCKey(fakeModule().M, { telemetry: '0' }), RangeError);
223 assert.equal(createCKey(fakeModule().M, { telemetry: 0 }).telemetry, 0);
224});
225
226test('loadCKey with an injected factory and wasm binary', async () => {
227 const f = fakeModule();
228 let args;
229 const ckey = await loadCKey({
230 moduleFactory: async (a) => ((args = a), f.M),
231 wasmBinary: new Uint8Array([0, 97, 115, 109]),
232 wasmUrl: 'https://cdn.example/ckey/ckey.wasm',
233 fetch: null,
234 });
235 assert.equal(args.locateFile('ckey-core.wasm'), 'https://cdn.example/ckey/ckey.wasm');
236 assert.equal(args.locateFile('other.js'), 'other.js');
237 assert.ok(args.wasmBinary instanceof Uint8Array);
238 assert.equal(ckey.classifyPayload('https://openckey.org').label, 'Open openckey.org');
239 assert.throws(() => loadCKey(5), TypeError);
240});