char/avril

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

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

Charlotte Somcore: use current error messages in stack tracese0f52fe

main
23.4 KiB717 linesraw
1use std::io;
2
3use avril_core::{Error, Extension, ModuleSource, Op, Runtime, RuntimeOptions};
4use serde_json::{Value, json};
5
6#[test]
7fn typed_ops_and_script_results() {
8    let mut runtime = Runtime::new(RuntimeOptions {
9        extensions: vec![Extension {
10            name: "ext:test",
11            dependencies: &[],
12            ops: vec![Op::new("echo", |value: Value| Ok::<_, io::Error>(value))],
13            modules: &[(
14                "ext:test/main.js",
15                r#"import {echo} from 'ext:core/ops';
16                globalThis.echo = echo;
17                globalThis.bootstrapped = echo({message: "hello", values: [1, true, null]});"#,
18            )],
19            entry_point: Some("ext:test/main.js"),
20        }],
21        ..Default::default()
22    })
23    .unwrap();
24
25    let value: Value = runtime
26        .execute_script("echo.js", "echo(bootstrapped)")
27        .unwrap();
28    assert_eq!(
29        value,
30        json!({"message": "hello", "values": [1, true, null]})
31    );
32}
33
34#[test]
35fn op_errors_are_javascript_exceptions() {
36    let mut runtime = Runtime::new(RuntimeOptions {
37        extensions: vec![Extension {
38            name: "ext:test",
39            dependencies: &[],
40            ops: vec![Op::new("fail", |_: Vec<String>| {
41                Err::<(), _>(io::Error::other("host failure"))
42            })],
43            modules: &[(
44                "ext:test/main.js",
45                "import {fail} from 'ext:core/ops'; globalThis.fail = fail;",
46            )],
47            entry_point: Some("ext:test/main.js"),
48        }],
49        ..Default::default()
50    })
51    .unwrap();
52
53    let errors: Vec<String> = runtime
54        .execute_script(
55            "errors.js",
56            r#"
57            [42, ["valid"]].map((args) => {
58                try {
59                    fail(args);
60                    throw new Error("Expected an op error");
61                } catch (error) {
62                    return error.name + ":" + error.message;
63                }
64            });
65            "#,
66        )
67        .unwrap();
68    assert!(errors[0].starts_with("TypeError:"));
69    assert_eq!(errors[1], "Error:host failure");
70}
71
72#[test]
73fn script_errors_do_not_poison_the_runtime() {
74    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
75    for source in ["const =", "throw new Error('oops')"] {
76        assert!(matches!(
77            runtime.execute_script::<()>("failure.js", source),
78            Err(Error::JavaScript(_))
79        ));
80    }
81    assert_eq!(runtime.execute_script::<u32>("ok.js", "6 * 7").unwrap(), 42);
82}
83
84#[test]
85fn duplicate_ops_are_rejected() {
86    let result = Runtime::new(RuntimeOptions {
87        extensions: vec![Extension {
88            name: "ext:test",
89            dependencies: &[],
90            ops: vec![
91                Op::new("duplicate", |_: ()| Ok::<_, io::Error>(())),
92                Op::new("duplicate", |_: ()| Ok::<_, io::Error>(())),
93            ],
94            modules: &[],
95            entry_point: None,
96        }],
97        ..Default::default()
98    });
99    assert!(matches!(result, Err(Error::DuplicateOp("duplicate"))));
100}
101
102#[test]
103fn failing_extensions_abort_initialization() {
104    let result = Runtime::new(RuntimeOptions {
105        extensions: vec![Extension {
106            name: "ext:broken",
107            dependencies: &[],
108            ops: vec![],
109            modules: &[("ext:broken/main.js", "throw new Error('bootstrap failed');")],
110            entry_point: Some("ext:broken/main.js"),
111        }],
112        ..Default::default()
113    });
114    assert!(
115        matches!(result, Err(Error::JavaScript(message)) if message.contains("bootstrap failed"))
116    );
117}
118
119#[test]
120fn script_turns_drain_microtasks_and_report_rejections_without_leaking_them() {
121    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
122    assert_eq!(
123        runtime
124            .execute_script::<Value>(
125                "result.js",
126                r#"
127        const result = { value: 0 };
128        Promise.resolve().then(() => result.value = 42);
129        result;
130    "#
131            )
132            .unwrap(),
133        json!({ "value": 42 })
134    );
135    runtime
136        .execute_script::<()>(
137            "jobs.js",
138            "globalThis.answer = 0; Promise.resolve().then(() => answer = 42);",
139        )
140        .unwrap();
141    assert_eq!(
142        runtime
143            .execute_script::<u32>("answer.js", "answer")
144            .unwrap(),
145        42
146    );
147    runtime
148        .execute_script::<()>(
149            "handled.js",
150            "const p = Promise.reject('handled'); Promise.resolve().then(() => p.catch(() => {}));",
151        )
152        .unwrap();
153    for source in [
154        "Promise.reject(new Error('first')); Promise.reject(new Error('second'));",
155        "Promise.reject(new Error('discarded')); throw new Error('first');",
156    ] {
157        let error = runtime
158            .execute_script::<()>("rejected.js", source)
159            .unwrap_err();
160        assert!(error.to_string().contains("first"), "{error}");
161        runtime.run_event_loop().unwrap();
162        assert_eq!(runtime.execute_script::<u32>("good.js", "42").unwrap(), 42);
163    }
164}
165
166#[test]
167fn embedding_event_loop_settles_async_ops_and_continuations() {
168    let mut runtime = Runtime::new(RuntimeOptions {
169        extensions: vec![Extension {
170            name: "ext:async",
171            dependencies: &[],
172            ops: vec![
173                Op::asynchronous(
174                    "echo",
175                    |value: u32| async move { Ok::<_, io::Error>(value) },
176                ),
177                Op::asynchronous("fail", |_: ()| async {
178                    Err::<(), _>(io::Error::other("host failure"))
179                }),
180            ],
181            modules: &[(
182                "ext:async/main.js",
183                "import {echo, fail} from 'ext:core/ops'; globalThis.echo = echo; globalThis.fail = fail;",
184            )],
185            entry_point: Some("ext:async/main.js"),
186        }],
187        ..Default::default()
188    })
189    .unwrap();
190    runtime
191        .execute_script::<()>(
192            "start.js",
193            r#"
194        globalThis.answer = 0;
195        echo(41).then(value => echo(value + 1)).then(value => answer = value);
196    "#,
197        )
198        .unwrap();
199    assert_eq!(
200        runtime
201            .execute_script::<u32>("before.js", "answer")
202            .unwrap(),
203        0
204    );
205    runtime.run_event_loop().unwrap();
206    assert_eq!(
207        runtime.execute_script::<u32>("after.js", "answer").unwrap(),
208        42
209    );
210    runtime.execute_script::<()>("fail.js", "fail();").unwrap();
211    assert!(
212        runtime
213            .run_event_loop()
214            .unwrap_err()
215            .to_string()
216            .contains("host failure")
217    );
218    runtime.run_event_loop().unwrap();
219}
220
221#[test]
222fn unreferenced_async_ops_do_not_keep_liveness_but_run_with_other_work() {
223    use std::cell::RefCell;
224    use std::rc::Rc;
225
226    let (wait_sender, wait_receiver) = futures::channel::oneshot::channel::<u32>();
227    let (reference_sender, reference_receiver) = futures::channel::oneshot::channel::<u32>();
228    let wait_receiver = Rc::new(RefCell::new(Some(wait_receiver)));
229    let reference_receiver = Rc::new(RefCell::new(Some(reference_receiver)));
230    let mut runtime = Runtime::new(RuntimeOptions {
231        extensions: vec![Extension {
232            name: "ext:async",
233            dependencies: &[],
234            ops: vec![
235                Op::asynchronous_unreferenced("wait", {
236                    let receiver = wait_receiver.clone();
237                    move |(): ()| {
238                        let receiver = receiver.borrow_mut().take().unwrap();
239                        async move { receiver.await.map_err(io::Error::other) }
240                    }
241                }),
242                Op::asynchronous_unreferenced("ready", |(): ()| async { Ok::<_, io::Error>(7u32) }),
243                Op::asynchronous("reference", move |(): ()| {
244                    let receiver = reference_receiver.borrow_mut().take().unwrap();
245                    async move { receiver.await.map_err(io::Error::other) }
246                }),
247            ],
248            modules: &[(
249                "ext:async/main.js",
250                "import {wait, ready, reference} from 'ext:core/ops'; globalThis.testWait = wait; globalThis.testReady = ready; globalThis.testReference = reference;",
251            )],
252            entry_point: Some("ext:async/main.js"),
253        }],
254        ..Default::default()
255    })
256    .unwrap();
257    runtime
258        .execute_script::<()>(
259            "unreferenced.js",
260            r#"
261            globalThis.waitResult = undefined;
262            globalThis.readyResult = undefined;
263            testWait().then(value => waitResult = value);
264            testReady().then(value => readyResult = value);
265            "#,
266        )
267        .unwrap();
268    runtime.run_event_loop().unwrap();
269    assert_eq!(
270        runtime
271            .execute_script::<u32>("ready.js", "readyResult")
272            .unwrap(),
273        7
274    );
275    assert_eq!(
276        runtime
277            .execute_script::<Option<u32>>("wait.js", "waitResult")
278            .unwrap(),
279        None
280    );
281    wait_sender.send(11).unwrap();
282    runtime.run_event_loop().unwrap();
283    assert_eq!(
284        runtime
285            .execute_script::<u32>("wait.js", "waitResult")
286            .unwrap(),
287        11
288    );
289
290    runtime
291        .execute_script::<()>(
292            "referenced.js",
293            r#"
294            globalThis.order = [];
295            testReady().then(() => order.push('unreferenced'));
296            testReference().then(() => order.push('referenced'));
297            "#,
298        )
299        .unwrap();
300    let sender = std::thread::spawn(move || {
301        std::thread::sleep(std::time::Duration::from_millis(10));
302        reference_sender.send(13).unwrap();
303    });
304    runtime.run_event_loop().unwrap();
305    sender.join().unwrap();
306    assert_eq!(
307        runtime
308            .execute_script::<Vec<String>>("order.js", "order")
309            .unwrap(),
310        ["unreferenced", "referenced"]
311    );
312}
313
314#[test]
315fn module_failures_do_not_contaminate_later_turns() {
316    struct Loader;
317    impl avril_core::ModuleLoader for Loader {
318        fn resolve(&self, specifier: &str, _: &str) -> Result<String, Error> {
319            Ok(specifier.into())
320        }
321        fn load(&self, name: &str) -> Result<ModuleSource, Error> {
322            Ok(match name {
323                "pending" => "await new Promise(() => {});",
324                "rejected" => "throw new Error('module failure');",
325                _ => "export {};",
326            }
327            .into())
328        }
329    }
330    let mut runtime = Runtime::new(RuntimeOptions {
331        module_loader: Some(std::rc::Rc::new(Loader)),
332        ..Default::default()
333    })
334    .unwrap();
335    assert!(
336        runtime
337            .execute_script::<()>("bad.js", "Promise.reject('script failure');")
338            .is_err()
339    );
340    runtime.execute_module("good").unwrap();
341    for (name, message) in [
342        ("pending", "no runnable host work"),
343        ("rejected", "module failure"),
344    ] {
345        assert!(
346            runtime
347                .execute_module(name)
348                .unwrap_err()
349                .to_string()
350                .contains(message)
351        );
352        runtime.run_event_loop().unwrap();
353    }
354}
355
356#[test]
357fn stack_formatting_preserves_native_headers_and_getter_guards() {
358    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
359    let stacks: Vec<String> = runtime
360        .execute_script(
361            "stacks.js",
362            r#"
363        const original = new Error("original");
364        original.message = Symbol("changed");
365        original.name = "Renamed";
366        const hostile = new Error("hidden");
367        Object.defineProperty(hostile, "name", {
368            get() { throw new Error("bad name"); }
369        });
370        const nested = new Error("hidden");
371        Object.defineProperty(nested, "name", { get() { throw null; } });
372        const plain = { name: "Captured", message: "plain" };
373        Error.captureStackTrace(plain);
374        [original.stack, hostile.stack, nested.stack, plain.stack];
375    "#,
376        )
377        .unwrap();
378    for (stack, header) in stacks.iter().zip([
379        "Renamed: original",
380        "<error: Error: bad name>",
381        "<error>",
382        "Captured: plain",
383    ]) {
384        assert!(
385            stack.starts_with(&format!("{header}\n    at stacks.js:")),
386            "{stack}"
387        );
388    }
389    let error = runtime
390        .execute_script::<()>(
391            "hostile.js",
392            r#"
393        const thrown = new Error("original");
394        Object.defineProperty(thrown, "name", { get() { throw new Error("bad name"); } });
395        throw thrown;
396    "#,
397        )
398        .unwrap_err()
399        .to_string();
400    assert!(
401        error.contains("<error: Error: bad name>\n    at hostile.js:"),
402        "{error}"
403    );
404    assert_eq!(runtime.execute_script::<u32>("ok.js", "6 * 7").unwrap(), 42);
405}
406
407#[test]
408fn stack_formatting_uses_current_message() {
409    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
410    let stacks: Vec<String> = runtime
411        .execute_script(
412            "current.js",
413            r#"
414        const changed = new Error("original");
415        changed.message = "changed";
416        const lazy = new Error("creation");
417        Object.defineProperty(lazy, "message", {
418            get() { return "lazy-now"; },
419            configurable: true,
420        });
421        const multiline = new Error("original");
422        multiline.message = "line1\nline2";
423        function absent() {}
424        const frameless = new Error("with frames");
425        frameless.message = "line1\nline2";
426        Error.captureStackTrace(frameless, absent);
427        const plain = { name: "Shaped", message: "plain" };
428        Object.defineProperty(plain, "message", {
429            get() { return "lazy-plain"; },
430            configurable: true,
431        });
432        Error.captureStackTrace(plain, absent);
433        const hostile = new Error("preserved");
434        Object.defineProperty(hostile, "message", {
435            get() { throw new Error("bad message"); },
436        });
437        [changed.stack, lazy.stack, multiline.stack, frameless.stack, plain.stack, hostile.stack];
438    "#,
439        )
440        .unwrap();
441    assert!(
442        stacks[0].starts_with("Error: changed\n    at current.js:"),
443        "{}",
444        stacks[0]
445    );
446    assert!(
447        stacks[1].starts_with("Error: lazy-now\n    at current.js:"),
448        "{}",
449        stacks[1]
450    );
451    assert!(
452        stacks[2].starts_with("Error: line1\nline2\n    at current.js:"),
453        "{}",
454        stacks[2]
455    );
456    assert_eq!(stacks[3], "Error: line1\nline2", "{}", stacks[3]);
457    assert_eq!(stacks[4], "Shaped: lazy-plain", "{}", stacks[4]);
458    assert!(
459        stacks[5].starts_with("Error: preserved\n    at current.js:"),
460        "{}",
461        stacks[5]
462    );
463}
464
465#[test]
466fn custom_stack_hooks_preserve_v8_behavior() {
467    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
468    let result: Value = runtime.execute_script("custom.js", r#"
469        const result = { unset: Error.prepareStackTrace === undefined };
470        const custom = function(error, frames) {
471            result.receiver = this === Error;
472            result.message = error.message;
473            return frames;
474        };
475        Error.prepareStackTrace = custom;
476        result.identity = Error.prepareStackTrace === custom;
477        result.frames = Array.isArray(new Error("custom").stack);
478        const retry = new Error("retry");
479        Error.prepareStackTrace = () => { throw "formatter threw"; };
480        try { retry.stack; } catch (error) { result.thrown = error; }
481        Error.prepareStackTrace = () => "retried";
482        result.retry = retry.stack;
483        Object.defineProperty(Error, "prepareStackTrace", {
484            get() { throw "getter threw"; }, configurable: true,
485        });
486        const getterRetry = new Error("getter retry");
487        try { getterRetry.stack; } catch (error) { result.getter = error; }
488        delete Error.prepareStackTrace;
489        result.getterRetry = getterRetry.stack.startsWith("Error: getter retry\n    at custom.js:");
490        result.restored = new Error("restored").stack.startsWith("Error: restored\n    at custom.js:");
491        result;
492    "#).unwrap();
493    assert_eq!(
494        result,
495        json!({
496            "unset": true, "receiver": true, "message": "custom", "identity": true,
497            "frames": true, "thrown": "formatter threw", "retry": "retried",
498            "getter": "getter threw", "getterRetry": true, "restored": true,
499        })
500    );
501}
502
503#[test]
504fn dynamic_imports_wait_for_static_async_dependencies_and_share_evaluation() {
505    struct Loader;
506    impl avril_core::ModuleLoader for Loader {
507        fn resolve(&self, specifier: &str, _: &str) -> Result<String, Error> {
508            Ok(specifier.into())
509        }
510        fn module_url(&self, name: &str) -> Option<String> {
511            Some(format!("test:{name}"))
512        }
513        fn load(&self, name: &str) -> Result<ModuleSource, Error> {
514            Ok(match name {
515                "entry" => {
516                    r#"
517                    import 'waiting'; import 'observer';
518                    globalThis.entryRuns = (globalThis.entryRuns ?? 0) + 1;
519                    globalThis.entryMeta = [import.meta.main, import.meta.url];
520                "#
521                }
522                "waiting" | "later" => {
523                    r#"
524                    export const value = await testReady();
525                    export const main = import.meta.main;
526                "#
527                }
528                "observer" => {
529                    r#"
530                    const [a, b] = await Promise.all([import('waiting'), import('waiting')]);
531                    globalThis.observed = [a === b, a.value, a.main];
532                "#
533                }
534                _ => "globalThis.otherMain = import.meta.main;",
535            }
536            .into())
537        }
538    }
539    let calls = std::rc::Rc::new(std::cell::Cell::new(0));
540    let counter = calls.clone();
541    let mut runtime = Runtime::new(RuntimeOptions {
542        module_loader: Some(std::rc::Rc::new(Loader)),
543        extensions: vec![Extension {
544            name: "ext:ready",
545            dependencies: &[],
546            modules: &[(
547                "ext:ready/main.js",
548                "import {ready} from 'ext:core/ops'; globalThis.testReady = ready;",
549            )],
550            entry_point: Some("ext:ready/main.js"),
551            ops: vec![Op::asynchronous("ready", move |_: ()| {
552                counter.set(counter.get() + 1);
553                async { Ok::<_, io::Error>(42) }
554            })],
555        }],
556    })
557    .unwrap();
558    runtime.execute_module("entry").unwrap();
559    runtime.execute_module("entry").unwrap();
560    runtime.execute_module("other").unwrap();
561    let state: Value = runtime
562        .execute_script(
563            "check.js",
564            r#"
565        ({ observed, entryMeta, entryRuns, otherMain })
566    "#,
567        )
568        .unwrap();
569    assert_eq!(
570        state,
571        json!({
572            "observed": [true, 42, false], "entryMeta": [true, "test:entry"],
573            "entryRuns": 1, "otherMain": false,
574        })
575    );
576    assert_eq!(calls.get(), 1);
577
578    let result: Value = runtime
579        .execute_script(
580            "launch.js",
581            r#"
582        globalThis.launched = null;
583        import('later').then(ns => { globalThis.launched = ns.value; });
584        launched;
585    "#,
586        )
587        .unwrap();
588    assert_eq!(result, Value::Null);
589    assert_eq!(
590        runtime
591            .execute_script::<Value>("check.js", "launched")
592            .unwrap(),
593        Value::Null
594    );
595    runtime.run_event_loop().unwrap();
596    assert_eq!(calls.get(), 2);
597    assert_eq!(
598        runtime
599            .execute_script::<u32>("check.js", "launched")
600            .unwrap(),
601        42
602    );
603}
604
605#[test]
606fn registered_modules_capture_exports_without_a_loader() {
607    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
608    runtime
609        .execute_script::<()>(
610            "bootstrap",
611            "globalThis.values = { answer: 42, object: {} };",
612        )
613        .unwrap();
614    runtime
615        .register_module("test:values", |scope| {
616            let key = avril_core::v8::String::new(scope, "values")?;
617            let global = scope.get_current_context().global(scope);
618            avril_core::v8::Local::<avril_core::v8::Object>::try_from(
619                global.get(scope, key.into())?,
620            )
621            .ok()
622        })
623        .unwrap();
624    assert!(
625        runtime
626            .register_module("test:values", |scope| Some(avril_core::v8::Object::new(
627                scope
628            )))
629            .unwrap_err()
630            .to_string()
631            .contains("Module already registered")
632    );
633    runtime.execute_script::<()>("modify", "globalThis.original = values.object; values.answer = 0; values.object = {}; globalThis.values = null;").unwrap();
634    runtime.execute_module("test:values").unwrap();
635    runtime.execute_script::<()>("import", "import('test:values').then(ns => { globalThis.answer = ns.answer; globalThis.same = ns.object === original; globalThis.namespace = ns; });").unwrap();
636    runtime.run_event_loop().unwrap();
637    assert_eq!(
638        runtime.execute_script::<u32>("answer", "answer").unwrap(),
639        42
640    );
641    assert!(runtime.execute_script::<bool>("identity", "same").unwrap());
642    runtime
643        .execute_script::<()>(
644            "reimport",
645            "import('test:values').then(ns => { globalThis.cached = ns === namespace; });",
646        )
647        .unwrap();
648    runtime.run_event_loop().unwrap();
649    assert!(runtime.execute_script::<bool>("cache", "cached").unwrap());
650    assert!(
651        runtime
652            .execute_module("missing")
653            .unwrap_err()
654            .to_string()
655            .contains("No module loader configured")
656    );
657}
658
659#[test]
660fn event_loop_runs_finalization_cleanup_tasks() {
661    let mut runtime = Runtime::new(RuntimeOptions {
662        extensions: vec![Extension {
663            name: "ext:gc-test",
664            dependencies: &[],
665            modules: &[(
666                "ext:gc-test/main.js",
667                "import {collect} from 'ext:core/ops'; globalThis.collect = collect;",
668            )],
669            entry_point: Some("ext:gc-test/main.js"),
670            ops: vec![Op::with_scope("collect", |scope, (): ()| {
671                scope.low_memory_notification();
672                Ok::<_, io::Error>(())
673            })],
674        }],
675        ..Default::default()
676    })
677    .unwrap();
678    runtime
679        .execute_script::<()>(
680            "garbage",
681            r#"
682        globalThis.cleaned = 0;
683        globalThis.registry = new FinalizationRegistry(() => cleaned++);
684        (()=>{for(let i=0;i<100;i++)registry.register({},i);})();
685        collect();
686        collect();
687    "#,
688        )
689        .unwrap();
690    runtime.run_event_loop().unwrap();
691    assert!(
692        runtime
693            .execute_script::<bool>("cleaned", "cleaned > 0")
694            .unwrap()
695    );
696}
697
698#[test]
699fn custom_stack_hooks_do_not_expose_private_formatter_functions() {
700    let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
701    assert!(runtime.execute_script::<bool>("hooks.js",r#"
702        let nested;
703        const hook=()=>{nested=new Error('inside hook');return 'custom';};
704        Error.prepareStackTrace=hook;
705        new Error('trigger').stack;
706        Error.prepareStackTrace=(_,frames)=>frames;
707        try {
708            const frames=nested.stack;
709            if (!frames.length) throw Error('Missing frames');
710            for (const frame of frames) {
711                const fn=frame.getFunction();
712                if (fn !== undefined && fn !== hook) throw Error('Private formatter function exposed');
713            }
714        } finally {delete Error.prepareStackTrace;}
715        true;
716    "#).unwrap());
717}