mod common;
use common::fixture;
use ifc_model::{Entity, EntityId, Model, Value};
use ifc_systems::{
ports, role_inconsistencies, spatial_placements, zones, ConnectionGraph, ElementRole,
FlowNetwork, RoleInconsistency, SystemAnomaly,
};
#[test]
fn element_roles_come_from_schema_ancestry() {
let model = fixture();
let (ports, _) = ports(&model);
let mut roles = std::collections::BTreeMap::new();
for port in &ports {
if let Some(element) = port.element {
if let Some(role) = ElementRole::of(&model, element) {
roles.insert(element, role);
}
}
}
let segments = roles
.values()
.filter(|r| **r == ElementRole::Segment)
.count();
assert_eq!(
segments, 6,
"three pipes, the backwards one, and the dead-end pair"
);
assert_eq!(
roles
.values()
.filter(|r| **r == ElementRole::Terminal)
.count(),
1,
"the radiator"
);
assert_eq!(
roles
.values()
.filter(|r| **r == ElementRole::MovingDevice)
.count(),
1,
"the pump"
);
}
#[test]
fn an_element_with_no_inlet_is_reported() {
let mut model = Model::default();
let mut add = |id: u64, ty: &str, attrs: Vec<Value>| {
model.insert(
EntityId(id),
Entity {
type_name: ty.into(),
attributes: attrs,
},
);
};
let port = |name: &str, dir: &str| {
vec![
Value::Text("g".into()),
Value::Null,
Value::Text(name.into()),
Value::Null,
Value::Null,
Value::Null,
Value::Null,
Value::Enum(dir.into()),
]
};
add(1, "IFCFLOWSEGMENT", vec![Value::Text("S".into())]);
add(2, "IFCDISTRIBUTIONPORT", port("a", "SOURCE"));
add(3, "IFCDISTRIBUTIONPORT", port("b", "SOURCE"));
add(
4,
"IFCRELNESTS",
vec![
Value::Text("g".into()),
Value::Null,
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::List(vec![Value::Ref(EntityId(2)), Value::Ref(EntityId(3))]),
],
);
let (ports, _) = ports(&model);
let found = role_inconsistencies(&model, &ports);
assert!(
found.iter().any(|i| matches!(
i,
RoleInconsistency::NoPath { element, has_inlet, .. }
if *element == EntityId(1) && !has_inlet
)),
"two SOURCE ports means nothing can enter: {found:?}"
);
}
#[test]
fn a_zone_member_that_wr1_forbids_is_excluded_and_reported() {
let model = fixture();
let (found, anomalies) = zones(&model);
let zone = found.first().expect("one zone");
assert_eq!(zone.members.len(), 2, "only the two spaces are members");
assert!(
anomalies.iter().any(|a| matches!(
a,
SystemAnomaly::ZoneMemberNotSpatial { type_name, .. }
if type_name.eq_ignore_ascii_case("IfcFlowTerminal")
)),
"the flow terminal violates WR1: {anomalies:?}"
);
}
#[test]
fn containment_and_referencing_are_not_merged() {
let model = fixture();
let (placements, anomalies) = spatial_placements(&model);
assert!(
anomalies.is_empty(),
"fixture is well-formed: {anomalies:?}"
);
let passing_through = placements
.values()
.find(|p| p.contained_in.is_none() && p.referenced_in.len() == 2)
.expect("seg2 is referenced by two spaces and contained by none");
assert_eq!(passing_through.referenced_in.len(), 2);
let contained = placements
.values()
.filter(|p| p.contained_in.is_some())
.count();
assert_eq!(contained, 4, "three heating elements plus the radiator");
}
#[test]
fn downstream_and_upstream_differ() {
let model = fixture();
let (ports, _) = ports(&model);
let (graph, _) = ConnectionGraph::build(&model);
let network = FlowNetwork::build(&graph, &ports);
let seg0 = ports
.iter()
.find(|p| p.name.as_deref() == Some("seg0-out"))
.and_then(|p| p.element)
.expect("seg0 has an outlet");
let down = network.downstream_of(seg0);
let up = network.upstream_of(seg0);
assert_ne!(
down.elements, up.elements,
"an oriented network must distinguish the two"
);
assert!(
!down.elements.is_empty(),
"seg0 feeds the rest of the chain"
);
}
#[test]
fn an_unstated_direction_is_flagged_not_hidden() {
let model = fixture();
let (ports, _) = ports(&model);
let (graph, _) = ConnectionGraph::build(&model);
let network = FlowNetwork::build(&graph, &ports);
let seg0 = ports
.iter()
.find(|p| p.name.as_deref() == Some("seg0-out"))
.and_then(|p| p.element)
.expect("seg0");
let pump = ports
.iter()
.find(|p| p.name.as_deref() == Some("pump-a"))
.and_then(|p| p.element)
.expect("pump");
let down = network.downstream_of(seg0);
assert!(
down.elements.contains(&pump),
"the pump is connected and must be reachable"
);
assert!(
down.used_undirected,
"reaching it relied on an unstated direction"
);
}
#[test]
fn a_directed_query_terminates_on_a_loop() {
let model = fixture();
let (ports, _) = ports(&model);
let (graph, _) = ConnectionGraph::build(&model);
let network = FlowNetwork::build(&graph, &ports);
for port in &ports {
if let Some(element) = port.element {
let _ = network.downstream_of(element);
let _ = network.upstream_of(element);
}
}
}
#[test]
fn an_element_that_cannot_pass_flow_is_reported() {
let model = fixture();
let (ports_list, _) = ports(&model);
let found = role_inconsistencies(&model, &ports_list);
let no_path = found
.iter()
.filter_map(|i| match i {
RoleInconsistency::NoPath {
element,
has_inlet,
has_outlet,
..
} => Some((*element, *has_inlet, *has_outlet)),
_ => None,
})
.collect::<Vec<_>>();
assert!(
no_path.iter().any(|(_, inlet, outlet)| !*inlet && *outlet),
"the two-SOURCE segment has an outlet but no inlet: {no_path:?}"
);
}
#[test]
fn a_sink_does_not_emit_downstream() {
let mut model = Model::default();
let mut add = |id: u64, ty: &str, attrs: Vec<Value>| {
model.insert(
EntityId(id),
Entity {
type_name: ty.into(),
attributes: attrs,
},
);
};
add(1, "IFCFLOWSEGMENT", vec![]);
add(2, "IFCFLOWSEGMENT", vec![]);
let port = |name: &str, flow: &str| {
vec![
Value::Text(name.into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Enum(flow.into()),
]
};
add(10, "IFCDISTRIBUTIONPORT", port("a-out", "SOURCE"));
add(11, "IFCDISTRIBUTIONPORT", port("b-in", "SINK"));
add(12, "IFCDISTRIBUTIONPORT", port("b-back", "SINK"));
add(
20,
"IFCRELNESTS",
vec![
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Ref(EntityId(1)),
Value::List(vec![Value::Ref(EntityId(10))]),
],
);
add(
21,
"IFCRELNESTS",
vec![
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Ref(EntityId(2)),
Value::List(vec![Value::Ref(EntityId(11)), Value::Ref(EntityId(12))]),
],
);
add(
30,
"IFCRELCONNECTSPORTS",
vec![
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Ref(EntityId(10)),
Value::Ref(EntityId(11)),
],
);
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let flow = FlowNetwork::build(&graph, &ports_list);
let down = flow.downstream_of(EntityId(1));
assert!(
down.elements.contains(&EntityId(2)),
"flow enters element 2 through its sink"
);
let from_two = flow.downstream_of(EntityId(2));
assert!(
from_two.elements.is_empty(),
"an element with only sinks emits nothing downstream: {:?}",
from_two.elements
);
}
#[test]
fn an_element_contained_twice_is_reported() {
let mut model = Model::default();
let mut add = |id: u64, ty: &str, attrs: Vec<Value>| {
model.insert(
EntityId(id),
Entity {
type_name: ty.into(),
attributes: attrs,
},
);
};
add(1, "IFCFLOWSEGMENT", vec![]);
add(2, "IFCSPACE", vec![]);
add(3, "IFCSPACE", vec![]);
for (id, structure) in [(10u64, 2u64), (11, 3)] {
add(
id,
"IFCRELCONTAINEDINSPATIALSTRUCTURE",
vec![
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::Text("".into()),
Value::List(vec![Value::Ref(EntityId(1))]),
Value::Ref(EntityId(structure)),
],
);
}
let (placements, anomalies) = spatial_placements(&model);
assert!(
anomalies
.iter()
.any(|a| matches!(a, SystemAnomaly::ContainedTwice { element, .. } if *element == EntityId(1))),
"the second containment must be reported: {anomalies:?}"
);
assert_eq!(
placements.get(&EntityId(1)).and_then(|p| p.contained_in),
Some(EntityId(2)),
"the first stated container is kept"
);
}
#[test]
fn flow_does_not_run_backwards_through_a_sink() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let flow = FlowNetwork::build(&graph, &ports_list);
let terminal = ports_list
.iter()
.find(|p| p.name.as_deref() == Some("term-in"))
.and_then(|p| p.element)
.expect("the radiator owns term-in");
assert!(
flow.downstream_of(terminal).elements.is_empty(),
"a radiator whose only port is a SINK feeds nothing"
);
assert!(
!flow.upstream_of(terminal).elements.is_empty(),
"the radiator is fed by the chain"
);
}
#[test]
fn a_sink_cannot_emit_even_towards_an_accepting_port() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let flow = FlowNetwork::build(&graph, &ports_list);
let owner = |port: &str| {
ports_list
.iter()
.find(|p| p.name.as_deref() == Some(port))
.and_then(|p| p.element)
.expect("fixture port is attached")
};
let dead = owner("dead-a");
let other = owner("dead-b");
assert!(
flow.downstream_of(dead).elements.is_empty(),
"a SINK port cannot emit, so nothing is downstream"
);
assert!(
!flow.upstream_of(dead).elements.contains(&other),
"and the mirrored rule holds walking upstream"
);
}
#[test]
fn a_one_way_spur_is_reachable_in_one_direction_only() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let flow = FlowNetwork::build(&graph, &ports_list);
let owner = |port: &str| {
ports_list
.iter()
.find(|p| p.name.as_deref() == Some(port))
.and_then(|p| p.element)
.expect("fixture port is attached")
};
let fitting = owner("fit-d");
let pump = owner("pump-a");
assert!(
flow.downstream_of(fitting).elements.contains(&pump),
"the fitting feeds the pump through fit-d"
);
assert!(
!flow.downstream_of(pump).elements.contains(&fitting),
"the spur is one-way: the pump does not feed the fitting back"
);
}