use std::collections::BTreeSet;
use std::path::PathBuf;
const CONCRETE_ENTITIES: &[&str] = &[
"IfcAdvancedBrep",
"IfcAdvancedBrepWithVoids",
"IfcArbitraryClosedProfileDef",
"IfcArbitraryOpenProfileDef",
"IfcArbitraryProfileDefWithVoids",
"IfcAsymmetricIShapeProfileDef",
"IfcAxis1Placement",
"IfcAxis2Placement2D",
"IfcAxis2Placement3D",
"IfcBlock",
"IfcBooleanClippingResult",
"IfcBooleanResult",
"IfcBoundaryCurve",
"IfcBoundingBox",
"IfcBoxedHalfSpace",
"IfcBSplineCurveWithKnots",
"IfcBSplineSurfaceWithKnots",
"IfcCartesianPoint",
"IfcCartesianPointList2D",
"IfcCartesianPointList3D",
"IfcCartesianTransformationOperator2D",
"IfcCartesianTransformationOperator2DnonUniform",
"IfcCartesianTransformationOperator3D",
"IfcCartesianTransformationOperator3DnonUniform",
"IfcCenterLineProfileDef",
"IfcCircle",
"IfcCircleHollowProfileDef",
"IfcCircleProfileDef",
"IfcCompositeCurve",
"IfcCompositeCurveOnSurface",
"IfcCompositeCurveSegment",
"IfcCompositeProfileDef",
"IfcConnectionCurveGeometry",
"IfcConnectionPointEccentricity",
"IfcConnectionPointGeometry",
"IfcConnectionSurfaceGeometry",
"IfcConnectionVolumeGeometry",
"IfcCsgSolid",
"IfcCShapeProfileDef",
"IfcCurveBoundedPlane",
"IfcCurveBoundedSurface",
"IfcCylindricalSurface",
"IfcDerivedProfileDef",
"IfcDirection",
"IfcEllipse",
"IfcEllipseProfileDef",
"IfcExtrudedAreaSolid",
"IfcExtrudedAreaSolidTapered",
"IfcFaceBasedSurfaceModel",
"IfcFacetedBrep",
"IfcFacetedBrepWithVoids",
"IfcFixedReferenceSweptAreaSolid",
"IfcGeometricCurveSet",
"IfcGeometricSet",
"IfcGridAxis",
"IfcGridPlacement",
"IfcHalfSpaceSolid",
"IfcIndexedPolyCurve",
"IfcIndexedPolygonalFace",
"IfcIndexedPolygonalFaceWithVoids",
"IfcIntersectionCurve",
"IfcIShapeProfileDef",
"IfcLine",
"IfcLocalPlacement",
"IfcLShapeProfileDef",
"IfcMappedItem",
"IfcMirroredProfileDef",
"IfcOffsetCurve2D",
"IfcOffsetCurve3D",
"IfcOuterBoundaryCurve",
"IfcPcurve",
"IfcPlane",
"IfcPointOnCurve",
"IfcPointOnSurface",
"IfcPolygonalBoundedHalfSpace",
"IfcPolygonalFaceSet",
"IfcPolyline",
"IfcProfileDef",
"IfcRationalBSplineCurveWithKnots",
"IfcRationalBSplineSurfaceWithKnots",
"IfcRectangleHollowProfileDef",
"IfcRectangleProfileDef",
"IfcRectangularPyramid",
"IfcRectangularTrimmedSurface",
"IfcReparametrisedCompositeCurveSegment",
"IfcRepresentationMap",
"IfcRevolvedAreaSolid",
"IfcRevolvedAreaSolidTapered",
"IfcRightCircularCone",
"IfcRightCircularCylinder",
"IfcRoundedRectangleProfileDef",
"IfcSeamCurve",
"IfcSectionedSpine",
"IfcShellBasedSurfaceModel",
"IfcSphere",
"IfcSphericalSurface",
"IfcSurfaceCurve",
"IfcSurfaceCurveSweptAreaSolid",
"IfcSurfaceOfLinearExtrusion",
"IfcSurfaceOfRevolution",
"IfcSweptDiskSolid",
"IfcSweptDiskSolidPolygonal",
"IfcToroidalSurface",
"IfcTrapeziumProfileDef",
"IfcTriangulatedFaceSet",
"IfcTrimmedCurve",
"IfcTShapeProfileDef",
"IfcUShapeProfileDef",
"IfcVector",
"IfcVirtualGridIntersection",
"IfcZShapeProfileDef",
];
fn crate_source() -> String {
fn strip_test_modules(text: &str) -> String {
match text.find("#[cfg(test)]") {
Some(i) => text[..i].to_string(),
None => text.to_string(),
}
}
fn walk(dir: &std::path::Path, out: &mut String) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
if path.file_stem().is_some_and(|s| s == "testkit") {
continue;
}
if let Ok(text) = std::fs::read_to_string(&path) {
out.push_str(&strip_test_modules(&text));
out.push('\n');
}
}
}
}
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut out = String::new();
walk(&src, &mut out);
out
}
#[test]
fn every_concrete_geometry_entity_is_covered() {
let source = crate_source();
let upper = source.to_ascii_uppercase();
let missing: BTreeSet<&str> = CONCRETE_ENTITIES
.iter()
.copied()
.filter(|entity| !is_covered(entity, &source, &upper))
.collect();
assert!(
missing.is_empty(),
"{} of {} concrete geometry entities have no view in this crate:\n{}",
missing.len(),
CONCRETE_ENTITIES.len(),
missing
.iter()
.map(|m| format!(" {m}"))
.collect::<Vec<_>>()
.join("\n")
);
}
fn is_covered(entity: &str, source: &str, upper_source: &str) -> bool {
if upper_source.contains(&format!("\"{}\"", entity.to_ascii_uppercase())) {
return true;
}
let view = entity.strip_prefix("Ifc").unwrap_or(entity);
source.contains(&format!("pub struct {view}"))
|| source.contains(&format!("pub enum {view}"))
|| source.contains(&format!("pub struct {view}<"))
}
#[test]
fn the_inventory_matches_the_published_schema_counts() {
assert_eq!(
CONCRETE_ENTITIES.len(),
111,
"IFC4 ADD2 TC1 declares 111 concrete entities across the four geometry \
schemas (135 total, 24 abstract): 89 concrete across IfcGeometryResource, \
IfcGeometricModelResource and IfcGeometricConstraintResource, plus 22 \
concrete in IfcProfileResource (23 declared, IfcParameterizedProfileDef \
abstract). Changing this number means the inventory was edited rather \
than the code fixed."
);
let unique: BTreeSet<&&str> = CONCRETE_ENTITIES.iter().collect();
assert_eq!(unique.len(), CONCRETE_ENTITIES.len(), "duplicate entries");
}
#[test]
fn inventory_contains_the_entities_that_matter_most() {
for expected in [
"IfcExtrudedAreaSolid",
"IfcPolygonalBoundedHalfSpace",
"IfcBooleanClippingResult",
"IfcMappedItem",
"IfcLocalPlacement",
"IfcTriangulatedFaceSet",
"IfcTrimmedCurve",
] {
assert!(
CONCRETE_ENTITIES.contains(&expected),
"{expected} missing from the inventory"
);
}
}
const SCHEMA_TYPES: &[(&str, &str)] = &[
("IfcArcIndex", "DEFINED"),
("IfcAxis2Placement", "SELECT"),
("IfcBSplineCurveForm", "ENUM"),
("IfcBSplineSurfaceForm", "ENUM"),
("IfcBooleanOperand", "SELECT"),
("IfcBooleanOperator", "ENUM"),
("IfcCsgSelect", "SELECT"),
("IfcCurveOnSurface", "SELECT"),
("IfcCurveOrEdgeCurve", "SELECT"),
("IfcDimensionCount", "DEFINED"),
("IfcGeometricSetSelect", "SELECT"),
("IfcGridPlacementDirectionSelect", "SELECT"),
("IfcKnotType", "ENUM"),
("IfcLineIndex", "DEFINED"),
("IfcPointOrVertexPoint", "SELECT"),
("IfcPreferredSurfaceCurveRepresentation", "ENUM"),
("IfcSegmentIndexSelect", "SELECT"),
("IfcSolidOrShell", "SELECT"),
("IfcSurfaceOrFaceSurface", "SELECT"),
("IfcTransitionCode", "ENUM"),
("IfcTrimmingPreference", "ENUM"),
("IfcTrimmingSelect", "SELECT"),
("IfcVectorOrDirection", "SELECT"),
];
#[test]
fn every_schema_type_is_modelled() {
let source = crate_source();
let missing: Vec<&(&str, &str)> = SCHEMA_TYPES
.iter()
.filter(|(name, _)| {
let stem = name.strip_prefix("Ifc").unwrap_or(name);
!(source.contains(&format!("pub enum {stem}"))
|| source.contains(&format!("pub struct {stem}"))
|| source.contains(&format!("pub type {stem}"))
|| source.contains(&format!("pub enum {name}"))
|| source.contains(&format!("pub struct {name}"))
|| source.contains(&format!("pub type {name}"))
|| source
.to_ascii_uppercase()
.contains(&format!("\"{}\"", name.to_ascii_uppercase())))
})
.collect();
assert!(
missing.is_empty(),
"{} of {} schema types are not modelled:\n{}",
missing.len(),
SCHEMA_TYPES.len(),
missing
.iter()
.map(|(n, f)| format!(" {f:8} {n}"))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn the_type_inventory_matches_the_schema() {
assert_eq!(
SCHEMA_TYPES.len(),
23,
"IFC4 ADD2 TC1 declares 23 types across the three geometry schemas \
(14 + 4 + 5). Editing this number means the inventory was trimmed \
rather than the code fixed."
);
let selects = SCHEMA_TYPES.iter().filter(|(_, f)| *f == "SELECT").count();
let enums = SCHEMA_TYPES.iter().filter(|(_, f)| *f == "ENUM").count();
let defined = SCHEMA_TYPES.iter().filter(|(_, f)| *f == "DEFINED").count();
assert_eq!(
(selects, enums, defined),
(13, 7, 3),
"flavour split per IFC4.exp"
);
}
#[test]
fn the_compiled_subtype_table_agrees_with_the_schema() {
use ifc_geometry::select::is_a;
for solid in [
"IFCEXTRUDEDAREASOLID",
"IFCREVOLVEDAREASOLID",
"IFCFACETEDBREP",
"IFCADVANCEDBREP",
"IFCCSGSOLID",
"IFCSWEPTDISKSOLID",
"IFCSURFACECURVESWEPTAREASOLID",
"IFCFIXEDREFERENCESWEPTAREASOLID",
] {
assert!(is_a(solid, "IFCSOLIDMODEL"), "{solid} is a solid model");
}
for half in [
"IFCHALFSPACESOLID",
"IFCPOLYGONALBOUNDEDHALFSPACE",
"IFCBOXEDHALFSPACE",
] {
assert!(
!is_a(half, "IFCSOLIDMODEL"),
"{half} must not classify as a solid model"
);
}
for curve in [
"IFCPOLYLINE",
"IFCCIRCLE",
"IFCTRIMMEDCURVE",
"IFCINDEXEDPOLYCURVE",
] {
assert!(is_a(curve, "IFCCURVE"), "{curve} is a curve");
}
for surface in [
"IFCPLANE",
"IFCCYLINDRICALSURFACE",
"IFCRECTANGULARTRIMMEDSURFACE",
] {
assert!(is_a(surface, "IFCSURFACE"), "{surface} is a surface");
}
assert!(!is_a("IFCPLANE", "IFCCURVE"));
assert!(!is_a("IFCPOLYLINE", "IFCSURFACE"));
assert!(!is_a("IFCCARTESIANPOINT", "IFCCURVE"));
}
#[test]
fn every_concrete_profile_is_lowered_or_declared_unlowered() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source =
std::fs::read_to_string(manifest.join("src/lower/profile.rs")).expect("profile lowerer");
let reader =
std::fs::read_to_string(manifest.join("src/input/profile/mod.rs")).expect("profile reader");
let dispatched: BTreeSet<String> = reader
.lines()
.filter_map(|line| {
let line = line.trim();
let name = line.strip_prefix('"')?;
let (name, rest) = name.split_once('"')?;
rest.trim_start()
.starts_with("=>")
.then(|| name.to_string())
})
.collect();
let declared: BTreeSet<String> = unlowered_table(&source);
let both: Vec<&String> = dispatched.intersection(&declared).collect();
assert!(
both.is_empty(),
"{} profile families are BOTH dispatched and declared unlowered; \
a stale UNLOWERED entry makes the crate understate itself:\n{}",
both.len(),
both.iter()
.map(|m| format!(" {m}"))
.collect::<Vec<_>>()
.join("\n")
);
let profiles: Vec<&str> = CONCRETE_ENTITIES
.iter()
.copied()
.filter(|e| e.contains("ProfileDef"))
.collect();
assert_eq!(profiles.len(), 22, "IfcProfileResource concrete entities");
let mut missing = Vec::new();
for entity in &profiles {
if *entity == "IfcProfileDef" {
continue;
}
let upper = entity.to_ascii_uppercase();
if !dispatched.contains(&upper) && !declared.contains(&upper) {
missing.push(*entity);
}
}
assert!(
missing.is_empty(),
"{} concrete profile families are neither read by \
src/input/profile/mod.rs nor declared unlowered in \
src/lower/profile.rs:\n{}",
missing.len(),
missing
.iter()
.map(|m| format!(" {m}"))
.collect::<Vec<_>>()
.join("\n")
);
}
fn unlowered_table(source: &str) -> BTreeSet<String> {
let start = source
.find("pub const UNLOWERED:")
.expect("profile lowerer declares an UNLOWERED table");
let rest = &source[start..];
let end = rest
.lines()
.scan(0usize, |acc, line| {
let at = *acc;
*acc += line.len() + 1;
Some((at, line))
})
.find(|(_, line)| {
let t = line.trim();
t == "];" || t == ")];" || t.ends_with("= &[];")
})
.map(|(at, line)| at + line.len())
.expect("UNLOWERED table is closed");
source[start..start + end]
.lines()
.filter_map(|line| {
let t = line.trim();
let name = t.strip_prefix('"')?;
let (name, _) = name.split_once('"')?;
name.starts_with("IFC").then(|| name.to_string())
})
.collect()
}
#[test]
fn the_subtype_table_knows_every_concrete_profile() {
use ifc_geometry::select::is_a;
let missing: Vec<&str> = CONCRETE_ENTITIES
.iter()
.copied()
.filter(|e| e.contains("ProfileDef"))
.filter(|e| !is_a(&e.to_ascii_uppercase(), "IFCPROFILEDEF"))
.collect();
assert!(
missing.is_empty(),
"{} concrete profile families are not resolvable through the subtype \
table, so is_a() cannot recognise them:\n{}",
missing.len(),
missing
.iter()
.map(|m| format!(" {m}"))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn the_connection_surface_select_matches_every_release() {
use ifc_schema::TypeKind;
let upper = |names: &[&str]| -> BTreeSet<String> {
names.iter().map(|n| n.to_ascii_uppercase()).collect()
};
let later = [
"IfcConnectionCurveGeometry",
"IfcConnectionPointEccentricity",
"IfcConnectionPointGeometry",
"IfcConnectionSurfaceGeometry",
"IfcConnectionVolumeGeometry",
];
let ifc2x3 = [
"IfcConnectionCurveGeometry",
"IfcConnectionPointEccentricity",
"IfcConnectionPointGeometry",
"IfcConnectionPortGeometry",
"IfcConnectionSurfaceGeometry",
];
for (release, schema, advanced, kinds) in [
("IFC2X3", ifc_schema::ifc2x3(), false, &ifc2x3[..]),
("IFC4", ifc_schema::ifc4(), true, &later[..]),
("IFC4X3", ifc_schema::ifc4x3(), true, &later[..]),
] {
let Some(TypeKind::Select(members)) = schema
.type_def("IfcSurfaceOrFaceSurface")
.map(|t| t.kind.clone())
else {
panic!("{release}: IfcSurfaceOrFaceSurface is a SELECT");
};
let members: BTreeSet<String> = members.iter().map(|m| m.to_ascii_uppercase()).collect();
assert_eq!(
members,
upper(&["IfcFaceBasedSurfaceModel", "IfcFaceSurface", "IfcSurface"]),
"{release}"
);
assert!(schema.is_a("IfcFaceSurface", "IfcFace"), "{release}");
assert!(!schema.is_a("IfcFaceSurface", "IfcSurface"), "{release}");
assert_eq!(
schema.entity("IfcAdvancedFace").is_some(),
advanced,
"{release}: IfcAdvancedFace presence"
);
if advanced {
assert!(
schema.is_a("IfcAdvancedFace", "IfcFaceSurface"),
"{release}"
);
}
assert_eq!(
schema.attribute_names("IfcFaceSurface"),
["Bounds", "FaceSurface", "SameSense"],
"{release}"
);
assert_eq!(
schema.attribute_names("IfcConnectionSurfaceGeometry"),
["SurfaceOnRelatingElement", "SurfaceOnRelatedElement"],
"{release}"
);
let subtypes: BTreeSet<String> = schema
.subtypes("IfcConnectionGeometry")
.into_iter()
.map(str::to_ascii_uppercase)
.collect();
assert_eq!(subtypes, upper(kinds), "{release}: connection kinds");
}
}