mod common;
use common::{fixture, model_with_system};
use ifc_model::{Codec, Entity, EntityId, Model, Value};
use ifc_systems::{
ports, systems, Attachment, ConnectionGraph, FlowDirection, NetworkGraph, SystemAnomaly,
};
#[test]
fn a_distribution_system_is_found_as_a_system() {
let (found, anomalies) = systems(&model_with_system());
assert_eq!(found.len(), 1, "the distribution system must be found");
assert_eq!(found[0].type_name, "IFCDISTRIBUTIONSYSTEM");
assert_eq!(found[0].name.as_deref(), Some("Heating"));
assert!(anomalies.is_empty(), "clean file: {anomalies:?}");
}
#[test]
fn members_are_read_from_the_relating_group_slot() {
let (found, _) = systems(&model_with_system());
assert_eq!(found[0].members, vec![EntityId(1), EntityId(2)]);
}
#[test]
fn a_system_with_no_members_is_still_a_system() {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new(
"IfcDistributionSystem",
vec![
Value::Text("guid".into()),
Value::Null,
Value::Text("Empty".into()),
],
),
);
let (found, anomalies) = systems(&model);
assert_eq!(found.len(), 1);
assert!(found[0].members.is_empty());
assert!(anomalies.is_empty());
}
#[test]
fn a_dangling_member_is_reported() {
let mut model = Model::new();
let system = EntityId(1);
model.insert(
system,
Entity::new(
"IfcDistributionSystem",
vec![Value::Text("guid".into()), Value::Null, Value::Null],
),
);
model.insert(
EntityId(2),
Entity::new(
"IfcRelAssignsToGroup",
vec![
Value::Text("rel".into()),
Value::Null,
Value::Null,
Value::Null,
Value::List(vec![Value::Ref(EntityId(99))]),
Value::Null,
Value::Ref(system),
],
),
);
let (found, anomalies) = systems(&model);
assert!(
found[0].members.is_empty(),
"the absent member is not a member"
);
assert_eq!(
anomalies,
vec![SystemAnomaly::Dangling {
relation: EntityId(2),
missing: EntityId(99),
}]
);
}
#[test]
fn assignment_to_a_non_system_group_is_not_a_system_membership() {
let mut model = Model::new();
let inventory = EntityId(1);
model.insert(
inventory,
Entity::new(
"IfcInventory",
vec![Value::Text("guid".into()), Value::Null, Value::Null],
),
);
model.insert(
EntityId(2),
Entity::new(
"IfcRelAssignsToGroup",
vec![
Value::Text("rel".into()),
Value::Null,
Value::Null,
Value::Null,
Value::List(vec![Value::Ref(EntityId(3))]),
Value::Null,
Value::Ref(inventory),
],
),
);
let (found, anomalies) = systems(&model);
assert!(found.is_empty(), "an IfcInventory is not an IfcSystem");
assert_eq!(
anomalies,
vec![SystemAnomaly::NotASystem {
relation: EntityId(2),
group: inventory,
type_name: "IFCINVENTORY".to_string(),
}]
);
}
#[test]
fn a_zone_is_discovered_because_the_schema_makes_it_a_system() {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new(
"IfcZone",
vec![
Value::Text("guid".into()),
Value::Null,
Value::Text("Fire compartment".into()),
],
),
);
let (found, _) = systems(&model);
assert_eq!(found.len(), 1, "IfcZone -> IfcSystem in the IFC4 schema");
assert_eq!(found[0].name.as_deref(), Some("Fire compartment"));
}
#[test]
fn the_committed_fixture_reads_its_systems() {
let model = fixture();
let (found, anomalies) = systems(&model);
let mut names: Vec<_> = found.iter().filter_map(|s| s.name.clone()).collect();
names.sort();
assert_eq!(names, vec!["Fire compartment", "Heating", "Ventilation"]);
let heating = found
.iter()
.find(|s| s.name.as_deref() == Some("Heating"))
.expect("heating system");
assert_eq!(
heating.members.len(),
5,
"two pipes, a fitting, a terminal and the pump"
);
assert!(
anomalies.iter().any(|a| matches!(
a,
SystemAnomaly::NotASystem { type_name, .. } if type_name == "IFCINVENTORY"
)),
"the inventory assignment must be reported: {anomalies:?}"
);
}
#[test]
fn systems_are_returned_in_ascending_id_order() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../test/fixtures/synthetic-systems/synthetic_systems.ifc");
let model = ifc_step::StepCodec
.read_path(&path)
.expect("fixture parses");
let (found, _) = systems(&model);
let ids: Vec<_> = found.iter().map(|s| s.id).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted, "system order must be deterministic");
assert!(ids.len() >= 3, "fixture states three systems");
}
#[test]
fn ports_attached_by_either_mechanism_resolve_to_their_element() {
let model = fixture();
let (ports, _) = ports(&model);
let nested = ports
.iter()
.find(|p| p.name.as_deref() == Some("seg0-in"))
.expect("nested port");
assert_eq!(nested.attachment, Some(Attachment::Nests));
assert!(nested.element.is_some(), "a nested port has an element");
let legacy = ports
.iter()
.find(|p| p.name.as_deref() == Some("seg1-in"))
.expect("legacy port");
assert_eq!(legacy.attachment, Some(Attachment::ConnectsPortToElement));
assert!(legacy.element.is_some(), "a legacy port has an element");
assert_ne!(
nested.element, legacy.element,
"the two ports belong to different segments"
);
}
#[test]
fn ports_are_found_by_ancestry_not_by_exact_type() {
let model = fixture();
assert!(
model.ids_of_type("IFCPORT").is_empty(),
"IfcPort is abstract; nothing is literally an IFCPORT"
);
let (ports, _) = ports(&model);
assert_eq!(ports.len(), 18, "every distribution port is found");
}
#[test]
fn an_unattached_port_keeps_its_place_with_no_element() {
let model = fixture();
let (ports, _) = ports(&model);
let orphan = ports
.iter()
.find(|p| p.name.as_deref() == Some("orphan"))
.expect("the unattached port is still listed");
assert_eq!(orphan.element, None);
assert_eq!(orphan.attachment, None);
}
#[test]
fn flow_direction_is_read_from_its_own_slot() {
let model = fixture();
let (ports, _) = ports(&model);
let by = |name: &str| {
ports
.iter()
.find(|p| p.name.as_deref() == Some(name))
.unwrap_or_else(|| panic!("fixture has {name}"))
.flow
};
assert_eq!(by("seg0-in"), FlowDirection::Sink);
assert_eq!(by("seg0-out"), FlowDirection::Source);
assert_eq!(by("fit-c"), FlowDirection::SourceAndSink);
assert_eq!(by("orphan"), FlowDirection::NotDefined);
}
#[test]
fn the_connection_graph_is_undirected() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let connection = graph
.connections()
.first()
.expect("the fixture states connections");
assert!(
graph
.neighbours(connection.relating)
.contains(&connection.related),
"forward edge"
);
assert!(
graph
.neighbours(connection.related)
.contains(&connection.relating),
"reverse edge: order is authoring order, not direction"
);
}
#[test]
fn a_connection_keeps_its_realizing_element() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let realized = graph
.connections()
.iter()
.filter(|c| c.realizing.is_some())
.count();
assert_eq!(
realized, 1,
"exactly one connection names a realizing element"
);
}
#[test]
fn stated_connections_alone_leave_a_chain_in_pieces() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let start = ports_list
.iter()
.find(|p| p.name.as_deref() == Some("seg0-in"))
.expect("fixture port")
.id;
assert_eq!(
graph.reachable_from(start).len(),
2,
"the raw connection graph reaches only the directly-named partner"
);
assert_eq!(
graph.components().len(),
7,
"seven stated connections, seven disconnected pairs"
);
}
#[test]
fn the_network_graph_connects_the_chain_and_terminates_on_a_ring() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let network = NetworkGraph::build(&graph, &ports_list);
let start = ports_list
.iter()
.find(|p| p.name.as_deref() == Some("seg0-in"))
.expect("fixture port")
.id;
assert_eq!(
network.reachable_from(start).len(),
11,
"the whole heating run is reachable, each port exactly once"
);
}
#[test]
fn a_disconnected_system_is_its_own_component() {
let model = fixture();
let (graph, _) = ConnectionGraph::build(&model);
let (ports_list, _) = ports(&model);
let network = NetworkGraph::build(&graph, &ports_list);
let mut sizes: Vec<_> = network.components().iter().map(Vec::len).collect();
sizes.sort_unstable();
assert_eq!(
sizes,
vec![2, 2, 2, 11],
"ventilation, the backwards segment and the dead-end pair each stay \
separate from the heating run"
);
}