mod common;
use common::{ALL_YEARS, sample_for_year, samples_dir};
use rvt::{RevitFile, compression, formats, streams};
const RESIDUAL: &[&str] = &[
"ClassDefinitionRef::m_ref",
"GInfo::m_next",
"GInfo::m_pFace",
"GInfo::m_prev",
"LFormatDefaults::m_defaultImperialFormatOptions",
"LFormatDefaults::m_defaultMetricFormatOptions",
"LFormatDefaults::m_unit",
"VarFunction::m_refCt",
"VarParam::m_pSubst",
"VarSketchObj::m_constrElems",
"VarSketchObj::m_dimValue",
"VarSketchObj::m_params",
"VarSketchObj::m_refPts",
"ZoningAppInfo::first",
];
#[test]
fn field_type_coverage_across_corpus() {
let mut missing: Vec<u32> = Vec::new();
let mut per_year: Vec<(u32, usize, usize)> = Vec::new();
let mut unexpected: Vec<(u32, String)> = Vec::new();
for year in ALL_YEARS {
let path = sample_for_year(year);
if !path.exists() {
missing.push(year);
continue;
}
let mut rf = RevitFile::open(&path).unwrap_or_else(|e| {
panic!("{year}: RevitFile::open failed at {}: {e}", path.display())
});
let raw = rf
.read_stream(streams::FORMATS_LATEST)
.unwrap_or_else(|e| panic!("{year}: read Formats/Latest: {e}"));
let decompressed = compression::inflate_stream_at(streams::FORMATS_LATEST, &raw, 0)
.unwrap_or_else(|e| panic!("{year}: inflate Formats/Latest: {e}"));
let schema = formats::parse_schema(&decompressed)
.unwrap_or_else(|e| panic!("{year}: parse_schema: {e}"));
let total: usize = schema.classes.iter().map(|c| c.fields.len()).sum();
let unknown: usize = schema
.classes
.iter()
.flat_map(|c| c.fields.iter())
.filter(|f| matches!(f.field_type, Some(formats::FieldType::Unknown { .. })))
.count();
for class in &schema.classes {
for field in &class.fields {
let name = format!("{}::{}", class.name, field.name);
if matches!(field.field_type, Some(formats::FieldType::Unknown { .. }))
&& !RESIDUAL.contains(&name.as_str())
{
unexpected.push((year, name));
}
}
}
per_year.push((year, total, unknown));
}
if !missing.is_empty() {
let strict = std::env::var("RVT_REQUIRE_CORPUS")
.ok()
.is_some_and(|v| v == "1" || v == "true");
if strict {
panic!(
"corpus incomplete — missing release(s): {:?}.\n \
Samples dir: {}\n \
RVT_REQUIRE_CORPUS is set, so this is treated as a regression. \
Either provide the phi-ag/rvt sample corpus via RVT_SAMPLES_DIR, \
or unset RVT_REQUIRE_CORPUS to allow a graceful skip during local dev. \
rvt-rs intentionally does not redistribute these files (see SECURITY.md).",
missing,
samples_dir().display()
);
} else {
eprintln!(
"\n \
┌─────────────────────────────────────────────────────────────────\n \
│ SKIP: field_type_coverage — corpus not available.\n \
│ Missing release(s): {:?}\n \
│ Samples dir: {}\n \
│ To run this test locally, fetch the phi-ag/rvt corpus:\n \
│ git clone https://github.com/phi-ag/rvt.git ../../samples/_phiag\n \
│ then copy or symlink the .rfa files from\n \
│ ../../samples/_phiag/examples/Autodesk/*.rfa\n \
│ into ../../samples/. CI sets RVT_SAMPLES_DIR directly and runs\n \
│ with RVT_REQUIRE_CORPUS=1 to hard-fail if any file is missing.\n \
└─────────────────────────────────────────────────────────────────\n",
missing,
samples_dir().display()
);
return;
}
}
for (year, total, unknown) in &per_year {
println!(" {year}: {total} schema fields, {unknown} Unknown (residual)");
}
assert!(
unexpected.is_empty(),
"field-type classification regressed — Unknown fields outside the documented \
residual: {unexpected:?}. Run \
`SHOW_UNKNOWN=1 cargo run --profile ci --example probe_schema_page_strip -- <file>` \
to see the byte patterns."
);
let corpus_total: usize = per_year.iter().map(|(_, t, _)| t).sum();
assert!(
corpus_total >= 100_000,
"corpus total ({corpus_total} fields across {} releases) is suspiciously low; \
the regression gate must see >= 100,000 fields to guarantee non-vacuous coverage",
per_year.len()
);
}