use super::*;
#[test]
fn test_schema_geometry_categories() {
let schema = IfcSchema::new();
assert_eq!(
schema.geometry_category(&IfcType::IfcExtrudedAreaSolid),
Some(GeometryCategory::SweptSolid)
);
assert_eq!(
schema.geometry_category(&IfcType::IfcBooleanResult),
Some(GeometryCategory::Boolean)
);
assert_eq!(
schema.geometry_category(&IfcType::IfcTriangulatedFaceSet),
Some(GeometryCategory::ExplicitMesh)
);
assert_eq!(
schema.profile_category(&IfcType::IfcRoundedRectangleProfileDef),
Some(ProfileCategory::Parametric)
);
}
#[test]
fn test_parse_index_list_rejects_out_of_range() {
let face = AttributeValue::List(vec![
AttributeValue::Integer(5_000_000_000), AttributeValue::Integer(0), AttributeValue::Integer(-4), ]);
let out = AttributeValue::parse_index_list(&[face]);
assert_eq!(out, vec![u32::MAX, u32::MAX, u32::MAX]);
let ok = AttributeValue::List(vec![
AttributeValue::Integer(1),
AttributeValue::Integer(2),
AttributeValue::Integer(3),
]);
assert_eq!(AttributeValue::parse_index_list(&[ok]), vec![0, 1, 2]);
}
#[test]
fn test_parse_index_list_extreme_i64_values() {
let face = AttributeValue::List(vec![
AttributeValue::Integer(i64::MIN),
AttributeValue::Integer(i64::MAX),
AttributeValue::Integer(4_294_967_297),
]);
let out = AttributeValue::parse_index_list(&[face]);
assert_eq!(out, vec![u32::MAX, u32::MAX, u32::MAX]);
let boundary = AttributeValue::List(vec![
AttributeValue::Integer(4_294_967_295), AttributeValue::Integer(4_294_967_296), AttributeValue::Integer(2),
]);
assert_eq!(
AttributeValue::parse_index_list(&[boundary]),
vec![u32::MAX - 1, u32::MAX, 1]
);
}
#[test]
fn test_attribute_value_conversion() {
let token = Token::EntityRef(123);
let attr = AttributeValue::from_token(&token);
assert_eq!(attr.as_entity_ref(), Some(123));
let token = Token::String(b"test");
let attr = AttributeValue::from_token(&token);
assert_eq!(attr.as_string(), Some("test"));
}
#[test]
fn typed_value_name_decodes_to_the_express_spelling_4707() {
for spelling in [&b"IFCPARAMETERVALUE"[..], b"ifcparametervalue", b"IfcParameterValue"] {
let token = Token::TypedValue(spelling, vec![Token::Float(0.5)]);
let attr = AttributeValue::from_token(&token);
let items = attr.as_list().expect("typed value decodes to a list");
assert_eq!(items[0].as_string(), Some("IFCPARAMETERVALUE"), "{}", String::from_utf8_lossy(spelling));
assert_eq!(items[1].as_float(), Some(0.5));
}
let token = Token::TypedValue(b"ifclabel", vec![Token::String(b"Mixed Case")]);
let attr = AttributeValue::from_token(&token);
assert_eq!(attr.as_list().unwrap()[1].as_string(), Some("Mixed Case"));
}
#[test]
fn test_decoded_entity() {
let entity = DecodedEntity::new(
1,
IfcType::IfcWall,
vec![
AttributeValue::EntityRef(2),
AttributeValue::String("Wall-001".to_string()),
AttributeValue::Float(3.5),
],
);
assert_eq!(entity.get_ref(0), Some(2));
assert_eq!(entity.get_string(1), Some("Wall-001"));
assert_eq!(entity.get_float(2), Some(3.5));
}
#[test]
fn decoded_entity_get_refs_accepts_a_list_or_bare_reference() {
let entity = DecodedEntity::new(
1,
IfcType::IfcWall,
vec![
AttributeValue::List(vec![
AttributeValue::EntityRef(2),
AttributeValue::Null,
AttributeValue::EntityRef(3),
]),
AttributeValue::EntityRef(4),
AttributeValue::List(Vec::new()),
AttributeValue::Null,
],
);
assert_eq!(entity.get_refs(0), Some(vec![2, 3]));
assert_eq!(entity.get_refs(1), Some(vec![4]));
assert_eq!(entity.get_refs(2), None);
assert_eq!(entity.get_refs(3), None);
assert_eq!(entity.get_refs(4), None);
}
#[test]
fn test_as_float_with_typed_value() {
let plain_float = AttributeValue::Float(0.5);
assert_eq!(plain_float.as_float(), Some(0.5));
let integer = AttributeValue::Integer(42);
assert_eq!(integer.as_float(), Some(42.0));
let typed_value = AttributeValue::List(vec![
AttributeValue::String("IFCNORMALISEDRATIOMEASURE".to_string()),
AttributeValue::Float(0.5),
]);
assert_eq!(typed_value.as_float(), Some(0.5));
let typed_int = AttributeValue::List(vec![
AttributeValue::String("IFCINTEGER".to_string()),
AttributeValue::Integer(100),
]);
assert_eq!(typed_int.as_float(), Some(100.0));
let regular_list =
AttributeValue::List(vec![AttributeValue::Float(1.0), AttributeValue::Float(2.0)]);
assert_eq!(regular_list.as_float(), None);
let empty_list = AttributeValue::List(vec![]);
assert_eq!(empty_list.as_float(), None);
}
#[test]
fn ifc4x1_alignment_curve_preserves_its_exact_supported_name_4203() {
let alignment_curve = IfcType::from_str("IFCALIGNMENTCURVE");
assert!(!matches!(alignment_curve, IfcType::Unknown(_)));
assert_eq!(alignment_curve.as_str(), "IFCALIGNMENTCURVE");
assert_eq!(alignment_curve.name(), "IfcAlignmentCurve");
assert!(alignment_curve.attribute_names().is_empty());
for non_entity in [
"IFCLENGTHMEASURE",
"IFCBINARY",
"IFCPROPERTYSETDEFINITIONSET",
] {
assert!(matches!(IfcType::from_str(non_entity), IfcType::Unknown(_)));
}
}
#[cfg(test)]
mod ifc_types_catalog {
use crate::{IfcType, IFC_TYPES};
#[test]
fn every_entry_round_trips_through_its_name() {
for t in IFC_TYPES {
let name = t.name();
assert_eq!(
IfcType::from_str(&name.to_uppercase()),
t.clone(),
"{name} did not round-trip"
);
assert_eq!(IfcType::from_id(t.id()), Some(t.clone()), "{name} did not round-trip by id");
}
}
#[test]
fn the_catalog_is_a_set_of_real_entities() {
let mut seen = std::collections::HashSet::new();
for t in IFC_TYPES {
assert!(seen.insert(t.known_as_str()), "{} appears twice", t.name());
assert!(
!matches!(t, IfcType::Unknown(_)),
"Unknown is the absence of a type, not one of them"
);
}
assert_eq!(seen.len(), IFC_TYPES.len());
}
#[test]
fn every_parent_is_itself_in_the_catalog() {
let known: std::collections::HashSet<&str> = IFC_TYPES.iter().filter_map(IfcType::known_as_str).collect();
for t in IFC_TYPES {
let mut cur = t.parent();
while let Some(p) = cur {
assert!(
known.contains(p.known_as_str().expect("catalog parent is known")),
"{} has an ancestor {} outside the catalog",
t.name(),
p.name()
);
cur = p.parent();
}
}
}
#[test]
fn it_holds_the_schema_and_not_a_sample_of_it() {
assert_eq!(
IFC_TYPES.len(),
876,
"the IFC4X3 catalog has an unexpected entity count"
);
let by_name: std::collections::HashMap<String, IfcType> =
IFC_TYPES.iter().map(|t| (t.name().to_string(), t.clone())).collect();
for name in ["IfcWall", "IfcSlab", "IfcDoor", "IfcWindow", "IfcBuildingStorey"] {
let t = by_name[name].clone();
assert!(!t.is_abstract(), "{name} is instantiable");
assert!(t.is_subtype_of(by_name["IfcProduct"].clone()), "{name} is a product");
}
assert!(by_name["IfcProduct"].is_abstract());
assert_eq!(
by_name["IfcWallStandardCase"].parent(),
Some(by_name["IfcWall"].clone()),
"the supertype chain is what makes ancestor mapping a fact"
);
}
}
#[cfg(test)]
mod attribute_names {
use crate::{IfcType, IFC_TYPES};
#[test]
fn inherited_attributes_come_first_and_in_root_to_leaf_order() {
for name in ["IfcWall", "IfcBuildingStorey", "IfcDoor", "IfcProject"] {
let t = IfcType::from_str(&name.to_uppercase());
assert_eq!(
&t.attribute_names()[..4],
&["GlobalId", "OwnerHistory", "Name", "Description"],
"{name} must inherit IfcRoot's four attributes at positions 0-3"
);
}
}
#[test]
fn it_agrees_with_the_positions_the_workspace_already_reads() {
let wall = IfcType::from_str("IFCWALL");
for (name, want) in [
("GlobalId", 0),
("Name", 2),
("Description", 3),
("ObjectType", 4),
("ObjectPlacement", 5),
("Representation", 6),
] {
assert_eq!(
wall.attribute_index(name),
Some(want),
"IfcProduct attribute {name} is read at index {want} in this workspace"
);
}
}
#[test]
fn a_storeys_elevation_is_reachable_by_name() {
let storey = IfcType::from_str("IFCBUILDINGSTOREY");
assert_eq!(storey.attribute_index("Elevation"), Some(9));
assert_eq!(storey.attribute_names().len(), 10);
}
#[test]
fn an_unknown_name_is_none_and_the_lookup_is_case_sensitive() {
let wall = IfcType::from_str("IFCWALL");
assert_eq!(wall.attribute_index("NotAnAttribute"), None);
assert_eq!(
wall.attribute_index("globalid"),
None,
"EXPRESS names are PascalCase; a case-insensitive match would let a \
typo resolve to the wrong attribute on some other entity"
);
}
#[test]
fn every_type_has_a_consistent_attribute_list() {
for t in IFC_TYPES {
let names = t.attribute_names();
let mut seen = std::collections::HashSet::new();
for n in names {
assert!(
seen.insert(*n),
"{} lists {n} twice; attribute_index would answer with the \
first of two real positions",
t.name()
);
}
for (i, n) in names.iter().enumerate() {
assert_eq!(t.attribute_index(n), Some(i));
}
}
}
#[test]
fn unknown_names_nothing() {
let u = IfcType::from_str("NOT_AN_IFC_TYPE_AT_ALL");
assert!(matches!(u, IfcType::Unknown(_)));
assert!(u.attribute_names().is_empty());
assert_eq!(u.attribute_index("GlobalId"), None);
}
}
#[test]
fn type_name_normalization_preserves_catalog_and_unknown_unicode_3987() {
for ty in crate::IFC_TYPES {
for spelling in [ty.name().to_string(), ty.name().to_uppercase(), ty.name().to_lowercase()] {
assert_eq!(IfcType::from_str(&spelling), ty.clone(), "{spelling}");
}
}
fn crc32(bytes: &[u8]) -> u32 {
let mut crc = !0u32;
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
}
}
!crc
}
assert_eq!(IfcType::from_str("IFCſPACE"), IfcType::IfcSpace);
for &name in crate::EXPORTER_STRATUM_ALIASES {
let upper = name.to_uppercase();
let expected = IfcType::from_str(name);
if matches!(
name,
"IFCSOLIDSTRATUM" | "IFCVOIDSTRATUM" | "IFCWATERSTRATUM"
) {
assert!(matches!(expected, IfcType::Unknown(_)), "{name}");
} else {
assert_eq!(expected.as_str(), upper, "{name}");
}
for spelling in [name.to_owned(), name.to_lowercase()] {
assert_eq!(IfcType::from_str(&spelling), expected, "{spelling}");
}
}
for (input, uppercase) in [
("", ""),
("IFCÉ", "IFCÉ"),
("IFCWALL ", "IFCWALL "),
("IFC\0WALL", "IFC\0WALL"),
("IFC_VENDOR_123", "IFC_VENDOR_123"),
("Ifc_Vendor_123", "IFC_VENDOR_123"),
("ifcstraße", "IFCSTRASSE"),
("ifcffi", "IFCFFI"),
("ifcé", "IFCÉ"),
("ifcı", "IFCI"),
] {
let parsed = IfcType::from_str(input);
let IfcType::Unknown(unknown) = parsed else {
panic!("{input} unexpectedly resolved to a schema type");
};
assert_eq!(unknown.as_str(), uppercase, "{input}");
assert_eq!(unknown.id(), crc32(uppercase.as_bytes()), "{input}");
}
}