1use std::{
15 collections::BTreeMap,
16 fs,
17 io::Write,
18 net::{SocketAddr, TcpListener, TcpStream},
19 path::{Path, PathBuf},
20 sync::{
21 Arc,
22 atomic::{AtomicBool, Ordering},
23 },
24 thread::JoinHandle,
25 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
26};
27
28use base64::Engine as _;
29use serde_json::{Value, json};
30
31use crate::{
32 integrity::{FrontendIntegrityInputs, create_explicit_run_integrity},
33 lifecycle::{note_kept_evidence, remove_stored_tree_deferred},
34 run_store::{RunIntegrity, RunMetadata},
35 rust_project::PreparedRustProject,
36 served_tests::ServedTest,
37};
38
39const ENTRY_FILE: &str = "supercov-worker-entry.mjs";
42const REGISTRY_FILE: &str = "supercov-worker-registry.mjs";
43
44const REGISTRY: &str = r#"// Generated by Supercov: remembers this isolate's wasm instances.
47const instances = (globalThis.__supercovWasm ??= []);
48const Instance = WebAssembly.Instance;
49function Recorded(module, imports) {
50 const instance = new Instance(module, imports);
51 instances.push(instance);
52 return instance;
53}
54Recorded.prototype = Instance.prototype;
55WebAssembly.Instance = Recorded;
56const instantiate = WebAssembly.instantiate;
57WebAssembly.instantiate = async function (source, imports) {
58 const result = await instantiate.call(WebAssembly, source, imports);
59 instances.push(result instanceof Instance ? result : result.instance);
60 return result;
61};
62"#;
63
64const TAKE: &str = r#"(() => {
69 const seen = (globalThis.__supercovTaken ??= new WeakSet());
70 const out = [];
71 for (const instance of globalThis.__supercovWasm ?? []) {
72 const exports = instance.exports;
73 for (const name of Object.keys(exports)) {
74 if (!name.startsWith('__supercov_region_')) continue;
75 const crate = name.slice('__supercov_region_'.length);
76 const packed = BigInt.asUintN(64, BigInt(exports[name]()));
77 const at = Number(packed >> 32n), length = Number(packed & 0xffffffffn);
78 if (!length || !exports.memory) continue;
79 const view = new DataView(exports.memory.buffer, at, length);
80 const first = !seen.has(exports);
81 if (!first && view.getUint32(40, true) === 0 && view.getUint32(44, true) === 0) continue;
82 seen.add(exports);
83 const bytes = new Uint8Array(exports.memory.buffer, at, length);
84 const encode = (from, to) => {
85 let text = '';
86 for (let index = from; index < to; index += 0x8000)
87 text += String.fromCharCode.apply(null, bytes.subarray(index, Math.min(to, index + 0x8000)));
88 return btoa(text);
89 };
90 const bodyAt = view.getUint32(32, true);
91 const segments = [[0, encode(0, bodyAt)]];
92 for (let page = 0; bodyAt + page * 4096 < length; page++) {
93 if (bytes[128 + (page >> 3)] & (1 << (page & 7))) {
94 const from = bodyAt + page * 4096;
95 segments.push([from, encode(from, Math.min(length, from + 4096))]);
96 }
97 }
98 out.push([crate, length, segments]);
99 exports['__supercov_clear_' + crate]();
100 }
101 }
102 return out;
103})()"#;
104
105const POLL_INTERVAL: Duration = Duration::from_millis(5);
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111struct WorkerConfig {
112 path: PathBuf,
113 main: String,
114 inspector_port: Option<u16>,
115}
116
117fn worker_configs(root: &Path) -> Vec<PathBuf> {
123 const SKIPPED: &[&str] = &[
124 "node_modules",
125 "target",
126 "build",
127 "dist",
128 ".git",
129 ".supercov",
130 ".wrangler",
131 ];
132 let mut found = Vec::new();
133 let mut directories = vec![(root.to_path_buf(), 0)];
134 while let Some((directory, depth)) = directories.pop() {
135 let Ok(entries) = fs::read_dir(&directory) else {
136 continue;
137 };
138 for entry in entries.flatten() {
139 let path = entry.path();
140 let name = entry.file_name();
141 let Some(name) = name.to_str() else {
142 continue;
143 };
144 let Ok(kind) = entry.file_type() else {
145 continue;
146 };
147 if kind.is_dir() {
148 if depth < 6 && !SKIPPED.contains(&name) {
149 directories.push((path, depth + 1));
150 }
151 continue;
152 }
153 if !kind.is_file()
154 || !matches!(name, "wrangler.toml" | "wrangler.json" | "wrangler.jsonc")
155 {
156 continue;
157 }
158 let builds_rust = read_config(&path).is_some_and(|(_, document)| {
159 build_command(&document).is_some_and(|command| command.contains("worker-build"))
160 });
161 if builds_rust && path.with_file_name("Cargo.toml").is_file() {
162 found.push(path);
163 }
164 }
165 }
166 found.sort();
167 found
168}
169
170fn read_config(path: &Path) -> Option<(String, Value)> {
172 let text = fs::read_to_string(path).ok()?;
173 let document = if path
174 .extension()
175 .is_some_and(|extension| extension == "toml")
176 {
177 let table = text.parse::<toml::Table>().ok()?;
178 serde_json::to_value(table).ok()?
179 } else {
180 serde_json::from_str(&strip_jsonc(&text)).ok()?
181 };
182 Some((text, document))
183}
184
185fn build_command(document: &Value) -> Option<String> {
186 match document.get("build")?.get("command")? {
187 Value::String(command) => Some(command.clone()),
188 Value::Array(parts) => Some(
189 parts
190 .iter()
191 .filter_map(Value::as_str)
192 .collect::<Vec<_>>()
193 .join(" "),
194 ),
195 _ => None,
196 }
197}
198
199fn parse_config(path: &Path) -> Option<WorkerConfig> {
200 let (_, document) = read_config(path)?;
201 Some(WorkerConfig {
202 path: path.to_owned(),
203 main: document.get("main")?.as_str()?.to_owned(),
204 inspector_port: document
205 .get("dev")
206 .and_then(|dev| dev.get("inspector_port"))
207 .and_then(Value::as_u64)
208 .and_then(|port| u16::try_from(port).ok()),
209 })
210}
211
212fn strip_jsonc(text: &str) -> String {
215 let mut out = String::with_capacity(text.len());
216 let mut characters = text.chars().peekable();
217 while let Some(character) = characters.next() {
218 match character {
219 '"' => {
220 out.push('"');
221 while let Some(inner) = characters.next() {
222 out.push(inner);
223 match inner {
224 '\\' => {
225 if let Some(escaped) = characters.next() {
226 out.push(escaped);
227 }
228 }
229 '"' => break,
230 _ => {}
231 }
232 }
233 }
234 '/' if characters.peek() == Some(&'/') => {
235 for inner in characters.by_ref() {
236 if inner == '\n' {
237 out.push('\n');
238 break;
239 }
240 }
241 }
242 '/' if characters.peek() == Some(&'*') => {
243 characters.next();
244 let mut previous = '\0';
245 for inner in characters.by_ref() {
246 if previous == '*' && inner == '/' {
247 break;
248 }
249 previous = inner;
250 }
251 }
252 _ => out.push(character),
253 }
254 }
255 let mut cleaned = String::with_capacity(out.len());
257 let mut in_string = false;
258 let mut escaped = false;
259 let bytes = out.as_bytes();
260 for (index, character) in out.char_indices() {
261 if in_string {
262 cleaned.push(character);
263 if escaped {
264 escaped = false;
265 } else if character == '\\' {
266 escaped = true;
267 } else if character == '"' {
268 in_string = false;
269 }
270 continue;
271 }
272 if character == '"' {
273 in_string = true;
274 } else if character == ',' {
275 let next = bytes[index + 1..]
276 .iter()
277 .find(|byte| !byte.is_ascii_whitespace());
278 if matches!(next, Some(b'}' | b']')) {
279 continue;
280 }
281 }
282 cleaned.push(character);
283 }
284 cleaned
285}
286
287fn rewrite_config(config: &WorkerConfig, entry: &str, port: Option<u16>) -> Result<(), String> {
290 let path = &config.path;
291 let text = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?;
292 let rewritten = if path
293 .extension()
294 .is_some_and(|extension| extension == "toml")
295 {
296 let mut document = text
297 .parse::<toml_edit::DocumentMut>()
298 .map_err(|error| format!("{}: {error}", path.display()))?;
299 document["main"] = toml_edit::value(entry);
300 if let (None, Some(port)) = (config.inspector_port, port) {
301 if !document.contains_key("dev") {
302 document["dev"] = toml_edit::table();
303 }
304 document["dev"]["inspector_port"] = toml_edit::value(i64::from(port));
305 }
306 document.to_string()
307 } else {
308 let mut document: Value = serde_json::from_str(&strip_jsonc(&text))
309 .map_err(|error| format!("{}: {error}", path.display()))?;
310 document["main"] = json!(entry);
311 if let (None, Some(port)) = (config.inspector_port, port) {
312 if !document.get("dev").is_some_and(Value::is_object) {
313 document["dev"] = json!({});
314 }
315 document["dev"]["inspector_port"] = json!(port);
316 }
317 serde_json::to_string_pretty(&document).map_err(|error| error.to_string())?
318 };
319 fs::write(path, rewritten).map_err(|error| format!("{}: {error}", path.display()))
320}
321
322fn point_at_entry(config: &WorkerConfig, port: Option<u16>) -> Result<(), String> {
327 if config.main.trim_start_matches("./") == ENTRY_FILE {
328 return Ok(());
329 }
330 let directory = config.path.parent().unwrap_or(Path::new("."));
331 fs::write(directory.join(REGISTRY_FILE), REGISTRY).map_err(|error| error.to_string())?;
332 let main = config.main.trim_start_matches("./");
333 let entry = format!(
334 "// Generated by Supercov: the project's entry, with its wasm instances remembered.\nimport './{REGISTRY_FILE}';\nexport * from './{main}';\nexport {{ default }} from './{main}';\n"
335 );
336 fs::write(directory.join(ENTRY_FILE), entry).map_err(|error| error.to_string())?;
337 rewrite_config(config, ENTRY_FILE, port)
338}
339
340fn free_port() -> Result<u16, String> {
342 let listener = TcpListener::bind("127.0.0.1:0").map_err(|error| error.to_string())?;
343 listener
344 .local_addr()
345 .map(|address| address.port())
346 .map_err(|error| error.to_string())
347}
348
349pub(crate) fn wasm_target_cfg(
352 crate_directory: &Path,
353) -> Option<crate::rust_instrumenter::TargetCfg> {
354 let output = std::process::Command::new("rustc")
355 .args(["--print", "cfg", "--target", "wasm32-unknown-unknown"])
356 .current_dir(crate_directory)
357 .output()
358 .ok()?;
359 if !output.status.success() {
360 return None;
361 }
362 let values = String::from_utf8_lossy(&output.stdout)
363 .lines()
364 .filter_map(|line| line.parse::<cargo_platform::Cfg>().ok())
365 .collect::<Vec<_>>();
366 Some(crate::rust_instrumenter::TargetCfg::new(&values))
367}
368
369fn now_ms() -> i64 {
370 SystemTime::now()
371 .duration_since(UNIX_EPOCH)
372 .map(|elapsed| elapsed.as_millis() as i64)
373 .unwrap_or(0)
374}
375
376struct Poller {
379 stop: Arc<AtomicBool>,
380 thread: Option<JoinHandle<()>>,
381}
382
383impl Poller {
384 fn start(port: u16, windows: PathBuf) -> Self {
385 let stop = Arc::new(AtomicBool::new(false));
386 let flag = Arc::clone(&stop);
387 let thread = std::thread::Builder::new()
388 .name("supercov-worker-poller".into())
389 .spawn(move || poll(port, &windows, &flag))
390 .ok();
391 Self { stop, thread }
392 }
393
394 fn finish(mut self) {
396 self.stop.store(true, Ordering::Release);
397 if let Some(thread) = self.thread.take() {
398 let _ = thread.join();
399 }
400 }
401}
402
403impl Drop for Poller {
404 fn drop(&mut self) {
405 self.stop.store(true, Ordering::Release);
406 }
407}
408
409fn poll(port: u16, windows: &Path, stop: &AtomicBool) {
410 let Ok(mut file) = fs::OpenOptions::new()
411 .create(true)
412 .append(true)
413 .open(windows)
414 else {
415 return;
416 };
417 let address = SocketAddr::from(([127, 0, 0, 1], port));
418 while !stop.load(Ordering::Acquire) {
421 let Ok(stream) = TcpStream::connect_timeout(&address, Duration::from_millis(200)) else {
422 std::thread::sleep(Duration::from_millis(100));
423 continue;
424 };
425 let _ = stream.set_nodelay(true);
426 let _ = stream.set_read_timeout(Some(Duration::from_millis(20)));
427 let Ok((mut socket, _)) =
428 tungstenite::client::client(format!("ws://127.0.0.1:{port}/ws"), stream)
429 else {
430 std::thread::sleep(Duration::from_millis(100));
431 continue;
432 };
433 session(&mut socket, &mut file, stop);
434 let _ = socket.close(None);
435 }
436}
437
438fn session(socket: &mut tungstenite::WebSocket<TcpStream>, file: &mut fs::File, stop: &AtomicBool) {
442 use tungstenite::{Error, Message};
443 let send = |socket: &mut tungstenite::WebSocket<TcpStream>, message: Value| {
444 socket
445 .send(Message::text(message.to_string()))
446 .map_err(|_| ())
447 };
448 if send(socket, json!({"id": 1, "method": "Runtime.enable"})).is_err() {
449 return;
450 }
451 let mut contexts = Vec::<i64>::new();
452 let mut next_id = 2_u64;
453 let mut awaiting: Option<(u64, Instant)> = None;
456 let mut window_start = now_ms();
457 let mut last_take = Instant::now() - POLL_INTERVAL;
458 let mut final_sent = false;
459 loop {
460 let stopping = stop.load(Ordering::Acquire);
461 if stopping && awaiting.is_none() && (final_sent || contexts.is_empty()) {
462 return;
463 }
464 if awaiting.is_none()
465 && let Some(context) = contexts.last()
466 && (stopping || last_take.elapsed() >= POLL_INTERVAL)
467 {
468 let id = next_id;
469 next_id += 1;
470 let request = json!({
471 "id": id,
472 "method": "Runtime.evaluate",
473 "params": {"expression": TAKE, "contextId": context, "returnByValue": true},
474 });
475 if send(socket, request).is_err() {
476 return;
477 }
478 awaiting = Some((id, Instant::now()));
479 last_take = Instant::now();
480 final_sent = stopping;
481 }
482 match socket.read() {
483 Ok(Message::Text(text)) => {
484 let Ok(message) = serde_json::from_str::<Value>(text.as_str()) else {
485 continue;
486 };
487 match message.get("method").and_then(Value::as_str) {
488 Some("Runtime.executionContextCreated") => {
489 if let Some(id) = message["params"]["context"]["id"].as_i64() {
490 contexts.push(id);
491 }
492 }
493 Some("Runtime.executionContextDestroyed") => {
494 if let Some(id) = message["params"]["executionContextId"].as_i64() {
495 contexts.retain(|context| *context != id);
496 }
497 }
498 Some("Runtime.executionContextsCleared") => contexts.clear(),
499 _ => {}
500 }
501 if let Some((id, _)) = awaiting
502 && message.get("id").and_then(Value::as_u64) == Some(id)
503 {
504 awaiting = None;
505 let end = now_ms();
506 let regions = &message["result"]["result"]["value"];
507 if regions
508 .as_array()
509 .is_some_and(|regions| !regions.is_empty())
510 {
511 let line = json!({"start": window_start, "end": end, "regions": regions});
512 let _ = writeln!(file, "{line}");
513 }
514 window_start = end;
515 }
516 }
517 Ok(Message::Close(_)) => return,
518 Ok(_) => {}
519 Err(Error::Io(error))
520 if matches!(
521 error.kind(),
522 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
523 ) => {}
524 Err(_) => return,
525 }
526 if awaiting.is_some_and(|(_, sent)| sent.elapsed() > Duration::from_secs(5)) {
529 awaiting = None;
530 }
531 }
532}
533
534struct Window {
536 start: i64,
537 end: i64,
538 regions: Vec<(String, Vec<u8>)>,
540}
541
542fn read_windows(directory: &Path) -> Result<Vec<Window>, String> {
543 let mut files = fs::read_dir(directory)
544 .map(|entries| {
545 entries
546 .filter_map(Result::ok)
547 .map(|entry| entry.path())
548 .filter(|path| {
549 path.extension()
550 .is_some_and(|extension| extension == "windows")
551 })
552 .collect::<Vec<_>>()
553 })
554 .unwrap_or_default();
555 files.sort();
556 let mut windows = Vec::new();
557 for file in &files {
558 let text =
559 fs::read_to_string(file).map_err(|error| format!("{}: {error}", file.display()))?;
560 for line in text.lines() {
562 let Ok(value) = serde_json::from_str::<Value>(line) else {
563 continue;
564 };
565 if let Some(window) = window_from(&value) {
566 windows.push(window);
567 }
568 }
569 }
570 Ok(windows)
571}
572
573fn window_from(value: &Value) -> Option<Window> {
574 let mut regions = Vec::new();
575 for region in value.get("regions")?.as_array()? {
576 let crate_key = region.get(0)?.as_str()?.to_owned();
577 let length = usize::try_from(region.get(1)?.as_u64()?).ok()?;
578 let mut bytes = vec![0_u8; length];
579 for segment in region.get(2)?.as_array()? {
580 let offset = usize::try_from(segment.get(0)?.as_u64()?).ok()?;
581 let data = base64::engine::general_purpose::STANDARD
582 .decode(segment.get(1)?.as_str()?)
583 .ok()?;
584 bytes
585 .get_mut(offset..offset + data.len())?
586 .copy_from_slice(&data);
587 }
588 regions.push((crate_key, bytes));
589 }
590 Some(Window {
591 start: value.get("start")?.as_i64()?,
592 end: value.get("end")?.as_i64()?,
593 regions,
594 })
595}
596
597pub struct RustWorkerServer {
599 project: PreparedRustProject,
600 pollers: Vec<Poller>,
601 integrity: RunIntegrity,
602 source_inputs: crate::source_manifest::Inputs,
603 source_roots: Option<Vec<String>>,
604 directory: PathBuf,
606}
607
608impl RustWorkerServer {
609 pub fn present(root: &Path) -> bool {
611 !worker_configs(root).is_empty()
612 }
613
614 pub fn prepare(
618 root: &Path,
619 workspace: &Path,
620 run_id: &str,
621 command: &[String],
622 diagnostics: &mut dyn Write,
623 ) -> Result<Vec<Self>, String> {
624 let configs = worker_configs(workspace);
625 let mut by_workspace = BTreeMap::<PathBuf, Vec<WorkerConfig>>::new();
628 for path in configs {
629 let Some(config) = parse_config(&path) else {
630 writeln!(
631 diagnostics,
632 "[supercov] {} names no `main`; its Worker is not measured",
633 path.display()
634 )
635 .map_err(|error| error.to_string())?;
636 continue;
637 };
638 let crate_directory = path.parent().unwrap_or(workspace).to_owned();
639 let cargo_root = cargo_workspace_root(&crate_directory).unwrap_or(crate_directory);
640 by_workspace.entry(cargo_root).or_default().push(config);
641 }
642 let mut servers = Vec::new();
643 for (index, (cargo_root, configs)) in by_workspace.into_iter().enumerate() {
644 let Ok(relative) = cargo_root.strip_prefix(workspace) else {
645 continue;
646 };
647 let source_roots = crate::source_discovery::configured_source_roots(
648 &std::env::vars().collect::<BTreeMap<_, _>>(),
649 );
650 let mut integrity_inputs = crate::rust_run::collect_integrity_inputs(root, command)?;
653 crate::source_discovery::fold_roots_into_configuration(
654 &mut integrity_inputs.execution_configuration,
655 source_roots.as_deref(),
656 );
657 let source_inputs =
658 crate::source_capture::capture(root, "rust", integrity_inputs.assertion_paths())?;
659 let integrity = create_explicit_run_integrity(
660 root,
661 &integrity_inputs,
662 &FrontendIntegrityInputs::embedded_rust(),
663 )
664 .map_err(|error| error.to_string())?;
665 let cfg = wasm_target_cfg(&cargo_root);
666 let mut project =
667 crate::rust_project::prepare_rust_project_for(&cargo_root, None, cfg.as_ref())
668 .map_err(|error| error.to_string())?;
669 crate::rust_wasm_evidence::root_paths(&mut project, relative);
670 let directory = root
671 .join(".supercov/work")
672 .join(run_id)
673 .join(format!("rust-worker-{index}"));
674 let evidence = directory.join("evidence");
675 fs::create_dir_all(&evidence).map_err(|error| error.to_string())?;
676 let mut pollers = Vec::new();
677 for (number, config) in configs.iter().enumerate() {
678 let port = match config.inspector_port {
679 Some(port) => port,
680 None => free_port()?,
681 };
682 point_at_entry(config, Some(port))?;
683 pollers.push(Poller::start(
684 port,
685 evidence.join(format!("{number:04}.windows")),
686 ));
687 }
688 servers.push(Self {
689 project,
690 pollers,
691 integrity,
692 source_inputs,
693 source_roots,
694 directory,
695 });
696 }
697 Ok(servers)
698 }
699
700 pub fn source_files(&self) -> usize {
701 self.project.source_files.len()
702 }
703
704 pub fn source_fingerprint(&self) -> String {
707 self.integrity.fingerprint.source.clone()
708 }
709
710 pub fn environment(&self) -> BTreeMap<String, String> {
719 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
720 if !rustflags.is_empty() {
721 rustflags.push(' ');
722 }
723 rustflags.push_str("--cap-lints=warn -A unused_parens");
724 BTreeMap::from([("RUSTFLAGS".into(), rustflags)])
725 }
726
727 pub fn publish(
732 mut self,
733 root: &Path,
734 _evidence_run_id: &str,
735 metadata: RunMetadata,
736 served: &[ServedTest],
737 diagnostics: &mut dyn Write,
738 ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
739 for poller in std::mem::take(&mut self.pollers) {
740 poller.finish();
741 }
742 let evidence_directory = self.directory.join("evidence");
743 let result = read_windows(&evidence_directory).and_then(|windows| {
744 crate::rust_wasm_evidence::publish(
745 root,
746 crate::rust_wasm_evidence::WasmRun {
747 language: "Rust Worker",
748 archive: "rust-worker-evidence.raw.gz",
749 project: &self.project,
750 integrity: &self.integrity,
751 source_inputs: &self.source_inputs,
752 source_roots: self.source_roots.clone(),
753 },
754 windows
755 .into_iter()
756 .map(|window| crate::rust_wasm_evidence::WasmRecord {
757 owner: crate::rust_wasm_evidence::Owner::Window(window.start, window.end),
758 regions: window.regions,
759 })
760 .collect(),
761 metadata,
762 served,
763 diagnostics,
764 )
765 });
766 let result = result.map_err(|error| {
767 let run = self
768 .directory
769 .parent()
770 .and_then(Path::file_name)
771 .map(|name| name.to_string_lossy().into_owned())
772 .unwrap_or_default();
773 note_kept_evidence(
774 root,
775 &evidence_directory,
776 &format!("{run}-rust-worker"),
777 error,
778 )
779 });
780 let _ = remove_stored_tree_deferred(root, &self.directory);
781 result
782 }
783}
784
785pub const REAL_WRANGLER: &str = "SUPERCOV_WRANGLER";
788pub const WRANGLER_COMMAND: &str = "__wrangler";
790pub const WRANGLER_POLL_COMMAND: &str = "__wrangler-poll";
791
792pub struct CargoTestWorkers {
803 environment: Vec<(String, std::ffi::OsString)>,
804}
805
806impl CargoTestWorkers {
807 pub fn prepare(workspace: &Path, tools: &Path) -> Result<Option<Self>, String> {
812 let configs = worker_configs(workspace)
813 .iter()
814 .filter_map(|path| parse_config(path))
815 .collect::<Vec<_>>();
816 if configs.is_empty() {
817 return Ok(None);
818 }
819 let path = std::env::var_os("PATH").unwrap_or_default();
820 let Some(real) = on_path(&path, "wrangler") else {
821 return Ok(None);
822 };
823 for config in &configs {
824 point_at_entry(config, None)?;
825 }
826 let supercov = std::env::current_exe().map_err(|error| error.to_string())?;
827 fs::create_dir_all(tools).map_err(|error| format!("{}: {error}", tools.display()))?;
828 write_stand_in(tools, &supercov)?;
829 let mut directories = vec![tools.to_owned()];
830 directories.extend(std::env::split_paths(&path));
831 let joined = std::env::join_paths(directories).map_err(|error| error.to_string())?;
832 Ok(Some(Self {
833 environment: vec![
834 ("PATH".into(), joined),
835 (REAL_WRANGLER.into(), real.into_os_string()),
836 ],
837 }))
838 }
839
840 pub fn environment(&self) -> impl Iterator<Item = (&str, &std::ffi::OsStr)> {
842 self.environment
843 .iter()
844 .map(|(name, value)| (name.as_str(), value.as_os_str()))
845 }
846}
847
848fn on_path(path: &std::ffi::OsStr, name: &str) -> Option<PathBuf> {
851 let names = if cfg!(windows) {
852 vec![
853 format!("{name}.cmd"),
854 format!("{name}.exe"),
855 name.to_owned(),
856 ]
857 } else {
858 vec![name.to_owned()]
859 };
860 std::env::split_paths(path).find_map(|directory| {
861 names
862 .iter()
863 .map(|name| directory.join(name))
864 .find(|candidate| fs::metadata(candidate).is_ok_and(|metadata| metadata.is_file()))
865 })
866}
867
868fn write_stand_in(tools: &Path, supercov: &Path) -> Result<(), String> {
869 #[cfg(unix)]
870 {
871 use std::os::unix::fs::PermissionsExt;
872 let quoted = supercov.to_string_lossy().replace('\'', r"'\''");
873 let script = tools.join("wrangler");
874 fs::write(
875 &script,
876 format!("#!/bin/sh\nexec '{quoted}' {WRANGLER_COMMAND} \"$@\"\n"),
877 )
878 .map_err(|error| format!("{}: {error}", script.display()))?;
879 fs::set_permissions(&script, fs::Permissions::from_mode(0o755))
880 .map_err(|error| format!("{}: {error}", script.display()))?;
881 }
882 #[cfg(windows)]
883 {
884 let script = tools.join("wrangler.cmd");
885 fs::write(
886 &script,
887 format!("@\"{}\" {WRANGLER_COMMAND} %*\r\n", supercov.display()),
888 )
889 .map_err(|error| format!("{}: {error}", script.display()))?;
890 }
891 Ok(())
892}
893
894fn starts_dev(arguments: &[std::ffi::OsString]) -> bool {
897 const VALUED: &[&str] = &["--cwd", "-c", "--config", "-e", "--env", "--env-file"];
898 let mut arguments = arguments.iter().map(|argument| argument.to_string_lossy());
899 while let Some(argument) = arguments.next() {
900 if VALUED.contains(&argument.as_ref()) {
901 arguments.next();
902 } else if !argument.starts_with('-') {
903 return argument == "dev";
904 }
905 }
906 false
907}
908
909fn runs_remote(arguments: &[std::ffi::OsString]) -> bool {
913 arguments.iter().any(|argument| {
914 matches!(
915 argument.to_string_lossy().as_ref(),
916 "--remote" | "-r" | "--remote=true"
917 )
918 })
919}
920
921const REMOTE_NOTE: &str = "worker-remote-";
924
925pub fn started_remote(directory: &Path) -> bool {
928 fs::read_dir(directory).is_ok_and(|entries| {
929 entries.filter_map(Result::ok).any(|entry| {
930 entry
931 .file_name()
932 .to_str()
933 .is_some_and(|name| name.starts_with(REMOTE_NOTE))
934 })
935 })
936}
937
938fn named_inspector_port(arguments: &[std::ffi::OsString]) -> Option<u16> {
941 let mut arguments = arguments.iter().map(|argument| argument.to_string_lossy());
942 while let Some(argument) = arguments.next() {
943 if argument == "--inspector-port" {
944 return arguments.next()?.parse().ok();
945 }
946 if let Some(port) = argument.strip_prefix("--inspector-port=") {
947 return port.parse().ok();
948 }
949 }
950 None
951}
952
953pub fn launch_wrangler(arguments: Vec<std::ffi::OsString>) -> i32 {
958 let Some(real) = std::env::var_os(REAL_WRANGLER) else {
959 eprintln!("[supercov] {WRANGLER_COMMAND} runs only as the wrangler of a measured test");
960 return 127;
961 };
962 let mut arguments = arguments;
963 let evidence = std::env::var_os("SUPERCOV_RUST_EVIDENCE_DIR").map(PathBuf::from);
964 if starts_dev(&arguments)
965 && let Some(evidence) = evidence
966 {
967 if runs_remote(&arguments) {
968 let _ = fs::create_dir_all(&evidence);
970 let _ = fs::write(
971 evidence.join(format!("{REMOTE_NOTE}{}", std::process::id())),
972 "",
973 );
974 return exec_wrangler(&real, &arguments);
975 }
976 let port = match named_inspector_port(&arguments) {
977 Some(port) => Some(port),
978 None => free_port().ok().inspect(|port| {
979 arguments.push("--inspector-port".into());
980 arguments.push(port.to_string().into());
981 }),
982 };
983 if let (Some(port), Ok(supercov)) = (port, std::env::current_exe()) {
984 let windows = evidence.join(format!("worker-{}.windows", std::process::id()));
985 let _ = std::process::Command::new(supercov)
986 .arg(WRANGLER_POLL_COMMAND)
987 .arg(std::process::id().to_string())
988 .arg(port.to_string())
989 .arg(&windows)
990 .stdin(std::process::Stdio::null())
991 .stdout(std::process::Stdio::null())
992 .stderr(std::process::Stdio::null())
993 .spawn();
994 }
995 }
996 exec_wrangler(&real, &arguments)
997}
998
999fn exec_wrangler(real: &std::ffi::OsStr, arguments: &[std::ffi::OsString]) -> i32 {
1002 let rustflags = worker_rustflags(&std::env::var("RUSTFLAGS").unwrap_or_default());
1003 let mut command = std::process::Command::new(real);
1004 command.args(arguments).env("RUSTFLAGS", rustflags);
1005 #[cfg(unix)]
1006 {
1007 use std::os::unix::process::CommandExt;
1008 let error = command.exec();
1009 eprintln!(
1010 "[supercov] could not start {}: {error}",
1011 Path::new(&real).display()
1012 );
1013 126
1014 }
1015 #[cfg(not(unix))]
1016 {
1017 match command.status() {
1018 Ok(status) => status.code().unwrap_or(1),
1019 Err(error) => {
1020 eprintln!(
1021 "[supercov] could not start {}: {error}",
1022 Path::new(&real).display()
1023 );
1024 126
1025 }
1026 }
1027 }
1028}
1029
1030fn worker_rustflags(inherited: &str) -> String {
1036 let mut kept = Vec::new();
1037 let mut words = inherited.split_whitespace();
1038 while let Some(word) = words.next() {
1039 if word == "--cap-lints" {
1040 words.next();
1041 } else if !word.starts_with("--cap-lints=") {
1042 kept.push(word);
1043 }
1044 }
1045 kept.extend(["--cap-lints=warn", "-A", "unused_parens"]);
1046 kept.join(" ")
1047}
1048
1049pub fn poll_wrangler(arguments: Vec<std::ffi::OsString>) -> i32 {
1053 let [pid, port, windows] = arguments.as_slice() else {
1054 return 2;
1055 };
1056 let (Ok(pid), Ok(port)) = (
1057 pid.to_string_lossy().parse::<u32>(),
1058 port.to_string_lossy().parse::<u16>(),
1059 ) else {
1060 return 2;
1061 };
1062 let windows = PathBuf::from(windows);
1063 let part = partial(&windows);
1064 let poller = Poller::start(port, part.clone());
1065 while process_alive(pid) {
1066 std::thread::sleep(Duration::from_millis(20));
1067 }
1068 poller.finish();
1069 if fs::symlink_metadata(&part).is_ok() {
1070 let _ = fs::rename(&part, &windows);
1071 }
1072 0
1073}
1074
1075fn partial(windows: &Path) -> PathBuf {
1076 let mut name = windows.as_os_str().to_owned();
1077 name.push(".part");
1078 PathBuf::from(name)
1079}
1080
1081#[cfg(unix)]
1082fn process_alive(pid: u32) -> bool {
1083 let Ok(pid) = libc::pid_t::try_from(pid) else {
1084 return false;
1085 };
1086 let answered = unsafe { libc::kill(pid, 0) };
1088 answered == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
1089}
1090
1091#[cfg(not(unix))]
1092fn process_alive(_pid: u32) -> bool {
1093 false
1094}
1095
1096const POLLER_SETTLE: Duration = Duration::from_secs(3);
1099
1100pub fn materialize_worker_windows(
1105 directory: &Path,
1106 token: &str,
1107) -> Result<std::collections::BTreeSet<String>, String> {
1108 let listed = |extension: &str| -> Vec<PathBuf> {
1109 let mut files = fs::read_dir(directory)
1110 .map(|entries| {
1111 entries
1112 .filter_map(Result::ok)
1113 .map(|entry| entry.path())
1114 .filter(|path| {
1115 path.file_name()
1116 .and_then(|name| name.to_str())
1117 .is_some_and(|name| {
1118 name.starts_with("worker-") && name.ends_with(extension)
1119 })
1120 })
1121 .collect::<Vec<_>>()
1122 })
1123 .unwrap_or_default();
1124 files.sort();
1125 files
1126 };
1127 let started = Instant::now();
1128 while !listed(".windows.part").is_empty() && started.elapsed() < POLLER_SETTLE {
1129 std::thread::sleep(Duration::from_millis(20));
1130 }
1131 let files = listed(".windows")
1134 .into_iter()
1135 .chain(listed(".windows.part"))
1136 .collect::<Vec<_>>();
1137 let mut crates = std::collections::BTreeSet::new();
1138 for (number, file) in files.iter().enumerate() {
1139 let text =
1140 fs::read_to_string(file).map_err(|error| format!("{}: {error}", file.display()))?;
1141 for (line, value) in text
1142 .lines()
1143 .filter_map(|line| serde_json::from_str::<Value>(line).ok())
1144 .enumerate()
1145 {
1146 let Some(window) = window_from(&value) else {
1147 continue;
1148 };
1149 for (index, (crate_key, bytes)) in window.regions.into_iter().enumerate() {
1150 if region_token(&bytes) != Some(token) {
1151 continue;
1152 }
1153 let region = directory.join(format!(
1154 "{token}-worker-{number:04}-{line:06}-{index}.region"
1155 ));
1156 fs::write(®ion, &bytes)
1157 .map_err(|error| format!("{}: {error}", region.display()))?;
1158 crates.insert(crate_key);
1159 }
1160 }
1161 fs::remove_file(file).map_err(|error| format!("{}: {error}", file.display()))?;
1162 }
1163 Ok(crates)
1164}
1165
1166pub(crate) fn region_token(bytes: &[u8]) -> Option<&str> {
1168 let field = bytes.get(56..72)?;
1169 let end = field
1170 .iter()
1171 .position(|byte| *byte == 0)
1172 .unwrap_or(field.len());
1173 std::str::from_utf8(&field[..end]).ok()
1174}
1175
1176pub(crate) fn worker_crate_directories(workspace: &Path) -> Vec<String> {
1180 worker_configs(workspace)
1181 .iter()
1182 .filter_map(|config| config.parent()?.strip_prefix(workspace).ok())
1183 .map(|directory| directory.to_string_lossy().replace('\\', "/"))
1184 .collect()
1185}
1186
1187pub(crate) fn cargo_workspace_root(directory: &Path) -> Option<PathBuf> {
1188 let output = std::process::Command::new("cargo")
1189 .args(["locate-project", "--workspace", "--message-format", "plain"])
1190 .current_dir(directory)
1191 .output()
1192 .ok()?;
1193 if !output.status.success() {
1194 return None;
1195 }
1196 let manifest = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
1197 manifest.parent().map(Path::to_path_buf)
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203
1204 #[test]
1205 fn jsonc_comments_and_trailing_commas_are_dropped_and_strings_kept() {
1206 let text = r#"{
1207 // the Worker
1208 "name": "a // not a comment",
1209 /* block */ "main": "build/worker/shim.mjs",
1210 "build": { "command": "worker-build --release", },
1211 "list": [1, 2, ],
1212 "quote": "say \"hi\", ]"
1213}"#;
1214 let value: Value = serde_json::from_str(&strip_jsonc(text)).unwrap();
1215 assert_eq!(value["name"], "a // not a comment");
1216 assert_eq!(value["main"], "build/worker/shim.mjs");
1217 assert_eq!(value["build"]["command"], "worker-build --release");
1218 assert_eq!(value["list"], json!([1, 2]));
1219 assert_eq!(value["quote"], "say \"hi\", ]");
1220 }
1221
1222 #[test]
1223 fn the_worker_build_caps_lints_at_warn_whatever_it_inherited() {
1224 assert_eq!(worker_rustflags(""), "--cap-lints=warn -A unused_parens");
1225 assert_eq!(
1226 worker_rustflags("-C debuginfo=1 --cap-lints=allow"),
1227 "-C debuginfo=1 --cap-lints=warn -A unused_parens"
1228 );
1229 assert_eq!(
1230 worker_rustflags("--cap-lints allow -Dwarnings"),
1231 "-Dwarnings --cap-lints=warn -A unused_parens"
1232 );
1233 }
1234
1235 #[test]
1236 fn the_stand_in_reads_dev_and_an_inspector_port_wherever_they_stand() {
1237 let os = |arguments: &[&str]| {
1238 arguments
1239 .iter()
1240 .map(std::ffi::OsString::from)
1241 .collect::<Vec<_>>()
1242 };
1243 assert!(runs_remote(&os(&["dev", "--remote"])));
1244 assert!(runs_remote(&os(&["dev", "-r", "--port", "8787"])));
1245 assert!(!runs_remote(&os(&["dev", "--remote=false"])));
1246 assert!(!runs_remote(&os(&["dev", "--local"])));
1247 assert!(starts_dev(&os(&["dev", "--local"])));
1248 assert!(starts_dev(&os(&["--cwd", "worker", "dev"])));
1249 assert!(starts_dev(&os(&[
1250 "-c",
1251 "wrangler.toml",
1252 "--env=staging",
1253 "dev"
1254 ])));
1255 assert!(starts_dev(&os(&["--local", "dev"])));
1256 assert!(!starts_dev(&os(&["deploy"])));
1257 assert!(!starts_dev(&os(&[])));
1258 assert_eq!(
1259 named_inspector_port(&os(&["dev", "--inspector-port", "9230"])),
1260 Some(9230)
1261 );
1262 assert_eq!(
1263 named_inspector_port(&os(&["dev", "--inspector-port=9231"])),
1264 Some(9231)
1265 );
1266 assert_eq!(named_inspector_port(&os(&["dev", "--port", "8787"])), None);
1267 assert_eq!(
1268 named_inspector_port(&os(&["dev", "--inspector-port"])),
1269 None
1270 );
1271 }
1272
1273 #[test]
1277 fn a_take_becomes_a_region_file_of_the_test_that_started_the_worker() {
1278 let directory = temporary("materialize");
1279 let region = |token: &str| {
1280 let mut bytes = vec![0_u8; 4096 + 256];
1281 bytes[56..56 + token.len()].copy_from_slice(token.as_bytes());
1282 bytes[200] = 7;
1283 bytes
1284 };
1285 let segment =
1286 |bytes: &[u8]| json!([0, base64::engine::general_purpose::STANDARD.encode(bytes)]);
1287 let ours = region("abc123");
1288 let theirs = region("fff999");
1289 let line = json!({
1290 "start": 1,
1291 "end": 2,
1292 "regions": [
1293 ["abc123k1", ours.len(), [segment(&ours)]],
1294 ["fff999k2", theirs.len(), [segment(&theirs)]],
1295 ],
1296 });
1297 fs::write(
1298 directory.join("worker-41.windows"),
1299 format!("{line}\n{{\"start\": 3, \"end\""),
1300 )
1301 .unwrap();
1302 let crates = materialize_worker_windows(&directory, "abc123").unwrap();
1303 assert_eq!(
1304 crates,
1305 std::collections::BTreeSet::from(["abc123k1".to_owned()])
1306 );
1307 let mut names = fs::read_dir(&directory)
1308 .unwrap()
1309 .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
1310 .collect::<Vec<_>>();
1311 names.sort();
1312 assert_eq!(names, ["abc123-worker-0000-000000-0.region"]);
1313 assert_eq!(fs::read(directory.join(&names[0])).unwrap(), ours);
1314 assert!(
1316 materialize_worker_windows(&directory, "abc123")
1317 .unwrap()
1318 .is_empty()
1319 );
1320 assert_eq!(region_token(&ours), Some("abc123"));
1321 assert_eq!(region_token(&[0_u8; 10]), None);
1322 fs::remove_dir_all(&directory).unwrap();
1323 }
1324
1325 #[cfg(unix)]
1330 #[test]
1331 fn the_stand_in_is_put_first_only_where_a_wrangler_is_installed() {
1332 let workspace = temporary("stand-in");
1333 fs::create_dir_all(workspace.join("worker/src")).unwrap();
1334 fs::write(
1335 workspace.join("worker/Cargo.toml"),
1336 "[package]\nname = \"w\"\n",
1337 )
1338 .unwrap();
1339 fs::write(workspace.join("worker/src/lib.rs"), "").unwrap();
1340 fs::write(
1341 workspace.join("worker/wrangler.toml"),
1342 "name = \"w\"\nmain = \"build/worker/shim.mjs\"\n\n[build]\ncommand = \"worker-build --release\"\n",
1343 )
1344 .unwrap();
1345 let bin = workspace.join("bin");
1346 fs::create_dir_all(&bin).unwrap();
1347 let real = bin.join("wrangler");
1348 fs::write(&real, "#!/bin/sh\n").unwrap();
1349 let found = on_path(bin.as_os_str(), "wrangler");
1350 assert_eq!(found.as_deref(), Some(real.as_path()));
1351 assert_eq!(on_path(std::ffi::OsStr::new(""), "wrangler"), None);
1352 let tools = workspace.join("tools");
1353 let config = parse_config(&workspace.join("worker/wrangler.toml")).unwrap();
1354 point_at_entry(&config, None).unwrap();
1355 let pointed = fs::read_to_string(workspace.join("worker/wrangler.toml")).unwrap();
1356 assert!(
1357 pointed.contains(&format!("main = \"{ENTRY_FILE}\"")),
1358 "{pointed}"
1359 );
1360 assert!(!pointed.contains("inspector_port"), "{pointed}");
1361 let entry = fs::read_to_string(workspace.join("worker").join(ENTRY_FILE)).unwrap();
1362 point_at_entry(
1364 &parse_config(&workspace.join("worker/wrangler.toml")).unwrap(),
1365 None,
1366 )
1367 .unwrap();
1368 assert_eq!(
1369 fs::read_to_string(workspace.join("worker").join(ENTRY_FILE)).unwrap(),
1370 entry
1371 );
1372 assert!(entry.contains("./build/worker/shim.mjs"), "{entry}");
1373 fs::create_dir_all(&tools).unwrap();
1374 write_stand_in(&tools, Path::new("/opt/it's/supercov")).unwrap();
1375 let script = fs::read_to_string(tools.join("wrangler")).unwrap();
1376 assert_eq!(
1377 script,
1378 format!("#!/bin/sh\nexec '/opt/it'\\''s/supercov' {WRANGLER_COMMAND} \"$@\"\n")
1379 );
1380 fs::remove_dir_all(&workspace).unwrap();
1381 }
1382
1383 fn temporary(name: &str) -> PathBuf {
1384 let path = std::env::temp_dir().join(format!(
1385 "supercov-rust-worker-{}-{}-{name}",
1386 std::process::id(),
1387 SystemTime::now()
1388 .duration_since(UNIX_EPOCH)
1389 .unwrap()
1390 .as_nanos()
1391 ));
1392 fs::create_dir_all(&path).unwrap();
1393 path
1394 }
1395
1396 #[test]
1397 fn only_a_configuration_that_builds_with_worker_build_beside_a_crate_is_a_rust_worker() {
1398 let root = temporary("configs");
1399 let write = |relative: &str, text: &str| {
1400 let path = root.join(relative);
1401 fs::create_dir_all(path.parent().unwrap()).unwrap();
1402 fs::write(path, text).unwrap();
1403 };
1404 write(
1405 "worker/wrangler.toml",
1406 "main = \"build/worker/shim.mjs\"\n[build]\ncommand = \"cargo install -q worker-build && worker-build --release\"\n",
1407 );
1408 write("worker/Cargo.toml", "[package]\nname = \"w\"\n");
1409 write("js/wrangler.toml", "main = \"src/index.js\"\n");
1411 write(
1412 "orphan/wrangler.jsonc",
1413 "{ \"main\": \"x.mjs\", // c\n \"build\": { \"command\": \"worker-build\" } }",
1414 );
1415 write(
1416 "json/wrangler.jsonc",
1417 "{ \"main\": \"build/worker/shim.mjs\", /* c */ \"build\": { \"command\": \"worker-build --dev\", }, }",
1418 );
1419 write("json/Cargo.toml", "[package]\nname = \"j\"\n");
1420 write(
1421 "node_modules/dep/wrangler.toml",
1422 "main = \"m\"\n[build]\ncommand = \"worker-build\"\n",
1423 );
1424 write("node_modules/dep/Cargo.toml", "");
1425 let found = worker_configs(&root)
1426 .into_iter()
1427 .map(|path| path.strip_prefix(&root).unwrap().to_owned())
1428 .collect::<Vec<_>>();
1429 assert_eq!(
1430 found,
1431 [
1432 PathBuf::from("json/wrangler.jsonc"),
1433 PathBuf::from("worker/wrangler.toml")
1434 ]
1435 );
1436 fs::remove_dir_all(root).unwrap();
1437 }
1438
1439 #[test]
1440 fn the_copy_points_at_the_entry_and_an_inspector_port_and_keeps_the_rest() {
1441 let root = temporary("rewrite");
1442 let toml_path = root.join("wrangler.toml");
1443 fs::write(
1444 &toml_path,
1445 "# the Worker\nname = \"grades\"\nmain = \"build/worker/shim.mjs\"\n\n[build]\ncommand = \"worker-build --release\"\n",
1446 )
1447 .unwrap();
1448 let config = parse_config(&toml_path).unwrap();
1449 rewrite_config(&config, ENTRY_FILE, Some(9444)).unwrap();
1450 let rewritten = fs::read_to_string(&toml_path).unwrap();
1451 assert!(rewritten.contains("# the Worker"), "{rewritten}");
1452 let (_, document) = read_config(&toml_path).unwrap();
1453 assert_eq!(document["main"], ENTRY_FILE);
1454 assert_eq!(document["dev"]["inspector_port"], 9444);
1455 assert_eq!(document["build"]["command"], "worker-build --release");
1456
1457 let json_path = root.join("wrangler.jsonc");
1459 fs::write(
1460 &json_path,
1461 "{\n // mine\n \"main\": \"./build/worker/shim.mjs\",\n \"dev\": { \"inspector_port\": 9555 },\n}\n",
1462 )
1463 .unwrap();
1464 let config = parse_config(&json_path).unwrap();
1465 assert_eq!(config.inspector_port, Some(9555));
1466 assert_eq!(config.main, "./build/worker/shim.mjs");
1467 rewrite_config(&config, ENTRY_FILE, Some(1)).unwrap();
1468 let (_, document) = read_config(&json_path).unwrap();
1469 assert_eq!(document["main"], ENTRY_FILE);
1470 assert_eq!(document["dev"]["inspector_port"], 9555);
1471 fs::remove_dir_all(root).unwrap();
1472 }
1473
1474 #[test]
1475 fn a_window_is_rebuilt_from_its_header_and_touched_pages() {
1476 let encode = |bytes: &[u8]| base64::engine::general_purpose::STANDARD.encode(bytes);
1477 let value = json!({
1478 "start": 10,
1479 "end": 15,
1480 "regions": [["abc", 8192, [[0, encode(&[1, 2, 3])], [4096, encode(&[9; 4])]]]],
1481 });
1482 let window = window_from(&value).unwrap();
1483 assert_eq!((window.start, window.end), (10, 15));
1484 let (crate_key, bytes) = &window.regions[0];
1485 assert_eq!(crate_key, "abc");
1486 assert_eq!(bytes.len(), 8192);
1487 assert_eq!(&bytes[..4], &[1, 2, 3, 0]);
1488 assert_eq!(&bytes[4096..4101], &[9, 9, 9, 9, 0]);
1489 let bad = json!({"start": 0, "end": 1, "regions": [["a", 4, [[2, encode(&[1, 2, 3])]]]]});
1491 assert!(window_from(&bad).is_none());
1492 }
1493}