char/avril

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

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

Charlotte Somsnapshot: capture and restore extension layouts in one artifactd71572b

main
11.1 KiB320 linesraw
1mod support;
2
3use std::cell::{Cell, RefCell};
4use std::io;
5use std::rc::Rc;
6
7use avril_core::{
8    Error, Extension, ModuleLoader, ModuleSource, Op, Runtime, RuntimeOptions, Snapshot,
9    SnapshotError, WasmStreamingRegistry, v8,
10};
11
12struct Loader(u32);
13
14impl ModuleLoader for Loader {
15    fn resolve(&self, specifier: &str, _: &str) -> Result<String, Error> {
16        Ok(if specifier == "private-alias" {
17            "ext:test/state.js"
18        } else {
19            specifier
20        }
21        .into())
22    }
23
24    fn load(&self, name: &str) -> Result<ModuleSource, Error> {
25        match name {
26            "guest:main" => Ok(include_str!("snapshot/guest.js").into()),
27            "guest:dep" => Ok(format!(
28                "export const seed = {}; export const url = import.meta.url;",
29                self.0
30            )
31            .into()),
32            _ => Err(Error::Module(format!("Missing {name}"))),
33        }
34    }
35
36    fn module_url(&self, name: &str) -> Option<String> {
37        Some(format!("file:///snapshot-{}/{name}.js", self.0))
38    }
39
40    fn map_source_location(
41        &self,
42        name: &str,
43        line: u32,
44        column: u32,
45    ) -> Option<(String, u32, u32)> {
46        (name == "guest:stack").then(|| (format!("fresh-{}.ts", self.0), line + 10, column + 2))
47    }
48}
49
50fn options(seed: u32, allowed: Rc<Cell<bool>>) -> RuntimeOptions {
51    let mut ops = vec![
52        Op::new("staticOnly", move |(): ()| {
53            assert_eq!(seed, 0, "Static entry reexecuted during restoration");
54            Ok::<_, io::Error>(())
55        }),
56        Op::new("nativeSeed", move |(): ()| {
57            assert_ne!(seed, 0, "Live initialization ran during snapshot creation");
58            if !allowed.get() {
59                return Err(io::Error::other("native access denied"));
60            }
61            Ok::<_, io::Error>(seed)
62        }),
63        Op::with_scope("collect", |scope, (): ()| {
64            scope.low_memory_notification();
65            Ok::<_, io::Error>(())
66        }),
67        Op::asynchronous("asyncSeed", move |(): ()| async move {
68            Ok::<_, io::Error>(seed)
69        }),
70        Op::try_raw("registerWasm", |scope, args| {
71            let callback =
72                v8::Local::<v8::Function>::try_from(args.get(0)).map_err(io::Error::other)?;
73            scope
74                .get_slot::<Rc<RefCell<WasmStreamingRegistry>>>()
75                .unwrap()
76                .borrow_mut()
77                .register(v8::Global::new(scope, callback))?;
78            Ok::<_, io::Error>(v8::undefined(scope).into())
79        }),
80        Op::with_scope("finishWasm", |scope, id: u64| {
81            let registry = scope
82                .get_slot::<Rc<RefCell<WasmStreamingRegistry>>>()
83                .unwrap();
84            let mut registry = registry.borrow_mut();
85            registry
86                .stream_mut(id)?
87                .on_bytes_received(b"\0asm\x01\0\0\0");
88            registry.finish(id)
89        }),
90    ];
91    if seed != 0 {
92        ops.reverse();
93    }
94    RuntimeOptions {
95        extensions: vec![Extension {
96            name: "ext:test",
97            dependencies: &[],
98            ops,
99            modules: &[
100                ("ext:test/main.js", include_str!("snapshot/main.js")),
101                (
102                    "ext:test/state.js",
103                    "export const state = {seed: undefined};",
104                ),
105                ("ext:test/lazy.js", include_str!("snapshot/lazy.js")),
106            ],
107            entry_point: Some("ext:test/main.js"),
108        }],
109        module_loader: Some(Rc::new(Loader(seed))),
110    }
111}
112
113#[test]
114fn core_snapshot_cross_process() {
115    let path = || support::directory().join("core.snapshot");
116    match support::role().as_deref() {
117        Some("create") => {
118            let snapshot =
119                Runtime::create_snapshot(vec![options(0, Rc::new(Cell::new(true)))]).unwrap();
120            std::fs::write(path(), snapshot.as_bytes()).unwrap();
121        }
122        Some("restore") => {
123            let bytes = std::fs::read(path()).unwrap();
124            // SAFETY: The sibling create role produced this artifact with this executable.
125            let snapshot = unsafe { Snapshot::from_bytes(bytes) }.unwrap();
126            for seed in [10, 100] {
127                let allowed = Rc::new(Cell::new(true));
128                let mut runtime =
129                    Runtime::from_snapshot(options(seed, allowed.clone()), &snapshot).unwrap();
130                runtime
131                    .register_extension_module("test:main", "ext:test/main.js")
132                    .unwrap();
133                runtime
134                    .register_extension_module("test:lazy", "ext:test/lazy.js")
135                    .unwrap();
136                runtime.execute_module("guest:main").unwrap();
137                runtime
138                    .execute_script::<()>("report events", "afterTasks()")
139                    .unwrap();
140                assert!(
141                    runtime
142                        .execute_script::<bool>("complete", "globalThis.complete === true")
143                        .unwrap()
144                );
145                assert_eq!(
146                    runtime
147                        .execute_script::<u32>("seed", "snapshotState.seed")
148                        .unwrap(),
149                    seed
150                );
151                assert_eq!(
152                    runtime
153                        .execute_script::<Vec<String>>("events", "events")
154                        .unwrap(),
155                    [
156                        "static",
157                        "live",
158                        "lazy",
159                        "task",
160                        "finish",
161                        "microtask",
162                        "rejection"
163                    ]
164                );
165                let stack: String = runtime
166                    .execute_script("guest:stack", "new Error('restored stack').stack")
167                    .unwrap();
168                assert!(stack.contains(&format!("fresh-{seed}.ts:11:")), "{stack}");
169                runtime
170                    .execute_script::<()>("handled", "rejected.catch(() => {});")
171                    .unwrap();
172                assert!(
173                    runtime
174                        .execute_script::<bool>("handled event", "events.at(-1) === 'handled'")
175                        .unwrap()
176                );
177                allowed.set(false);
178                assert!(
179                    matches!(runtime.execute_script::<u32>("denied", "readNative()"), Err(Error::JavaScript(message)) if message.contains("native access denied"))
180                );
181            }
182        }
183        _ => support::roles("core_snapshot_cross_process", &["create", "restore"]),
184    }
185}
186
187#[test]
188fn snapshot_layout_mismatches_are_rejected_before_live_initialization() {
189    if support::role().is_none() {
190        return support::roles(
191            "snapshot_layout_mismatches_are_rejected_before_live_initialization",
192            &["run"],
193        );
194    }
195    let snapshot = Runtime::create_snapshot(vec![options(0, Rc::new(Cell::new(true)))]).unwrap();
196    for change in 0..3 {
197        let mut options = options(10, Rc::new(Cell::new(false)));
198        match change {
199            0 => options.extensions[0].ops[0].name = "renamed",
200            1 => {
201                options.extensions[0].modules =
202                    &[("ext:test/main.js", "throw Error('changed source');")]
203            }
204            2 => options.extensions[0].entry_point = None,
205            _ => unreachable!(),
206        }
207        assert!(matches!(
208            Runtime::from_snapshot(options, &snapshot),
209            Err(Error::Snapshot(SnapshotError::IncompatibleLayout))
210        ));
211    }
212}
213
214#[test]
215fn restored_live_failure_does_not_run_later_initializers() {
216    if support::role().is_none() {
217        return support::roles(
218            "restored_live_failure_does_not_run_later_initializers",
219            &["run"],
220        );
221    }
222    let called = Rc::new(Cell::new(false));
223    let options = || RuntimeOptions {
224        extensions: vec![Extension {
225            name: "ext:test",
226            dependencies: &[],
227            ops: vec![
228                Op::new("fail", |(): ()| {
229                    Err::<(), _>(io::Error::other("failed live setup"))
230                }),
231                Op::new("later", {
232                    let called = called.clone();
233                    move |(): ()| {
234                        called.set(true);
235                        Ok::<_, io::Error>(())
236                    }
237                }),
238            ],
239            modules: &[(
240                "ext:test/main.js",
241                r#"
242                import {onRuntimeStart} from 'ext:core/bootstrap';
243                import {fail, later} from 'ext:core/ops';
244                onRuntimeStart(() => { fail(); });
245                onRuntimeStart(() => { later(); });
246            "#,
247            )],
248            entry_point: Some("ext:test/main.js"),
249        }],
250        ..Default::default()
251    };
252    let snapshot = Runtime::create_snapshot(vec![options()]).unwrap();
253    assert!(matches!(
254        Runtime::from_snapshot(options(), &snapshot),
255        Err(Error::JavaScript(message)) if message.contains("failed live setup")
256    ));
257    assert!(!called.get());
258}
259
260struct DropProbe(Rc<Cell<bool>>);
261
262impl Drop for DropProbe {
263    fn drop(&mut self) {
264        self.0.set(true);
265    }
266}
267
268#[test]
269fn abandoned_snapshot_builders_release_native_state_and_return_errors() {
270    if support::role().is_none() {
271        return support::roles(
272            "abandoned_snapshot_builders_release_native_state_and_return_errors",
273            &["run"],
274        );
275    }
276    let cases: &[&[(&str, &str)]] = &[
277        &[("ext:test/main.js", "throw Error('failed static setup');")],
278        &[(
279            "ext:test/main.js",
280            "import {pending} from 'ext:core/ops'; pending();",
281        )],
282        &[(
283            "ext:test/main.js",
284            "import {setEventHandlers} from 'ext:core/event_hooks'; setEventHandlers(() => true, () => true, () => {});",
285        )],
286    ];
287    for (index, modules) in cases.iter().enumerate() {
288        let dropped = Rc::new(Cell::new(false));
289        let probe = DropProbe(dropped.clone());
290        let result = Runtime::create_snapshot(vec![RuntimeOptions {
291            extensions: vec![Extension {
292                name: "ext:test",
293                dependencies: &[],
294                ops: vec![
295                    Op::new("retain", move |(): ()| {
296                        let _retain = &probe;
297                        Ok::<_, io::Error>(())
298                    }),
299                    Op::asynchronous_unreferenced("pending", |(): ()| {
300                        futures::future::pending::<Result<(), io::Error>>()
301                    }),
302                ],
303                modules,
304                entry_point: Some("ext:test/main.js"),
305            }],
306            ..Default::default()
307        }]);
308        if index == 0 {
309            assert!(
310                matches!(result, Err(Error::JavaScript(message)) if message.contains("failed static setup"))
311            );
312        } else {
313            assert!(matches!(
314                result,
315                Err(Error::Snapshot(SnapshotError::ActiveState(_)))
316            ));
317        }
318        assert!(dropped.get(), "Native op state leaked");
319    }
320}