From d97e78921e37138fa946ccf766c8f229ab973002 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 11 Oct 2026 10:25:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Worker=20=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=20options.name=20=E5=8A=A0=20self.name=20?= =?UTF-8?q?=E4=B8=8E=20self.location?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 38 ++++---- Cargo.toml | 2 +- crates/oxide_vm/src/bindings/bind_worker.rs | 58 ++++++++---- crates/oxide_vm/src/worker/bindings.rs | 71 ++++++++++++--- crates/oxide_vm/src/worker/mod.rs | 98 ++++++++++++++++++--- 5 files changed, 204 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6613da7c..76e5b422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.16" +version = "0.14.17" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.16" +version = "0.14.17" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.16" +version = "0.14.17" dependencies = [ "ansi_term", "bumpalo", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.16" +version = "0.14.17" dependencies = [ "lru", "oxide_bytecode", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1256,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.16" +version = "0.14.17" dependencies = [ "num-bigint", "oxc_allocator", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.16" +version = "0.14.17" dependencies = [ "bumpalo", "dashmap", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.16" +version = "0.14.17" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.16" +version = "0.14.17" dependencies = [ "num-bigint", "num-traits", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.16" +version = "0.14.17" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.16" +version = "0.14.17" dependencies = [ "bumpalo", "dashmap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.16" +version = "0.14.17" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 72944783..0c6dcedb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.16" +version = "0.14.17" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_vm/src/bindings/bind_worker.rs b/crates/oxide_vm/src/bindings/bind_worker.rs index 95069078..05b9e9f1 100644 --- a/crates/oxide_vm/src/bindings/bind_worker.rs +++ b/crates/oxide_vm/src/bindings/bind_worker.rs @@ -9,10 +9,11 @@ use std::sync::Arc; use crate::bind_constructor; -use crate::bindings::{apply_binding_table, bind_accessor_getset, configure_native_constructor}; +use crate::bindings::{apply_binding_table, bind_accessor_getset, bind_accessor_getter, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::string_forge::PermInterner; use oxide_types::value::JsValue; /// 把 Worker 构造器(Box 分配)与 self 面方法绑定到 global。 @@ -222,19 +223,42 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut ); } } - // name / location getter(幂等守卫)。 - // let si_name_global = sf.intern("name").0; - // if sh.lookup_position(global.shape_id(), si_name_global).is_none() { - // bind_accessor_getter(core, session, global, "name", crate::worker::bindings::self_name as *const ()); - // } - // let si_location = sf.intern("location").0; - // if sh.lookup_position(global.shape_id(), si_location).is_none() { - // bind_accessor_getter( - // core, - // session, - // global, - // "location", - // crate::worker::bindings::self_location as *const (), - // ); - // } + // name / location:worker realm 绑 getter(读 thread-local 真值,规范中 + // WorkerGlobalScope.name 为只读访问器);主 realm 绑空串数据属性(与浏览器 + // window.name 同一形状的无害占位,不挡全局 var name 赋值)。幂等守卫。 + let worker_realm = crate::worker::bindings::worker_thread(); + let si_name_global = sf.intern("name").0; + if sh.lookup_position(global.shape_id(), si_name_global).is_none() { + if worker_realm { + bind_accessor_getter(core, session, global, "name", crate::worker::bindings::self_name as *const ()); + } else { + bind_global_empty_string_property(global, sf, sh, si_name_global); + } + } + let si_location = sf.intern("location").0; + if sh.lookup_position(global.shape_id(), si_location).is_none() { + if worker_realm { + bind_accessor_getter( + core, + session, + global, + "location", + crate::worker::bindings::self_location as *const (), + ); + } else { + bind_global_empty_string_property(global, sf, sh, si_location); + } + } +} + +/// 在 global 上绑一个空串数据属性(主 realm 的 name / location 占位)。 +fn bind_global_empty_string_property(global: &mut JsObject, sf: &PermInterner, sh: &ShapeForge, si: u32) { + let shape = sh.make_shape(global.shape_id(), si); + global.set_shape_id(shape); + global + .ensure_hash_props() + .push(JsValue::perm_string(sf.string_ptr(sf.intern("").0))); + let pos = global.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); + global.set_data_meta(pos, PropAttributes::new(true, false, true)); + global.bump_generation(); } diff --git a/crates/oxide_vm/src/worker/bindings.rs b/crates/oxide_vm/src/worker/bindings.rs index 7178708b..c16b199c 100644 --- a/crates/oxide_vm/src/worker/bindings.rs +++ b/crates/oxide_vm/src/worker/bindings.rs @@ -36,6 +36,16 @@ thread_local! { static WORKER_CLOSE_REQUESTED: Cell = const { Cell::new(false) }; } +// worker 名称(构造器 options.name,worker 线程独占,主 realm 不置位)。 +thread_local! { + static WORKER_NAME: RefCell> = const { RefCell::new(None) }; +} + +// worker 位置(构造器 url 字符串,worker 线程独占,主 realm 不置位)。 +thread_local! { + static WORKER_LOCATION: RefCell> = const { RefCell::new(None) }; +} + /// 注入 worker → 主线程输出通道(`worker_event_loop` 建 Vm 后调用)。 /// /// # 副作用 @@ -57,17 +67,34 @@ pub(crate) fn worker_close_requested() -> bool { WORKER_CLOSE_REQUESTED.with(|flag| flag.get()) } -/// `new Worker(url)`:读 url 文件、派生 worker、建 Worker 对象。 +/// 注入 worker 名称与位置(`worker_event_loop` 建 Vm 前调用)。 +/// +/// # 副作用 +/// - 覆盖本线程的 `WORKER_NAME` 与 `WORKER_LOCATION`(worker 线程内恰好一次)。 +/// - 建 Vm 前调用:`bind_worker` 据本线程是否置位区分 worker realm 与主 realm。 +pub(crate) fn set_worker_identity(name: String, location: String) { + WORKER_NAME.with(|slot| *slot.borrow_mut() = Some(name)); + WORKER_LOCATION.with(|slot| *slot.borrow_mut() = Some(location)); +} + +/// 查询当前线程是否 worker 线程(绑定层区分 worker realm 与主 realm)。 +pub(crate) fn worker_thread() -> bool { + WORKER_NAME.with(|slot| slot.borrow().is_some()) +} + +/// `new Worker(url, options)`:读 url 文件与 options.name、派生 worker、建 Worker 对象。 /// /// # 步骤 /// 1. 构造形态校验(HTML 规范只有 `[[Construct]]`,非构造调用抛 TypeError)。 /// 2. 读 `args[1]` 为 url 字符串(非字符串抛 TypeError)。 -/// 3. 主线程侧读 url 文件(`std::fs::read_to_string`),失败抛 TypeError。 -/// 4. `vm.spawn_worker(source)` 派生 worker,得 worker 编号。 -/// 5. 建 Worker 对象:PLAIN 对象(`[[Prototype]]` → Worker.prototype)加非枚举 +/// 3. 读 `args[2]` 为 options 对象(缺省 undefined);是对象时经 `ordinary_get` +/// 读 `name` 属性(getter 副作用与异常原值传播),是字符串取之、非字符串取空串。 +/// 4. 主线程侧读 url 文件(`std::fs::read_to_string`),失败抛 TypeError。 +/// 5. `vm.spawn_worker(source, name, url)` 派生 worker,得 worker 编号。 +/// 6. 建 Worker 对象:PLAIN 对象(`[[Prototype]]` → Worker.prototype)加非枚举 /// `workerId` 数据属性(值为 worker 编号)。`onmessage`/`onerror`/ /// `onmessageerror` 不在对象上建自有属性(改原型访问器属性)。无 native 盒、无 GC 家族。 -/// 6. 返回 Worker 对象。 +/// 7. 返回 Worker 对象。 /// /// # 边界与前提 /// - `url` 是 worker 脚本的文件路径(主线程侧读取)。 @@ -89,6 +116,24 @@ pub(crate) fn worker_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { None => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, "Worker url must be a string")), }; + // 读 options 对象(args[2],缺省 undefined)。 + let options_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + // name:options 是对象时经 ordinary_get 读 name 属性(getter 副作用与异常 + // 原值传播),是字符串取之,非字符串或缺省取空串。 + let name = if options_val.is_object() { + let opts_ptr = options_val.as_js_object_ptr(); + // SAFETY: opts_ptr 是 options 对象,native 调用期间存活。 + let opts_obj = unsafe { &*opts_ptr }; + let si_name = vm.perm_intern("name"); + let name_val = match vm.ordinary_get(opts_obj, si_name, options_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, &e)), + }; + vm.lookup_str(name_val).unwrap_or_default() + } else { + String::new() + }; + // 主线程侧读 url 文件(worker 脚本源码)。 let source = match std::fs::read_to_string(&url) { Ok(s) => s, @@ -100,8 +145,8 @@ pub(crate) fn worker_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } }; - // 派生 worker(建 WorkerHandle 并 spawn OS 线程)。 - let id = match vm.spawn_worker(&source) { + // 派生 worker(建 WorkerHandle 并 spawn OS 线程),传 name 与 url(location)。 + let id = match vm.spawn_worker(&source, &name, &url) { Ok(id) => id, Err(e) => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, &e)), }; @@ -275,16 +320,16 @@ pub(crate) fn self_close(_vm: &mut Vm, _args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::undefined()) } -/// `self.name` getter(worker 侧):返空串(首版占位,规范面对齐留后续)。 -#[expect(dead_code)] +/// `self.name` getter(worker 侧):读本线程 worker 名称(未置位时返空串)。 pub(crate) fn self_name(vm: &mut Vm, _args: &[u8]) -> NativeResult { - NativeResult::Ok(vm.new_string("")) + let name = WORKER_NAME.with(|slot| slot.borrow().as_deref().unwrap_or("").to_string()); + NativeResult::Ok(vm.new_string(&name)) } -/// `self.location` getter(worker 侧):返空串(首版占位,规范面对齐留后续)。 -#[expect(dead_code)] +/// `self.location` getter(worker 侧):读本线程 worker 位置(未置位时返空串)。 pub(crate) fn self_location(vm: &mut Vm, _args: &[u8]) -> NativeResult { - NativeResult::Ok(vm.new_string("")) + let location = WORKER_LOCATION.with(|slot| slot.borrow().as_deref().unwrap_or("").to_string()); + NativeResult::Ok(vm.new_string(&location)) } /// 从当前函数对象的 `name` 读事件类型(`get onmessage` / `set onmessage` → diff --git a/crates/oxide_vm/src/worker/mod.rs b/crates/oxide_vm/src/worker/mod.rs index 8cd4690d..95593819 100644 --- a/crates/oxide_vm/src/worker/mod.rs +++ b/crates/oxide_vm/src/worker/mod.rs @@ -81,23 +81,31 @@ pub struct WorkerHandle { /// /// # 步骤 /// 1. 经 `Vm::with_kernel_core` 建专属 Vm(新 realm)并注入编译服务。 -/// 2. 编译 worker 脚本(worker 的程序);失败则经 worker → 主线程通道错误面上报 +/// 2. 注入 worker → 主线程输出通道与名称、位置(thread-local,模式同 `WORKER_OUT_TX`)。 +/// 3. 编译 worker 脚本(worker 的程序);失败则经 worker → 主线程通道错误面上报 /// 错误串,不 panic,worker 继续事件循环(可被干净终止)。 -/// 3. 编译成功则运行脚本(worker 的程序);运行期未捕获异常同样经错误面上报。 -/// 4. 循环 `recv_timeout(100ms)`:处理 `Message` / `Error` / `Terminate` / 超时。 +/// 4. 编译成功则运行脚本(worker 的程序);运行期未捕获异常同样经错误面上报。 +/// 5. 循环 `recv_timeout(100ms)`:处理 `Message` / `Error` / `Terminate` / 超时。 /// /// # 边界与前提 /// - `script` 是 worker 的程序源码(普通脚本),编译与运行均在 worker 线程内完成。 +/// - `name` / `location` 是 worker 的名称与位置(构造器 options.name 与 url), +/// 置入 thread-local 供 `self.name` / `self.location` getter 读取。 /// - 超时后查 `rx.is_disconnected()`:主线程全部发送端 drop 即断开,worker 退出。 /// /// # 副作用 /// - 创建并 drop 一个 Vm(退出时 drop 触发 `Drop for Vm` → `Drop for Realm`)。 +/// - 置位本线程的 `WORKER_NAME` / `WORKER_LOCATION` thread-local(模式同 +/// `WORKER_OUT_TX`,线程退出随 thread-local 释放)。 /// - 每条 `Message` 经 rehydrate → onmessage 交付 → drain_microtasks 处理; /// 编译或执行失败经 `WorkerOutMail::Error` 上报主线程(主线程交付 onerror)。 fn worker_event_loop( core: Arc, compiler: Arc, script: String, rx: Receiver, - out_tx: Sender, + out_tx: Sender, name: String, location: String, ) { + // 注入 worker 名称与位置(建 Vm 前:bind_worker 据本线程 thread-local 区分 + // worker realm 与主 realm;self.name / self.location 经 thread-local 读取)。 + bindings::set_worker_identity(name, location); let mut vm = Vm::with_kernel_core(core); vm.set_compiler_service(Arc::clone(&compiler)); // 注入 worker → 主线程输出通道(self.postMessage 经 thread-local 发回主线程)。 @@ -157,12 +165,15 @@ impl Vm { /// # 步骤 /// 1. 分配 worker 编号(`worker_next_id` 自增)。 /// 2. 建主线程 → worker 与 worker → 主线程两条通道。 - /// 3. spawn 线程,线程内建专属 Vm 并进入事件循环。 + /// 3. spawn 线程,线程内建专属 Vm 并进入事件循环(`name` / `location` + /// 物化为 `String` 随线程闭包传入,置入 worker 线程 thread-local)。 /// 4. 把 `WorkerHandle` 登记进 `worker_registry`。 /// /// # 边界与前提 /// - `script` 是 worker 的程序源码(普通脚本);编译在 worker 线程内完成, /// 编译失败经 worker → 主线程通道上报,不 panic。 + /// - `name` 是 worker 名称(构造器 options.name,缺省空串);`location` + /// 是 worker 位置(构造器 url 字符串),供 `self.name` / `self.location` 读取。 /// - realm 计数受 512 上界约束(每 worker 一个 realm),worker 数须远低于 512。 /// /// # 返回值 @@ -170,7 +181,7 @@ impl Vm { /// /// # 副作用 /// - 派生一个 OS 线程;登记一个 `WorkerHandle`。 - pub fn spawn_worker(&mut self, script: &str) -> Result { + pub fn spawn_worker(&mut self, script: &str, name: &str, location: &str) -> Result { let id = self.worker_next_id; self.worker_next_id += 1; @@ -180,9 +191,11 @@ impl Vm { let core = Arc::clone(&self.kernel_core); let compiler = Arc::clone(&self.compiler); let script_owned = script.to_string(); + let name_owned = name.to_string(); + let location_owned = location.to_string(); let handle = std::thread::spawn(move || { - worker_event_loop(core, compiler, script_owned, rx, out_tx); + worker_event_loop(core, compiler, script_owned, rx, out_tx, name_owned, location_owned); }); let worker = WorkerHandle { id, tx, rx_out: out_rx, handle }; @@ -550,7 +563,7 @@ mod tests { // worker 脚本设 addEventListener 回显(注册表交付替换早期 onmessage 属性语义)。 let id = vm - .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });", "", "") .expect("worker 应派生成功"); assert_eq!(vm.active_workers(), vec![id], "应有唯一活跃 worker"); @@ -569,7 +582,7 @@ mod tests { fn worker_script_compile_failure_reports_error() { let mut vm = vm_with_compiler(); - let id = vm.spawn_worker("function { 语法错误").expect("worker 应派生成功"); + let id = vm.spawn_worker("function { 语法错误", "", "").expect("worker 应派生成功"); let poll = poll_until_mail(&mut vm, id); assert!(!poll.errors.is_empty(), "编译失败应经错误面上报错误串"); assert!(!poll.errors[0].is_empty(), "错误串应非空"); @@ -586,10 +599,10 @@ mod tests { // 两 worker 各设 addEventListener 回显(注册表交付语义)。 let id_a = vm - .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });", "", "") .expect("worker A 应派生成功"); let id_b = vm - .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });", "", "") .expect("worker B 应派生成功"); assert_eq!(vm.active_workers(), vec![id_a, id_b], "应有两个活跃 worker"); @@ -689,7 +702,7 @@ mod tests { fn worker_e2e_terminate() { let mut vm = vm_with_compiler(); - let id = vm.spawn_worker("1").expect("worker 应派生成功"); + let id = vm.spawn_worker("1", "", "").expect("worker 应派生成功"); assert_eq!(vm.active_workers(), vec![id], "应有唯一活跃 worker"); vm.worker_terminate(id).expect("终止应成功"); @@ -707,7 +720,7 @@ mod tests { let core = Arc::clone(vm.kernel_core()); let baseline = core.active_vms(); - let id = vm.spawn_worker("1").expect("worker 应派生成功"); + let id = vm.spawn_worker("1", "", "").expect("worker 应派生成功"); // worker 线程异步建 Vm:轮询至 realm 计数 +1(带截止,防 flaky)。 let deadline = Instant::now() + Duration::from_secs(5); while core.active_vms() < baseline + 1 { @@ -865,6 +878,65 @@ mod tests { let _ = std::fs::remove_file(&worker_path); } + /// 端到端:带 name 的 Worker 内 self.name 为 name、self.location 为 url 字符串; + /// 无 options 的 Worker 内 self.name 为空串。 + #[test] + fn worker_e2e_name_and_location() { + let mut vm = vm_with_compiler(); + + // worker 脚本:收到消息后回发 self.name 与 self.location(以 | 连接)。 + let worker_script = + "self.addEventListener('message', function(e) { self.postMessage(self.name + '|' + self.location); });"; + let path_a = write_worker_script(worker_script); + let path_b = write_worker_script(worker_script); + // 主脚本:A 带 options.name 建 Worker、B 无 options,各自回发后记录。 + let main_script = format!( + "var wa = new Worker('{a}', {{ name: 'n' }}); var wb = new Worker('{b}'); \ + wa.addEventListener('message', function(e) {{ globalThis.idA = e.data; }}); \ + wb.addEventListener('message', function(e) {{ globalThis.idB = e.data; }}); \ + wa.postMessage('check'); wb.postMessage('check');", + a = path_a.display(), + b = path_b.display() + ); + vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); + + // 事件循环轮询至两个回发都到达(带截止,防 flaky)。 + let deadline = Instant::now() + Duration::from_secs(5); + loop { + vm.deliver_worker_messages(); + vm.cleanup_disconnected_workers(); + let a = global_value_opt(&vm, "idA"); + let b = global_value_opt(&vm, "idB"); + if a.is_some_and(|v| v.is_string()) && b.is_some_and(|v| v.is_string()) { + break; + } + if Instant::now() >= deadline { + panic!("5 秒内未收齐 self.name 与 self.location 回发"); + } + std::thread::sleep(Duration::from_millis(10)); + } + // A:self.name 为 "n"、self.location 为 url 字符串。 + let id_a = global_value_opt(&vm, "idA").expect("idA 应被设置"); + let id_a_text = vm.lookup_str(id_a).expect("idA 应可读"); + assert_eq!( + id_a_text, + format!("n|{}", path_a.display()), + "带 name 的 worker 内 self.name 应为 \"n\"、self.location 应为 url 字符串" + ); + // B:无 options 时 self.name 为空串、self.location 仍为 url 字符串。 + let id_b = global_value_opt(&vm, "idB").expect("idB 应被设置"); + let id_b_text = vm.lookup_str(id_b).expect("idB 应可读"); + assert_eq!( + id_b_text, + format!("|{}", path_b.display()), + "无 options 的 worker 内 self.name 应为空串、self.location 应为 url 字符串" + ); + + vm.shutdown_workers(); + let _ = std::fs::remove_file(&path_a); + let _ = std::fs::remove_file(&path_b); + } + /// onmessageerror 交付辅助:无自然触发,直接单测交付辅助(handler 被调用、 /// 事件 type 为 "messageerror")。不钉触发可达性(当前架构不可达)。 #[test]