Skip to main content

rpytest_daemon/
models.rs

1//! Data models for the rpytest daemon.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::path::PathBuf;
6
7/// Test outcome types.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "lowercase")]
10pub enum TestOutcome {
11    Passed,
12    Failed,
13    Skipped,
14    Error,
15    Xfail,
16    Xpass,
17}
18
19impl From<&str> for TestOutcome {
20    fn from(s: &str) -> Self {
21        match s {
22            "passed" => TestOutcome::Passed,
23            "failed" => TestOutcome::Failed,
24            "skipped" => TestOutcome::Skipped,
25            "error" => TestOutcome::Error,
26            "xfail" => TestOutcome::Xfail,
27            "xpass" => TestOutcome::Xpass,
28            _ => TestOutcome::Error,
29        }
30    }
31}
32
33impl From<TestOutcome> for String {
34    fn from(outcome: TestOutcome) -> Self {
35        match outcome {
36            TestOutcome::Passed => "passed".to_string(),
37            TestOutcome::Failed => "failed".to_string(),
38            TestOutcome::Skipped => "skipped".to_string(),
39            TestOutcome::Error => "error".to_string(),
40            TestOutcome::Xfail => "xfail".to_string(),
41            TestOutcome::Xpass => "xpass".to_string(),
42        }
43    }
44}
45
46/// Represents a single test node.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct TestNode {
49    pub node_id: String,
50    pub file_path: String,
51    pub name: String,
52    pub class_name: Option<String>,
53    pub line_number: u32,
54    pub markers: Vec<String>,
55    pub skip: bool,
56    pub xfail: bool,
57}
58
59impl From<rpytest_core::protocol::TestNodeInfo> for TestNode {
60    fn from(info: rpytest_core::protocol::TestNodeInfo) -> Self {
61        TestNode {
62            node_id: info.node_id,
63            file_path: info.file_path,
64            name: info.name,
65            class_name: info.class_name,
66            line_number: info.lineno.unwrap_or(0),
67            markers: info.markers,
68            skip: info.skip,
69            xfail: info.xfail,
70        }
71    }
72}
73
74impl From<TestNode> for rpytest_core::protocol::TestNodeInfo {
75    fn from(node: TestNode) -> Self {
76        rpytest_core::protocol::TestNodeInfo {
77            node_id: node.node_id,
78            file_path: node.file_path,
79            lineno: Some(node.line_number),
80            name: node.name,
81            class_name: node.class_name,
82            markers: node.markers,
83            skip: node.skip,
84            xfail: node.xfail,
85        }
86    }
87}
88
89/// Result of a single test execution.
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct TestResult {
92    pub node_id: String,
93    pub outcome: TestOutcome,
94    pub duration_ms: u64,
95    pub message: Option<String>,
96    pub stdout: Option<String>,
97    pub stderr: Option<String>,
98}
99
100/// Summary of a test run.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct RunSummary {
103    pub total: usize,
104    pub passed: usize,
105    pub failed: usize,
106    pub skipped: usize,
107    pub errors: usize,
108    pub duration_ms: u64,
109}
110
111/// Native test node discovered via AST parsing.
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct NativeTestNode {
114    pub node_id: String,
115    pub file_path: String,
116    pub name: String,
117    pub class_name: Option<String>,
118    pub line_number: u32,
119    pub markers: Vec<String>,
120    pub is_simple: bool,
121    pub parameters: Vec<Value>,
122    pub skip: bool,
123    pub skip_reason: Option<String>,
124    pub xfail: bool,
125    pub xfail_reason: Option<String>,
126    pub xfail_strict: bool,
127}
128
129/// Information about a parametrized test variant.
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
131pub struct ParameterizedTestNode {
132    /// Parameter names (e.g., ["x", "y"] for "x,y")
133    pub param_names: Vec<String>,
134    /// Parameter values, custom IDs, and per-variant marks for each variant.
135    /// Each tuple is (values, custom_id, marks) where:
136    /// - values: the parameter values
137    /// - custom_id: custom ID from pytest.param(id="...") if provided
138    /// - marks: per-variant marks from pytest.param(marks=...)
139    pub param_values: Vec<(Vec<String>, Option<String>, Vec<String>)>,
140    /// Generated test ID for a specific variant
141    pub test_id: String,
142}
143
144/// Configuration for auto-rerun behavior.
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146pub struct RerunConfig {
147    pub enabled: bool,
148    pub max_reruns: u32,
149    pub only_flaky: bool,
150    pub delay_ms: u32,
151}
152
153impl Default for RerunConfig {
154    fn default() -> Self {
155        RerunConfig {
156            enabled: false,
157            max_reruns: 2,
158            only_flaky: false,
159            delay_ms: 0,
160        }
161    }
162}
163
164/// Result of a rerun attempt.
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166pub struct RerunResult {
167    pub node_id: String,
168    pub original_outcome: TestOutcome,
169    pub rerun_outcomes: Vec<TestOutcome>,
170    pub final_outcome: TestOutcome,
171    pub is_flaky: bool,
172    pub message: Option<String>,
173}
174
175/// Record of test flakiness over time.
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177pub struct FlakinessRecord {
178    pub node_id: String,
179    pub outcomes: Vec<String>, // Last N outcomes (stored as strings)
180    pub consecutive_failures: u32,
181    pub consecutive_passes: u32,
182    pub flaky_streak: u32,
183    pub total_runs: u32,
184    pub last_failure_message: Option<String>,
185}
186
187/// Fixture scope levels.
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189#[serde(rename_all = "lowercase")]
190pub enum FixtureScope {
191    Session,
192    Package,
193    Module,
194    Class,
195    Function,
196}
197
198impl From<&str> for FixtureScope {
199    fn from(s: &str) -> Self {
200        match s {
201            "session" => FixtureScope::Session,
202            "package" => FixtureScope::Package,
203            "module" => FixtureScope::Module,
204            "class" => FixtureScope::Class,
205            _ => FixtureScope::Function,
206        }
207    }
208}
209
210/// State of a session fixture.
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212pub struct FixtureState {
213    pub name: String,
214    pub scope: FixtureScope,
215    pub created_at: f64,
216    pub last_used: f64,
217    pub use_count: u32,
218    pub teardown_pending: bool,
219}
220
221/// Fixture configuration.
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
223pub struct FixtureConfig {
224    pub enabled: bool,
225    pub max_age_seconds: f64,
226    pub teardown_on_conftest_change: bool,
227    pub scopes_to_reuse: Vec<String>,
228}
229
230/// A test with scheduling metadata.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct ScheduledTest {
233    pub node_id: String,
234    pub estimated_duration_ms: u64,
235    pub priority: u64,
236}
237
238/// Shard configuration.
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
240pub struct ShardConfig {
241    pub shard_index: u32,
242    pub total_shards: u32,
243    pub strategy: String,
244}
245
246/// Executor configuration.
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
248pub struct ExecutorConfig {
249    pub workers: Option<u32>,
250    pub maxfail: Option<u32>,
251    pub batch_size: usize,
252}
253
254/// Execution mode for running tests.
255#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
256#[serde(rename_all = "lowercase")]
257pub enum ExecutionMode {
258    /// Use embedded Python via PyO3 (fastest, requires embedded-python feature)
259    #[default]
260    Embedded,
261    /// Use subprocess execution (compatible with all Python environments)
262    Subprocess,
263    /// Use worker pool for parallel subprocess execution (best throughput)
264    Pooled,
265    /// Automatically select based on availability
266    Auto,
267}
268
269impl std::fmt::Display for ExecutionMode {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        match self {
272            ExecutionMode::Embedded => write!(f, "embedded"),
273            ExecutionMode::Subprocess => write!(f, "subprocess"),
274            ExecutionMode::Pooled => write!(f, "pooled"),
275            ExecutionMode::Auto => write!(f, "auto"),
276        }
277    }
278}
279
280impl std::str::FromStr for ExecutionMode {
281    type Err = String;
282
283    fn from_str(s: &str) -> Result<Self, Self::Err> {
284        match s.to_lowercase().as_str() {
285            "embedded" => Ok(ExecutionMode::Embedded),
286            "subprocess" => Ok(ExecutionMode::Subprocess),
287            "pooled" => Ok(ExecutionMode::Pooled),
288            "auto" => Ok(ExecutionMode::Auto),
289            _ => Err(format!("Invalid execution mode: {}. Use 'embedded', 'subprocess', 'pooled', or 'auto'", s)),
290        }
291    }
292}
293
294/// Daemon runtime configuration.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct DaemonConfig {
297    pub socket_path: PathBuf,
298    pub storage_path: PathBuf,
299    pub python_path: Option<PathBuf>,
300    pub idle_timeout_secs: u32,
301    pub max_workers: u32,
302    /// Execution mode for running tests
303    pub execution_mode: ExecutionMode,
304}
305
306impl Default for DaemonConfig {
307    fn default() -> Self {
308        DaemonConfig {
309            socket_path: PathBuf::from("/tmp/rpytest.sock"),
310            storage_path: dirs::data_dir()
311                .unwrap_or_else(|| PathBuf::from("."))
312                .join("rpytest"),
313            python_path: None,
314            idle_timeout_secs: 0,
315            max_workers: 4,
316            execution_mode: ExecutionMode::Auto,
317        }
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_execution_mode_from_str() {
327        assert_eq!(
328            "embedded".parse::<ExecutionMode>().unwrap(),
329            ExecutionMode::Embedded
330        );
331        assert_eq!(
332            "subprocess".parse::<ExecutionMode>().unwrap(),
333            ExecutionMode::Subprocess
334        );
335        assert_eq!(
336            "auto".parse::<ExecutionMode>().unwrap(),
337            ExecutionMode::Auto
338        );
339
340        // Case insensitive
341        assert_eq!(
342            "EMBEDDED".parse::<ExecutionMode>().unwrap(),
343            ExecutionMode::Embedded
344        );
345        assert_eq!(
346            "Auto".parse::<ExecutionMode>().unwrap(),
347            ExecutionMode::Auto
348        );
349
350        // Invalid
351        assert!("invalid".parse::<ExecutionMode>().is_err());
352    }
353
354    #[test]
355    fn test_execution_mode_display() {
356        assert_eq!(ExecutionMode::Embedded.to_string(), "embedded");
357        assert_eq!(ExecutionMode::Subprocess.to_string(), "subprocess");
358        assert_eq!(ExecutionMode::Auto.to_string(), "auto");
359    }
360
361    #[test]
362    fn test_execution_mode_default() {
363        assert_eq!(ExecutionMode::default(), ExecutionMode::Embedded);
364    }
365
366    #[test]
367    fn test_daemon_config_default() {
368        let config = DaemonConfig::default();
369        assert_eq!(config.execution_mode, ExecutionMode::Auto);
370        assert_eq!(config.max_workers, 4);
371    }
372
373    #[test]
374    fn test_test_outcome_conversions() {
375        assert_eq!(TestOutcome::from("passed"), TestOutcome::Passed);
376        assert_eq!(TestOutcome::from("failed"), TestOutcome::Failed);
377        assert_eq!(TestOutcome::from("skipped"), TestOutcome::Skipped);
378        assert_eq!(TestOutcome::from("error"), TestOutcome::Error);
379        assert_eq!(TestOutcome::from("xfail"), TestOutcome::Xfail);
380        assert_eq!(TestOutcome::from("xpass"), TestOutcome::Xpass);
381        assert_eq!(TestOutcome::from("unknown"), TestOutcome::Error);
382
383        assert_eq!(String::from(TestOutcome::Passed), "passed");
384        assert_eq!(String::from(TestOutcome::Failed), "failed");
385    }
386}