1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::path::PathBuf;
6
7#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
48pub enum StabilityState {
49 #[default]
51 Unknown,
52 Stable { consecutive_passes: u32 },
54 Unstable { consecutive_failures: u32 },
56 Flaky { streak_count: u32 },
58 ConfirmedFlaky,
60}
61
62impl StabilityState {
63 pub fn is_flaky(&self) -> bool {
68 matches!(
69 self,
70 StabilityState::Flaky { streak_count: 2.. } | StabilityState::ConfirmedFlaky
71 )
72 }
73
74 pub fn is_confirmed_flaky(&self) -> bool {
76 matches!(self, StabilityState::ConfirmedFlaky)
77 }
78
79 pub fn is_stable(&self) -> bool {
81 matches!(self, StabilityState::Stable { .. })
82 }
83}
84
85#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct ParameterizedTestNode {
171 pub param_names: Vec<String>,
173 pub param_values: Vec<(Vec<String>, Option<String>, Vec<String>)>,
179 pub test_id: String,
181}
182
183#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct FlakinessRecord {
217 pub node_id: String,
218 pub outcomes: Vec<String>, 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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
297#[serde(rename_all = "lowercase")]
298pub enum ExecutionMode {
299 #[default]
301 Embedded,
302 Subprocess,
304 Pooled,
306 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#[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 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 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 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}