use super::*;
fn units_of(ifc: &str) -> ProjectUnits {
let mut decoder = EntityDecoder::new(ifc);
ProjectUnits::resolve(&mut decoder, 1)
}
const VZT_LIKE: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#20,$);
#3=IFCUNITASSIGNMENT((#7,#8,#9,#10));
#7=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#8=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#9=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#5=IFCDERIVEDUNITELEMENT(#4,3);
#6=IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#11=IFCDERIVEDUNITELEMENT(#6,-1);
#10=IFCDERIVEDUNIT((#5,#11),.VOLUMETRICFLOWRATEUNIT.,$,$);
#20=IFCAXIS2PLACEMENT3D(#21,$,$);
#21=IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;
"#;
#[test]
fn resolves_vzt_flow_rate_as_derived_m3_per_s() {
let units = units_of(VZT_LIKE);
let u = units.unit_for_measure("IfcVolumetricFlowRateMeasure").unwrap();
assert_eq!(u.symbol, "m\u{00B3}/s");
assert!((u.si_scale - 1.0).abs() < 1e-12);
}
#[test]
fn resolves_length_area_volume_from_file() {
let units = units_of(VZT_LIKE);
let len = units.unit_for_measure("IfcPositiveLengthMeasure").unwrap();
assert_eq!(len.symbol, "mm");
assert!((len.si_scale - 1e-3).abs() < 1e-15);
assert_eq!(units.unit_for_measure("IfcAreaMeasure").unwrap().symbol, "m\u{00B2}");
assert_eq!(units.unit_for_measure("IfcVolumeMeasure").unwrap().symbol, "m\u{00B3}");
}
#[test]
fn falls_back_to_si_default_when_unit_type_absent() {
let units = units_of(VZT_LIKE);
let p = units.unit_for_measure("IfcPressureMeasure").unwrap();
assert_eq!(p.symbol, "Pa");
assert!((p.si_scale - 1.0).abs() < 1e-12);
}
#[test]
fn dimensionless_and_nonmeasure_have_no_unit() {
let units = units_of(VZT_LIKE);
assert!(units.unit_for_measure("IfcRatioMeasure").is_none());
assert!(units.unit_for_measure("IfcLabel").is_none());
assert!(units.unit_for_measure("IfcCountMeasure").is_none());
}
#[test]
fn conversion_based_degree_plane_angle() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#5,#10));
#5=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#8=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#9=IFCMEASUREWITHUNIT(IFCRATIOMEASURE(0.0174532925199433),#8);
#10=IFCCONVERSIONBASEDUNIT(#11,.PLANEANGLEUNIT.,'DEGREE',#9);
#11=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
let a = units.unit_for_measure("IfcPlaneAngleMeasure").unwrap();
assert_eq!(a.symbol, "\u{00B0}");
assert!((a.si_scale - 0.0174532925199433).abs() < 1e-15);
}
#[test]
fn conversion_based_unit_folds_prefixed_component_scale() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#10));
#8=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#9=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(25.4),#8);
#10=IFCCONVERSIONBASEDUNIT(#11,.LENGTHUNIT.,'INCH',#9);
#11=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
let len = units.unit_for_measure("IfcLengthMeasure").unwrap();
assert_eq!(len.symbol, "in");
assert!((len.si_scale - 0.0254).abs() < 1e-12, "expected 0.0254, got {}", len.si_scale);
}
#[test]
fn conversion_based_unit_resolves_chained_conversion_component() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#20));
#5=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#6=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(0.3048),#5);
#7=IFCCONVERSIONBASEDUNIT(#11,.LENGTHUNIT.,'FOOT',#6);
#9=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(3.0),#7);
#20=IFCCONVERSIONBASEDUNIT(#11,.LENGTHUNIT.,'YARD',#9);
#11=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
let len = units.unit_for_measure("IfcLengthMeasure").unwrap();
assert!((len.si_scale - 0.9144).abs() < 1e-12, "expected 0.9144, got {}", len.si_scale);
}
#[test]
fn kilogram_mass_unit() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#5));
#5=IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
let m = units.unit_for_measure("IfcMassMeasure").unwrap();
assert_eq!(m.symbol, "kg");
assert!((m.si_scale - 1.0).abs() < 1e-12);
}
#[test]
fn monetary_unit_currency_symbol() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#5,#6));
#5=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#6=IFCMONETARYUNIT('EUR');
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
let money = units.unit_for_measure("IfcMonetaryMeasure").unwrap();
assert_eq!(money.symbol, "\u{20AC}");
}
#[test]
fn empty_when_no_assignment() {
let ifc = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),$);
ENDSEC;
END-ISO-10303-21;
"#;
let units = units_of(ifc);
assert_eq!(units.declared_len(), 0);
assert_eq!(units.unit_for_measure("IfcAreaMeasure").unwrap().symbol, "m\u{00B2}");
}
#[test]
fn cyclic_derived_unit_terminates_not_stack_overflow() {
let content = "\
#10=IFCDERIVEDUNIT((#11),.USERDEFINED.);
#11=IFCDERIVEDUNITELEMENT(#10,1);
";
let mut decoder = EntityDecoder::new(content);
assert!(resolve_unit_by_ref(&mut decoder, 10).is_none());
}
fn resolve_within(
content: String,
limit: std::time::Duration,
) -> Option<(Option<String>, ResolvedUnit, bool)> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut decoder = EntityDecoder::new(&content);
let _ = tx.send(resolve_unit_by_ref(&mut decoder, 10));
});
match rx.recv_timeout(limit) {
Ok(out) => out,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
panic!("resolve_unit_by_ref did not return within {limit:?}")
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
panic!("resolve_unit_by_ref's worker panicked (not a hang)")
}
}
}
#[test]
fn cyclic_derived_unit_with_fan_out_returns_in_bounded_time() {
let content = "\
#10=IFCDERIVEDUNIT((#11,#12,#13),.USERDEFINED.);
#11=IFCDERIVEDUNITELEMENT(#10,1);
#12=IFCDERIVEDUNITELEMENT(#10,1);
#13=IFCDERIVEDUNITELEMENT(#10,1);
"
.to_string();
let out = resolve_within(content, std::time::Duration::from_secs(1));
assert!(out.is_none(), "a derived unit made only of itself resolves to nothing");
}
#[test]
fn acyclic_derived_unit_fan_out_is_refused_in_bounded_time() {
let mut content = String::new();
for level in 0..12u32 {
let unit = 10 + level * 10;
let next = unit + 10;
content.push_str(&format!(
"#{unit}=IFCDERIVEDUNIT((#{a},#{b},#{c},#{d}),.USERDEFINED.);\n\
#{a}=IFCDERIVEDUNITELEMENT(#{next},1);\n\
#{b}=IFCDERIVEDUNITELEMENT(#{next},1);\n\
#{c}=IFCDERIVEDUNITELEMENT(#{next},1);\n\
#{d}=IFCDERIVEDUNITELEMENT(#{next},1);\n",
a = unit + 1,
b = unit + 2,
c = unit + 3,
d = unit + 4,
));
}
content.push_str("#130=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n");
let out = resolve_within(content, std::time::Duration::from_secs(1));
assert!(out.is_none(), "an over-budget walk must refuse the unit, got {out:?}");
}
#[test]
fn an_over_budget_unit_graph_is_refused_not_truncated() {
let n = 300u32;
let elements: Vec<String> = (0..n).map(|i| format!("#{}", 100 + 2 * i)).collect();
let mut content = format!("#10=IFCDERIVEDUNIT(({}),.USERDEFINED.);\n", elements.join(","));
for i in 0..n {
let (elem, unit) = (100 + 2 * i, 101 + 2 * i);
content.push_str(&format!(
"#{elem}=IFCDERIVEDUNITELEMENT(#{unit},1);\n#{unit}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n"
));
}
let out = resolve_within(content, std::time::Duration::from_secs(1));
assert!(out.is_none(), "an over-budget walk must refuse the unit, got {out:?}");
}
#[test]
fn real_derived_units_stay_well_under_the_budget() {
let content = "\
#10=IFCDERIVEDUNIT((#11,#12),.VOLUMETRICFLOWRATEUNIT.,$);
#11=IFCDERIVEDUNITELEMENT(#20,3);
#12=IFCDERIVEDUNITELEMENT(#21,-1);
#20=IFCCONVERSIONBASEDUNIT(*,.LENGTHUNIT.,'FOOT',#30);
#30=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(0.3048),#22);
#21=IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#22=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
";
let mut decoder = EntityDecoder::new(content);
let (_, resolved, _) = resolve_unit_by_ref(&mut decoder, 10).expect("resolves");
assert_eq!(resolved.symbol, "ft\u{00B3}/s");
assert!((resolved.si_scale - 0.3048f64.powi(3)).abs() < 1e-12);
}
#[test]
fn a_cyclic_element_refuses_the_whole_unit_instead_of_truncating_it() {
let content = "\
#10=IFCDERIVEDUNIT((#11,#12,#13),.LINEARVELOCITYUNIT.,$);
#11=IFCDERIVEDUNITELEMENT(#10,1);
#12=IFCDERIVEDUNITELEMENT(#20,1);
#13=IFCDERIVEDUNITELEMENT(#21,-1);
#20=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#21=IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
";
let mut decoder = EntityDecoder::new(content);
assert!(resolve_unit_by_ref(&mut decoder, 10).is_none());
}
#[test]
fn derived_unit_element_exponent_i32_min_resolves_without_panic() {
let content = "\
#10=IFCDERIVEDUNIT((#11),.USERDEFINED.);
#11=IFCDERIVEDUNITELEMENT(#12,-2147483648);
#12=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#20=IFCDERIVEDUNIT((#21),.USERDEFINED.);
#21=IFCDERIVEDUNITELEMENT(#12,-9223372036854775808);
";
let mut decoder = EntityDecoder::new(content);
let (_, resolved, _) = resolve_unit_by_ref(&mut decoder, 10).expect("resolves");
assert!(resolved.symbol.starts_with("1/m"), "got {}", resolved.symbol);
let (_, saturated, _) = resolve_unit_by_ref(&mut decoder, 20).expect("resolves");
assert_eq!(saturated.symbol, resolved.symbol);
}
#[test]
fn issue_4690_an_unresolved_conversion_unit_is_not_a_resolved_unit() {
let unknown = "DATA;\n#1=IFCPROJECT('guid',$,'Test',$,$,$,$,$,#2);\n#2=IFCUNITASSIGNMENT((#3));\n\
#3=IFCCONVERSIONBASEDUNIT(#4,.LENGTHUNIT.,'CUBIT',#5);\n\
#4=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);\n\
#5=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(457.2),#99);\nENDSEC;\n";
assert_eq!(units_of(unknown).resolved_for_unit_type("LENGTHUNIT"), None);
let mut decoder = EntityDecoder::new(unknown);
assert!(resolve_unit_by_ref(&mut decoder, 3).is_none());
}