use super::*;
use crate::IfcType;
#[test]
fn test_decode_entity() {
let content = r#"
#1=IFCPROJECT('2vqT3bvqj9RBFjLlXpN8n9',$,$,$,$,$,$,$,$);
#2=IFCWALL('3a4T3bvqj9RBFjLlXpN8n0',$,$,$,'Wall-001',$,#3,#4);
#3=IFCLOCALPLACEMENT($,#4);
#4=IFCAXIS2PLACEMENT3D(#5,$,$);
#5=IFCCARTESIANPOINT((0.,0.,0.));
"#;
let mut decoder = EntityDecoder::new(content);
let start = content.find("#2=").unwrap();
let end = content[start..].find(';').unwrap() + start + 1;
let entity = decoder.decode_at(start, end).unwrap();
assert_eq!(entity.id, 2);
assert_eq!(entity.ifc_type, IfcType::IfcWall);
assert_eq!(entity.attributes.len(), 8);
assert_eq!(entity.get_string(4), Some("Wall-001"));
assert_eq!(entity.get_ref(6), Some(3));
assert_eq!(entity.get_ref(7), Some(4));
}
#[test]
fn test_decode_by_id() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#5=IFCWALL('guid2',$,$,$,'Wall-001',$,$,$);
#10=IFCDOOR('guid3',$,$,$,'Door-001',$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
let entity = decoder.decode_by_id(5).unwrap();
assert_eq!(entity.id, 5);
assert_eq!(entity.ifc_type, IfcType::IfcWall);
assert_eq!(entity.get_string(4), Some("Wall-001"));
assert_eq!(decoder.cache_size(), 1);
let cached = decoder.get_cached(5).unwrap();
assert_eq!(cached.id, 5);
}
#[test]
fn test_build_entity_index_matches_scanner_header_semantics() {
let content = "ISO-10303-21;\nHEADER;\n\
FILE_DESCRIPTION(('ViewDefinition [ReferenceView]'),'2;1');\n\
FILE_NAME('26-IFC\\X2\\00B1\\X0\\2#.ifc','2026-04-29T18:21:27',$,$,'CATIA','CATIA',$);\n\
FILE_SCHEMA(('IFC4'));\nENDSEC;\n\
DATA;\n\
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);\n\
#2=IFCWALL('guid2',$,$,$,'Wall; with semicolon',$,$,$);\n\
ENDSEC;\nEND-ISO-10303-21;\n";
let index = build_entity_index(content);
assert_eq!(index.len(), 2);
assert!(!index.contains_key(&26));
let (start, end) = index.get(&2).copied().unwrap();
assert_eq!(
&content[start..end],
"#2=IFCWALL('guid2',$,$,$,'Wall; with semicolon',$,$,$);"
);
}
#[test]
fn test_decode_by_id_handles_quoted_semicolon_from_shared_index() {
let content = "#1=IFCWALL('guid',$,$,$,'Wall; with semicolon',$,$,$);\n";
let mut decoder = EntityDecoder::new(content);
let wall = decoder.decode_by_id(1).unwrap();
assert_eq!(wall.id, 1);
assert_eq!(wall.ifc_type, IfcType::IfcWall);
assert_eq!(wall.get_string(4), Some("Wall; with semicolon"));
}
#[test]
fn test_resolve_ref() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,#1,$);
"#;
let mut decoder = EntityDecoder::new(content);
let wall = decoder.decode_by_id(2).unwrap();
let placement_attr = wall.get(6).unwrap();
let referenced = decoder.resolve_ref(placement_attr).unwrap().unwrap();
assert_eq!(referenced.id, 1);
assert_eq!(referenced.ifc_type, IfcType::IfcProject);
}
#[test]
fn test_resolve_ref_list() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid1',$,$,$,$,$,$,$);
#3=IFCDOOR('guid2',$,$,$,$,$,$,$);
#4=IFCRELCONTAINEDINSPATIALSTRUCTURE('guid3',$,$,$,(#2,#3),$,#1);
"#;
let mut decoder = EntityDecoder::new(content);
let rel = decoder.decode_by_id(4).unwrap();
let elements_attr = rel.get(4).unwrap();
let elements = decoder.resolve_ref_list(elements_attr).unwrap();
assert_eq!(elements.len(), 2);
assert_eq!(elements[0].id, 2);
assert_eq!(elements[0].ifc_type, IfcType::IfcWall);
assert_eq!(elements[1].id, 3);
assert_eq!(elements[1].ifc_type, IfcType::IfcDoor);
}
#[test]
fn test_cache() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.cache_size(), 0);
decoder.decode_by_id(1).unwrap();
assert_eq!(decoder.cache_size(), 1);
decoder.decode_by_id(2).unwrap();
assert_eq!(decoder.cache_size(), 2);
decoder.decode_by_id(1).unwrap();
assert_eq!(decoder.cache_size(), 2);
decoder.clear_cache();
assert_eq!(decoder.cache_size(), 0);
}
#[test]
fn polyloop_point_cache_memoizes_shared_points() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#13=IFCCARTESIANPOINT((0.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12,#13));
#21=IFCPOLYLOOP((#10,#11,#12,#13));
";
let mut decoder = EntityDecoder::new(content);
let first = decoder.get_polyloop_coords_cached(20).expect("first loop resolves");
let (hits_after_first, misses_after_first) = decoder.point_cache_stats();
assert_eq!(hits_after_first, 0);
assert_eq!(misses_after_first, 4);
let second = decoder.get_polyloop_coords_cached(21).expect("second loop resolves");
let (hits, misses) = decoder.point_cache_stats();
assert!(hits > 0, "expected point-cache hits across loops, got {hits}");
assert_eq!(hits, 4);
assert_eq!(misses, 4);
assert_eq!(first, second);
assert_eq!(
first,
vec![(0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.)]
);
}
#[test]
fn point_cache_survives_take_and_set_across_decoders() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12));
#21=IFCPOLYLOOP((#10,#11,#12));
";
let mut warm = EntityDecoder::new(content);
let a = warm.get_polyloop_coords_cached(20).expect("warm loop resolves");
assert_eq!(warm.point_cache_stats(), (0, 3));
let mut adopter = EntityDecoder::new(content);
adopter.set_point_cache(warm.take_point_cache());
let b = adopter.get_polyloop_coords_cached(21).expect("adopter loop resolves");
let (hits, misses) = adopter.point_cache_stats();
assert_eq!(hits, 3, "adopted cache should serve every point as a hit");
assert_eq!(misses, 0);
assert_eq!(a, b);
assert!(warm.take_point_cache().is_empty());
}
#[test]
fn point_cache_survives_a_failed_decode_between_elements() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12));
#21=IFCPOLYLOOP((#10,#11,#12));
";
let mut warm = EntityDecoder::new(content);
warm.get_polyloop_coords_cached(20).expect("warm loop resolves");
assert_eq!(warm.point_cache_stats(), (0, 3));
let carried = warm.take_point_cache();
let mut failing = EntityDecoder::new(content);
failing.set_point_cache(carried);
assert!(
failing.decode_at(10_000, 10_010).is_err(),
"out-of-range decode should fail without clearing the cache"
);
let recovered = failing.take_point_cache();
assert_eq!(
recovered.len(),
3,
"a failed decode must not drop the worker's warm point cache"
);
let mut next = EntityDecoder::new(content);
next.set_point_cache(recovered);
next.get_polyloop_coords_cached(21).expect("next loop resolves");
let (hits, misses) = next.point_cache_stats();
assert!(
hits > 0,
"expected warm-cache hits after a failed decode, got {hits}"
);
assert_eq!((hits, misses), (3, 0));
}