Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack. Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
5.8 KiB
Rust
209 lines
5.8 KiB
Rust
//! Integration tests for SystemBuilder::edge_with_ports()
|
||
|
||
use entropyk::SystemBuilder;
|
||
use entropyk_components::{ComponentError, ConnectedPort, JacobianBuilder, ResidualVector};
|
||
|
||
/// A minimal mock component with configurable ports for testing.
|
||
struct MockComponentWithPorts {
|
||
n_eqs: usize,
|
||
ports: Vec<ConnectedPort>,
|
||
}
|
||
|
||
impl MockComponentWithPorts {
|
||
fn new(n_eqs: usize) -> Self {
|
||
Self {
|
||
n_eqs,
|
||
ports: Vec::new(),
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
fn with_ports(n_eqs: usize, ports: Vec<ConnectedPort>) -> Self {
|
||
Self { n_eqs, ports }
|
||
}
|
||
}
|
||
|
||
impl entropyk_components::Component for MockComponentWithPorts {
|
||
fn compute_residuals(
|
||
&self,
|
||
_state: &[f64],
|
||
_residuals: &mut ResidualVector,
|
||
) -> Result<(), ComponentError> {
|
||
Ok(())
|
||
}
|
||
|
||
fn jacobian_entries(
|
||
&self,
|
||
_state: &[f64],
|
||
_jacobian: &mut JacobianBuilder,
|
||
) -> Result<(), ComponentError> {
|
||
Ok(())
|
||
}
|
||
|
||
fn n_equations(&self) -> usize {
|
||
self.n_eqs
|
||
}
|
||
|
||
fn get_ports(&self) -> &[ConnectedPort] {
|
||
&self.ports
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_component_not_found() {
|
||
let result = SystemBuilder::new()
|
||
.component("a", Box::new(MockComponentWithPorts::new(1)))
|
||
.unwrap()
|
||
.edge_with_ports("missing", "outlet", "a", "inlet");
|
||
|
||
assert!(result.is_err());
|
||
if let Err(ref err) = result {
|
||
assert!(
|
||
err.to_string().contains("not found"),
|
||
"Expected ComponentNotFound error, got: {}",
|
||
err
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_target_not_found() {
|
||
let result = SystemBuilder::new()
|
||
.component("a", Box::new(MockComponentWithPorts::new(1)))
|
||
.unwrap()
|
||
.edge_with_ports("a", "outlet", "missing", "inlet");
|
||
|
||
assert!(result.is_err());
|
||
if let Err(ref err) = result {
|
||
assert!(
|
||
err.to_string().contains("not found"),
|
||
"Expected ComponentNotFound error, got: {}",
|
||
err
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_cross_circuit_rejected() {
|
||
use entropyk_core::CircuitId;
|
||
|
||
let result = SystemBuilder::new()
|
||
.component_in_circuit(
|
||
"a",
|
||
Box::new(MockComponentWithPorts::new(1)),
|
||
CircuitId::ZERO,
|
||
)
|
||
.unwrap()
|
||
.component_in_circuit("b", Box::new(MockComponentWithPorts::new(1)), CircuitId(1))
|
||
.unwrap()
|
||
.edge_with_ports("a", "outlet", "b", "inlet");
|
||
|
||
assert!(result.is_err());
|
||
if let Err(ref err) = result {
|
||
assert!(
|
||
err.to_string().contains("different circuits"),
|
||
"Expected CrossCircuitEdge error, got: {}",
|
||
err
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_convention_based_names() {
|
||
let builder = SystemBuilder::new()
|
||
.component("a", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.component("b", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.edge_with_ports("a", "outlet", "b", "inlet")
|
||
.expect("edge_with_ports should resolve convention-based port names");
|
||
|
||
assert_eq!(builder.edge_count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_suction_discharge_names() {
|
||
let builder = SystemBuilder::new()
|
||
.component("comp", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.component("pipe", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.edge_with_ports("comp", "discharge", "pipe", "inlet")
|
||
.expect("edge_with_ports should resolve suction/discharge convention names");
|
||
|
||
assert_eq!(builder.edge_count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_unknown_port_name_error() {
|
||
use entropyk::SystemBuilderError;
|
||
|
||
let result = SystemBuilder::new()
|
||
.component("a", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.component("b", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.edge_with_ports("a", "bogus_port", "b", "inlet");
|
||
|
||
assert!(result.is_err());
|
||
if let Err(SystemBuilderError::PortNotFound {
|
||
component,
|
||
port_name,
|
||
}) = result
|
||
{
|
||
assert_eq!(component, "a");
|
||
assert!(
|
||
port_name.starts_with("bogus_port"),
|
||
"port_name should start with the port name, got: {port_name}"
|
||
);
|
||
} else {
|
||
panic!("Expected PortNotFound error");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_edge_with_ports_same_circuit_succeeds() {
|
||
use entropyk_core::CircuitId;
|
||
|
||
let builder = SystemBuilder::new()
|
||
.component_in_circuit(
|
||
"a",
|
||
Box::new(MockComponentWithPorts::new(2)),
|
||
CircuitId::ZERO,
|
||
)
|
||
.unwrap()
|
||
.component_in_circuit(
|
||
"b",
|
||
Box::new(MockComponentWithPorts::new(2)),
|
||
CircuitId::ZERO,
|
||
)
|
||
.unwrap()
|
||
.edge_with_ports("a", "outlet", "b", "inlet")
|
||
.expect("edge_with_ports should succeed for same-circuit components");
|
||
|
||
assert_eq!(builder.edge_count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_build_system_with_port_validated_edges() {
|
||
// DoF ledger post-CM1.4: 2-edge chain → 1 branch ṁ + 2×(P,h) = 5 unknowns,
|
||
// so component equations must total 5 for the finalize DoF gate (2+2+1).
|
||
// The last mock contributes a single equation to keep the system square.
|
||
let system = SystemBuilder::new()
|
||
.component("a", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.component("b", Box::new(MockComponentWithPorts::new(2)))
|
||
.unwrap()
|
||
.component("c", Box::new(MockComponentWithPorts::new(1)))
|
||
.unwrap()
|
||
.edge_with_ports("a", "outlet", "b", "inlet")
|
||
.unwrap()
|
||
.edge_with_ports("b", "outlet", "c", "inlet")
|
||
.unwrap()
|
||
.build()
|
||
.expect("build should succeed");
|
||
|
||
assert_eq!(system.node_count(), 3);
|
||
assert_eq!(system.edge_count(), 2);
|
||
}
|