use std::collections::BTreeSet;
use anyhow::{bail, Result};
pub const INTRODUCER: &str = concat!("omni-dev", ": coverage");
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MarkerKind {
Ignore,
Tolerate,
}
impl MarkerKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Ignore => "ignore",
Self::Tolerate => "tolerate",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Region {
pub kind: MarkerKind,
pub start: u32,
pub end: u32,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileMarkers {
pub ignored: BTreeSet<u32>,
pub tolerated: BTreeSet<u32>,
pub regions: Vec<Region>,
}
impl FileMarkers {
pub fn new(regions: Vec<Region>) -> Self {
let mut ignored = BTreeSet::new();
let mut tolerated = BTreeSet::new();
for region in ®ions {
let set = match region.kind {
MarkerKind::Ignore => &mut ignored,
MarkerKind::Tolerate => &mut tolerated,
};
set.extend(region.start..=region.end);
}
Self {
ignored,
tolerated,
regions,
}
}
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
}
const KEYWORDS: &[(&str, MarkerKind, bool)] = &[
("ignore-line", MarkerKind::Ignore, true),
("tolerate-line", MarkerKind::Tolerate, true),
("ignore", MarkerKind::Ignore, false),
("tolerate", MarkerKind::Tolerate, false),
];
struct Open {
kind: MarkerKind,
start: u32,
reason: String,
}
pub fn scan(path: &str, text: &str) -> Result<Vec<Region>> {
if !text.contains(INTRODUCER) {
return Ok(Vec::new());
}
let mut regions = Vec::new();
let mut open: Option<Open> = None;
for (index, raw) in text.lines().enumerate() {
let line = u32::try_from(index + 1).unwrap_or(u32::MAX);
let Some(rest) = raw.trim_end_matches('\r').split(INTRODUCER).nth(1) else {
continue;
};
let rest = rest.trim_start();
if let Some(tail) = strip_keyword(rest, "end") {
if !tail.trim().is_empty() {
bail!(
"{path}:{line}: unexpected text after `{INTRODUCER} end`: `{}`",
tail.trim()
);
}
let Some(open) = open.take() else {
bail!("{path}:{line}: `{INTRODUCER} end` without a matching region start");
};
regions.push(Region {
kind: open.kind,
start: open.start,
end: line,
reason: open.reason,
});
continue;
}
let Some((keyword, kind, single)) = KEYWORDS
.iter()
.find(|(keyword, _, _)| strip_keyword(rest, keyword).is_some())
.copied()
else {
bail!(
"{path}:{line}: unrecognised coverage marker `{INTRODUCER} {}` \
(expected `ignore`, `tolerate`, `ignore-line`, `tolerate-line`, or `end`)",
rest.split_whitespace().next().unwrap_or("")
);
};
let tail = strip_keyword(rest, keyword).unwrap_or("");
let reason = parse_reason(path, line, keyword, tail)?;
if single {
regions.push(Region {
kind,
start: line,
end: line,
reason,
});
continue;
}
if let Some(previous) = &open {
bail!(
"{path}:{line}: nested coverage region; the `{}` region opened at line {} is \
still open (regions may not overlap)",
previous.kind.as_str(),
previous.start
);
}
open = Some(Open {
kind,
start: line,
reason,
});
}
if let Some(open) = open {
bail!(
"{path}:{}: unterminated `{INTRODUCER} {}` region (add `{INTRODUCER} end`)",
open.start,
open.kind.as_str()
);
}
Ok(regions)
}
fn strip_keyword<'a>(rest: &'a str, keyword: &str) -> Option<&'a str> {
let tail = rest.strip_prefix(keyword)?;
if tail.is_empty() || tail.starts_with(|c: char| c.is_whitespace()) {
Some(tail)
} else {
None
}
}
fn parse_reason(path: &str, line: u32, keyword: &str, tail: &str) -> Result<String> {
let tail = tail.trim();
let Some(after) = tail.split_once("reason=\"").map(|(_, after)| after) else {
bail!(
"{path}:{line}: `{INTRODUCER} {keyword}` needs a reason \
(write `{INTRODUCER} {keyword} reason=\"why this is silenced\"`)"
);
};
let Some((reason, _)) = after.split_once('"') else {
bail!("{path}:{line}: unterminated `reason=\"…\"` (missing closing quote)");
};
let reason = reason.trim();
if reason.is_empty() {
bail!("{path}:{line}: `reason=\"\"` is empty; explain why the region is silenced");
}
Ok(reason.to_string())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn mark(comment: &str, rest: &str) -> String {
format!("{comment} {INTRODUCER} {rest}")
}
fn file(lines: &[String]) -> String {
format!("{}\n", lines.join("\n"))
}
fn rs(rest: &str) -> String {
mark("//", rest)
}
fn ok(text: &str) -> Vec<Region> {
scan("src/a.rs", text).unwrap()
}
fn err(text: &str) -> String {
scan("src/a.rs", text).unwrap_err().to_string()
}
fn region(kind: MarkerKind, start: u32, end: u32, reason: &str) -> Region {
Region {
kind,
start,
end,
reason: reason.to_string(),
}
}
#[test]
fn self_source_contains_no_literal_introducer() {
let source = include_str!("markers.rs");
assert!(
!source.contains(INTRODUCER),
"src/coverage/markers.rs must not contain a literal marker introducer; \
build fixtures with `mark()` and put examples in docs/coverage.md"
);
}
#[test]
fn file_without_the_introducer_yields_nothing() {
assert!(ok("fn a() {}\n// ordinary comment\n").is_empty());
}
#[test]
fn scans_an_ignore_region() {
let text = file(&[
"fn a() {}".to_string(),
rs("ignore reason=\"CPU-gated\""),
"fn b() {}".to_string(),
rs("end"),
"fn c() {}".to_string(),
]);
assert_eq!(
ok(&text),
vec![region(MarkerKind::Ignore, 2, 4, "CPU-gated")]
);
}
#[test]
fn scans_a_tolerate_region() {
let text = file(&[
rs("tolerate reason=\"avx512f arm\""),
"fn b() {}".to_string(),
rs("end"),
]);
assert_eq!(
ok(&text),
vec![region(MarkerKind::Tolerate, 1, 3, "avx512f arm")]
);
}
#[test]
fn scans_both_single_line_forms() {
let text = file(&[
rs("ignore-line reason=\"one off\""),
rs("tolerate-line reason=\"flaky\""),
]);
assert_eq!(
ok(&text),
vec![
region(MarkerKind::Ignore, 1, 1, "one off"),
region(MarkerKind::Tolerate, 2, 2, "flaky"),
]
);
}
#[test]
fn single_line_keyword_wins_over_its_prefix() {
let text = file(&[rs("ignore-line reason=\"one off\"")]);
let regions = ok(&text);
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].end, 1, "must not open a region");
}
#[test]
fn regions_may_repeat_within_a_file() {
let text = file(&[
rs("ignore reason=\"a\""),
"x".to_string(),
rs("end"),
"y".to_string(),
rs("tolerate reason=\"b\""),
"z".to_string(),
rs("end"),
]);
assert_eq!(
ok(&text),
vec![
region(MarkerKind::Ignore, 1, 3, "a"),
region(MarkerKind::Tolerate, 5, 7, "b"),
]
);
}
#[test]
fn any_comment_syntax_matches() {
let text = file(&[
mark("#", "ignore-line reason=\"shell\""),
mark("<!--", "ignore-line reason=\"html\" -->"),
]);
assert_eq!(ok(&text).len(), 2);
}
#[test]
fn marker_inside_a_string_literal_is_matched() {
let text = file(&[mark("let s = '", "ignore-line reason=\"in a literal\"';")]);
assert_eq!(ok(&text).len(), 1);
}
#[test]
fn crlf_line_endings_are_handled() {
let lines = [rs("ignore reason=\"crlf\""), "x".to_string(), rs("end")];
let text = format!("{}\r\n", lines.join("\r\n"));
assert_eq!(ok(&text), vec![region(MarkerKind::Ignore, 1, 3, "crlf")]);
}
#[test]
fn marker_on_the_last_line_without_a_trailing_newline() {
let text = format!("x\n{}", rs("ignore-line reason=\"last\""));
assert_eq!(ok(&text), vec![region(MarkerKind::Ignore, 2, 2, "last")]);
}
#[test]
fn reason_is_mandatory() {
let text = file(&[rs("ignore"), "x".to_string(), rs("end")]);
let message = err(&text);
assert!(message.contains("src/a.rs:1"), "{message}");
assert!(message.contains("needs a reason"), "{message}");
}
#[test]
fn reason_must_not_be_empty() {
let message = err(&file(&[rs("ignore-line reason=\"\"")]));
assert!(message.contains("is empty"), "{message}");
}
#[test]
fn reason_quote_must_be_closed() {
let message = err(&file(&[rs("ignore-line reason=\"unclosed")]));
assert!(message.contains("unterminated `reason"), "{message}");
}
#[test]
fn nested_regions_are_rejected() {
let text = file(&[
rs("ignore reason=\"a\""),
rs("tolerate reason=\"b\""),
rs("end"),
]);
let message = err(&text);
assert!(message.contains("src/a.rs:2"), "{message}");
assert!(message.contains("nested"), "{message}");
assert!(message.contains("opened at line 1"), "{message}");
}
#[test]
fn stray_end_is_rejected() {
let message = err(&file(&["x".to_string(), rs("end")]));
assert!(message.contains("src/a.rs:2"), "{message}");
assert!(
message.contains("without a matching region start"),
"{message}"
);
}
#[test]
fn unterminated_region_is_rejected() {
let text = file(&[rs("ignore reason=\"a\""), "x".to_string(), "y".to_string()]);
let message = err(&text);
assert!(message.contains("src/a.rs:1"), "{message}");
assert!(message.contains("unterminated"), "{message}");
}
#[test]
fn unknown_keyword_is_rejected() {
let message = err(&file(&[rs("skip reason=\"a\"")]));
assert!(message.contains("unrecognised"), "{message}");
assert!(message.contains("skip"), "{message}");
}
#[test]
fn text_after_end_is_rejected() {
let text = file(&[rs("ignore reason=\"a\""), rs("end reason=\"b\"")]);
let message = err(&text);
assert!(message.contains("unexpected text after"), "{message}");
}
#[test]
fn file_markers_expand_regions_to_line_sets() {
let markers = FileMarkers::new(vec![
region(MarkerKind::Ignore, 2, 4, "a"),
region(MarkerKind::Tolerate, 7, 7, "b"),
]);
assert_eq!(markers.ignored, BTreeSet::from([2, 3, 4]));
assert_eq!(markers.tolerated, BTreeSet::from([7]));
assert!(!markers.is_empty());
assert!(FileMarkers::default().is_empty());
}
}