char/avril

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

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

Charlotte Somunify public module registration and declarations6534c3e

main
13.0 KiB322 linesraw
1use std::cell::RefCell;
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use avril_caps::{Capabilities, Initial};
7use avril_core::{Error, Extension, Op, Runtime, RuntimeOptions};
8use avril_host::environment::Environment;
9use avril_runtime::authority::SourceAuthority;
10use avril_runtime::modules::ModuleGraph;
11use avril_runtime::registry::{Options, PackageStore};
12
13use super::{Control, Settings, WorkerScope};
14use avril_ext_process::SignalMode;
15use avril_ext_stdio::Stdio;
16use avril_host::Host;
17
18#[path = "../../../core/tests/support/mod.rs"]
19mod support;
20
21fn settings(root: &Path, host: &Host) -> Settings {
22    let store = PackageStore::new(
23        host.handle(),
24        Options {
25            jsr_registry: "https://jsr.invalid/".parse().unwrap(),
26            npm_registry: "https://npm.invalid/".parse().unwrap(),
27            cache: root.join("cache"),
28            lock: root.join("avril.lock"),
29            project_root: root.to_owned(),
30            offline: true,
31            frozen: false,
32            update: false,
33            ephemeral_lock: true,
34            vendor: None,
35        },
36    )
37    .unwrap();
38    Settings {
39        snapshot: None,
40        storage: avril_webstorage::Persistent::new(
41            root.join("storage"),
42            &root.join("avril.conf"),
43            "main.js",
44        ),
45        caps: Capabilities::new(Initial::new(false), root.to_owned()),
46        environment: Environment::default(),
47        args: vec![],
48        store: Arc::new(store),
49        runtime: host.handle(),
50        broker: Arc::default(),
51        process_signal: SignalMode::Run,
52    }
53}
54
55#[test]
56fn static_bootstrap_never_calls_live_ops_in_main_or_worker_runtimes() {
57    let directory = tempfile::tempdir().unwrap();
58    let host = Host::new().unwrap();
59    for worker in [false, true] {
60        let settings = settings(directory.path(), &host);
61        let graph = Rc::new(ModuleGraph::open_shared(
62            SourceAuthority::execute(settings.caps.clone()),
63            settings.store.clone(),
64        ));
65        let scope = worker.then(|| WorkerScope {
66            control: Arc::new(Control::default()),
67            inbox: settings.broker.pair().unwrap().1,
68            url: "https://worker.invalid/entry.js?fresh#scope".into(),
69            name: "bootstrap worker".into(),
70        });
71        let mut extensions =
72            super::extensions(graph, settings, Stdio::new().unwrap(), scope, None, None).unwrap();
73        let calls = Rc::new(RefCell::new(Vec::new()));
74        for extension in &mut extensions {
75            for op in &mut extension.ops {
76                // These depend only on the target and codec tables, never on process state.
77                if matches!(op.name, "op_path_platform" | "op_encoding_label") {
78                    continue;
79                }
80                let name = op.name;
81                let calls = calls.clone();
82                *op = Op::try_raw(name, move |_, _| {
83                    calls.borrow_mut().push(name);
84                    Err(std::io::Error::other(format!(
85                        "Live op during static bootstrap: {name}"
86                    )))
87                });
88            }
89        }
90        extensions.push(Extension {
91            name: "ext:audit",
92            dependencies: &[],
93            ops: vec![],
94            modules: &[(
95                "ext:audit/main.js",
96                r#"
97                import 'ext:path/main.js';
98                import 'ext:path/posix.js';
99                import 'ext:path/win32.js';
100                import 'ext:base/sync.js';
101                import 'ext:file/fs.js';
102                import 'ext:file/fs_sync.js';
103                import 'ext:test/main.js';
104                for (const name of ['Worker', 'URL', 'ReadableStream', 'TextDecoder', 'Performance']) {
105                    if (typeof globalThis[name] !== 'function') throw Error('Missing static interface: ' + name);
106                }
107                throw Error('static bootstrap complete');
108            "#,
109            )],
110            entry_point: Some("ext:audit/main.js"),
111        });
112        let result = Runtime::new(RuntimeOptions { extensions, ..Default::default() });
113        assert!(
114            calls.borrow().is_empty(),
115            "worker={worker}: {:?}",
116            calls.borrow()
117        );
118        assert!(
119            matches!(result, Err(Error::JavaScript(message)) if message.contains("static bootstrap complete"))
120        );
121    }
122}
123
124#[test]
125fn deferred_public_entries_are_loaded_by_registration_not_static_bootstrap() {
126    let directory = tempfile::tempdir().unwrap();
127    let host = Host::new().unwrap();
128    for entry in ["ext:base/sync.js", "ext:file/fs.js", "ext:file/fs_sync.js"] {
129        let settings = settings(directory.path(), &host);
130        let graph = Rc::new(ModuleGraph::open_shared(
131            SourceAuthority::execute(settings.caps.clone()),
132            settings.store.clone(),
133        ));
134        let mut extensions =
135            super::extensions(graph, settings, Stdio::new().unwrap(), None, None, None).unwrap();
136        let extension = extensions
137            .iter_mut()
138            .find(|extension| extension.modules.iter().any(|(name, _)| *name == entry))
139            .unwrap();
140        extension.modules = extension
141            .modules
142            .iter()
143            .map(|&(name, source)| {
144                if name == entry {
145                    (name, "throw Error('registered public entry evaluated');")
146                } else {
147                    (name, source)
148                }
149            })
150            .collect::<Vec<_>>()
151            .leak();
152        let mut runtime = Runtime::new(RuntimeOptions { extensions, ..Default::default() })
153            .expect("Deferred public entries must not be static entry points");
154        assert!(
155            matches!(crate::stdlib::register(&mut runtime),
156                Err(Error::JavaScript(message)) if message.contains("registered public entry evaluated")),
157            "{entry} was not evaluated by public registration"
158        );
159    }
160}
161
162#[test]
163fn live_metadata_is_initialized_per_runtime_before_public_exports_are_registered() {
164    let directory = tempfile::tempdir().unwrap();
165    let host = Host::new().unwrap();
166    for label in ["first", "second"] {
167        let mut settings = settings(directory.path(), &host);
168        settings.args = vec![label.into(), "argument with spaces".into()];
169        settings.environment =
170            Environment::new([("BOOTSTRAP_VALUE".into(), label.into())]).unwrap();
171        let graph = Rc::new(ModuleGraph::open_shared(
172            SourceAuthority::execute(settings.caps.clone()),
173            settings.store.clone(),
174        ));
175        let mut runtime =
176            super::runtime(graph, settings, Stdio::new().unwrap(), None, None).unwrap();
177        runtime.execute_script::<()>("metadata.js", r#"
178            import('avril:std').then(({process, runtime, env}) => {
179                if (!Object.isFrozen(process) || !Object.isFrozen(process.args) || !Object.isFrozen(runtime)) {
180                    throw Error('Mutable metadata');
181                }
182                const pid = Object.getOwnPropertyDescriptor(process, 'pid');
183                if (pid.get !== undefined || pid.writable || pid.configurable || !pid.enumerable) {
184                    throw Error('Changed metadata descriptor');
185                }
186                if (!(performance.timeOrigin > 0) || !(performance.now() >= 0)) throw Error('Missing clock');
187                if (!(navigator.hardwareConcurrency >= 1) || !navigator.userAgent.startsWith('avril/')) {
188                    throw Error('Missing navigator metadata');
189                }
190                if (!Object.isFrozen(navigator.userAgentData.brands)) throw Error('Mutable brands');
191                globalThis.metadata = [process.pid, process.args, env.get('BOOTSTRAP_VALUE')];
192                env.set('BOOTSTRAP_VALUE', 'changed');
193                navigator.userAgent = 'changed';
194            });
195        "#).unwrap();
196        runtime.run_event_loop().unwrap();
197        assert_eq!(
198            runtime
199                .execute_script::<serde_json::Value>("result.js", "metadata")
200                .unwrap(),
201            serde_json::json!([std::process::id(), [label, "argument with spaces"], label]),
202        );
203    }
204}
205
206#[test]
207fn snapshots_restore_main_and_worker_runtime_state() {
208    if let Some(role) = support::role() {
209        let root = support::directory();
210        if role.starts_with("create-") {
211            let snapshot = super::create_snapshot().unwrap();
212            std::fs::write(root.join(format!("{role}.snapshot")), snapshot.as_bytes()).unwrap();
213        } else {
214            let host = Host::new().unwrap();
215            let snapshot = (role == "restore").then(|| {
216                // SAFETY: Our parent produced this artifact with this same executable.
217                Arc::new(unsafe {
218                    avril_core::Snapshot::from_bytes(
219                        std::fs::read(root.join("create-first.snapshot")).unwrap(),
220                    )
221                    .unwrap()
222                })
223            });
224            for worker in [false, true] {
225                for label in ["first", "second"] {
226                    let mut settings = settings(&root, &host);
227                    settings.snapshot = snapshot.clone();
228                    settings.args = vec![label.into()];
229                    settings.environment =
230                        Environment::new([("SNAPSHOT_VALUE".into(), label.into())]).unwrap();
231                    let graph = Rc::new(ModuleGraph::open_shared(
232                        SourceAuthority::execute(settings.caps.clone()),
233                        settings.store.clone(),
234                    ));
235                    let scope = worker.then(|| WorkerScope {
236                        control: Arc::new(Control::default()),
237                        inbox: settings.broker.pair().unwrap().1,
238                        url: format!("https://worker.invalid/{label}.js"),
239                        name: label.into(),
240                    });
241                    let mut runtime = super::create_runtime(
242                        graph,
243                        settings,
244                        Stdio::new().unwrap(),
245                        scope,
246                        None,
247                        None,
248                    )
249                    .unwrap();
250                    let surface = runtime
251                        .execute_script::<Vec<String>>(
252                            "surface.js",
253                            include_str!("snapshot_surface.js"),
254                        )
255                        .unwrap();
256                    std::fs::write(
257                        root.join(format!("surface-{role}-{worker}-{label}.json")),
258                        serde_json::to_vec_pretty(&surface).unwrap(),
259                    )
260                    .unwrap();
261                    runtime
262                        .execute_script::<()>("snapshot-test.js", include_str!("snapshot_test.js"))
263                        .unwrap();
264                    runtime.run_event_loop().unwrap();
265                    assert_eq!(
266                        runtime
267                            .execute_script::<serde_json::Value>("metadata", "metadata")
268                            .unwrap(),
269                        serde_json::json!([
270                            std::process::id(),
271                            label,
272                            label,
273                            if worker { label } else { "" },
274                            if worker {
275                                format!("https://worker.invalid/{label}.js")
276                            } else {
277                                String::new()
278                            },
279                            !worker
280                        ]),
281                    );
282                    assert!(
283                        runtime
284                            .execute_script::<bool>("complete", "snapshotTestComplete === true")
285                            .unwrap()
286                    );
287                }
288            }
289        }
290    } else {
291        let directory = tempfile::tempdir().unwrap();
292        for role in ["create-first", "create-second", "cold", "restore"] {
293            support::child(
294                "assembly::tests::snapshots_restore_main_and_worker_runtime_state",
295                role,
296                directory.path(),
297            );
298        }
299        assert!(
300            std::fs::read(directory.path().join("create-first.snapshot")).unwrap()
301                == std::fs::read(directory.path().join("create-second.snapshot")).unwrap(),
302            "Production snapshot generation must be reproducible across processes",
303        );
304        for worker in [false, true] {
305            for label in ["first", "second"] {
306                let surface = |role| {
307                    std::fs::read_to_string(
308                        directory
309                            .path()
310                            .join(format!("surface-{role}-{worker}-{label}.json")),
311                    )
312                    .unwrap()
313                };
314                assert_eq!(
315                    surface("cold"),
316                    surface("restore"),
317                    "worker={worker}, label={label}"
318                );
319            }
320        }
321    }
322}