use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf},
};
const FIXTURE: &str = include_str!("fixtures/api_coverage_symbols.txt");
#[derive(Debug, Clone)]
struct CoverageEntry {
symbol: String,
status: CoverageStatus,
area: String,
header: String,
note: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum CoverageStatus {
Safe,
Raw,
Omitted,
Deferred,
}
impl CoverageStatus {
const ALL: [Self; 4] = [Self::Safe, Self::Raw, Self::Omitted, Self::Deferred];
fn parse(value: &str) -> Option<Self> {
match value {
"safe" => Some(Self::Safe),
"raw" => Some(Self::Raw),
"omitted" => Some(Self::Omitted),
"deferred" => Some(Self::Deferred),
_ => None,
}
}
fn label(self) -> &'static str {
match self {
Self::Safe => "safe",
Self::Raw => "raw",
Self::Omitted => "omitted",
Self::Deferred => "deferred",
}
}
}
#[test]
fn coverage_fixture_matches_vendored_public_api() {
let header_symbols = extract_header_symbols();
let entries = parse_fixture(FIXTURE).expect("coverage fixture is well formed");
assert!(
header_symbols.len() > 500,
"expected a broad Box3D public API surface, got {} symbols",
header_symbols.len()
);
let classified: BTreeMap<_, _> = entries
.iter()
.map(|entry| (entry.symbol.as_str(), entry))
.collect();
let missing: Vec<_> = header_symbols
.keys()
.filter(|symbol| !classified.contains_key(symbol.as_str()))
.cloned()
.collect();
assert!(
missing.is_empty(),
"unclassified B3_API symbols: {missing:#?}"
);
let unknown: Vec<_> = classified
.keys()
.filter(|symbol| !header_symbols.contains_key(**symbol))
.copied()
.collect();
assert!(
unknown.is_empty(),
"fixture contains symbols not found in vendored headers: {unknown:#?}"
);
for entry in &entries {
let actual_header = header_symbols
.get(entry.symbol.as_str())
.expect("checked by unknown-symbol assertion");
assert_eq!(
actual_header, &entry.header,
"{} should be classified under {}",
entry.symbol, actual_header
);
assert!(
!entry.area.trim().is_empty() && !entry.note.trim().is_empty(),
"{} must include area and note context",
entry.symbol
);
}
}
#[test]
fn coverage_fixture_has_policy_buckets_and_high_priority_symbols() {
let entries = parse_fixture(FIXTURE).expect("coverage fixture is well formed");
let by_symbol: BTreeMap<_, _> = entries
.iter()
.map(|entry| (entry.symbol.as_str(), entry))
.collect();
let statuses: BTreeSet<_> = entries.iter().map(|entry| entry.status).collect();
assert!(statuses.contains(&CoverageStatus::Safe));
assert!(statuses.contains(&CoverageStatus::Raw));
assert!(statuses.contains(&CoverageStatus::Omitted));
assert_eq!(by_symbol["b3World_Step"].status, CoverageStatus::Safe);
assert_eq!(by_symbol["b3SetAllocator"].status, CoverageStatus::Raw);
assert_eq!(by_symbol["b3GetWorldCount"].status, CoverageStatus::Omitted);
assert_eq!(by_symbol["b3Body_GetWorld"].status, CoverageStatus::Omitted);
assert_eq!(
by_symbol["b3Shape_GetWorld"].status,
CoverageStatus::Omitted
);
assert_eq!(
by_symbol["b3Joint_GetWorld"].status,
CoverageStatus::Omitted
);
assert_eq!(
by_symbol["b3LoadRecordingFromFile"].status,
CoverageStatus::Safe
);
assert_eq!(
by_symbol["b3SaveRecordingToFile"].status,
CoverageStatus::Raw
);
for entry in entries
.iter()
.filter(|entry| entry.status == CoverageStatus::Deferred)
{
assert!(
!is_placeholder_deferred_note(&entry.note),
"{} has a placeholder deferred note",
entry.symbol
);
}
for entry in entries
.iter()
.filter(|entry| entry.status == CoverageStatus::Raw)
{
assert!(
!is_placeholder_raw_note(&entry.note),
"{} has a placeholder raw-boundary note",
entry.symbol
);
}
}
#[test]
fn docs_api_snapshot_matches_fixture_counts() {
let entries = parse_fixture(FIXTURE).expect("coverage fixture is well formed");
let expected = status_counts(&entries);
let docs =
fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs/api-coverage.md"))
.expect("API coverage docs can be read");
let actual = parse_docs_counts(&docs).expect("API coverage docs contain status counts");
assert_eq!(
actual, expected,
"docs/api-coverage.md Current Snapshot counts must match the fixture"
);
}
#[test]
fn fixture_parser_rejects_duplicate_symbols_and_bad_statuses() {
let duplicate = "\
b3World_Step|safe|world|box3d.h|covered
b3World_Step|safe|world|box3d.h|duplicate
";
assert!(parse_fixture(duplicate).is_err());
let bad_status = "b3World_Step|maybe|world|box3d.h|bad status\n";
assert!(parse_fixture(bad_status).is_err());
}
fn parse_fixture(input: &str) -> Result<Vec<CoverageEntry>, String> {
let mut entries = Vec::new();
let mut seen = BTreeSet::new();
for (index, line) in input.lines().enumerate() {
let line_number = index + 1;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let fields: Vec<_> = line.splitn(5, '|').collect();
if fields.len() != 5 {
return Err(format!(
"line {line_number}: expected 5 pipe-separated fields"
));
}
let symbol = fields[0].trim();
if !symbol.starts_with("b3") {
return Err(format!("line {line_number}: symbol must start with b3"));
}
if !seen.insert(symbol.to_owned()) {
return Err(format!("line {line_number}: duplicate symbol {symbol}"));
}
let status = CoverageStatus::parse(fields[1].trim())
.ok_or_else(|| format!("line {line_number}: invalid status {}", fields[1]))?;
entries.push(CoverageEntry {
symbol: symbol.to_owned(),
status,
area: fields[2].trim().to_owned(),
header: fields[3].trim().to_owned(),
note: fields[4].trim().to_owned(),
});
}
if entries.is_empty() {
return Err("coverage fixture is empty".to_owned());
}
Ok(entries)
}
fn is_placeholder_deferred_note(note: &str) -> bool {
let lower = note.to_ascii_lowercase();
lower.contains("known upstream api awaiting safe design or implementation")
|| lower.contains("planned safe wrapper in api completion work")
|| lower.contains("todo")
}
fn is_placeholder_raw_note(note: &str) -> bool {
let lower = note.to_ascii_lowercase();
lower.contains("process-global, file io, debug dump, or raw pointer policy")
|| lower.contains("raw pointer policy")
}
fn status_counts(entries: &[CoverageEntry]) -> BTreeMap<CoverageStatus, usize> {
let mut counts = BTreeMap::new();
for status in CoverageStatus::ALL {
counts.insert(status, 0);
}
for entry in entries {
*counts.entry(entry.status).or_insert(0) += 1;
}
counts
}
fn parse_docs_counts(input: &str) -> Result<BTreeMap<CoverageStatus, usize>, String> {
let mut counts = BTreeMap::new();
for line in input.lines() {
let fields: Vec<_> = line.split('|').map(str::trim).collect();
if fields.len() < 4 {
continue;
}
let status = fields[1].trim_matches('`');
let Some(status) = CoverageStatus::parse(status) else {
continue;
};
let count_field = fields[2].replace(',', "");
if !count_field
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_digit())
{
continue;
}
let count = count_field
.parse::<usize>()
.map_err(|err| format!("invalid docs count for {}: {err}", status.label()))?;
counts.insert(status, count);
}
for status in CoverageStatus::ALL {
counts
.get(&status)
.ok_or_else(|| format!("docs missing {} status count", status.label()))?;
}
Ok(counts)
}
fn extract_header_symbols() -> BTreeMap<String, String> {
let header_dir = header_dir();
let mut symbols = BTreeMap::new();
for entry in fs::read_dir(&header_dir).expect("Box3D header directory exists") {
let path = entry.expect("header directory entry").path();
if path.extension().and_then(|ext| ext.to_str()) != Some("h") {
continue;
}
let header = path
.file_name()
.and_then(|name| name.to_str())
.expect("header has utf-8 name")
.to_owned();
let contents = fs::read_to_string(&path).expect("header can be read");
for symbol in extract_symbols_from_header(&contents) {
symbols.entry(symbol).or_insert_with(|| header.clone());
}
}
symbols
}
fn header_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../boxddd-sys/third-party/box3d/include/box3d")
}
fn extract_symbols_from_header(contents: &str) -> Vec<String> {
let mut symbols = Vec::new();
let mut rest = contents;
while let Some(index) = rest.find("B3_API") {
rest = &rest[index + "B3_API".len()..];
let Some(end) = rest.find(';') else {
break;
};
let declaration = &rest[..end];
rest = &rest[end + 1..];
let Some(open_paren) = declaration.find('(') else {
continue;
};
let before_paren = &declaration[..open_paren];
let Some(symbol) = last_identifier(before_paren) else {
continue;
};
if symbol.starts_with("b3") {
symbols.push(symbol.to_owned());
}
}
symbols
}
fn last_identifier(input: &str) -> Option<&str> {
let end = input
.char_indices()
.rev()
.find(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == '_')
.map(|(index, ch)| index + ch.len_utf8())?;
let start = input[..end]
.char_indices()
.rev()
.find(|(_, ch)| !(ch.is_ascii_alphanumeric() || *ch == '_'))
.map_or(0, |(index, ch)| index + ch.len_utf8());
Some(&input[start..end])
}