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