1use serde::{Deserialize, Serialize};
10use std::{fs, path::Path};
11
12#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
13#[serde(rename_all = "camelCase", tag = "kind")]
14pub enum RustProbeObservation {
15 Hit {
16 id: String,
17 },
18 Decision {
19 id: String,
20 values: Vec<Option<bool>>,
21 outcome: bool,
22 },
23 Assertion {
26 id: String,
27 },
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct RustProbeEntry {
35 pub thread: u64,
36 pub observation: RustProbeObservation,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum RustProbeReadError {
41 Io(String),
42 UnsafeEntry(String),
43 InvalidHeader,
44 InvalidRecord(usize),
45}
46
47impl std::fmt::Display for RustProbeReadError {
48 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Io(error) => write!(formatter, "Rust probe I/O failed: {error}"),
51 Self::UnsafeEntry(path) => write!(formatter, "unsafe Rust probe entry: {path}"),
52 Self::InvalidHeader => write!(formatter, "invalid Rust probe header"),
53 Self::InvalidRecord(line) => {
54 write!(formatter, "invalid Rust probe record at line {line}")
55 }
56 }
57 }
58}
59
60impl std::error::Error for RustProbeReadError {}
61
62const REGION_TEMPLATE: &str = include_str!("../runtime-assets/rust-region-runtime.rs");
63const REGION_MAGIC: &[u8; 8] = b"SCVRGN01";
64const REGION_VERSION: u32 = 1;
65
66pub fn render_rust_region_runtime(
71 module_name: &str,
72 token: &str,
73 crate_key: &str,
74 layout: &crate::rust_instrumenter::RustProbeLayout,
75 crate_cfgs: &[usize],
77) -> Result<String, String> {
78 let valid_identifier = !module_name.is_empty()
79 && module_name.bytes().enumerate().all(|(index, byte)| {
80 byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
81 });
82 if !valid_identifier {
83 return Err("invalid Rust runtime module name".into());
84 }
85 if token.is_empty() || token.len() > 16 || !token.bytes().all(|byte| byte.is_ascii_hexdigit()) {
86 return Err("invalid Rust region token".into());
87 }
88 if crate_key.is_empty() || !crate_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
89 return Err("invalid Rust runtime crate key".into());
90 }
91 let mut table = String::new();
92 for ordinal in crate_cfgs {
93 let gate = layout
94 .cfgs
95 .get(*ordinal)
96 .ok_or("a crate names a cfg gate the layout lacks")?;
97 let predicate = gate
99 .predicate
100 .parse::<cargo_platform::CfgExpr>()
101 .map_err(|error| format!("invalid cfg predicate {:?}: {error}", gate.predicate))?;
102 table.push_str(&format!("({ordinal}, cfg!({predicate})), "));
103 }
104 Ok(REGION_TEMPLATE
105 .replace("__SUPERCOV_MODULE__", module_name)
106 .replace("__SUPERCOV_HITS__", &layout.hits.len().to_string())
107 .replace(
108 "__SUPERCOV_ASSERTIONS__",
109 &layout.assertions.len().to_string(),
110 )
111 .replace(
112 "__SUPERCOV_DECISIONS__",
113 &layout.decisions.len().to_string(),
114 )
115 .replace("__SUPERCOV_CFGS__", &layout.cfgs.len().to_string())
116 .replace("__SUPERCOV_CFG_TABLE__", &table)
117 .replace("__SUPERCOV_TOKEN__", token)
118 .replace(
119 "__SUPERCOV_REGION_EXPORT__",
120 &format!("__supercov_region_{crate_key}"),
121 )
122 .replace(
123 "__SUPERCOV_CLEAR_EXPORT__",
124 &format!("__supercov_clear_{crate_key}"),
125 ))
126}
127
128struct RegionHeader {
129 entries_per_decision: usize,
130 body_at: usize,
131}
132
133fn region_header(
136 bytes: &[u8],
137 token: &str,
138 layout: &crate::rust_instrumenter::RustProbeLayout,
139) -> Result<RegionHeader, RustProbeReadError> {
140 const HEADER: usize = 128;
141 let u32_at = |offset: usize| -> Result<u32, RustProbeReadError> {
142 bytes
143 .get(offset..offset + 4)
144 .map(|word| u32::from_le_bytes(word.try_into().unwrap_or([0; 4])))
145 .ok_or(RustProbeReadError::InvalidHeader)
146 };
147 if bytes.get(..8) != Some(REGION_MAGIC.as_slice())
148 || u32_at(8)? != REGION_VERSION
149 || u32_at(12)? as usize != HEADER
150 || u32_at(16)? as usize != layout.hits.len()
151 || u32_at(20)? as usize != layout.assertions.len()
152 || u32_at(24)? as usize != layout.decisions.len()
153 || u32_at(72)? as usize != layout.cfgs.len()
154 {
155 return Err(RustProbeReadError::InvalidHeader);
156 }
157 let stored_token = bytes
158 .get(56..56 + token.len())
159 .ok_or(RustProbeReadError::InvalidHeader)?;
160 if stored_token != token.as_bytes() {
161 return Err(RustProbeReadError::InvalidHeader);
162 }
163 Ok(RegionHeader {
164 entries_per_decision: u32_at(28)? as usize,
165 body_at: u32_at(32)? as usize,
166 })
167}
168
169pub fn parse_rust_region(
174 bytes: &[u8],
175 overflow: Option<&[u8]>,
176 token: &str,
177 layout: &crate::rust_instrumenter::RustProbeLayout,
178) -> Result<Vec<RustProbeEntry>, RustProbeReadError> {
179 const HEADER: usize = 128;
180 const PAGE: usize = 4096;
181 const ENTRY: usize = 32;
182 let u64_at = |offset: usize| -> Option<u64> {
183 bytes
184 .get(offset..offset + 8)
185 .map(|word| u64::from_le_bytes(word.try_into().unwrap_or([0; 8])))
186 };
187 let RegionHeader {
188 entries_per_decision,
189 body_at,
190 } = region_header(bytes, token, layout)?;
191 let touched = |offset: usize| -> bool {
192 let page = (offset - body_at) / PAGE;
193 bytes
194 .get(HEADER + page / 8)
195 .is_some_and(|byte| byte & (1 << (page % 8)) != 0)
196 };
197 let slot = |offset: usize| -> Option<(u64, u64)> {
198 if !touched(offset) {
199 return None;
200 }
201 let value = u64_at(offset)?;
202 (value != 0).then_some((value >> 32, value & 0xffff_ffff))
203 };
204 let mut records = Vec::<(u64, RustProbeEntry)>::new();
205 let hits_at = body_at;
206 for (ordinal, id) in layout.hits.iter().enumerate() {
207 if let Some((sequence, thread)) = slot(hits_at + ordinal * 8) {
208 records.push((
209 sequence,
210 RustProbeEntry {
211 thread,
212 observation: RustProbeObservation::Hit { id: id.clone() },
213 },
214 ));
215 }
216 }
217 let assertions_at = hits_at + layout.hits.len() * 8;
218 for (ordinal, id) in layout.assertions.iter().enumerate() {
219 if let Some((sequence, thread)) = slot(assertions_at + ordinal * 8) {
220 records.push((
221 sequence,
222 RustProbeEntry {
223 thread,
224 observation: RustProbeObservation::Assertion { id: id.clone() },
225 },
226 ));
227 }
228 }
229 let decisions_at = assertions_at + layout.assertions.len() * 8;
230 let vector = |values: u128, width: usize| -> Vec<Option<bool>> {
231 (0..width)
232 .map(|index| match (values >> (2 * index)) & 3 {
233 1 => Some(false),
234 2 => Some(true),
235 _ => None,
236 })
237 .collect()
238 };
239 for (ordinal, id) in layout.decisions.iter().enumerate() {
240 for entry in 0..entries_per_decision {
241 let at = decisions_at + (ordinal * entries_per_decision + entry) * ENTRY;
242 if !touched(at) || bytes.get(at) != Some(&2) {
243 continue;
244 }
245 let outcome = bytes.get(at + 1).copied().unwrap_or(0) == 2;
246 let width = bytes.get(at + 2).copied().unwrap_or(0) as usize;
247 let values = bytes
248 .get(at + 16..at + 32)
249 .map(|word| u128::from_le_bytes(word.try_into().unwrap_or([0; 16])))
250 .unwrap_or(0);
251 let Some(first) = u64_at(at + 8).filter(|value| *value != 0) else {
252 continue;
253 };
254 records.push((
255 first >> 32,
256 RustProbeEntry {
257 thread: first & 0xffff_ffff,
258 observation: RustProbeObservation::Decision {
259 id: id.clone(),
260 values: vector(values, width),
261 outcome,
262 },
263 },
264 ));
265 }
266 }
267 for (index, line) in overflow
269 .unwrap_or_default()
270 .split(|byte| *byte == b'\n')
271 .filter(|line| !line.is_empty())
272 .enumerate()
273 {
274 let line =
275 std::str::from_utf8(line).map_err(|_| RustProbeReadError::InvalidRecord(index + 1))?;
276 let fields = line.split('\t').collect::<Vec<_>>();
277 let invalid = || RustProbeReadError::InvalidRecord(index + 1);
278 let [kind, sequence, thread, ordinal, values, outcome] = fields.as_slice() else {
279 continue;
281 };
282 if *kind != "D" {
283 return Err(invalid());
284 }
285 let id = layout
286 .decisions
287 .get(ordinal.parse::<usize>().map_err(|_| invalid())?)
288 .ok_or_else(invalid)?;
289 let values = values
290 .bytes()
291 .map(|digit| match digit {
292 b'1' => Ok(Some(false)),
293 b'2' => Ok(Some(true)),
294 b'0' => Ok(None),
295 _ => Err(invalid()),
296 })
297 .collect::<Result<Vec<_>, _>>()?;
298 records.push((
299 sequence.parse().map_err(|_| invalid())?,
300 RustProbeEntry {
301 thread: thread.parse().map_err(|_| invalid())?,
302 observation: RustProbeObservation::Decision {
303 id: id.clone(),
304 values,
305 outcome: *outcome == "1",
306 },
307 },
308 ));
309 }
310 records.sort_by_key(|(sequence, _)| *sequence);
311 Ok(records.into_iter().map(|(_, entry)| entry).collect())
312}
313
314pub fn parse_rust_region_cfgs(
318 bytes: &[u8],
319 token: &str,
320 layout: &crate::rust_instrumenter::RustProbeLayout,
321) -> Result<Vec<u8>, RustProbeReadError> {
322 let RegionHeader {
323 entries_per_decision,
324 body_at,
325 } = region_header(bytes, token, layout)?;
326 let cfgs_at = body_at
327 + layout.hits.len() * 8
328 + layout.assertions.len() * 8
329 + layout.decisions.len() * entries_per_decision * 32;
330 Ok((0..layout.cfgs.len())
331 .map(|ordinal| bytes.get(cfgs_at + ordinal).copied().unwrap_or(0) & 3)
332 .collect())
333}
334
335pub fn read_rust_cfg_settlement(
339 root: &Path,
340 token: &str,
341 layout: &crate::rust_instrumenter::RustProbeLayout,
342) -> Result<Vec<u8>, RustProbeReadError> {
343 let mut settled = vec![0_u8; layout.cfgs.len()];
344 if layout.cfgs.is_empty() {
345 return Ok(settled);
346 }
347 let mut directories = vec![root.to_path_buf()];
348 while let Some(directory) = directories.pop() {
349 let entries = match fs::read_dir(&directory) {
350 Ok(entries) => entries,
351 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
352 Err(error) => return Err(RustProbeReadError::Io(error.to_string())),
353 };
354 for entry in entries {
355 let entry = entry.map_err(|error| RustProbeReadError::Io(error.to_string()))?;
356 let kind = entry
357 .file_type()
358 .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
359 let name = entry.file_name().to_string_lossy().into_owned();
360 if kind.is_dir() {
361 directories.push(entry.path());
362 } else if kind.is_file()
363 && name.starts_with(&format!("{token}-"))
364 && name.ends_with(".region")
365 {
366 let bytes = fs::read(entry.path())
367 .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
368 for (slot, bits) in settled
369 .iter_mut()
370 .zip(parse_rust_region_cfgs(&bytes, token, layout)?)
371 {
372 *slot |= bits;
373 }
374 }
375 }
376 }
377 Ok(settled)
378}
379
380pub fn read_rust_region_directory(
385 directory: &Path,
386 token: &str,
387 layout: &crate::rust_instrumenter::RustProbeLayout,
388) -> Result<Vec<(String, Vec<RustProbeEntry>)>, RustProbeReadError> {
389 let mut files = Vec::new();
390 let entries = match fs::read_dir(directory) {
391 Ok(entries) => entries,
392 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
393 Err(error) => return Err(RustProbeReadError::Io(error.to_string())),
394 };
395 for entry in entries {
396 let entry = entry.map_err(|error| RustProbeReadError::Io(error.to_string()))?;
397 let name = entry.file_name().to_string_lossy().into_owned();
398 if !name.starts_with(&format!("{token}-")) || !name.ends_with(".region") {
399 continue;
400 }
401 if !entry
402 .file_type()
403 .map_err(|error| RustProbeReadError::Io(error.to_string()))?
404 .is_file()
405 {
406 return Err(RustProbeReadError::UnsafeEntry(name));
407 }
408 files.push(name);
409 }
410 files.sort();
411 let mut read = Vec::new();
412 for name in files {
413 let bytes = fs::read(directory.join(&name))
414 .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
415 let overflow_name = format!("{}.overflow", name.trim_end_matches(".region"));
416 let overflow = match fs::read(directory.join(&overflow_name)) {
417 Ok(bytes) => Some(bytes),
418 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
419 Err(error) => return Err(RustProbeReadError::Io(error.to_string())),
420 };
421 read.push((
422 name,
423 parse_rust_region(&bytes, overflow.as_deref(), token, layout)?,
424 ));
425 }
426 Ok(read)
427}
428
429#[cfg(test)]
430mod tests {
431 use std::{
432 fs,
433 process::Command,
434 time::{SystemTime, UNIX_EPOCH},
435 };
436
437 use super::*;
438 use crate::rust_instrumenter::{RustProbeLayout, instrument_rust_source};
439 use std::collections::BTreeMap;
440
441 const TOKEN: &str = "0123456789ab";
442
443 fn region_runtime(probes: &RustProbeLayout) -> String {
446 let crate_cfgs = (0..probes.cfgs.len()).collect::<Vec<_>>();
448 render_rust_region_runtime(
449 "__supercov_runtime_v1",
450 TOKEN,
451 "abcdef012345",
452 probes,
453 &crate_cfgs,
454 )
455 .unwrap()
456 }
457
458 fn read_regions(
460 evidence: &std::path::Path,
461 probes: &RustProbeLayout,
462 ) -> std::collections::BTreeMap<String, Vec<RustProbeEntry>> {
463 read_rust_region_directory(evidence, TOKEN, probes)
464 .unwrap()
465 .into_iter()
466 .collect()
467 }
468
469 fn temporary_directory(name: &str) -> std::path::PathBuf {
470 let nonce = SystemTime::now()
471 .duration_since(UNIX_EPOCH)
472 .unwrap()
473 .as_nanos();
474 let path = std::env::temp_dir().join(format!(
475 "supercov-rust-runtime-{}-{nonce}-{name}",
476 std::process::id()
477 ));
478 fs::create_dir(&path).unwrap();
479 path
480 }
481
482 #[test]
488 #[ignore = "needs a Docker daemon; run by scripts/rust-region-container.mjs"]
489 fn a_region_in_a_container_is_read_live_from_the_host() {
490 let source = r#"fn work(n: u32) -> u32 {
495 if n > 2 { n * 2 } else { n }
496}
497
498fn main() {
499 std::fs::write("/w/ready", "ready").unwrap();
500 while !std::path::Path::new("/w/go").exists() {
501 std::thread::sleep(std::time::Duration::from_millis(10));
502 }
503 println!("{}", work(5));
504 std::fs::write("/w/done", "done").unwrap();
505 std::thread::sleep(std::time::Duration::from_secs(120));
506}
507"#;
508 let transformed =
509 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
510 let runtime = region_runtime(&transformed.probes);
511 let directory = temporary_directory("container");
512 fs::write(
513 directory.join("main.rs"),
514 format!("{}\n{runtime}", transformed.code),
515 )
516 .unwrap();
517 let image =
518 std::env::var("SUPERCOV_RUST_IMAGE").unwrap_or_else(|_| "rust:1-bookworm".into());
519 let name = format!(
520 "supercov-region-{}-{}",
521 std::process::id(),
522 SystemTime::now()
523 .duration_since(UNIX_EPOCH)
524 .unwrap()
525 .as_nanos()
526 );
527 let started = Command::new("docker")
528 .args(["run", "-d", "--name", &name])
529 .arg("-v")
530 .arg(format!("{}:/w", directory.display()))
531 .args([
532 "-e",
533 "SUPERCOV_CONTAINER=1",
534 "-e",
535 "SUPERCOV_RUST_EVIDENCE_DIR=/w/evidence",
536 &image,
537 "sh",
538 "-c",
539 "cd /tmp && rustc --edition=2024 -O /w/main.rs -o program && ./program",
540 ])
541 .output()
542 .unwrap();
543 assert!(
544 started.status.success(),
545 "{}",
546 String::from_utf8_lossy(&started.stderr)
547 );
548 struct Removed(String);
549 impl Drop for Removed {
550 fn drop(&mut self) {
551 let _ = Command::new("docker").args(["rm", "-f", &self.0]).output();
552 }
553 }
554 let _container = Removed(name.clone());
555 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(600);
556 while !directory.join("ready").exists() {
557 assert!(
558 std::time::Instant::now() < deadline,
559 "the program never got ready: {}",
560 String::from_utf8_lossy(
561 &Command::new("docker")
562 .args(["logs", &name])
563 .output()
564 .unwrap()
565 .stderr
566 )
567 );
568 std::thread::sleep(std::time::Duration::from_millis(100));
569 }
570 std::thread::sleep(std::time::Duration::from_secs(6));
572 fs::write(directory.join("go"), "go").unwrap();
573 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
574 while !directory.join("done").exists() {
575 assert!(
576 std::time::Instant::now() < deadline,
577 "the program never finished"
578 );
579 std::thread::sleep(std::time::Duration::from_millis(5));
580 }
581 let evidence = directory.join("evidence");
584 let work = transformed
585 .manifest
586 .decisions
587 .iter()
588 .find(|decision| decision.line == 2)
589 .unwrap()
590 .id
591 .clone();
592 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
593 let (decisions, epoch) = loop {
594 let regions = read_regions(&evidence, &transformed.probes);
595 let decisions = regions
596 .values()
597 .flatten()
598 .filter(|entry| {
599 matches!(&entry.observation, RustProbeObservation::Decision { id, .. } if *id == work)
600 })
601 .count();
602 let epoch = fs::read_dir(&evidence)
603 .ok()
604 .and_then(|mut entries| entries.next())
605 .and_then(|entry| fs::read(entry.ok()?.path()).ok())
606 .and_then(|bytes| {
607 bytes
608 .get(48..52)
609 .map(|word| u32::from_le_bytes(word.try_into().unwrap()))
610 })
611 .unwrap_or(0);
612 if decisions > 0 && epoch > 0 || std::time::Instant::now() > deadline {
613 break (decisions, epoch);
614 }
615 std::thread::sleep(std::time::Duration::from_millis(50));
616 };
617 let running = Command::new("docker")
618 .args(["inspect", "-f", "{{.State.Running}}", &name])
619 .output()
620 .unwrap();
621 assert_eq!(String::from_utf8_lossy(&running.stdout).trim(), "true");
622 assert_eq!(
623 decisions, 1,
624 "the decision the program took is visible while it runs"
625 );
626 assert!(epoch > 0, "the flusher stamps its epoch");
627 drop(_container);
628 let _ = Command::new("docker")
629 .args(["run", "--rm", "-v"])
630 .arg(format!("{}:/w", directory.display()))
631 .args([
632 &image,
633 "rm",
634 "-rf",
635 "/w/evidence",
636 "/w/ready",
637 "/w/go",
638 "/w/done",
639 ])
640 .output();
641 fs::remove_dir_all(directory).ok();
642 }
643
644 #[test]
645 fn a_statement_after_one_the_target_strips_keeps_its_probe() {
646 let source = r#"fn width(value: u64) -> u32 {
650 #[cfg(target_endian = "little")]
651 let zero = value.leading_zeros();
652 #[cfg(target_endian = "big")]
653 let zero = value.trailing_zeros();
654
655 let bytes = 8 - zero / 8;
656 bytes
657}
658
659fn main() {
660 println!("{}", width(1));
661}
662"#;
663 let endian = if cfg!(target_endian = "little") {
664 "little"
665 } else {
666 "big"
667 };
668 let cfg =
669 crate::rust_instrumenter::TargetCfg::new(&[format!("target_endian=\"{endian}\"")
670 .parse::<cargo_platform::Cfg>()
671 .unwrap()]);
672 let transformed = crate::rust_instrumenter::instrument_rust_file(
673 "src/main.rs",
674 source,
675 "crate::__supercov_runtime_v1",
676 false,
677 Some(&cfg),
678 )
679 .unwrap();
680 let after = transformed
681 .manifest
682 .points
683 .iter()
684 .find(|point| point.line == 7)
685 .unwrap()
686 .id
687 .clone();
688 let runtime = region_runtime(&transformed.probes);
689 let directory = temporary_directory("stripped-neighbour");
690 let input = directory.join("main.rs");
691 let binary = directory.join("program");
692 let evidence = directory.join("evidence");
693 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
694 let compile = Command::new("rustc")
695 .arg("--edition=2024")
696 .arg(&input)
697 .arg("-o")
698 .arg(&binary)
699 .output()
700 .unwrap();
701 assert!(
702 compile.status.success(),
703 "{}",
704 String::from_utf8_lossy(&compile.stderr)
705 );
706 let output = Command::new(&binary)
707 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
708 .output()
709 .unwrap();
710 assert!(output.status.success());
711 let hits = read_regions(&evidence, &transformed.probes)
712 .into_values()
713 .flatten()
714 .filter_map(|entry| match entry.observation {
715 RustProbeObservation::Hit { id } => Some(id),
716 _ => None,
717 })
718 .collect::<Vec<_>>();
719 assert!(hits.contains(&after), "{after} not among {hits:?}");
720 fs::remove_dir_all(directory).unwrap();
721 }
722
723 #[cfg(unix)]
727 #[test]
728 fn what_a_program_ran_before_exec_is_kept() {
729 let source = r#"use std::os::unix::process::CommandExt;
730
731fn restore(key: &str) -> &'static str {
732 if key.starts_with("v1-") { "hit" } else { "miss" }
733}
734
735fn main() {
736 let outcome = restore("v1-abc");
737 let error = std::process::Command::new("/bin/sh")
738 .args(["-c", &format!("echo {outcome}")])
739 .exec();
740 panic!("exec failed: {error}");
741}
742"#;
743 let transformed =
744 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
745 let runtime = region_runtime(&transformed.probes);
746 let directory = temporary_directory("exec");
747 let input = directory.join("main.rs");
748 let binary = directory.join("program");
749 let evidence = directory.join("evidence");
750 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
751 let compile = Command::new("rustc")
752 .arg("--edition=2024")
753 .arg(&input)
754 .arg("-o")
755 .arg(&binary)
756 .output()
757 .unwrap();
758 assert!(
759 compile.status.success(),
760 "{}",
761 String::from_utf8_lossy(&compile.stderr)
762 );
763 let output = Command::new(&binary)
764 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
765 .output()
766 .unwrap();
767 assert!(output.status.success());
768 assert_eq!(output.stdout, b"hit\n");
769 let decisions = read_regions(&evidence, &transformed.probes)
770 .into_values()
771 .flatten()
772 .filter_map(|entry| match entry.observation {
773 RustProbeObservation::Decision {
774 values, outcome, ..
775 } => Some((values, outcome)),
776 _ => None,
777 })
778 .collect::<Vec<_>>();
779 assert_eq!(decisions, [(vec![Some(true)], true)]);
780 fs::remove_dir_all(directory).unwrap();
781 }
782
783 #[test]
784 fn the_runtime_records_which_gated_code_the_compiler_built() {
785 let source = r#"#[cfg(feature = "on")]
786fn built() -> u8 {
787 if std::env::args().count() > 99 { 1 } else { 2 }
788}
789
790#[cfg(feature = "off")]
791fn left_out() -> u8 {
792 3
793}
794
795#[cfg(not(feature = "on"))]
796fn not_on() -> u8 {
797 4
798}
799
800fn main() {
801 println!("{}", built());
802}
803"#;
804 let transformed =
805 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
806 let predicates = transformed
807 .probes
808 .cfgs
809 .iter()
810 .map(|gate| gate.predicate.as_str())
811 .collect::<Vec<_>>();
812 assert_eq!(
813 predicates,
814 [
815 "feature = \"on\"",
816 "feature = \"off\"",
817 "not(feature = \"on\")"
818 ]
819 );
820 let runtime = region_runtime(&transformed.probes);
821 let directory = temporary_directory("cfg-table");
822 let input = directory.join("main.rs");
823 let binary = directory.join("program");
824 let evidence = directory.join("evidence").join("test-1");
825 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
826 let compile = Command::new("rustc")
827 .args(["--edition=2024", "--cfg", "feature=\"on\""])
828 .arg(&input)
829 .arg("-o")
830 .arg(&binary)
831 .output()
832 .unwrap();
833 assert!(
834 compile.status.success(),
835 "{}",
836 String::from_utf8_lossy(&compile.stderr)
837 );
838 let output = Command::new(&binary)
839 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
840 .output()
841 .unwrap();
842 assert!(output.status.success());
843 assert_eq!(output.stdout, b"2\n");
844 let settled =
846 read_rust_cfg_settlement(&directory.join("evidence"), TOKEN, &transformed.probes)
847 .unwrap();
848 assert_eq!(settled, [2, 1, 1]);
849 let silent = directory.join("silent");
851 fs::create_dir(&silent).unwrap();
852 assert_eq!(
853 read_rust_cfg_settlement(&silent, TOKEN, &transformed.probes).unwrap(),
854 [0, 0, 0]
855 );
856 fs::remove_dir_all(directory).unwrap();
857 }
858
859 #[test]
860 fn generated_runtime_records_owned_points_and_exact_short_circuit_vectors() {
861 let source = r#"fn choose(first: bool, second: bool) -> i32 {
862 if first && second { 7 } else { 3 }
863}
864
865fn main() {
866 println!("{} {}", choose(false, true), choose(true, true));
867}
868"#;
869 let transformed =
870 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
871 let runtime = region_runtime(&transformed.probes);
872 let directory = temporary_directory("record");
873 let input = directory.join("main.rs");
874 let binary = directory.join("program");
875 let evidence = directory.join("evidence");
876 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
877 let compile = Command::new("rustc")
878 .arg("--edition=2024")
879 .arg(&input)
880 .arg("-o")
881 .arg(&binary)
882 .output()
883 .unwrap();
884 assert!(
885 compile.status.success(),
886 "{}",
887 String::from_utf8_lossy(&compile.stderr)
888 );
889 let output = Command::new(&binary)
890 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
891 .output()
892 .unwrap();
893 assert!(output.status.success());
894 assert_eq!(output.stdout, b"3 7\n");
895 let files = read_regions(&evidence, &transformed.probes);
896 assert_eq!(files.len(), 1);
897 let observations = files.values().next().unwrap();
898 let decisions = observations
899 .iter()
900 .filter_map(|entry| match &entry.observation {
901 RustProbeObservation::Decision {
902 values, outcome, ..
903 } => Some((values.clone(), *outcome)),
904 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
905 })
906 .collect::<Vec<_>>();
907 assert_eq!(
908 decisions,
909 [
910 (vec![Some(false), None], false),
911 (vec![Some(true), Some(true)], true)
912 ]
913 );
914 fs::remove_dir_all(directory).unwrap();
915 }
916
917 #[test]
918 fn generated_runtime_compiles_into_a_no_std_host_crate() {
919 let source = r#"#![no_std]
926
927extern crate std;
928
929fn choose(first: bool, second: bool) -> i32 {
930 if first && second { 7 } else { 3 }
931}
932
933fn main() {
934 std::println!("{} {}", choose(false, true), choose(true, true));
935}
936"#;
937 let transformed =
938 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
939 let runtime = region_runtime(&transformed.probes);
940 let directory = temporary_directory("no-std");
941 let input = directory.join("main.rs");
942 let binary = directory.join("program");
943 let evidence = directory.join("evidence");
944 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
945 let compile = Command::new("rustc")
946 .arg("--edition=2024")
947 .arg(&input)
948 .arg("-o")
949 .arg(&binary)
950 .output()
951 .unwrap();
952 assert!(
953 compile.status.success(),
954 "{}",
955 String::from_utf8_lossy(&compile.stderr)
956 );
957 let output = Command::new(&binary)
958 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
959 .output()
960 .unwrap();
961 assert!(output.status.success());
962 assert_eq!(output.stdout, b"3 7\n");
963 let files = read_regions(&evidence, &transformed.probes);
965 assert_eq!(files.len(), 1);
966 assert!(!files.values().next().unwrap().is_empty());
967 fs::remove_dir_all(directory).unwrap();
968 }
969
970 #[test]
971 fn probes_reached_through_a_global_allocator_do_not_recurse() {
972 let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
980use std::sync::atomic::{AtomicUsize, Ordering};
981
982static SEEN: AtomicUsize = AtomicUsize::new(0);
983
984fn note(size: usize) {
985 if size > 0 {
986 SEEN.fetch_add(1, Ordering::SeqCst);
987 }
988}
989
990struct Ledger;
991
992impl Ledger {
993 fn record(&self, size: usize) {
994 note(size);
995 }
996}
997
998unsafe impl GlobalAlloc for Ledger {
999 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1000 self.record(layout.size());
1001 System.alloc(layout)
1002 }
1003
1004 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
1005 // dealloc must be instrumented too, or the test never exercises the
1006 // ladder that actually crashed: freeing the probe's OWN buffer re-enters
1007 // here, and a guard released before that free recurses without bound.
1008 self.record(layout.size());
1009 System.dealloc(pointer, layout);
1010 }
1011}
1012
1013#[global_allocator]
1014static LEDGER: Ledger = Ledger;
1015
1016fn classify(flag: bool) -> usize {
1017 if flag { 1 } else { 2 }
1018}
1019
1020fn main() {
1021 let held = std::vec![7u8; 32];
1022 println!("{} {}", classify(!held.is_empty()), held.len());
1023}
1024"#;
1025 let transformed =
1026 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1027 assert!(
1030 transformed
1031 .code
1032 .contains("fn note(size: usize) {crate::__supercov_runtime_v1::hit("),
1033 "the free function reached from the allocator should still be probed"
1034 );
1035 let runtime = region_runtime(&transformed.probes);
1036 let directory = temporary_directory("allocator-reentry");
1037 let input = directory.join("main.rs");
1038 let binary = directory.join("program");
1039 let evidence = directory.join("evidence");
1040 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1041 let compile = Command::new("rustc")
1042 .arg("--edition=2024")
1043 .arg(&input)
1044 .arg("-o")
1045 .arg(&binary)
1046 .output()
1047 .unwrap();
1048 assert!(
1049 compile.status.success(),
1050 "{}",
1051 String::from_utf8_lossy(&compile.stderr)
1052 );
1053 let output = Command::new(&binary)
1054 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1055 .output()
1056 .unwrap();
1057 assert!(
1058 output.status.success(),
1059 "instrumented allocator did not survive: {:?}",
1060 output.status
1061 );
1062 assert_eq!(output.stdout, b"1 32\n");
1063 let files = read_regions(&evidence, &transformed.probes);
1064 let observations = files.values().next().unwrap();
1065 let decisions = observations
1066 .iter()
1067 .filter_map(|entry| match &entry.observation {
1068 RustProbeObservation::Decision {
1069 id,
1070 values,
1071 outcome,
1072 } => Some((id.clone(), values.clone(), *outcome)),
1073 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1074 })
1075 .collect::<Vec<_>>();
1076 let classify = transformed
1079 .manifest
1080 .decisions
1081 .iter()
1082 .find(|decision| decision.source == "flag")
1083 .expect("classify's decision reached the manifest");
1084 assert!(
1085 decisions
1086 .iter()
1087 .any(|(id, values, outcome)| id == &classify.id
1088 && values == &[Some(true)]
1089 && *outcome),
1090 "classify's decision was lost: {decisions:?}"
1091 );
1092 let note = transformed
1095 .manifest
1096 .decisions
1097 .iter()
1098 .find(|decision| decision.source == "size > 0")
1099 .expect("note's decision reached the manifest");
1100 assert!(decisions.iter().any(|(id, ..)| id == ¬e.id));
1101 assert!(
1104 decisions.iter().all(|(_, values, _)| values.len() == 1),
1105 "a suppressed frame emitted a malformed vector: {decisions:?}"
1106 );
1107 fs::remove_dir_all(directory).unwrap();
1108 }
1109
1110 #[test]
1111 fn a_hit_in_a_loop_is_written_once_but_decisions_keep_every_vector() {
1112 let source = r#"fn step(value: usize) -> bool {
1118 let doubled = value * 2;
1119 doubled > 4
1120}
1121
1122fn main() {
1123 let mut seen = 0;
1124 for value in 0..64 {
1125 if step(value) { seen += 1; }
1126 }
1127 println!("{seen}");
1128}
1129"#;
1130 let transformed =
1131 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1132 let runtime = region_runtime(&transformed.probes);
1133 let directory = temporary_directory("dedup");
1134 let input = directory.join("main.rs");
1135 let binary = directory.join("program");
1136 let evidence = directory.join("evidence");
1137 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1138 let compile = Command::new("rustc")
1139 .arg("--edition=2024")
1140 .arg(&input)
1141 .arg("-o")
1142 .arg(&binary)
1143 .output()
1144 .unwrap();
1145 assert!(
1146 compile.status.success(),
1147 "{}",
1148 String::from_utf8_lossy(&compile.stderr)
1149 );
1150 let output = Command::new(&binary)
1151 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1152 .output()
1153 .unwrap();
1154 assert_eq!(output.stdout, b"61\n");
1155 let files = read_regions(&evidence, &transformed.probes);
1156 let observations = files.values().next().unwrap();
1157
1158 let mut hits = BTreeMap::<&str, usize>::new();
1160 for entry in observations {
1161 if let RustProbeObservation::Hit { id } = &entry.observation {
1162 *hits.entry(id.as_str()).or_default() += 1;
1163 }
1164 }
1165 assert!(!hits.is_empty(), "no hits recorded at all");
1166 assert!(
1167 hits.values().all(|count| *count == 1),
1168 "a repeated hit was written more than once: {hits:?}"
1169 );
1170
1171 let decisions = observations
1176 .iter()
1177 .filter_map(|entry| match &entry.observation {
1178 RustProbeObservation::Decision {
1179 values, outcome, ..
1180 } => Some((values.clone(), *outcome)),
1181 RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1182 })
1183 .collect::<Vec<_>>();
1184 assert_eq!(
1185 decisions,
1186 [(vec![Some(false)], false), (vec![Some(true)], true)],
1189 "both distinct vectors must survive, and neither may repeat"
1190 );
1191 fs::remove_dir_all(directory).unwrap();
1192 }
1193
1194 #[test]
1195 fn generated_runtime_survives_a_host_without_the_prelude() {
1196 let source = r#"#![no_implicit_prelude]
1201
1202fn parse(text: &str) -> ::std::option::Option<i32> {
1203 let value: i32 = text.parse().ok()?;
1204 ::std::option::Option::Some(value * 2)
1205}
1206
1207fn main() {
1208 ::std::println!("{:?} {:?}", parse("4"), parse("x"));
1209}
1210"#;
1211 let transformed =
1212 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1213 assert!(transformed.code.contains("TryProbe"));
1215 let runtime = region_runtime(&transformed.probes);
1216 let directory = temporary_directory("no-implicit-prelude");
1217 let input = directory.join("main.rs");
1218 let binary = directory.join("program");
1219 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1220 let compile = Command::new("rustc")
1221 .arg("--edition=2024")
1222 .arg("--cap-lints=allow")
1223 .arg(&input)
1224 .arg("-o")
1225 .arg(&binary)
1226 .output()
1227 .unwrap();
1228 assert!(
1229 compile.status.success(),
1230 "{}",
1231 String::from_utf8_lossy(&compile.stderr)
1232 );
1233 let output = Command::new(&binary).output().unwrap();
1234 assert_eq!(output.stdout, b"Some(8) None\n");
1235 fs::remove_dir_all(directory).unwrap();
1236 }
1237
1238 #[test]
1239 fn generated_runtime_survives_a_deny_warnings_host() {
1240 let source = r#"#![deny(warnings)]
1245
1246fn choose(first: bool, second: bool) -> i32 {
1247 if first && second { 7 } else { 3 }
1248}
1249
1250fn main() {
1251 println!("{} {}", choose(false, true), choose(true, true));
1252}
1253"#;
1254 let transformed =
1255 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1256 let runtime = region_runtime(&transformed.probes);
1257 let directory = temporary_directory("deny-warnings");
1258 let input = directory.join("main.rs");
1259 let binary = directory.join("program");
1260 fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1261 let compile = Command::new("rustc")
1265 .arg("--edition=2024")
1266 .arg("--cap-lints=allow")
1267 .arg(&input)
1268 .arg("-o")
1269 .arg(&binary)
1270 .output()
1271 .unwrap();
1272 assert!(
1273 compile.status.success(),
1274 "{}",
1275 String::from_utf8_lossy(&compile.stderr)
1276 );
1277 let output = Command::new(&binary).output().unwrap();
1278 assert_eq!(output.stdout, b"3 7\n");
1279 fs::remove_dir_all(directory).unwrap();
1280 }
1281
1282 fn layout(hits: &[&str], decisions: &[&str]) -> RustProbeLayout {
1283 RustProbeLayout {
1284 token: TOKEN.into(),
1285 hits: hits.iter().map(|id| (*id).to_owned()).collect(),
1286 assertions: Vec::new(),
1287 decisions: decisions.iter().map(|id| (*id).to_owned()).collect(),
1288 cfgs: Vec::new(),
1289 crates: Default::default(),
1290 }
1291 }
1292
1293 #[test]
1294 fn a_region_of_another_layout_or_instrumentation_is_refused() {
1295 let probes = layout(&["rs:point:a"], &[]);
1296 assert_eq!(
1297 parse_rust_region(b"not a region", None, TOKEN, &probes),
1298 Err(RustProbeReadError::InvalidHeader)
1299 );
1300 let mut bytes = vec![0u8; 4096];
1302 bytes[..8].copy_from_slice(b"SCVRGN01");
1303 bytes[8..12].copy_from_slice(&1u32.to_le_bytes());
1304 bytes[12..16].copy_from_slice(&128u32.to_le_bytes());
1305 bytes[16..20].copy_from_slice(&2u32.to_le_bytes());
1306 bytes[56..56 + TOKEN.len()].copy_from_slice(TOKEN.as_bytes());
1307 assert_eq!(
1308 parse_rust_region(&bytes, None, TOKEN, &probes),
1309 Err(RustProbeReadError::InvalidHeader)
1310 );
1311 bytes[16..20].copy_from_slice(&1u32.to_le_bytes());
1313 assert_eq!(
1314 parse_rust_region(&bytes, None, "ba9876543210", &probes),
1315 Err(RustProbeReadError::InvalidHeader)
1316 );
1317 assert_eq!(
1318 parse_rust_region(&bytes, None, TOKEN, &probes),
1319 Ok(Vec::new())
1320 );
1321 }
1322
1323 #[test]
1324 fn the_directory_reader_takes_only_this_instrumentations_regular_files() {
1325 let directory = temporary_directory("regions");
1326 let probes = layout(&["rs:point:a"], &[]);
1327 fs::write(
1328 directory.join("ba9876543210-7.region"),
1329 b"another instrumentation",
1330 )
1331 .unwrap();
1332 fs::write(directory.join("notes.txt"), b"unrelated").unwrap();
1333 assert_eq!(
1334 read_rust_region_directory(&directory, TOKEN, &probes),
1335 Ok(Vec::new())
1336 );
1337 fs::create_dir(directory.join(format!("{TOKEN}-9.region"))).unwrap();
1338 assert!(matches!(
1339 read_rust_region_directory(&directory, TOKEN, &probes),
1340 Err(RustProbeReadError::UnsafeEntry(_))
1341 ));
1342 assert!(
1343 read_rust_region_directory(&directory.join("absent"), TOKEN, &probes)
1344 .unwrap()
1345 .is_empty()
1346 );
1347 fs::remove_dir_all(directory).unwrap();
1348 }
1349
1350 #[test]
1351 fn combinations_beyond_a_decisions_entries_overflow_and_are_all_read_back() {
1352 let source = r#"fn all(a: bool, b: bool, c: bool, d: bool, e: bool) -> bool {
1355 if (a || b) && (c || d) && e { true } else { false }
1356}
1357
1358fn main() {
1359 let mut taken = 0;
1360 for bits in 0..32u8 {
1361 for _ in 0..3 {
1362 if all(bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0, bits & 16 != 0) {
1363 taken += 1;
1364 }
1365 }
1366 }
1367 println!("{taken}");
1368}
1369"#;
1370 let transformed =
1371 instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1372 let directory = temporary_directory("overflow");
1373 let input = directory.join("main.rs");
1374 let binary = directory.join("program");
1375 let evidence = directory.join("evidence");
1376 fs::write(
1377 &input,
1378 format!(
1379 "{}\n{}",
1380 transformed.code,
1381 region_runtime(&transformed.probes)
1382 ),
1383 )
1384 .unwrap();
1385 let compile = Command::new("rustc")
1386 .arg("--edition=2024")
1387 .arg(&input)
1388 .arg("-o")
1389 .arg(&binary)
1390 .output()
1391 .unwrap();
1392 assert!(
1393 compile.status.success(),
1394 "{}",
1395 String::from_utf8_lossy(&compile.stderr)
1396 );
1397 let output = Command::new(&binary)
1398 .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1399 .output()
1400 .unwrap();
1401 assert!(output.status.success());
1402 assert_eq!(output.stdout, b"27\n");
1403 let mut vectors = read_regions(&evidence, &transformed.probes)
1404 .into_values()
1405 .flatten()
1406 .filter_map(|entry| match entry.observation {
1407 RustProbeObservation::Decision {
1409 values, outcome, ..
1410 } if values.len() == 5 => Some((values, outcome)),
1411 _ => None,
1412 })
1413 .collect::<Vec<_>>();
1414 let total = vectors.len();
1415 vectors.sort();
1416 vectors.dedup();
1417 assert_eq!(total, vectors.len(), "a combination was recorded twice");
1420 assert_eq!(vectors.len(), 11, "{vectors:?}");
1421 let overflow = fs::read_dir(&evidence)
1423 .unwrap()
1424 .flatten()
1425 .find(|entry| entry.file_name().to_string_lossy().ends_with(".overflow"));
1426 let overflow = overflow.expect("the combinations past eight overflowed");
1427 {
1428 let mut bytes = fs::read(overflow.path()).unwrap();
1429 bytes.extend_from_slice(b"D\t99\t1");
1430 fs::write(overflow.path(), bytes).unwrap();
1431 assert!(read_rust_region_directory(&evidence, TOKEN, &transformed.probes).is_ok());
1432 }
1433 fs::remove_dir_all(directory).unwrap();
1434 }
1435}