oxify_model/
test_utils.rs

1//! Testing Utilities - Helper functions for creating test workflows and mock data
2//!
3//! This module provides convenient functions to create test workflows, nodes,
4//! and analytics data for testing purposes. This is especially useful when
5//! writing tests for applications that use oxify-model.
6//!
7//! # Example
8//!
9//! ```
10//! use oxify_model::test_utils::{create_test_workflow, create_test_analytics};
11//!
12//! # fn example() {
13//! // Create a simple test workflow
14//! let workflow = create_test_workflow("test", 5);
15//! assert_eq!(workflow.nodes.len(), 5);
16//!
17//! // Create mock analytics data
18//! let analytics = create_test_analytics("test", 100, 0.85);
19//! assert_eq!(analytics.execution_stats.total_executions, 100);
20//! assert_eq!(analytics.execution_stats.success_rate, 0.85);
21//! # }
22//! ```
23
24use crate::{
25    analytics::{
26        AnalyticsPeriod, ExecutionStats, PerformanceMetrics, PeriodType, WorkflowAnalytics,
27    },
28    execution::{ExecutionContext, ExecutionResult, NodeExecutionResult, NodeMetrics, TokenUsage},
29    node::{LlmConfig, LoopConfig, Node, NodeKind},
30    workflow::{Workflow, WorkflowMetadata},
31    Edge, WorkflowBuilder,
32};
33use chrono::Utc;
34use std::collections::HashMap;
35use uuid::Uuid;
36
37/// Create a simple test workflow with the specified number of nodes
38///
39/// This creates a linear workflow with:
40/// - 1 Start node
41/// - N-2 LLM nodes (where N is the node_count parameter)
42/// - 1 End node
43///
44/// # Example
45///
46/// ```
47/// use oxify_model::test_utils::create_test_workflow;
48///
49/// let workflow = create_test_workflow("my_test", 5);
50/// assert_eq!(workflow.nodes.len(), 5);
51/// ```
52pub fn create_test_workflow(name: &str, node_count: usize) -> Workflow {
53    let mut builder = WorkflowBuilder::new(name);
54
55    // Start node
56    builder = builder.start("Start");
57
58    // Add intermediate LLM nodes
59    for i in 1..node_count.saturating_sub(1) {
60        let llm_config = LlmConfig {
61            provider: "test".to_string(),
62            model: "gpt-4".to_string(),
63            system_prompt: None,
64            prompt_template: format!("Process step {}", i),
65            temperature: Some(0.7),
66            max_tokens: Some(1000),
67            tools: vec![],
68            images: vec![],
69            extra_params: serde_json::Value::Null,
70        };
71        builder = builder.llm(format!("LLM_{}", i), llm_config);
72    }
73
74    // End node
75    builder = builder.end("End");
76
77    builder.build()
78}
79
80/// Create a branching test workflow with a switch node
81///
82/// This creates a workflow with:
83/// - 1 Start node
84/// - 1 LLM node
85/// - 1 Switch node (multi-branch routing)
86/// - 1 End node
87///
88/// # Example
89///
90/// ```
91/// use oxify_model::test_utils::create_branching_workflow;
92///
93/// let workflow = create_branching_workflow("branching_test");
94/// assert!(workflow.nodes.len() >= 4);
95/// ```
96pub fn create_branching_workflow(name: &str) -> Workflow {
97    use crate::node::{SwitchCase, SwitchConfig};
98
99    let mut builder = WorkflowBuilder::new(name);
100
101    // Start node
102    builder = builder.start("Start");
103
104    // First LLM node
105    let llm_config = LlmConfig {
106        provider: "test".to_string(),
107        model: "gpt-4".to_string(),
108        system_prompt: None,
109        prompt_template: "Initial processing".to_string(),
110        temperature: Some(0.7),
111        max_tokens: Some(1000),
112        tools: vec![],
113        images: vec![],
114        extra_params: serde_json::Value::Null,
115    };
116    builder = builder.llm("Process", llm_config);
117
118    // Switch branching node
119    let switch_config = SwitchConfig {
120        switch_on: "{{status}}".to_string(),
121        cases: vec![
122            SwitchCase {
123                match_value: "success".to_string(),
124                action: "Process success".to_string(),
125            },
126            SwitchCase {
127                match_value: "error".to_string(),
128                action: "Handle error".to_string(),
129            },
130        ],
131        default_case: Some("Default handling".to_string()),
132    };
133    builder = builder.switch("Router", switch_config);
134
135    // End node
136    builder = builder.end("End");
137
138    builder.build()
139}
140
141/// Create mock analytics data for testing
142///
143/// # Arguments
144///
145/// * `workflow_name` - Name of the workflow
146/// * `total_executions` - Total number of executions
147/// * `success_rate` - Success rate (0.0 to 1.0)
148///
149/// # Example
150///
151/// ```
152/// use oxify_model::test_utils::create_test_analytics;
153///
154/// let analytics = create_test_analytics("test", 100, 0.90);
155/// assert_eq!(analytics.execution_stats.total_executions, 100);
156/// assert_eq!(analytics.execution_stats.success_rate, 0.90);
157/// ```
158pub fn create_test_analytics(
159    workflow_name: &str,
160    total_executions: u64,
161    success_rate: f64,
162) -> WorkflowAnalytics {
163    let successful = (total_executions as f64 * success_rate) as u64;
164    let failed = total_executions - successful;
165
166    WorkflowAnalytics {
167        workflow_id: Uuid::new_v4(),
168        workflow_name: workflow_name.to_string(),
169        period: AnalyticsPeriod {
170            start: Utc::now(),
171            end: Utc::now(),
172            period_type: PeriodType::Daily,
173        },
174        execution_stats: ExecutionStats {
175            total_executions,
176            successful_executions: successful,
177            failed_executions: failed,
178            cancelled_executions: 0,
179            success_rate,
180            failure_rate: 1.0 - success_rate,
181            executions_per_hour: total_executions as f64 / 24.0,
182        },
183        performance_metrics: PerformanceMetrics {
184            avg_duration_ms: 1500.0,
185            p50_duration_ms: 1200,
186            p95_duration_ms: 3000,
187            p99_duration_ms: 4500,
188            min_duration_ms: 500,
189            max_duration_ms: 5000,
190            total_tokens: total_executions * 1000,
191            avg_tokens: 1000.0,
192            total_cost_usd: total_executions as f64 * 0.01,
193            avg_cost_usd: 0.01,
194        },
195        node_analytics: vec![],
196        error_patterns: vec![],
197        updated_at: Utc::now(),
198    }
199}
200
201/// Create a test execution context with the given workflow ID
202///
203/// # Example
204///
205/// ```
206/// use oxify_model::test_utils::create_test_execution_context;
207///
208/// let context = create_test_execution_context();
209/// assert_eq!(context.state, oxify_model::ExecutionState::Running);
210/// ```
211pub fn create_test_execution_context() -> ExecutionContext {
212    ExecutionContext::new(Uuid::new_v4())
213}
214
215/// Create a test node execution result with success status
216///
217/// # Example
218///
219/// ```
220/// use oxify_model::test_utils::create_test_node_result;
221///
222/// let result = create_test_node_result(true);
223/// assert!(result.completed_at.is_some());
224/// ```
225pub fn create_test_node_result(success: bool) -> NodeExecutionResult {
226    let result = NodeExecutionResult::new();
227
228    if success {
229        result
230            .with_metrics(NodeMetrics {
231                duration_ms: Some(100),
232                token_usage: Some(TokenUsage {
233                    input_tokens: 50,
234                    output_tokens: 100,
235                    total_tokens: 150,
236                    cached_tokens: None,
237                }),
238                cost_usd: Some(0.001),
239                api_calls: 1,
240                bytes_transferred: 1024,
241                memory_bytes: None,
242                custom: HashMap::new(),
243            })
244            .complete(ExecutionResult::Success(
245                serde_json::json!({"status": "success"}),
246            ))
247    } else {
248        result.complete(ExecutionResult::Failure("Test error".to_string()))
249    }
250}
251
252/// Create a test workflow metadata with the given name
253///
254/// # Example
255///
256/// ```
257/// use oxify_model::test_utils::create_test_metadata;
258///
259/// let metadata = create_test_metadata("my_workflow", "1.0.0");
260/// assert_eq!(metadata.name, "my_workflow");
261/// assert_eq!(metadata.version, "1.0.0");
262/// ```
263pub fn create_test_metadata(name: &str, version: &str) -> WorkflowMetadata {
264    WorkflowMetadata {
265        id: Uuid::new_v4(),
266        name: name.to_string(),
267        description: Some(format!("Test workflow: {}", name)),
268        version: version.to_string(),
269        created_at: Utc::now(),
270        updated_at: Utc::now(),
271        tags: vec!["test".to_string()],
272        parent_id: None,
273        change_description: None,
274        schedule: None,
275    }
276}
277
278/// Create a simple LLM node for testing
279///
280/// # Example
281///
282/// ```
283/// use oxify_model::test_utils::create_test_llm_node;
284///
285/// let node = create_test_llm_node("test_node", "Test prompt");
286/// assert_eq!(node.name, "test_node");
287/// ```
288pub fn create_test_llm_node(name: &str, prompt: &str) -> Node {
289    Node {
290        id: Uuid::new_v4(),
291        name: name.to_string(),
292        kind: NodeKind::LLM(LlmConfig {
293            provider: "test".to_string(),
294            model: "gpt-4".to_string(),
295            system_prompt: None,
296            prompt_template: prompt.to_string(),
297            temperature: Some(0.7),
298            max_tokens: Some(1000),
299            tools: vec![],
300            images: vec![],
301            extra_params: serde_json::Value::Null,
302        }),
303        position: None,
304        retry_config: None,
305        timeout_config: None,
306    }
307}
308
309/// Create a test edge connecting two nodes
310///
311/// # Example
312///
313/// ```
314/// use oxify_model::test_utils::create_test_edge;
315/// use uuid::Uuid;
316///
317/// let from = Uuid::new_v4();
318/// let to = Uuid::new_v4();
319/// let edge = create_test_edge(from, to, Some("success"));
320/// assert_eq!(edge.from, from);
321/// assert_eq!(edge.to, to);
322/// ```
323pub fn create_test_edge(from: Uuid, to: Uuid, label: Option<&str>) -> Edge {
324    Edge {
325        id: Uuid::new_v4(),
326        from,
327        to,
328        label: label.map(String::from),
329        condition: None,
330    }
331}
332
333/// Create a batch of test workflows with different configurations
334///
335/// This creates a collection of workflows for testing batch operations.
336///
337/// # Arguments
338///
339/// * `count` - Number of workflows to create
340/// * `nodes_per_workflow` - Number of nodes in each workflow
341///
342/// # Example
343///
344/// ```
345/// use oxify_model::test_utils::create_test_workflow_batch;
346///
347/// let workflows = create_test_workflow_batch(3, 5);
348/// assert_eq!(workflows.len(), 3);
349/// assert_eq!(workflows[0].nodes.len(), 5);
350/// ```
351pub fn create_test_workflow_batch(count: usize, nodes_per_workflow: usize) -> Vec<Workflow> {
352    (0..count)
353        .map(|i| create_test_workflow(&format!("test_workflow_{}", i), nodes_per_workflow))
354        .collect()
355}
356
357/// Create a workflow with a loop for testing iteration
358///
359/// # Example
360///
361/// ```
362/// use oxify_model::test_utils::create_loop_workflow;
363/// use oxify_model::LoopConfig;
364///
365/// let workflow = create_loop_workflow("loop_test");
366/// assert!(!workflow.nodes.is_empty());
367/// ```
368pub fn create_loop_workflow(name: &str) -> Workflow {
369    use crate::node::LoopType;
370
371    let mut builder = WorkflowBuilder::new(name);
372
373    let loop_config = LoopConfig {
374        loop_type: LoopType::ForEach {
375            collection_path: "{{items}}".to_string(),
376            item_variable: "item".to_string(),
377            index_variable: None,
378            body_expression: "process item".to_string(),
379            parallel: false,
380            max_concurrency: None,
381        },
382        max_iterations: 10,
383    };
384
385    builder = builder
386        .start("Start")
387        .loop_node("Loop", loop_config)
388        .end("End");
389
390    builder.build()
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::execution::ExecutionState;
397
398    #[test]
399    fn test_create_test_workflow() {
400        let workflow = create_test_workflow("test", 5);
401        assert_eq!(workflow.metadata.name, "test");
402        assert_eq!(workflow.nodes.len(), 5);
403        assert!(!workflow.edges.is_empty());
404    }
405
406    #[test]
407    fn test_create_test_workflow_minimum() {
408        let workflow = create_test_workflow("minimal", 2);
409        assert_eq!(workflow.nodes.len(), 2);
410    }
411
412    #[test]
413    fn test_create_branching_workflow() {
414        let workflow = create_branching_workflow("branch");
415        assert!(workflow.nodes.len() >= 4);
416    }
417
418    #[test]
419    fn test_create_test_analytics() {
420        let analytics = create_test_analytics("test", 100, 0.85);
421        assert_eq!(analytics.execution_stats.total_executions, 100);
422        assert_eq!(analytics.execution_stats.success_rate, 0.85);
423        assert_eq!(analytics.execution_stats.successful_executions, 85);
424        assert_eq!(analytics.execution_stats.failed_executions, 15);
425    }
426
427    #[test]
428    fn test_create_test_execution_context() {
429        let context = create_test_execution_context();
430        assert_eq!(context.state, ExecutionState::Running);
431    }
432
433    #[test]
434    fn test_create_test_node_result_success() {
435        let result = create_test_node_result(true);
436        assert!(result.completed_at.is_some());
437        assert!(matches!(result.result, ExecutionResult::Success(_)));
438        assert!(result.metrics.is_some());
439    }
440
441    #[test]
442    fn test_create_test_node_result_failure() {
443        let result = create_test_node_result(false);
444        assert!(result.completed_at.is_some());
445        assert!(matches!(result.result, ExecutionResult::Failure(_)));
446    }
447
448    #[test]
449    fn test_create_test_metadata() {
450        let metadata = create_test_metadata("my_workflow", "1.0.0");
451        assert_eq!(metadata.name, "my_workflow");
452        assert_eq!(metadata.version, "1.0.0");
453        assert!(metadata.description.is_some());
454        assert!(!metadata.tags.is_empty());
455    }
456
457    #[test]
458    fn test_create_test_llm_node() {
459        let node = create_test_llm_node("test", "prompt");
460        assert_eq!(node.name, "test");
461        match node.kind {
462            NodeKind::LLM(config) => {
463                assert_eq!(config.prompt_template, "prompt");
464            }
465            _ => panic!("Expected LLM node"),
466        }
467    }
468
469    #[test]
470    fn test_create_test_edge() {
471        let from = Uuid::new_v4();
472        let to = Uuid::new_v4();
473        let edge = create_test_edge(from, to, Some("success"));
474        assert_eq!(edge.from, from);
475        assert_eq!(edge.to, to);
476        assert_eq!(edge.label, Some("success".to_string()));
477    }
478
479    #[test]
480    fn test_create_test_workflow_batch() {
481        let workflows = create_test_workflow_batch(3, 5);
482        assert_eq!(workflows.len(), 3);
483        for workflow in workflows {
484            assert_eq!(workflow.nodes.len(), 5);
485        }
486    }
487
488    #[test]
489    fn test_create_loop_workflow() {
490        let workflow = create_loop_workflow("loop");
491        assert!(!workflow.nodes.is_empty());
492        assert_eq!(workflow.metadata.name, "loop");
493    }
494
495    #[test]
496    fn test_analytics_calculations() {
497        let analytics = create_test_analytics("test", 200, 0.75);
498        assert_eq!(analytics.execution_stats.successful_executions, 150);
499        assert_eq!(analytics.execution_stats.failed_executions, 50);
500        assert_eq!(analytics.execution_stats.failure_rate, 0.25);
501    }
502}