#![allow(missing_docs)]
use openbim_step::schema::{ComplexIssue, ComplexLayout};
use openbim_step::{parse, SchemaGraph};
const GEOMETRY: &str = "\
SCHEMA G;
ENTITY item; name : STRING; END_ENTITY;
ENTITY geo SUBTYPE OF (item); END_ENTITY;
ENTITY crv SUBTYPE OF (geo); END_ENTITY;
ENTITY bounded SUBTYPE OF (crv); END_ENTITY;
ENTITY spline SUBTYPE OF (bounded);
degree : INTEGER;
points : LIST [2:?] OF item;
END_ENTITY;
ENTITY spline_with_knots SUBTYPE OF (spline);
knots : LIST [2:?] OF REAL;
END_ENTITY;
ENTITY rational_spline SUBTYPE OF (spline);
weights : LIST [2:?] OF REAL;
END_ENTITY;
ENTITY unit; dims : INTEGER; END_ENTITY;
ENTITY length_unit SUBTYPE OF (unit); END_ENTITY;
ENTITY si_unit SUBTYPE OF (unit);
prefix : OPTIONAL INTEGER;
kind : INTEGER;
DERIVE
SELF\\unit.dims : INTEGER := 0;
END_ENTITY;
END_SCHEMA;";
fn graph() -> SchemaGraph {
SchemaGraph::from_express(GEOMETRY)
}
fn slot_names(layout: &ComplexLayout<'_>) -> Vec<(String, Vec<String>)> {
layout
.parts()
.iter()
.map(|part| {
(
part.name().to_owned(),
part.slots()
.iter()
.map(|slot| slot.attribute().name.clone())
.collect(),
)
})
.collect()
}
fn owned(pairs: &[(&str, &[&str])]) -> Vec<(String, Vec<String>)> {
pairs
.iter()
.map(|(name, slots)| {
(
(*name).to_owned(),
slots.iter().map(|slot| (*slot).to_owned()).collect(),
)
})
.collect()
}
#[test]
fn each_part_carries_only_its_own_explicit_attributes() {
let g = graph();
let layout = g.resolve_complex(&[
"BOUNDED",
"CRV",
"GEO",
"ITEM",
"RATIONAL_SPLINE",
"SPLINE",
"SPLINE_WITH_KNOTS",
]);
assert!(layout.is_valid(), "{:?}", layout.issues());
assert_eq!(
slot_names(&layout),
owned(&[
("BOUNDED", &[]),
("CRV", &[]),
("GEO", &[]),
("ITEM", &["name"]),
("RATIONAL_SPLINE", &["weights"]),
("SPLINE", &["degree", "points"]),
("SPLINE_WITH_KNOTS", &["knots"]),
])
);
}
#[test]
fn a_partial_record_is_not_the_internal_mapping() {
let g = graph();
assert_eq!(
g.attribute_names("rational_spline"),
["name", "degree", "points", "weights"]
);
let layout = g.resolve_complex(&["BOUNDED", "CRV", "GEO", "ITEM", "RATIONAL_SPLINE", "SPLINE"]);
assert_eq!(layout.parts()[4].slots().len(), 1);
}
#[test]
fn types_are_the_supertype_closure_in_name_order() {
let g = graph();
let layout = g.resolve_complex(&["LENGTH_UNIT", "SI_UNIT", "UNIT"]);
assert!(layout.is_valid(), "{:?}", layout.issues());
assert_eq!(layout.types(), ["length_unit", "si_unit", "unit"]);
}
#[test]
fn a_slot_redeclared_as_derived_by_another_part_is_marked() {
let g = graph();
let layout = g.resolve_complex(&["LENGTH_UNIT", "SI_UNIT", "UNIT"]);
let unit = &layout.parts()[2];
assert_eq!(unit.slots()[0].attribute().name, "dims");
assert!(unit.slots()[0].is_derived());
let si = &layout.parts()[1];
assert!(si.slots().iter().all(|slot| !slot.is_derived()));
let alone = g.resolve_complex(&["LENGTH_UNIT", "UNIT"]);
assert!(!alone.parts()[1].slots()[0].is_derived());
}
#[test]
fn an_unknown_part_is_reported_and_the_rest_still_resolve() {
let g = graph();
let layout = g.resolve_complex(&["LENGTH_UNIT", "MYSTERY", "UNIT"]);
assert_eq!(layout.issues(), [ComplexIssue::UnknownType { index: 1 }]);
assert!(layout.parts()[1].entity().is_none());
assert!(layout.parts()[1].slots().is_empty());
assert_eq!(layout.parts()[2].slots().len(), 1);
assert!(!layout.is_valid());
}
#[test]
fn parts_out_of_name_order_are_reported() {
let g = graph();
let layout = g.resolve_complex(&["UNIT", "LENGTH_UNIT"]);
assert_eq!(layout.issues(), [ComplexIssue::OutOfOrder { index: 1 }]);
assert_eq!(layout.parts()[0].name(), "UNIT");
}
#[test]
fn ordering_is_by_octet_so_underscore_sorts_after_letters() {
let schema = SchemaGraph::from_express(
"SCHEMA O; ENTITY b_x; END_ENTITY; ENTITY bounded; END_ENTITY; \
ENTITY both SUBTYPE OF (b_x, bounded); END_ENTITY; END_SCHEMA;",
);
assert!(schema
.resolve_complex(&["BOTH", "BOUNDED", "B_X"])
.is_valid());
let wrong = schema.resolve_complex(&["BOTH", "B_X", "BOUNDED"]);
assert_eq!(wrong.issues(), [ComplexIssue::OutOfOrder { index: 2 }]);
}
#[test]
fn a_duplicate_part_is_reported() {
let g = graph();
let layout = g.resolve_complex(&["LENGTH_UNIT", "UNIT", "unit"]);
assert_eq!(layout.issues(), [ComplexIssue::Duplicate { index: 2 }]);
}
#[test]
fn a_missing_supertype_is_reported_by_name() {
let g = graph();
let layout = g.resolve_complex(&["BOUNDED", "ITEM", "RATIONAL_SPLINE", "SPLINE"]);
assert_eq!(
layout.issues(),
[
ComplexIssue::MissingSupertype {
name: "crv".to_owned()
},
ComplexIssue::MissingSupertype {
name: "geo".to_owned()
},
]
);
assert!(layout.types().contains(&"crv"));
}
#[test]
fn issues_combine_and_come_out_in_a_stable_order() {
let g = graph();
let layout = g.resolve_complex(&["UNIT", "NOPE", "LENGTH_UNIT", "UNIT"]);
assert_eq!(
layout.issues(),
[
ComplexIssue::UnknownType { index: 1 },
ComplexIssue::OutOfOrder { index: 1 },
ComplexIssue::OutOfOrder { index: 2 },
ComplexIssue::Duplicate { index: 3 },
]
);
}
#[test]
fn an_empty_set_is_empty_and_valid() {
let g = graph();
let layout = g.resolve_complex(&[]);
assert!(layout.parts().is_empty());
assert!(layout.types().is_empty());
assert!(layout.is_valid());
}
#[test]
fn the_specification_example_resolves_to_one_slot_per_part() {
let schema = SchemaGraph::from_express(
"SCHEMA E; ENTITY aa SUPERTYPE OF (bb ANDOR cc); attrib_a : STRING; END_ENTITY; \
ENTITY bb SUBTYPE OF (aa); attrib_b : INTEGER; END_ENTITY; \
ENTITY cc SUBTYPE OF (aa); attrib_c : REAL; END_ENTITY; END_SCHEMA;",
);
let exchange = parse(
b"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
FILE_NAME('','',(''),(''),'','','');\nFILE_SCHEMA(('E'));\nENDSEC;\n\
DATA;\n#3=(AA('ASTRID')BB(17)CC(4.0));\nENDSEC;\nEND-ISO-10303-21;\n",
)
.expect("parse");
let record = &exchange.data.records[0];
let names: Vec<&str> = record
.records
.iter()
.map(|part| part.name.as_str())
.collect();
let layout = schema.resolve_complex(&names);
assert!(layout.is_valid(), "{:?}", layout.issues());
for (part, written) in layout.parts().iter().zip(&record.records) {
assert_eq!(
part.slots().len(),
written.parameters.len(),
"{}",
part.name()
);
}
assert_eq!(layout.types(), ["aa", "bb", "cc"]);
}
#[test]
fn every_complex_instance_in_real_ap214_files_resolves() {
let (Some(schemas), Some(data)) = (
std::env::var_os("STEP_AP_SCHEMA_DIR"),
std::env::var_os("STEP_OCCT_DATA_DIR"),
) else {
return;
};
let source = std::fs::read_to_string(std::path::Path::new(&schemas).join("AP214E3_2010.exp"))
.expect("AP214 schema readable");
let g = SchemaGraph::from_express(&source);
let mut resolved = 0;
for file in ["linkrods.step", "screw.step"] {
let bytes = std::fs::read(std::path::Path::new(&data).join(file)).expect("data readable");
let exchange = parse(&bytes).expect("parse");
for record in exchange.data.records.iter().filter(|r| r.records.len() > 1) {
let names: Vec<&str> = record
.records
.iter()
.map(|part| part.name.as_str())
.collect();
let layout = g.resolve_complex(&names);
assert!(
layout.is_valid(),
"{file} #{}: {:?}",
record.id.as_str(),
layout.issues()
);
for (part, written) in layout.parts().iter().zip(&record.records) {
assert_eq!(
part.slots().len(),
written.parameters.len(),
"{file} #{} {}",
record.id.as_str(),
part.name()
);
for (slot, value) in part.slots().iter().zip(&written.parameters) {
if matches!(value, openbim_step::Parameter::Derived) {
assert!(
slot.is_derived(),
"{file} #{} {}.{} is `*` but not derived",
record.id.as_str(),
part.name(),
slot.attribute().name
);
}
}
}
resolved += 1;
}
}
assert_eq!(resolved, 314, "255 in linkrods.step + 59 in screw.step");
}
#[test]
fn a_derivation_on_a_missing_supertype_still_marks_the_slot() {
let g = SchemaGraph::from_express(&format!(
"{}\nENTITY metric_si_unit SUBTYPE OF (si_unit); END_ENTITY; END_SCHEMA;",
GEOMETRY.trim_end_matches("END_SCHEMA;")
));
let layout = g.resolve_complex(&["METRIC_SI_UNIT", "UNIT"]);
assert_eq!(
layout.issues(),
[ComplexIssue::MissingSupertype {
name: "si_unit".to_owned()
}]
);
let unit = &layout.parts()[1];
assert_eq!(unit.slots()[0].attribute().name, "dims");
assert!(unit.slots()[0].is_derived());
}
#[test]
fn a_known_part_without_attributes_resolves_to_its_entity_and_no_slots() {
let g = graph();
let layout = g.resolve_complex(&["BOUNDED", "CRV", "GEO", "ITEM", "SPLINE"]);
assert!(layout.is_valid(), "{:?}", layout.issues());
let crv = &layout.parts()[1];
assert_eq!(crv.name(), "CRV");
assert_eq!(crv.entity().map(|e| e.name.as_str()), Some("crv"));
assert!(crv.slots().is_empty());
}