Skip to main content

jellyflow_runtime/runtime/conformance/scenario/
suite.rs

1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::Graph;
4
5use super::ConformanceScenarioCompiled;
6use super::action::ConformanceAction;
7use super::behavior::ConformanceBehavior;
8use super::constants::default_schema_version;
9use super::setup::{ConformanceSetup, ConformanceTraceConfig};
10use super::trace::ConformanceTraceEvent;
11use crate::runtime::conformance::capability::{
12    ConformanceCapabilityMatrix, ConformanceCapabilityRequirement,
13};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ConformanceScenario {
17    #[serde(default = "default_schema_version")]
18    pub schema_version: u32,
19    pub name: String,
20    #[serde(default)]
21    setup: ConformanceSetup,
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub actions: Vec<ConformanceAction>,
24    #[serde(default, skip_serializing_if = "Vec::is_empty")]
25    pub behaviors: Vec<ConformanceBehavior>,
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub expected_trace: Vec<ConformanceTraceEvent>,
28}
29
30impl ConformanceScenario {
31    pub fn new(name: impl Into<String>, graph: Graph) -> Self {
32        Self {
33            schema_version: default_schema_version(),
34            name: name.into(),
35            setup: ConformanceSetup::from_graph(graph),
36            actions: Vec::new(),
37            behaviors: Vec::new(),
38            expected_trace: Vec::new(),
39        }
40    }
41
42    pub fn with_view_state(mut self, view_state: crate::io::NodeGraphViewState) -> Self {
43        self.setup.view_state = view_state;
44        self
45    }
46
47    pub fn with_editor_config(mut self, editor_config: crate::io::NodeGraphEditorConfig) -> Self {
48        self.setup.editor_config = editor_config;
49        self
50    }
51
52    pub fn with_xyflow_callbacks(mut self) -> Self {
53        self.setup.trace = ConformanceTraceConfig::with_xyflow_callbacks();
54        self
55    }
56
57    pub fn with_actions(mut self, actions: impl IntoIterator<Item = ConformanceAction>) -> Self {
58        self.actions = actions.into_iter().collect();
59        self
60    }
61
62    pub fn with_behaviors(
63        mut self,
64        behaviors: impl IntoIterator<Item = ConformanceBehavior>,
65    ) -> Self {
66        self.behaviors = behaviors.into_iter().collect();
67        self
68    }
69
70    pub fn with_behavior(mut self, behavior: ConformanceBehavior) -> Self {
71        self.behaviors.push(behavior);
72        self
73    }
74
75    pub fn with_expected_trace(
76        mut self,
77        expected_trace: impl IntoIterator<Item = ConformanceTraceEvent>,
78    ) -> Self {
79        self.expected_trace = expected_trace.into_iter().collect();
80        self
81    }
82
83    pub(crate) fn compiled(&self) -> ConformanceScenarioCompiled {
84        ConformanceScenarioCompiled::from_scenario(self)
85    }
86
87    pub(crate) fn setup(&self) -> &ConformanceSetup {
88        &self.setup
89    }
90
91    pub fn expanded_actions(&self) -> Vec<ConformanceAction> {
92        self.compiled().actions().to_vec()
93    }
94
95    pub fn expanded_expected_trace(&self) -> Vec<ConformanceTraceEvent> {
96        self.compiled().expected_trace().to_vec()
97    }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ConformanceSuite {
102    #[serde(default = "default_schema_version")]
103    pub schema_version: u32,
104    pub name: String,
105    #[serde(default, skip_serializing_if = "ConformanceCapabilityMatrix::is_empty")]
106    pub capabilities: ConformanceCapabilityMatrix,
107    #[serde(default, skip_serializing_if = "Vec::is_empty")]
108    pub capability_requirements: Vec<ConformanceCapabilityRequirement>,
109    #[serde(default, skip_serializing_if = "Vec::is_empty")]
110    pub scenarios: Vec<ConformanceScenario>,
111}
112
113impl ConformanceSuite {
114    pub fn new(name: impl Into<String>) -> Self {
115        Self {
116            schema_version: default_schema_version(),
117            name: name.into(),
118            capabilities: ConformanceCapabilityMatrix::default(),
119            capability_requirements: Vec::new(),
120            scenarios: Vec::new(),
121        }
122    }
123
124    pub fn with_capabilities(mut self, capabilities: ConformanceCapabilityMatrix) -> Self {
125        self.capabilities = capabilities;
126        self
127    }
128
129    pub fn with_capability_requirement(
130        mut self,
131        requirement: ConformanceCapabilityRequirement,
132    ) -> Self {
133        self.capability_requirements.push(requirement);
134        self
135    }
136
137    pub fn with_capability_requirements(
138        mut self,
139        requirements: impl IntoIterator<Item = ConformanceCapabilityRequirement>,
140    ) -> Self {
141        self.capability_requirements.extend(requirements);
142        self
143    }
144
145    pub fn with_scenarios(
146        mut self,
147        scenarios: impl IntoIterator<Item = ConformanceScenario>,
148    ) -> Self {
149        self.scenarios = scenarios.into_iter().collect();
150        self
151    }
152
153    pub fn push_scenario(&mut self, scenario: ConformanceScenario) {
154        self.scenarios.push(scenario);
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::runtime::conformance::ConformanceNodeDragSessionContract;
162    use jellyflow_core::core::{CanvasPoint, Graph, GraphId, NodeId};
163
164    #[test]
165    fn compiled_scenario_strips_behavior_trace_suffix_from_approved_trace() {
166        let node_id = NodeId::from_u128(1);
167        let scenario = ConformanceScenario::new("compiled approval", Graph::new(GraphId::new()))
168            .with_behavior(ConformanceBehavior::node_drag_session(
169                ConformanceNodeDragSessionContract::new(
170                    node_id,
171                    CanvasPoint { x: 1.0, y: 2.0 },
172                    CanvasPoint { x: 3.0, y: 4.0 },
173                ),
174            ));
175        let compiled = scenario.compiled();
176        let approved =
177            compiled.approval_expected_trace(&scenario.expected_trace, compiled.expected_trace());
178
179        assert!(scenario.expected_trace.is_empty());
180        assert!(approved.is_empty());
181        assert_eq!(
182            compiled.expected_event_count(),
183            compiled.expected_trace().len()
184        );
185        assert_eq!(compiled.actions().len(), 1);
186    }
187}