Skip to main content

supercov_engine/
rust_worker.rs

1//! A Rust Worker a browser suite drives through `wrangler dev`.
2//!
3//! The crate is instrumented in the suite's copy and `worker-build` compiles
4//! it to wasm, where the region runtime keeps its region in linear memory
5//! behind two exports per crate: its address and a clear. Supercov points the
6//! copy's wrangler configuration at an entry module that remembers every wasm
7//! instance the isolate creates, and at an inspector port of its own, so the
8//! suite's own `wrangler dev` command is left as written. While the suite
9//! runs, a poller takes each instance's region through the inspector and
10//! clears it every few milliseconds. Each take is a window, credited by time
11//! to the one browser test whose span it overlaps, as for a Go or JVM server;
12//! what no single test's window holds is the run's background.
13
14use 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
39/// The module wrangler bundles in place of the project's own entry: it
40/// installs the registry, then is the project's entry.
41const ENTRY_FILE: &str = "supercov-worker-entry.mjs";
42const REGISTRY_FILE: &str = "supercov-worker-registry.mjs";
43
44/// Remember every wasm instance the isolate creates, for the inspector:
45/// workerd answers no heap queries, so instances cannot be found later.
46const 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
64/// Evaluated in the Worker's context on every poll: each instrumented
65/// crate's region as its header and the pages a probe touched since the
66/// last take, then cleared. A region with nothing new is left out, except
67/// the first time, which carries what `cfg!` said of the crate's gates.
68const 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
105/// How often the poller takes the regions. A window shorter than the gap
106/// between two tests is what lets each be credited its own.
107const POLL_INTERVAL: Duration = Duration::from_millis(5);
108
109/// A wrangler configuration Supercov can point at its entry.
110#[derive(Debug, Clone, PartialEq, Eq)]
111struct WorkerConfig {
112    path: PathBuf,
113    main: String,
114    inspector_port: Option<u16>,
115}
116
117/// The wrangler configurations under `root` that build a Rust Worker: a
118/// `[build] command` running `worker-build`, beside the crate's Cargo.toml.
119///
120/// A plain walk: a `.gitignore` above the project (a monorepo's, or the
121/// checkout a fixture sits in) must not hide the project itself.
122fn 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
170/// A configuration as JSON whatever it was written in.
171fn 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
212/// JSON with comments and trailing commas, as wrangler.jsonc allows, made
213/// plain JSON. Strings are copied as they are.
214fn 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    // A comma before a closing bracket, past any whitespace.
256    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
287/// Point the copy's configuration at the entry module and, given one, the
288/// inspector port. The rest of the configuration is the project's.
289fn 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
322/// Write the registry and Supercov's entry beside a configuration and point
323/// the configuration at it. A configuration that already names the entry --
324/// a copy kept from an earlier run -- is left as it is: the entry would
325/// otherwise import itself.
326fn 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
340/// A port nothing listens on now.
341fn 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
349/// The cfg values of wasm32-unknown-unknown, as the crate's own toolchain
350/// reports them: what decides which items the target leaves out.
351pub(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
376/// Takes the regions of one `wrangler dev` through its inspector until told
377/// to stop, appending a window per take with anything in it.
378struct 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    /// Take once more if wrangler is still there, then stop.
395    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    // wrangler starts when the suite asks for it, and restarts the Worker on
419    // a rebuild: connect, and connect again, until the run is over.
420    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
438/// One inspector connection: enable the runtime, learn the Worker's
439/// execution context, and take its regions on the clock. Returns when the
440/// connection ends, or after the last take once the run is over.
441fn 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    // The take in flight: its id, when it was sent, and where its window
454    // starts.
455    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        // A take the Worker never answered (it was replaced mid-evaluation)
527        // is given up; the next one covers the same span.
528        if awaiting.is_some_and(|(_, sent)| sent.elapsed() > Duration::from_secs(5)) {
529            awaiting = None;
530        }
531    }
532}
533
534/// One take as the poller appended it.
535struct Window {
536    start: i64,
537    end: i64,
538    /// Each region, rebuilt: crate key and bytes.
539    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        // A line cut short by Supercov being stopped mid-write is skipped.
561        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
597/// The Rust Workers of one Cargo workspace a browser suite may start.
598pub 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    /// `.supercov/work/<run>/rust-worker`, holding `evidence/`.
605    directory: PathBuf,
606}
607
608impl RustWorkerServer {
609    /// Whether `root` holds a Rust Worker a suite could start.
610    pub fn present(root: &Path) -> bool {
611        !worker_configs(root).is_empty()
612    }
613
614    /// Instrument the copy's Worker crates and point their configurations at
615    /// Supercov's entry, one server per Cargo workspace. Empty when the
616    /// project has no Rust Worker.
617    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        // Configurations by the Cargo workspace their crate belongs to: a
626        // workspace is instrumented once, whatever number of Workers it holds.
627        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            // The project's inputs, as a later query recomputes them to judge
651            // whether the run is current.
652            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    /// The source this Worker was instrumented from: an image built from an
705    /// earlier copy holds probes placed in other source.
706    pub fn source_fingerprint(&self) -> String {
707        self.integrity.fingerprint.source.clone()
708    }
709
710    /// What the suite's environment needs: a lint on the rewritten source
711    /// must not fail the Worker's build under the crate's own lint policy.
712    ///
713    /// Capped at `warn`, not `allow` as for `cargo test`: Cargo learns which
714    /// crate types a target supports from rustc's "dropping unsupported crate
715    /// type" warnings, and wasm32 drops `dylib` and `proc-macro`, so with
716    /// every lint allowed it cannot read the answer and stops the build. The
717    /// probes' parentheses, the one lint they raise, are allowed by name.
718    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    /// Publish what the Workers ran as a Rust run of its own: each window
728    /// credited, by time, to the one browser test whose span it overlaps;
729    /// what no single test's does is the run's background. None when no
730    /// Worker ran or no browser test reported.
731    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
785/// The program Supercov's `wrangler` stands in for, named in the test
786/// processes' environment.
787pub const REAL_WRANGLER: &str = "SUPERCOV_WRANGLER";
788/// The hidden commands the stand-in and its poller run as.
789pub const WRANGLER_COMMAND: &str = "__wrangler";
790pub const WRANGLER_POLL_COMMAND: &str = "__wrangler-poll";
791
792/// The Rust Workers a `cargo test` suite starts itself, with `wrangler dev`.
793///
794/// The crate is instrumented with the rest of the workspace, and `wrangler
795/// dev` builds it in the copy as it builds it anywhere. What it ran is read
796/// by a `wrangler` of Supercov's, first on the test processes' `PATH`: it
797/// starts a poller on an inspector port of its own and then becomes the real
798/// `wrangler`, so the test keeps the process and the signals it expects. The
799/// poller writes what it takes into the evidence directory of the test that
800/// started the Worker, and the run reads it there as that test's own: exact,
801/// not credited by time.
802pub struct CargoTestWorkers {
803    environment: Vec<(String, std::ffi::OsString)>,
804}
805
806impl CargoTestWorkers {
807    /// Point the copy's Worker configurations at Supercov's entry and put the
808    /// stand-in in `tools`. None where the workspace has no Rust Worker, or
809    /// where no `wrangler` is installed: a test that looks for one must find
810    /// none, as it would without Supercov.
811    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    /// What each test process is started with.
841    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
848/// A program on `path`: a file of that name, or on Windows one with a
849/// PATHEXT extension.
850fn 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
894/// `wrangler dev`, with options before or after the command: wrangler's
895/// global options that take a value (`--cwd worker dev`) are read past.
896fn 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
909/// `wrangler dev --remote` (or `-r`): the Worker runs on Cloudflare's edge,
910/// and wrangler opens no inspector into it there, so nothing can read what
911/// it runs.
912fn 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
921/// The name of the note a test's `wrangler dev --remote` leaves in its
922/// evidence.
923const REMOTE_NOTE: &str = "worker-remote-";
924
925/// Whether a test, by its evidence, started a Worker with `wrangler dev
926/// --remote`.
927pub 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
938/// The inspector port the arguments name, as `--inspector-port N` or
939/// `--inspector-port=N`.
940fn 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
953/// Supercov's `wrangler`: under a test, `wrangler dev` gets an inspector
954/// port and a poller writing into the test's evidence; then this process
955/// becomes the real `wrangler`. The build it runs caps lints at `warn`, as
956/// for a Worker under a browser suite.
957pub 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            // No poller: the run says why the Worker has no coverage.
969            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
999/// Becomes the real `wrangler`, so the test keeps the process and the
1000/// signals it expects; where it cannot (Windows), runs it to its end.
1001fn 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
1030/// The flags the Worker's wasm build gets: the test's own, with lints capped
1031/// at `warn`. A cap the test inherited is dropped first: under nextest the
1032/// tests inherit Supercov's `--cap-lints=allow`, and with it Cargo cannot
1033/// learn wasm32's crate types ("output of --print=file-names missing"), even
1034/// with a `warn` after it.
1035fn 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
1049/// The poller of one `wrangler dev` a test started: takes through `port`
1050/// while process `pid` -- that wrangler -- lives, appending to a `.part`
1051/// file it renames to `windows` when done, so the run knows it is whole.
1052pub 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    // SAFETY: signal 0 only asks whether the process exists.
1087    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
1096/// How long the run waits for a poller to finish after its test: the poller
1097/// notices wrangler is gone within a few polls.
1098const POLLER_SETTLE: Duration = Duration::from_secs(3);
1099
1100/// The Worker takes in a test's evidence directory, written out as region
1101/// files the run reads as it reads a native process's; and the crates they
1102/// came from, which `wrangler dev` built though the run's own build did not.
1103/// A take whose region names another instrumentation is not this run's.
1104pub 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    // A poller still writing (its wrangler outlived the test) has given what
1132    // it has so far: every line it wrote is whole.
1133    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(&region, &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
1166/// The instrumentation token a region's header carries.
1167pub(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
1176/// The root of the Cargo workspace `directory`'s crate belongs to.
1177/// The directories, relative to `workspace` and `/`-separated, of the Rust
1178/// Workers in it: each wrangler configuration that builds with worker-build.
1179pub(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    /// A take becomes a region file of the test's own evidence, named by the
1274    /// run's token, and says which crate it came from; a take of another
1275    /// instrumentation, or a line cut short, is left out.
1276    #[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        // Read once: a second pass finds nothing new.
1315        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    /// A workspace with a Worker and a `wrangler` on PATH gets the stand-in,
1326    /// and its configuration points at Supercov's entry once: a copy kept
1327    /// from an earlier run is not pointed at itself. Without a `wrangler`,
1328    /// nothing is put first on PATH.
1329    #[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        // Again, as on a kept copy: the entry still names the project's.
1363        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        // JavaScript Workers and a Rust one without its crate are not.
1410        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        // A port the author chose is theirs, and the poller uses it.
1458        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        // A segment past the region is a malformed window, not a panic.
1490        let bad = json!({"start": 0, "end": 1, "regions": [["a", 4, [[2, encode(&[1, 2, 3])]]]]});
1491        assert!(window_from(&bad).is_none());
1492    }
1493}