#![allow(clippy::unwrap_used)]
use crate::products::{
AssemblyJoint, JointConnector, JointId, JointOperand, OperandContainer, PairedJointKind,
PlaneSense,
};
use crate::transform::Transform;
fn connector() -> JointConnector {
JointConnector {
operand: JointOperand::root("root:object", Vec::new()),
frame: Transform::identity(),
detached: false,
}
}
fn joint_id() -> JointId {
JointId::mint("test:model:joint#0").expect("identity grammar")
}
#[test]
fn a_joint_family_refuses_a_scalar_outside_it_by_name() {
let families: [(PairedJointKind, &[&str]); 19] = [
(
PairedJointKind::Fixed {
angle: None,
translation_offset: None,
angular_limits: None,
linear_limits: None,
},
&[
"angle",
"translation_offset",
"angular_limits",
"linear_limits",
],
),
(
PairedJointKind::Revolute {
angle: None,
angular_limits: None,
},
&["angle", "angular_limits"],
),
(
PairedJointKind::Slider {
distance: None,
translation_offset: None,
linear_limits: None,
},
&["distance", "translation_offset", "linear_limits"],
),
(
PairedJointKind::Cylindrical {
angle: None,
distance: None,
angular_limits: None,
linear_limits: None,
},
&["angle", "distance", "angular_limits", "linear_limits"],
),
(PairedJointKind::Ball {}, &[]),
(PairedJointKind::Distance { distance: None }, &["distance"]),
(PairedJointKind::Parallel {}, &[]),
(PairedJointKind::Perpendicular {}, &[]),
(PairedJointKind::Angle { angle: None }, &["angle"]),
(
PairedJointKind::RackPinion {
distance: None,
distance2: None,
},
&["distance", "distance2"],
),
(PairedJointKind::Screw { distance: None }, &["distance"]),
(
PairedJointKind::Gears {
distance: None,
distance2: None,
},
&["distance", "distance2"],
),
(
PairedJointKind::Belt {
distance: None,
distance2: None,
},
&["distance", "distance2"],
),
(
PairedJointKind::PlaneCoincidence {
sense: PlaneSense::Opposed,
offset: None,
},
&["offset"],
),
(PairedJointKind::AxisCoincidence {}, &[]),
(
PairedJointKind::PointOnAxis { distance: None },
&["distance"],
),
(
PairedJointKind::Insert {
sense: PlaneSense::Opposed,
offset: None,
},
&["offset"],
),
(
PairedJointKind::Tangent {
contact: crate::products::TangentContact::Outside,
},
&[],
),
(
PairedJointKind::Native {
name: "application_joint".into(),
angle: None,
translation_offset: None,
distance: None,
distance2: None,
angular_limits: None,
linear_limits: None,
},
&[
"angle",
"translation_offset",
"distance",
"distance2",
"angular_limits",
"linear_limits",
],
),
];
let fields = [
("angle", serde_json::json!(0.5)),
("translation_offset", serde_json::json!([1.0, 2.0, 3.0])),
("distance", serde_json::json!(2.0)),
("distance2", serde_json::json!(3.0)),
("offset", serde_json::json!(1.5)),
(
"angular_limits",
serde_json::json!({"bounds": "range", "minimum": 0.0, "maximum": 1.0}),
),
(
"linear_limits",
serde_json::json!({"bounds": "range", "minimum": 0.0, "maximum": 2.0}),
),
];
for (kind, allowed) in families {
let joint =
AssemblyJoint::paired(joint_id(), kind.clone(), [connector(), connector()], None);
let wire = serde_json::to_value(&joint).unwrap();
assert_eq!(
serde_json::from_value::<AssemblyJoint>(wire.clone()).unwrap(),
joint
);
for (field, value) in &fields {
let mut candidate = wire.clone();
candidate["operands"]["kind"][*field] = value.clone();
let result = serde_json::from_value::<AssemblyJoint>(candidate.clone());
assert_eq!(result.is_ok(), allowed.contains(field), "{kind:?}: {field}");
match result {
Ok(joint) => assert_eq!(
serde_json::to_value(joint).unwrap(),
candidate,
"{kind:?}: {field}"
),
Err(error) => assert!(
error
.to_string()
.contains(&format!("unknown field `{field}`")),
"{kind:?}: {field}: {error}"
),
}
}
}
}
#[test]
fn the_joint_wire_refuses_the_deleted_keys() {
let joint = AssemblyJoint::grounded(joint_id(), connector(), None);
let wire = serde_json::to_value(&joint).unwrap();
assert_eq!(wire["operands"]["arity"], "grounded");
assert!(wire.get("kind").is_none());
assert!(wire.get("frames").is_none());
assert!(wire.get("detached").is_none());
assert_eq!(
serde_json::from_value::<AssemblyJoint>(wire.clone()).unwrap(),
joint
);
for (level, key, value) in [
("properties", serde_json::json!({})),
("frames", serde_json::json!([])),
("detached", serde_json::json!([false, false])),
]
.map(|(key, value)| ("joint", key, value))
{
let mut candidate = wire.clone();
candidate.as_object_mut().unwrap().insert(key.into(), value);
let error = serde_json::from_value::<AssemblyJoint>(candidate)
.unwrap_err()
.to_string();
assert!(
error.contains(&format!("unknown field `{key}`")),
"{level}: {key}: {error}"
);
}
let mut candidate = wire;
candidate["operands"]["distance"] = serde_json::json!(1);
let error = serde_json::from_value::<AssemblyJoint>(candidate)
.unwrap_err()
.to_string();
assert!(error.contains("unknown field `distance`"), "{error}");
}
#[test]
fn a_nonfinite_joint_scalar_is_refused() {
let joint = AssemblyJoint::paired(
joint_id(),
PairedJointKind::Native {
name: "custom".into(),
angle: None,
translation_offset: None,
distance: None,
distance2: None,
angular_limits: None,
linear_limits: None,
},
[connector(), connector()],
None,
);
let wire = serde_json::to_string(&joint).unwrap();
for field in ["angle", "distance", "distance2"] {
let candidate = wire.replace(
"\"name\":\"custom\"",
&format!("\"name\":\"custom\",\"{field}\":1e400"),
);
assert!(
serde_json::from_str::<AssemblyJoint>(&candidate).is_err(),
"{field}"
);
}
}
#[test]
fn a_plane_coincidence_keeps_its_sense_path_and_driving_parameter() {
let occurrence = crate::ids::OccurrenceId::mint("test:model:occurrence#4").unwrap();
let nested = JointConnector {
operand: JointOperand::nested_occurrence(
occurrence.clone(),
vec!["2".into(), "7".into()],
"part:object",
vec!["face:3".into()],
),
frame: Transform::identity(),
detached: false,
};
let mut joint = AssemblyJoint::paired(
joint_id(),
PairedJointKind::PlaneCoincidence {
sense: PlaneSense::Aligned,
offset: Some(crate::scalar::FiniteReal::new(-2.5).unwrap()),
},
[connector(), nested],
None,
);
joint.driving_parameter =
Some(crate::features::ParameterId::mint("test:model:parameter#0").unwrap());
let wire = serde_json::to_value(&joint).unwrap();
assert_eq!(wire["operands"]["kind"]["sense"], "aligned");
assert_eq!(wire["operands"]["kind"]["offset"], -2.5);
assert_eq!(
wire["operands"]["connectors"][1]["operand"]["container"]["occurrence_path"],
serde_json::json!(["2", "7"])
);
assert_eq!(wire["driving_parameter"], "test:model:parameter#0");
assert_eq!(
serde_json::from_value::<AssemblyJoint>(wire.clone()).unwrap(),
joint
);
let mut missing_sense = wire.clone();
missing_sense["operands"]["kind"]
.as_object_mut()
.unwrap()
.remove("sense");
assert!(serde_json::from_value::<AssemblyJoint>(missing_sense).is_err());
let flat = JointOperand::occurrence(occurrence, "part:object", Vec::new());
let wire = serde_json::to_value(&flat).unwrap();
assert!(wire["container"].get("occurrence_path").is_none());
assert!(matches!(
serde_json::from_value::<JointOperand>(wire).unwrap().container,
OperandContainer::Occurrence { occurrence_path, .. } if occurrence_path.is_empty()
));
}
#[test]
fn insert_and_tangent_joints_keep_their_sense_and_contact() {
for (kind, key, value) in [
(
PairedJointKind::Insert {
sense: PlaneSense::Opposed,
offset: Some(crate::scalar::FiniteReal::new(2.0).unwrap()),
},
"sense",
"opposed",
),
(
PairedJointKind::Tangent {
contact: crate::products::TangentContact::Inside,
},
"contact",
"inside",
),
] {
let joint = AssemblyJoint::paired(joint_id(), kind, [connector(), connector()], None);
let wire = serde_json::to_value(&joint).unwrap();
assert_eq!(wire["operands"]["kind"][key], value);
assert_eq!(
serde_json::from_value::<AssemblyJoint>(wire.clone()).unwrap(),
joint
);
let mut missing = wire;
missing["operands"]["kind"]
.as_object_mut()
.unwrap()
.remove(key);
assert!(serde_json::from_value::<AssemblyJoint>(missing).is_err());
}
}