use std::cell::RefCell; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use avril_caps::{Capabilities, Initial}; use avril_core::{Error, Extension, Op, Runtime, RuntimeOptions}; use avril_host::environment::Environment; use avril_runtime::authority::SourceAuthority; use avril_runtime::modules::ModuleGraph; use avril_runtime::registry::{Options, PackageStore}; use super::{Control, Settings, WorkerScope}; use avril_ext_process::SignalMode; use avril_ext_stdio::Stdio; use avril_host::Host; #[path = "../../../core/tests/support/mod.rs"] mod support; fn settings(root: &Path, host: &Host) -> Settings { let store = PackageStore::new( host.handle(), Options { jsr_registry: "https://jsr.invalid/".parse().unwrap(), npm_registry: "https://npm.invalid/".parse().unwrap(), cache: root.join("cache"), lock: root.join("avril.lock"), project_root: root.to_owned(), offline: true, frozen: false, update: false, ephemeral_lock: true, vendor: None, }, ) .unwrap(); Settings { snapshot: None, storage: avril_webstorage::Persistent::new( root.join("storage"), &root.join("avril.conf"), "main.js", ), caps: Capabilities::new(Initial::new(false), root.to_owned()), environment: Environment::default(), args: vec![], store: Arc::new(store), runtime: host.handle(), broker: Arc::default(), process_signal: SignalMode::Run, } } #[test] fn static_bootstrap_never_calls_live_ops_in_main_or_worker_runtimes() { let directory = tempfile::tempdir().unwrap(); let host = Host::new().unwrap(); for worker in [false, true] { let settings = settings(directory.path(), &host); let graph = Rc::new(ModuleGraph::open_shared( SourceAuthority::execute(settings.caps.clone()), settings.store.clone(), )); let scope = worker.then(|| WorkerScope { control: Arc::new(Control::default()), inbox: settings.broker.pair().unwrap().1, url: "https://worker.invalid/entry.js?fresh#scope".into(), name: "bootstrap worker".into(), }); let mut extensions = super::extensions(graph, settings, Stdio::new().unwrap(), scope, None, None).unwrap(); let calls = Rc::new(RefCell::new(Vec::new())); for extension in &mut extensions { for op in &mut extension.ops { // These depend only on the target and codec tables, never on process state. if matches!(op.name, "op_path_platform" | "op_encoding_label") { continue; } let name = op.name; let calls = calls.clone(); *op = Op::try_raw(name, move |_, _| { calls.borrow_mut().push(name); Err(std::io::Error::other(format!( "Live op during static bootstrap: {name}" ))) }); } } extensions.push(Extension { name: "ext:audit", dependencies: &[], ops: vec![], modules: &[( "ext:audit/main.js", r#" import 'ext:path/main.js'; import 'ext:path/posix.js'; import 'ext:path/win32.js'; import 'ext:base/sync.js'; import 'ext:file/fs.js'; import 'ext:file/fs_sync.js'; import 'ext:test/main.js'; for (const name of ['Worker', 'URL', 'ReadableStream', 'TextDecoder', 'Performance']) { if (typeof globalThis[name] !== 'function') throw Error('Missing static interface: ' + name); } throw Error('static bootstrap complete'); "#, )], entry_point: Some("ext:audit/main.js"), }); let result = Runtime::new(RuntimeOptions { extensions, ..Default::default() }); assert!( calls.borrow().is_empty(), "worker={worker}: {:?}", calls.borrow() ); assert!( matches!(result, Err(Error::JavaScript(message)) if message.contains("static bootstrap complete")) ); } } #[test] fn deferred_public_entries_are_loaded_by_registration_not_static_bootstrap() { let directory = tempfile::tempdir().unwrap(); let host = Host::new().unwrap(); for entry in ["ext:base/sync.js", "ext:file/fs.js", "ext:file/fs_sync.js"] { let settings = settings(directory.path(), &host); let graph = Rc::new(ModuleGraph::open_shared( SourceAuthority::execute(settings.caps.clone()), settings.store.clone(), )); let mut extensions = super::extensions(graph, settings, Stdio::new().unwrap(), None, None, None).unwrap(); let extension = extensions .iter_mut() .find(|extension| extension.modules.iter().any(|(name, _)| *name == entry)) .unwrap(); extension.modules = extension .modules .iter() .map(|&(name, source)| { if name == entry { (name, "throw Error('registered public entry evaluated');") } else { (name, source) } }) .collect::>() .leak(); let mut runtime = Runtime::new(RuntimeOptions { extensions, ..Default::default() }) .expect("Deferred public entries must not be static entry points"); assert!( matches!(crate::stdlib::register(&mut runtime), Err(Error::JavaScript(message)) if message.contains("registered public entry evaluated")), "{entry} was not evaluated by public registration" ); } } #[test] fn live_metadata_is_initialized_per_runtime_before_public_exports_are_registered() { let directory = tempfile::tempdir().unwrap(); let host = Host::new().unwrap(); for label in ["first", "second"] { let mut settings = settings(directory.path(), &host); settings.args = vec![label.into(), "argument with spaces".into()]; settings.environment = Environment::new([("BOOTSTRAP_VALUE".into(), label.into())]).unwrap(); let graph = Rc::new(ModuleGraph::open_shared( SourceAuthority::execute(settings.caps.clone()), settings.store.clone(), )); let mut runtime = super::runtime(graph, settings, Stdio::new().unwrap(), None, None).unwrap(); runtime.execute_script::<()>("metadata.js", r#" import('avril:std').then(({process, runtime, env}) => { if (!Object.isFrozen(process) || !Object.isFrozen(process.args) || !Object.isFrozen(runtime)) { throw Error('Mutable metadata'); } const pid = Object.getOwnPropertyDescriptor(process, 'pid'); if (pid.get !== undefined || pid.writable || pid.configurable || !pid.enumerable) { throw Error('Changed metadata descriptor'); } if (!(performance.timeOrigin > 0) || !(performance.now() >= 0)) throw Error('Missing clock'); if (!(navigator.hardwareConcurrency >= 1) || !navigator.userAgent.startsWith('avril/')) { throw Error('Missing navigator metadata'); } if (!Object.isFrozen(navigator.userAgentData.brands)) throw Error('Mutable brands'); globalThis.metadata = [process.pid, process.args, env.get('BOOTSTRAP_VALUE')]; env.set('BOOTSTRAP_VALUE', 'changed'); navigator.userAgent = 'changed'; }); "#).unwrap(); runtime.run_event_loop().unwrap(); assert_eq!( runtime .execute_script::("result.js", "metadata") .unwrap(), serde_json::json!([std::process::id(), [label, "argument with spaces"], label]), ); } } #[test] fn snapshots_restore_main_and_worker_runtime_state() { if let Some(role) = support::role() { let root = support::directory(); if role.starts_with("create-") { let snapshot = super::create_snapshot().unwrap(); std::fs::write(root.join(format!("{role}.snapshot")), snapshot.as_bytes()).unwrap(); } else { let host = Host::new().unwrap(); let snapshot = (role == "restore").then(|| { // SAFETY: Our parent produced this artifact with this same executable. Arc::new(unsafe { avril_core::Snapshot::from_bytes( std::fs::read(root.join("create-first.snapshot")).unwrap(), ) .unwrap() }) }); for worker in [false, true] { for label in ["first", "second"] { let mut settings = settings(&root, &host); settings.snapshot = snapshot.clone(); settings.args = vec![label.into()]; settings.environment = Environment::new([("SNAPSHOT_VALUE".into(), label.into())]).unwrap(); let graph = Rc::new(ModuleGraph::open_shared( SourceAuthority::execute(settings.caps.clone()), settings.store.clone(), )); let scope = worker.then(|| WorkerScope { control: Arc::new(Control::default()), inbox: settings.broker.pair().unwrap().1, url: format!("https://worker.invalid/{label}.js"), name: label.into(), }); let mut runtime = super::create_runtime( graph, settings, Stdio::new().unwrap(), scope, None, None, ) .unwrap(); let surface = runtime .execute_script::>( "surface.js", include_str!("snapshot_surface.js"), ) .unwrap(); std::fs::write( root.join(format!("surface-{role}-{worker}-{label}.json")), serde_json::to_vec_pretty(&surface).unwrap(), ) .unwrap(); runtime .execute_script::<()>("snapshot-test.js", include_str!("snapshot_test.js")) .unwrap(); runtime.run_event_loop().unwrap(); assert_eq!( runtime .execute_script::("metadata", "metadata") .unwrap(), serde_json::json!([ std::process::id(), label, label, if worker { label } else { "" }, if worker { format!("https://worker.invalid/{label}.js") } else { String::new() }, !worker ]), ); assert!( runtime .execute_script::("complete", "snapshotTestComplete === true") .unwrap() ); } } } } else { let directory = tempfile::tempdir().unwrap(); for role in ["create-first", "create-second", "cold", "restore"] { support::child( "assembly::tests::snapshots_restore_main_and_worker_runtime_state", role, directory.path(), ); } assert!( std::fs::read(directory.path().join("create-first.snapshot")).unwrap() == std::fs::read(directory.path().join("create-second.snapshot")).unwrap(), "Production snapshot generation must be reproducible across processes", ); for worker in [false, true] { for label in ["first", "second"] { let surface = |role| { std::fs::read_to_string( directory .path() .join(format!("surface-{role}-{worker}-{label}.json")), ) .unwrap() }; assert_eq!( surface("cold"), surface("restore"), "worker={worker}, label={label}" ); } } } }