from __future__ import annotations
import os
import pathlib
import pytest
import rvt
SAMPLE_YEARS = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]
def samples_dir() -> pathlib.Path:
env = os.environ.get("RVT_SAMPLES_DIR")
if env:
return pathlib.Path(env)
here = pathlib.Path(__file__).resolve()
return here.parent.parent.parent.parent / "samples"
def sample_for_year(year: int) -> pathlib.Path:
if 2016 <= year <= 2019:
return samples_dir() / f"rac_basic_sample_family-{year}.rfa"
if 2020 <= year <= 2026:
return samples_dir() / f"racbasicsamplefamily-{year}.rfa"
raise ValueError(f"unknown sample year {year}")
def corpus_available() -> bool:
return all(sample_for_year(y).exists() for y in SAMPLE_YEARS)
def test_module_version_is_a_nonempty_string():
assert isinstance(rvt.__version__, str)
assert rvt.__version__ != ""
def test_module_exposes_RevitFile():
assert hasattr(rvt, "RevitFile")
def test_module_exposes_rvt_to_ifc():
assert hasattr(rvt, "rvt_to_ifc")
assert callable(rvt.rvt_to_ifc)
def test_nonexistent_file_raises_ioerror(tmp_path):
missing = tmp_path / "does-not-exist.rfa"
with pytest.raises((OSError, IOError)):
rvt.RevitFile(str(missing))
def test_non_cfb_input_raises(tmp_path):
not_cfb = tmp_path / "hello.rfa"
not_cfb.write_bytes(b"this is not a CFB file")
with pytest.raises((OSError, IOError, ValueError)):
rvt.RevitFile(str(not_cfb))
@pytest.fixture
def sample_2024():
p = sample_for_year(2024)
if not p.exists():
pytest.skip(f"corpus not available at {p}")
return rvt.RevitFile(str(p))
def test_version_reports_2024(sample_2024):
assert sample_2024.version == 2024
def test_part_atom_title_present(sample_2024):
assert sample_2024.part_atom_title is not None
assert sample_2024.part_atom_title == "0610 x 0915mm"
def test_stream_names_returns_expected_set(sample_2024):
names = sample_2024.stream_names()
assert isinstance(names, list)
assert len(names) == 13
for required in [
"BasicFileInfo",
"Formats/Latest",
"Global/ElemTable",
"Global/Latest",
"PartAtom",
]:
assert required in names, f"missing required stream {required}"
def test_read_stream_returns_bytes_for_known_stream(sample_2024):
data = sample_2024.read_stream("BasicFileInfo")
assert isinstance(data, bytes)
assert len(data) > 0, "BasicFileInfo stream should be non-empty"
def test_read_stream_accepts_leading_slash(sample_2024):
without_slash = sample_2024.read_stream("Formats/Latest")
with_slash = sample_2024.read_stream("/Formats/Latest")
assert without_slash == with_slash
def test_read_stream_raises_on_missing_name(sample_2024):
with pytest.raises((OSError, IOError)):
sample_2024.read_stream("does/not/exist")
def test_missing_required_streams_empty_on_valid_file(sample_2024):
assert sample_2024.missing_required_streams() == []
def test_schema_summary_has_expected_counts(sample_2024):
summary = sample_2024.schema_summary()
assert summary["classes"] > 100
assert summary["fields"] > 1000
assert summary["cpp_types"] >= 0
def test_schema_json_parses_and_matches_summary(sample_2024):
import json
summary = sample_2024.schema_summary()
js = sample_2024.schema_json()
assert isinstance(js, str)
assert len(js) > 10_000, "full schema JSON should be substantial"
parsed = json.loads(js)
assert isinstance(parsed, dict)
assert "classes" in parsed
assert "cpp_types" in parsed
assert len(parsed["classes"]) == summary["classes"]
assert len(parsed["cpp_types"]) == summary["cpp_types"]
total_fields = sum(len(c["fields"]) for c in parsed["classes"])
assert total_fields == summary["fields"]
def test_basic_file_info_json_matches_getters(sample_2024):
import json
js = sample_2024.basic_file_info_json()
assert js is not None, "2024 sample should parse BasicFileInfo"
parsed = json.loads(js)
assert isinstance(parsed, dict)
assert parsed["version"] == sample_2024.version
if sample_2024.original_path is not None:
assert parsed["original_path"] == sample_2024.original_path
if sample_2024.build is not None:
assert parsed["build"] == sample_2024.build
if sample_2024.guid is not None:
assert parsed["guid"] == sample_2024.guid
def test_part_atom_json_matches_title_getter(sample_2024):
import json
js = sample_2024.part_atom_json()
if js is None:
pytest.skip("PartAtom stream absent in this sample")
parsed = json.loads(js)
assert isinstance(parsed, dict)
assert parsed.get("title") == sample_2024.part_atom_title
for key in ("taxonomies", "categories", "raw_xml"):
assert key in parsed, f"PartAtom JSON missing key {key!r}"
def test_schema_json_contains_adocument_class(sample_2024):
import json
parsed = json.loads(sample_2024.schema_json())
names = [c["name"] for c in parsed["classes"]]
assert "ADocument" in names
def test_read_adocument_returns_dict_with_13_fields(sample_2024):
doc = sample_2024.read_adocument()
assert doc is not None
assert "entry_offset" in doc
assert "version" in doc
assert doc["version"] == 2024
assert len(doc["fields"]) == 13
def test_read_adocument_last_three_elementids_match_rust(sample_2024):
doc = sample_2024.read_adocument()
expected = [("m_ownerFamilyId", 27), ("m_ownerFamilyContainingGroupId", 31), ("m_devBranchInfo", 35)]
last_three = doc["fields"][-3:]
for field, (exp_name, exp_id) in zip(last_three, expected):
assert field["name"] == exp_name
assert field["kind"] == "element_id"
assert field["tag"] == 0
assert field["id"] == exp_id
def test_decoded_elements_and_counts_match_export_diagnostics(sample_2024):
elements = sample_2024.decoded_elements()
counts = sample_2024.element_counts()
diagnostics = sample_2024.export_diagnostics()
assert isinstance(elements, list)
assert isinstance(counts, dict)
assert isinstance(counts["by_class"], dict)
assert counts["total"] == len(elements)
assert counts["total"] == diagnostics["decoded"]["production_walker_elements"]
by_class = {}
for element in elements:
assert "id" in element
assert isinstance(element["class_name"], str)
assert set(element["byte_range"]) == {"start", "end"}
assert isinstance(element["fields"], list)
by_class[element["class_name"]] = by_class.get(element["class_name"], 0) + 1
assert counts["by_class"] == by_class
def test_export_diagnostics_dict_matches_json(sample_2024):
import json
parsed = json.loads(sample_2024.export_diagnostics_json())
direct = sample_2024.export_diagnostics()
assert direct == parsed
assert direct["schema_version"] == 1
assert "confidence" in direct
def test_write_ifc_produces_valid_ifc4(sample_2024):
ifc = sample_2024.write_ifc()
assert isinstance(ifc, str)
assert ifc.startswith("ISO-10303-21;\n")
assert ifc.endswith("END-ISO-10303-21;\n")
assert "FILE_SCHEMA(('IFC4'));" in ifc
assert "IFCPROJECT" in ifc
assert "0610 x 0915mm" in ifc
def test_write_ifc_accepts_explicit_scaffold_mode(sample_2024):
ifc = sample_2024.write_ifc(mode="scaffold")
assert "IFCPROJECT" in ifc
def test_write_ifc_strict_mode_rejects_incomplete_export(sample_2024):
with pytest.raises(ValueError, match="strict"):
sample_2024.write_ifc(mode="strict")
def test_write_ifc_produces_exactly_one_ifcproject(sample_2024):
ifc = sample_2024.write_ifc()
assert ifc.count("IFCPROJECT(") == 1
def test_repr_contains_version(sample_2024):
r = repr(sample_2024)
assert "RevitFile" in r
assert "2024" in r
def test_elem_table_header_fields_present(sample_2024):
h = sample_2024.elem_table_header()
for key in ("element_count", "record_count", "header_flag", "decompressed_bytes"):
assert key in h, f"missing key {key}"
assert isinstance(h[key], int)
assert h["header_flag"] == 0x0011
assert h["element_count"] > 0
assert h["record_count"] > 0
def test_elem_table_records_are_well_formed(sample_2024):
recs = sample_2024.elem_table_records()
assert isinstance(recs, list)
assert len(recs) > 0, "expected at least one record on the family sample"
for r in recs:
for key in ("offset", "id_primary", "id_secondary"):
assert key in r, f"record missing key {key}: {r}"
assert isinstance(r[key], int)
assert r["owner_id"] is None or isinstance(r["owner_id"], int)
offsets = [r["offset"] for r in recs]
assert all(a < b for a, b in zip(offsets, offsets[1:])), (
"ElemTable records not in ascending offset order"
)
def test_declared_element_ids_are_sorted_and_unique(sample_2024):
ids = sample_2024.declared_element_ids()
assert isinstance(ids, list)
assert len(ids) > 0
assert all(a < b for a, b in zip(ids, ids[1:])), "ids not sorted"
def test_rvt_to_ifc_matches_write_ifc_method(sample_2024):
import re
p = sample_for_year(2024)
via_fn = rvt.rvt_to_ifc(str(p))
via_method = sample_2024.write_ifc()
def norm(s):
s = re.sub(
r"^(FILE_NAME\('[^']*',)'[^']*'(,.*\);)$",
r"\g<1>'<timestamp>'\g<2>",
s,
flags=re.MULTILINE,
)
return re.sub(
r"(IFCOWNERHISTORY\([^;\n]*,)\d+(\);)",
r"\g<1><timestamp>\g<2>",
s,
)
assert norm(via_fn) == norm(via_method)
@pytest.mark.parametrize("year", SAMPLE_YEARS)
def test_every_release_opens_and_reports_version(year):
p = sample_for_year(year)
if not p.exists():
pytest.skip(f"corpus not available at {p}")
f = rvt.RevitFile(str(p))
assert f.version == year
@pytest.mark.parametrize("year", SAMPLE_YEARS)
def test_every_release_produces_valid_ifc(year):
p = sample_for_year(year)
if not p.exists():
pytest.skip(f"corpus not available at {p}")
ifc = rvt.rvt_to_ifc(str(p))
assert ifc.startswith("ISO-10303-21;\n")
assert "IFC4" in ifc
assert ifc.count("IFCPROJECT(") == 1
def test_metadata_reports_worksharing_and_save_counter(sample_2024):
m = sample_2024.metadata()
assert m["revit_version"] == 2024
assert m["worksharing"] == "Not enabled"
assert m["workshared"] is False
assert m["document_increments"] == 68
assert m["title"] == "racbasicsamplefamily"
assert m["last_saved"] == "2023-03-27T11:56:02Z"
assert m["single_user_cloud_model"] is False
keys = [p["key"] for p in m["properties"]]
assert keys[0] == "Worksharing"
assert "Unique Document GUID" in keys
def test_read_metadata_matches_the_open_file(sample_2024):
fast = rvt.read_metadata(str(sample_for_year(2024)))
assert fast == sample_2024.metadata()
def test_metadata_redact_scrubs_user_paths(sample_2024):
def user_folders_redacted(value):
parts = (value or "").split("\\Users\\")[1:]
return all(part.startswith("<redacted>") for part in parts)
m = sample_2024.metadata(redact=True)
assert "\\Users\\" in m["last_save_path"]
assert user_folders_redacted(m["last_save_path"])
assert all(user_folders_redacted(p["value"]) for p in m["properties"])
def test_read_metadata_raises_file_not_found():
with pytest.raises(FileNotFoundError):
rvt.read_metadata("definitely/not/here.rvt")
def test_read_metadata_rejects_a_non_revit_file(tmp_path):
bogus = tmp_path / "bogus.rvt"
bogus.write_bytes(b"not a revit file")
with pytest.raises(ValueError):
rvt.read_metadata(str(bogus))
def test_schedule_csv_has_the_documented_header(sample_2024):
csv = sample_2024.schedule_csv()
header = csv.split("\r\n")[0]
assert header.startswith("revit_element_id,ifc_type,predefined_type,name,level,")
rooms = sample_2024.schedule_csv(kind="rooms", metric=True, excel=True)
assert rooms.startswith("\ufeffrevit_element_id,number,name,level,level_elevation_m")
def test_schedule_csv_rejects_an_unknown_kind(sample_2024):
with pytest.raises(ValueError):
sample_2024.schedule_csv(kind="doors")