char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somclean up tests and clippy4d5ee6a

main
25.5 KiB684 linesraw
1use std::fs;
2use std::io::Read;
3use std::path::Path;
4use std::process::{Command, Output, Stdio};
5use std::time::{Duration, Instant};
6
7const DEADLINE: Duration = Duration::from_secs(30);
8
9fn run(dir: &Path, script: &str, source: &str, flags: &[&str]) -> Output {
10    fs::write(dir.join(script), source).unwrap();
11    let mut child = Command::new(env!("CARGO_BIN_EXE_avril"))
12        .arg("run")
13        .current_dir(dir)
14        .args(flags)
15        .arg(script)
16        .stdin(Stdio::null())
17        .stdout(Stdio::piped())
18        .stderr(Stdio::piped())
19        .spawn()
20        .unwrap();
21    let mut stdout = child.stdout.take().unwrap();
22    let mut stderr = child.stderr.take().unwrap();
23    let threads = std::thread::scope(|scope| {
24        let out = scope.spawn(|| {
25            let mut bytes = Vec::new();
26            stdout.read_to_end(&mut bytes).unwrap();
27            bytes
28        });
29        let err = scope.spawn(|| {
30            let mut bytes = Vec::new();
31            stderr.read_to_end(&mut bytes).unwrap();
32            bytes
33        });
34        let started = Instant::now();
35        let status = loop {
36            if let Some(status) = child.try_wait().unwrap() {
37                break status;
38            }
39            if started.elapsed() >= DEADLINE {
40                child.kill().unwrap();
41                panic!("avril run did not exit within {DEADLINE:?}");
42            }
43            std::thread::sleep(Duration::from_millis(10));
44        };
45        (out.join().unwrap(), err.join().unwrap(), status)
46    });
47    Output {
48        status: threads.2,
49        stdout: threads.0,
50        stderr: threads.1,
51    }
52}
53
54fn assert_marker(output: &Output, marker: &str) {
55    assert!(
56        output.status.success(),
57        "exit={}\nstdout={}\nstderr={}",
58        output.status,
59        String::from_utf8_lossy(&output.stdout),
60        String::from_utf8_lossy(&output.stderr)
61    );
62    assert!(
63        String::from_utf8_lossy(&output.stdout).contains(marker),
64        "missing {marker}\nstdout={}\nstderr={}",
65        String::from_utf8_lossy(&output.stdout),
66        String::from_utf8_lossy(&output.stderr)
67    );
68}
69
70const PRELUDE: &str = r#"
71const failures = [];
72function check(cond, message) {
73  if (!cond) failures[failures.length] = message;
74}
75function done(name) {
76  if (failures.length !== 0) {
77    for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
78    throw new Error(failures.length + " failure(s) in " + name);
79  }
80  console.log("KV-OK " + name);
81}
82"#;
83
84#[test]
85fn roundtrip_and_persistence() {
86    let dir = tempfile::tempdir().unwrap();
87    let write = format!(
88        r#"{PRELUDE}
89import {{ openKv }} from "avril:kv";
90const db = await openKv("state.kv", {{ create: true }});
91const ks = db.keyspace("v");
92// Strings and equal byte sequences are the same key and value.
93await ks.set("str", "hello");
94await ks.set(new Uint8Array([98, 121, 116, 101, 115]), new Uint8Array([1, 2, 3]));
95await ks.set("abc", "as-string");
96await ks.set(new Uint8Array([120]), "json-value");
97const first = await ks.set("rewrite", "one");
98const second = await ks.set("rewrite", "two");
99check(first.versionstamp !== second.versionstamp, "versionstamp changes on rewrite");
100check((await db.keyspaces()).join(",") === "v", "keyspaces lists v");
101db.close();
102const reopened = await openKv("state.kv", {{ create: false }});
103const again = reopened.keyspace("v");
104const entry = await again.entry("str");
105check(entry !== null && entry.keyspace === "v", "entry keyspace");
106check(entry !== null && Array.from(entry.key).join(",") === "115,116,114", "entry key is canonical bytes");
107check(entry !== null && typeof entry.versionstamp === "bigint", "entry carries a versionstamp");
108const str = await again.get("str");
109check(str !== null && Array.from(str).join(",") === "104,101,108,108,111", "get returns raw bytes");
110const text = await again.getText("str");
111check(text === "hello", "getText decodes strings");
112const asBytes = await again.get(new Uint8Array([115, 116, 114]));
113check(asBytes !== null && asBytes.length === 5, "byte key hits string key");
114const overwritten = await again.get("abc");
115check(overwritten !== null && overwritten instanceof Uint8Array, "overwritten value is bytes");
116await again.set("abc", "as-string-again");
117const back = await again.get(new Uint8Array([97, 98, 99]));
118check(back !== null && (await again.getText(new Uint8Array([97, 98, 99]))) === "as-string-again", "bytes and strings are one key");
119const json = await again.getText("json-holder");
120check(json === null, "missing key reads null");
121await again.set("json-holder", JSON.stringify({{ list: [1, "two"], map: {{ a: 1 }} }}));
122const parsed = JSON.parse((await again.getText("json-holder")) ?? "null");
123check(parsed.list[1] === "two" && parsed.map.a === 1, "JSON roundtrips through strings");
124// Non-UTF-8 bytes: get works byte-exact, getText rejects.
125await again.set("raw", new Uint8Array([0xff, 0x00, 0xfe]));
126const raw = await again.get("raw");
127check(raw !== null && Array.from(raw).join(",") === "255,0,254", "non-UTF8 roundtrips as bytes");
128try {{
129  await again.getText("raw");
130  check(false, "getText of non-UTF8 must reject");
131}} catch (e) {{
132  check(e instanceof TypeError, "getText rejection is a TypeError, got: " + e);
133}}
134done("roundtrip");
135"#
136    );
137    assert_marker(&run(dir.path(), "write.js", &write, &[]), "KV-OK roundtrip");
138    let read = r#"import { openKv } from "avril:kv";
139const failures = [];
140function check(cond, message) {
141  if (!cond) failures[failures.length] = message;
142}
143const db = await openKv("state.kv", { create: false });
144const ks = db.keyspace("v");
145const str = await ks.getText("str");
146check(str === "hello", "string persists across processes");
147const bytes = await ks.get("bytes");
148check(bytes !== null && Array.from(bytes).join(",") === "1,2,3", "bytes persist across processes");
149const missing = await ks.get("nope");
150check(missing === null, "missing reads null");
151if (failures.length !== 0) {
152  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
153  throw new Error(failures.length + " failure(s)");
154}
155console.log("KV-OK reread");
156"#;
157    assert_marker(&run(dir.path(), "read.js", read, &[]), "KV-OK reread");
158}
159
160#[test]
161fn keys_and_ordering() {
162    let dir = tempfile::tempdir().unwrap();
163    let source = r#"import { openKv } from "avril:kv";
164const failures = [];
165function check(cond, message) {
166  if (!cond) failures[failures.length] = message;
167}
168const db = await openKv("state.kv", { create: true });
169const ks = db.keyspace("o");
170// "b" as a string and as bytes [98] is one key; the second write wins.
171await ks.set("b", "string-b");
172await ks.set(new Uint8Array([98]), "bytes-b");
173await ks.set("", "empty");
174await ks.set(new Uint8Array([0]), "zero");
175await ks.set("a", "a");
176await ks.set("a\x00", "a-nul");
177await ks.set("a\x00b", "a-nul-b");
178await ks.set("aa", "aa");
179await ks.set(new Uint8Array([255]), "max");
180const tag = (key) => Array.from(key).join(",");
181const listed = [];
182for await (const e of ks.list()) listed[listed.length] = tag(e.key);
183check(JSON.stringify(listed) === JSON.stringify(["", "0", "97", "97,0", "97,0,98", "97,97", "98", "255"]), "byte ordering:\n" + JSON.stringify(listed));
184const b = await ks.getText("b");
185check(b === "bytes-b", "string and bytes are one key");
186const prefixed = [];
187for await (const e of ks.list({ prefix: "user:" })) prefixed[prefixed.length] = tag(e.key);
188const emptyBefore = prefixed.length;
189await ks.set("user:1", "u1");
190await ks.set("user:2", "u2");
191await ks.set("admin:1", "a1");
192const prefixedAfter = [];
193for await (const e of ks.list({ prefix: "user:" })) prefixedAfter[prefixedAfter.length] = tag(e.key);
194check(emptyBefore === 0, "no user: keys before insert");
195check(JSON.stringify(prefixedAfter) === JSON.stringify(["117,115,101,114,58,49", "117,115,101,114,58,50"]), "string prefix listing");
196const bytePrefixed = [];
197for await (const e of ks.list({ prefix: new Uint8Array([0x89, 0x50]) })) bytePrefixed[bytePrefixed.length] = tag(e.key);
198check(bytePrefixed.length === 0, "binary prefix with no match");
199await ks.set(new Uint8Array([0x89, 0x50, 0x01]), "bin1");
200await ks.set(new Uint8Array([0x89, 0x50, 0x02]), "bin2");
201await ks.set(new Uint8Array([0x89, 0x51]), "other");
202const bytePrefixedAfter = [];
203for await (const e of ks.list({ prefix: new Uint8Array([0x89, 0x50]) })) bytePrefixedAfter[bytePrefixedAfter.length] = tag(e.key);
204check(JSON.stringify(bytePrefixedAfter) === JSON.stringify(["137,80,1", "137,80,2"]), "binary prefix listing");
205const bounded = [];
206for await (const e of ks.list({ start: "a", end: "b" })) bounded[bounded.length] = tag(e.key);
207check(JSON.stringify(bounded) === JSON.stringify(["97", "97,0", "97,0,98", "97,97", "97,100,109,105,110,58,49"]), "start/end bounds");
208const limited = [];
209for await (const e of ks.list({ limit: 2 })) limited[limited.length] = tag(e.key);
210check(limited.length === 2, "limit");
211const reversed = [];
212for await (const e of ks.list({ reverse: true })) reversed[reversed.length] = tag(e.key);
213const forward = [];
214for await (const e of ks.list()) forward[forward.length] = tag(e.key);
215check(JSON.stringify(reversed) === JSON.stringify(forward.slice().reverse()), "reverse");
216const all = [];
217for await (const e of ks.list({ prefix: "" })) all[all.length] = tag(e.key);
218check(all.length === forward.length, "empty prefix scans all");
219if (failures.length !== 0) {
220  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
221  throw new Error(failures.length + " failure(s)");
222}
223console.log("KV-OK ordering");
224"#;
225    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK ordering");
226}
227
228#[test]
229fn keyspaces_isolation_clear_and_get_many() {
230    let dir = tempfile::tempdir().unwrap();
231    let source = r#"import { openKv } from "avril:kv";
232const failures = [];
233function check(cond, message) {
234  if (!cond) failures[failures.length] = message;
235}
236const db = await openKv("state.kv", { create: true });
237const a = db.keyspace("a");
238const b = db.keyspace("b");
239check(a.name === "a" && b.name === "b", "keyspace names");
240check(a === db.keyspace("a"), "keyspace identity is stable");
241await a.set("k", "from-a");
242await b.set("k", "from-b");
243const ra = await a.get("k");
244const rb = await b.get("k");
245check(ra !== null && Array.from(ra).join(",") === "102,114,111,109,45,97", "space a isolated");
246check(rb !== null && (await b.getText("k")) === "from-b", "space b isolated");
247check(JSON.stringify(await db.keyspaces()) === '["a","b"]', "keyspaces listed");
248const many = await b.getMany(["k", "missing"]);
249check(many.length === 2 && many[0] !== null && many[1] === null, "getMany");
250await a.clear();
251check(await a.get("k") === null, "clear empties its space");
252const still = await b.getText("k");
253check(still === "from-b", "clear scoped to its space");
254if (failures.length !== 0) {
255  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
256  throw new Error(failures.length + " failure(s)");
257}
258console.log("KV-OK spaces");
259"#;
260    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK spaces");
261}
262
263#[test]
264fn atomic_batches() {
265    let dir = tempfile::tempdir().unwrap();
266    let source = r#"import { openKv } from "avril:kv";
267const failures = [];
268function check(cond, message) {
269  if (!cond) failures[failures.length] = message;
270}
271const db = await openKv("state.kv", { create: true });
272const other = await openKv("other.kv", { create: true });
273const a = db.keyspace("a");
274const b = db.keyspace("b");
275const stamp = (await a.set("k1", "v1")).versionstamp;
276const entryBefore = await a.entry("k1");
277check(entryBefore !== null && entryBefore.versionstamp === stamp, "entry() exposes the commit stamp");
278const ok = await db.atomic((tx) => {
279  tx.check(a, "k1", stamp);
280  tx.set(a, "k1", "v2");
281  tx.set(b, "k2", "w2");
282  tx.delete(a, "ghost");
283});
284check(ok.ok === true, "commit succeeds");
285const ra = await a.get("k1");
286const rb = await b.get("k2");
287check(ra !== null && rb !== null && (await a.entry("k1"))?.versionstamp === (await b.entry("k2"))?.versionstamp, "same stamp across keyspaces");
288check(ra !== null && (await a.getText("k1")) === "v2" && (await b.getText("k2")) === "w2", "both writes visible");
289const bad = await db.atomic((tx) => {
290  tx.check(a, "k1", 255n);
291  tx.set(a, "k1", "lost");
292  tx.set(b, "k2", "lost");
293});
294check(bad.ok === false, "bad check aborts");
295check((await a.getText("k1")) === "v2", "aborted set invisible");
296check(await b.get("missing") === null, "aborted second-space write invisible");
297const fresh = await db.atomic((tx) => {
298  tx.check(b, "fresh", null);
299  tx.set(b, "fresh", "1");
300});
301check(fresh.ok === true, "null check passes when absent");
302const stale = await db.atomic((tx) => {
303  tx.check(b, "fresh", null);
304  tx.set(b, "fresh", "2");
305});
306check(stale.ok === false, "null check fails when present");
307const only = await db.atomic((tx) => {
308  tx.check(b, "definitely-absent", null);
309});
310check(only.ok === true && only.versionstamp === null, "check-only batch has null versionstamp");
311const manyText = await b.getTextMany(["fresh", "missing", "fresh"]);
312check(JSON.stringify(manyText) === JSON.stringify(["1", null, "1"]), "getTextMany preserves order and nulls");
313const stamped = await a.entries(["k1", "missing"]);
314check(stamped.length === 2 && stamped[0] !== null && stamped[1] === null, "entries preserves order and nulls");
315const accepted = await db.atomic((tx) => {
316  tx.check(a, "k1", stamped[0].versionstamp);
317  tx.set(a, "k1", "v3");
318});
319check(accepted.ok === true, "entries() stamps are accepted by check");
320check((await a.getText("k1")) === "v3", "check-then-set via entry stamp commits");
321try {
322  await db.atomic(() => {
323    throw new Error("boom");
324  });
325  check(false, "throwing callback must reject");
326} catch (e) {
327  check(e.message === "boom", "callback error propagates");
328}
329check((await b.getText("fresh")) === "1", "thrown callback commits nothing");
330try {
331  await db.atomic(async (tx) => {
332    tx.set(a, "async", "v");
333  });
334  check(false, "async callback must reject");
335} catch (e) {
336  check(e instanceof TypeError, "async callback is a TypeError");
337}
338check(await a.get("async") === null, "async callback commits nothing");
339let stolen = null;
340await db.atomic((tx) => {
341  stolen = tx;
342});
343try {
344  stolen.set(a, "late", "v");
345  check(false, "retained tx must throw");
346} catch (e) {
347  check(e instanceof TypeError, "retained tx is a TypeError");
348}
349try {
350  await db.atomic((tx) => {
351    tx.set(other.keyspace("x"), "k", "v");
352  });
353  check(false, "foreign keyspace must throw");
354} catch (e) {
355  check(e instanceof TypeError, "foreign keyspace is a TypeError");
356}
357if (failures.length !== 0) {
358  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
359  throw new Error(failures.length + " failure(s)");
360}
361console.log("KV-OK atomic");
362"#;
363    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK atomic");
364}
365
366#[test]
367fn modes_exclusive_blocks_second_process_and_read_only() {
368    let dir = tempfile::tempdir().unwrap();
369    fs::write(
370        dir.path().join("hold.js"),
371        r#"import { openKv } from "avril:kv";
372import { stdin } from "avril:std";
373const db = await openKv("held.kv", { access: "exclusive", create: true });
374await db.keyspace("a").set("k", "held");
375console.log("READY");
376const reader = stdin.getReader();
377while (!(await reader.read()).done) {}
378"#,
379    )
380    .unwrap();
381    let mut holder = Command::new(env!("CARGO_BIN_EXE_avril"))
382        .arg("run")
383        .current_dir(dir.path())
384        .arg("hold.js")
385        .stdin(Stdio::piped())
386        .stdout(Stdio::piped())
387        .stderr(Stdio::piped())
388        .spawn()
389        .unwrap();
390    let (ready_tx, ready_rx) = std::sync::mpsc::channel();
391    let mut holder_stdout = holder.stdout.take().unwrap();
392    std::thread::spawn(move || {
393        let mut prefix = [0u8; 6];
394        let _ = holder_stdout.read_exact(&mut prefix);
395        let _ = ready_tx.send(prefix);
396    });
397    let started = Instant::now();
398    loop {
399        if let Ok(prefix) = ready_rx.try_recv() {
400            assert_eq!(&prefix, b"READY\n", "holder handshake");
401            break;
402        }
403        if started.elapsed() >= DEADLINE {
404            holder.kill().unwrap();
405            panic!("holder did not print READY");
406        }
407        std::thread::sleep(Duration::from_millis(10));
408    }
409    let attempt = run(
410        dir.path(),
411        "take.js",
412        r#"import { openKv } from "avril:kv";
413await openKv("held.kv", { access: "exclusive", create: false });
414console.log("OPENED-UNEXPECTEDLY");
415"#,
416        &[],
417    );
418    assert!(!attempt.status.success(), "second exclusive open must fail");
419    assert!(
420        String::from_utf8_lossy(&attempt.stderr).contains("already open"),
421        "already-open error, stderr={}",
422        String::from_utf8_lossy(&attempt.stderr)
423    );
424    drop(holder.stdin.take());
425    let status = loop {
426        if let Some(status) = holder.try_wait().unwrap() {
427            break status;
428        }
429        if started.elapsed() >= DEADLINE {
430            panic!("holder did not exit");
431        }
432        std::thread::sleep(Duration::from_millis(10));
433    };
434    assert!(status.success(), "holder exits cleanly");
435    let modes = r#"import { openKv } from "avril:kv";
436const failures = [];
437function check(cond, message) {
438  if (!cond) failures[failures.length] = message;
439}
440try {
441  await openKv("missing.kv", { create: false });
442  check(false, "create:false on missing must reject");
443} catch (e) {
444  check(String(e.message).includes("does not exist"), "missing message: " + e.message);
445}
446const ro = await openKv("missing.kv", { access: "read-only" });
447check(await ro.keyspace("a").get("k") === null, "read-only missing reads null");
448try {
449  await ro.keyspace("a").set("k", "v");
450  check(false, "read-only set must reject");
451} catch (e) {
452  check(String(e.message).includes("read-only"), "read-only set rejects: " + e.message);
453}
454const existing = await openKv("held.kv", { access: "read-only" });
455const held = await existing.keyspace("a").getText("k");
456check(held === "held", "read-only reads existing");
457if (failures.length !== 0) {
458  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
459  throw new Error(failures.length + " failure(s)");
460}
461console.log("KV-OK modes");
462"#;
463    assert_marker(&run(dir.path(), "modes.js", modes, &[]), "KV-OK modes");
464}
465
466#[test]
467fn closed_handles_reject() {
468    let dir = tempfile::tempdir().unwrap();
469    let source = r#"import { openKv } from "avril:kv";
470const failures = [];
471function check(cond, message) {
472  if (!cond) failures[failures.length] = message;
473}
474const db = await openKv("state.kv", { create: true });
475const ks = db.keyspace("a");
476await ks.set("k", "v");
477db.close();
478db.close();
479try {
480  await ks.get("k");
481  check(false, "get after close must reject");
482} catch (e) {
483  check(e instanceof TypeError && e.message.includes("closed"), "get rejects: " + e.message);
484}
485try {
486  db.keyspace("b");
487  check(false, "keyspace after close must throw");
488} catch (e) {
489  check(e instanceof TypeError, "keyspace throws TypeError");
490}
491await db[Symbol.asyncDispose]();
492let retained = null;
493{
494  await using live = await openKv("state.kv", { create: true });
495  retained = live.keyspace("a");
496}
497try {
498  await retained.get("k");
499  check(false, "use after await using must reject");
500} catch (e) {
501  check(e instanceof TypeError, "await using disposes");
502}
503if (failures.length !== 0) {
504  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
505  throw new Error(failures.length + " failure(s)");
506}
507console.log("KV-OK closed");
508"#;
509    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK closed");
510}
511
512#[test]
513fn capability_denial_at_open_and_after_pledge() {
514    let dir = tempfile::tempdir().unwrap();
515    let denied = run(
516        dir.path(),
517        "main.js",
518        r#"import { openKv } from "avril:kv";
519try {
520  await openKv("state.kv", { create: true });
521  console.log("OPENED-UNEXPECTEDLY");
522} catch (e) {
523  console.log("KV-OK denied: " + e.message);
524}
525"#,
526        &["--deny-write=."],
527    );
528    assert_marker(&denied, "KV-OK denied: write state.kv");
529    assert!(
530        fs::metadata(dir.path().join("state.kv")).is_err(),
531        "denied open must not create the file"
532    );
533    let pledged = run(
534        dir.path(),
535        "pledge.js",
536        r#"import { openKv } from "avril:kv";
537import { pledge } from "avril:std";
538const failures = [];
539function check(cond, message) {
540  if (!cond) failures[failures.length] = message;
541}
542const db = await openKv("state.kv", { create: true });
543await db.keyspace("a").set("k", "v");
544pledge({ read: false, write: false });
545try {
546  await db.keyspace("a").get("k");
547  check(false, "get after pledge must reject");
548} catch (e) {
549  check(String(e.message).includes("read state.kv"), "get denial: " + e.message);
550}
551try {
552  await db.keyspace("a").set("k", "v2");
553  check(false, "set after pledge must reject");
554} catch (e) {
555  // Reads are re-authorized before writes, so the read denial surfaces first.
556  check(String(e.message).includes("read state.kv"), "set denial: " + e.message);
557}
558if (failures.length !== 0) {
559  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
560  throw new Error(failures.length + " failure(s)");
561}
562console.log("KV-OK pledge");
563"#,
564        &[],
565    );
566    assert_marker(&pledged, "KV-OK pledge");
567}
568
569#[test]
570fn invalid_keys_and_values_reject() {
571    let dir = tempfile::tempdir().unwrap();
572    let source = r#"import { openKv } from "avril:kv";
573const failures = [];
574function check(cond, message) {
575  if (!cond) failures[failures.length] = message;
576}
577async function rejectsTypeError(fn, label) {
578  try {
579    await fn();
580  } catch (e) {
581    check(e instanceof TypeError, label + " is a TypeError, got: " + e);
582    return;
583  }
584  check(false, label + " must reject");
585}
586const db = await openKv("state.kv", { create: true });
587const ks = db.keyspace("a");
588await rejectsTypeError(() => ks.set("k", 42), "number value");
589await rejectsTypeError(() => ks.set("k", { nested: true }), "object value");
590await rejectsTypeError(() => ks.set("k", () => {}), "function value");
591await rejectsTypeError(() => ks.set("k", Symbol("s")), "symbol value");
592await rejectsTypeError(() => ks.set("k", undefined), "top-level undefined");
593await rejectsTypeError(() => ks.get(42), "number key");
594await rejectsTypeError(() => ks.get(Object.create(Uint8Array.prototype)), "forged Uint8Array key");
595class Bytes extends Uint8Array {}
596await ks.set(new Bytes([7]), "subclass");
597check((await ks.getText(new Uint8Array([7]))) === "subclass", "Uint8Array subclass key works");
598await rejectsTypeError(() => ks.set("k", "\ud800"), "lone-surrogate value");
599await rejectsTypeError(() => ks.get("\ud800"), "ill-formed string key");
600await rejectsTypeError(() => db.atomic((tx) => tx.check(ks, "k", 42)), "number stamp");
601await rejectsTypeError(() => db.atomic((tx) => tx.check(ks, "k", "1")), "string stamp");
602try {
603  db.keyspace("");
604  check(false, "empty keyspace name must throw");
605} catch (e) {
606  check(e instanceof TypeError, "empty keyspace is a TypeError");
607}
608// Empty key and empty value are valid: bytes, including empty, are keys.
609await ks.set("", "empty-key");
610await ks.set("empty-value", new Uint8Array([]));
611const ek = await ks.getText("");
612check(ek === "empty-key", "empty string key works");
613const ev = await ks.get("empty-value");
614check(ev !== null && ev instanceof Uint8Array && ev.length === 0, "empty bytes value works");
615// Non-UTF-8 bytes: get works, getText rejects.
616await ks.set("raw", new Uint8Array([0xff, 0x00]));
617const gotRaw = await ks.get("raw");
618check(gotRaw !== null && Array.from(gotRaw).join(",") === "255,0", "non-UTF8 set works and reads bytes");
619try {
620  await ks.getText("raw");
621  check(false, "getText of non-UTF8 must reject");
622} catch (e) {
623  check(e instanceof TypeError, "getText rejection is a TypeError, got: " + e);
624}
625// Oversized batches: the offending tx call itself throws RangeError, so the
626// callback aborts and nothing commits.
627const huge = new Uint8Array(4 * 1024 * 1024);
628try {
629  await db.atomic((tx) => {
630    tx.set(ks, "huge", huge);
631  });
632  check(false, "oversized atomic batch must throw");
633} catch (e) {
634  check(e instanceof RangeError, "oversized atomic batch is a RangeError, got: " + e);
635}
636check(await ks.get("huge") === null, "oversized atomic batch commits nothing");
637let opCount = 0;
638try {
639  await db.atomic((tx) => {
640    for (; opCount <= 1000; opCount++) tx.delete(ks, "k" + opCount);
641  });
642  check(false, "too many atomic ops must throw");
643} catch (e) {
644  check(e instanceof RangeError && opCount === 1000, "op 1001 is a RangeError, got: " + e + " at " + opCount);
645}
646if (failures.length !== 0) {
647  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
648  throw new Error(failures.length + " failure(s)");
649}
650console.log("KV-OK invalid");
651"#;
652    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK invalid");
653}
654
655#[test]
656fn poisoned_builtins_do_not_affect_roundtrip() {
657    let dir = tempfile::tempdir().unwrap();
658    let source = r#"import { openKv } from "avril:kv";
659const failures = [];
660function check(cond, message) {
661  if (!cond) failures[failures.length] = message;
662}
663const db = await openKv("state.kv", { create: true });
664const ks = db.keyspace("p");
665await ks.set("s", "hello");
666await ks.set(new Uint8Array([1, 2]), new Uint8Array([3, 4]));
667Array.prototype.push = function () { throw new Error("poisoned"); };
668Map.prototype.set = function () { throw new Error("poisoned"); };
669Object.prototype.evil = 1;
670const text = await ks.getText("s");
671check(text === "hello", "string roundtrip under poisoning");
672const bytes = await ks.get(new Uint8Array([1, 2]));
673check(bytes !== null && Array.from(bytes).join(",") === "3,4", "bytes roundtrip under poisoning");
674const listed = [];
675for await (const e of ks.list()) listed[listed.length] = e.key.length;
676check(listed.length === 2, "list works under poisoning");
677if (failures.length !== 0) {
678  for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]);
679  throw new Error(failures.length + " failure(s)");
680}
681console.log("KV-OK poison");
682"#;
683    assert_marker(&run(dir.path(), "main.js", source, &[]), "KV-OK poison");
684}