1use crate::collector::NativeCollector;
4use crate::error::Result;
5use crate::executor::{create_executor, create_pooled_executor, ExecutorConfig, TestExecutor};
6use crate::fixtures::FixtureManager;
7use crate::flakiness::FlakinessTracker;
8use crate::models::{ExecutionMode, RerunConfig, RunSummary, TestNode, TestOutcome};
9use crate::scheduler::TestScheduler;
10use crate::storage::DaemonStorage;
11use parking_lot::Mutex as PLMutex;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, SystemTime};
18use tracing::{info, warn};
19
20fn find_python_with_pytest(repo_path: &Path) -> PathBuf {
22 if let Ok(venv) = std::env::var("VIRTUAL_ENV") {
24 let venv_python = PathBuf::from(&venv).join("bin").join("python");
25 if venv_python.exists() {
26 return venv_python;
27 }
28 }
29
30 let local_venv = repo_path.join(".venv").join("bin").join("python");
32 if local_venv.exists() {
33 return local_venv;
34 }
35
36 let venv_dir = repo_path.join("venv").join("bin").join("python");
38 if venv_dir.exists() {
39 return venv_dir;
40 }
41
42 if let Ok(python_path) = std::env::var("PYTHON_PATH") {
44 return PathBuf::from(python_path);
45 }
46
47 PathBuf::from("python3")
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TestNodeInternal {
54 pub node_id: String,
55 pub file_path: String,
56 pub name: String,
57 pub class_name: Option<String>,
58 pub line_number: u32,
59 pub markers: Vec<String>,
60 pub skip: bool,
61 pub xfail: bool,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ExecutionState {
70 Fixed { mode: ExecutionMode },
72}
73
74impl ExecutionState {
75 pub fn is_pooled(&self) -> bool {
77 matches!(
78 self,
79 ExecutionState::Fixed {
80 mode: ExecutionMode::Pooled
81 }
82 )
83 }
84
85 pub fn current_mode(&self) -> ExecutionMode {
87 match self {
88 ExecutionState::Fixed { mode } => *mode,
89 }
90 }
91}
92
93#[derive(Debug)]
95pub struct RepoContext {
96 pub context_id: String,
98 pub repo_path: PathBuf,
100 pub python_path: PathBuf,
102 inventory: Arc<Mutex<HashMap<String, TestNode>>>,
104 pub inventory_hash: String,
106 duration_history: Arc<Mutex<HashMap<String, Vec<u64>>>>,
108 outcome_history: Arc<Mutex<HashMap<String, Vec<String>>>>,
110 scheduler: Arc<Mutex<TestScheduler>>,
112 executor: Arc<PLMutex<Box<dyn TestExecutor>>>,
114 execution_state: ExecutionState,
116 native_collector: NativeCollector,
118 flakiness_tracker: Arc<Mutex<FlakinessTracker>>,
120 #[allow(dead_code)]
122 fixture_manager: Arc<Mutex<FixtureManager>>,
123 #[allow(dead_code)]
125 rerun_config: RerunConfig,
126 storage: Option<DaemonStorage>,
128 use_native: bool,
130 pub last_collection_time: f64,
132 total_runs: u32,
134}
135
136impl RepoContext {
137 pub async fn new(
146 context_id: &str,
147 repo_path: &Path,
148 python_path: Option<PathBuf>,
149 storage: Option<DaemonStorage>,
150 execution_mode: ExecutionMode,
151 ) -> Result<Self> {
152 let python_path = python_path.unwrap_or_else(|| find_python_with_pytest(repo_path));
153
154 let storage_path = repo_path.join(".rpytest");
155
156 let flakiness_tracker = FlakinessTracker::new(Some(storage_path.join("flakiness.json")));
157
158 let (executor, execution_state): (Box<dyn TestExecutor>, ExecutionState) =
160 match execution_mode {
161 ExecutionMode::Pooled => {
162 let worker_count = num_cpus::get();
163 info!(
164 "Creating pooled executor with {} workers in {}",
165 worker_count,
166 repo_path.display()
167 );
168 let executor = create_pooled_executor(
169 python_path.clone(),
170 Some(worker_count),
171 repo_path.to_path_buf(),
172 )
173 .await?;
174 (
175 executor,
176 ExecutionState::Fixed {
177 mode: ExecutionMode::Pooled,
178 },
179 )
180 }
181 ExecutionMode::Auto => {
182 info!(
186 "Auto mode: using subprocess executor (use --execution-mode pooled for warm workers)"
187 );
188 let executor = create_executor(ExecutionMode::Subprocess, python_path.clone())?;
189 (
190 executor,
191 ExecutionState::Fixed {
192 mode: ExecutionMode::Subprocess,
193 },
194 )
195 }
196 other => {
197 let executor = create_executor(other, python_path.clone())?;
198 let mode = match executor.execution_mode() {
199 "embedded" => ExecutionMode::Embedded,
200 "pooled" => ExecutionMode::Pooled,
201 _ => ExecutionMode::Subprocess,
202 };
203 (executor, ExecutionState::Fixed { mode })
204 }
205 };
206
207 info!(
208 "Created context {} with {} executor (state: {:?})",
209 context_id,
210 executor.execution_mode(),
211 execution_state
212 );
213
214 Ok(RepoContext {
215 context_id: context_id.to_string(),
216 repo_path: repo_path.to_path_buf(),
217 python_path,
218 inventory: Arc::new(Mutex::new(HashMap::new())),
219 inventory_hash: String::new(),
220 duration_history: Arc::new(Mutex::new(HashMap::new())),
221 outcome_history: Arc::new(Mutex::new(HashMap::new())),
222 scheduler: Arc::new(Mutex::new(TestScheduler::new())),
223 executor: Arc::new(PLMutex::new(executor)),
224 execution_state,
225 native_collector: NativeCollector::new(repo_path),
226 flakiness_tracker: Arc::new(Mutex::new(flakiness_tracker)),
227 fixture_manager: Arc::new(Mutex::new(FixtureManager::new())),
228 rerun_config: RerunConfig::default(),
229 storage,
230 use_native: true,
231 last_collection_time: 0.0,
232 total_runs: 0,
233 })
234 }
235
236 pub fn collect(&mut self, force: bool) -> Result<(usize, u64)> {
238 let start_time = SystemTime::now();
239 let start_secs = start_time
240 .duration_since(SystemTime::UNIX_EPOCH)
241 .unwrap()
242 .as_secs_f64();
243
244 if !force {
246 if let Some(ref storage) = self.storage {
247 let cached_inventory = storage.get_all_inventory()?;
248 if !cached_inventory.is_empty() {
249 let mut inventory = self.inventory.lock().unwrap();
250 for node in cached_inventory {
251 inventory.insert(node.node_id.clone(), node);
252 }
253 self.inventory_hash = self.compute_hash();
254 self.last_collection_time = start_secs;
255 let duration_ms =
256 start_time.elapsed().unwrap_or(Duration::ZERO).as_millis() as u64;
257 return Ok((inventory.len(), duration_ms));
258 }
259 }
260 }
261
262 if self.use_native {
264 let native_tests = self.native_collector.collect()?;
265
266 let mut inventory = self.inventory.lock().unwrap();
267 for test in native_tests {
268 inventory.insert(
269 test.node_id.clone(),
270 TestNode {
271 node_id: test.node_id,
272 file_path: test.file_path,
273 name: test.name,
274 class_name: test.class_name,
275 line_number: test.line_number,
276 markers: test.markers,
277 skip: test.skip,
278 xfail: test.xfail,
279 },
280 );
281 }
282
283 if let Some(ref storage) = self.storage {
285 storage.clear_inventory()?;
286 let nodes: Vec<TestNode> = inventory.values().cloned().collect();
287 storage.save_test_nodes_batch(&nodes)?;
288 }
289 } else {
290 warn!("Pytest collection not yet implemented in pure Rust daemon");
292 }
293
294 self.inventory_hash = self.compute_hash();
295 self.last_collection_time = start_secs;
296
297 let duration_ms = start_time.elapsed().unwrap_or(Duration::ZERO).as_millis() as u64;
298
299 info!(
300 "Collected {} tests in {}ms",
301 self.inventory.lock().unwrap().len(),
302 duration_ms
303 );
304
305 Ok((self.inventory.lock().unwrap().len(), duration_ms))
306 }
307
308 pub fn get_node_ids(&self) -> Vec<String> {
310 self.inventory.lock().unwrap().keys().cloned().collect()
311 }
312
313 pub fn get_inventory(&self) -> Vec<TestNode> {
315 self.inventory.lock().unwrap().values().cloned().collect()
316 }
317
318 pub fn get_test_node(&self, node_id: &str) -> Option<TestNode> {
320 self.inventory.lock().unwrap().get(node_id).cloned()
321 }
322
323 pub fn filter_by_keyword(&self, keyword: &str) -> Vec<TestNode> {
325 if keyword.is_empty() {
326 return self.get_inventory();
327 }
328
329 self.inventory
330 .lock()
331 .unwrap()
332 .values()
333 .filter(|node| {
334 node.node_id.contains(keyword)
335 || node.name.contains(keyword)
336 || node.markers.iter().any(|m| m.contains(keyword))
337 })
338 .cloned()
339 .collect()
340 }
341
342 pub fn filter_by_marker(&self, marker: &str) -> Vec<TestNode> {
344 if marker.is_empty() {
345 return self.get_inventory();
346 }
347
348 self.inventory
349 .lock()
350 .unwrap()
351 .values()
352 .filter(|node| node.markers.iter().any(|m| m.contains(marker)))
353 .cloned()
354 .collect()
355 }
356
357 pub async fn run_tests(
359 &mut self,
360 node_ids: &[String],
361 workers: Option<u32>,
362 maxfail: Option<u32>,
363 ) -> Result<RunSummary> {
364 self.total_runs += 1;
365
366 let mut config = ExecutorConfig::new();
368 config.workers = workers;
369 config.maxfail = maxfail;
370 {
371 let mut executor = self.executor.lock();
372 executor.configure(config);
373 }
374
375 let (runnable_node_ids, pre_skipped_count): (Vec<String>, usize) = {
377 let inventory = self.inventory.lock().unwrap();
378 let mut runnable = Vec::with_capacity(node_ids.len());
379 let mut skipped_count = 0;
380
381 for node_id in node_ids {
382 if let Some(node) = inventory.get(node_id) {
383 if node.skip {
384 skipped_count += 1;
385 } else {
386 runnable.push(node_id.clone());
387 }
388 } else {
389 runnable.push(node_id.clone());
391 }
392 }
393 (runnable, skipped_count)
394 };
395
396 {
398 let durations: Vec<(String, u64)> = {
399 let history = self.duration_history.lock().unwrap();
400 history
401 .iter()
402 .filter_map(|(node_id, durations)| {
403 durations.last().map(|d| (node_id.clone(), *d))
404 })
405 .collect()
406 };
407
408 let mut scheduler = self.scheduler.lock().unwrap();
409 for (node_id, duration) in durations {
410 scheduler.update_duration(&node_id, duration);
411 }
412 }
413
414 let executor = self.executor.clone();
416 let start_time = SystemTime::now();
417 let results = {
418 let executor = executor.lock();
419 executor.run_tests(&runnable_node_ids).await
420 };
421
422 let mut passed = 0;
424 let mut failed = 0;
425 let mut skipped = 0;
426 let mut errors = 0;
427
428 for result in &results {
429 {
431 let mut durations = self.duration_history.lock().unwrap();
432 let entry = durations.entry(result.node_id.clone()).or_default();
433 entry.push(result.duration_ms);
434 if entry.len() > 10 {
435 *entry = entry[entry.len() - 10..].to_vec();
436 }
437 }
438
439 {
441 let mut outcomes = self.outcome_history.lock().unwrap();
442 let entry = outcomes.entry(result.node_id.clone()).or_default();
443 entry.push(result.outcome.clone().into());
444 }
445
446 {
448 let mut tracker = self.flakiness_tracker.lock().unwrap();
449 tracker.record_outcome(
450 &result.node_id,
451 result.outcome.clone(),
452 result.message.as_deref(),
453 );
454 }
455
456 {
458 let mut scheduler = self.scheduler.lock().unwrap();
459 scheduler.update_duration(&result.node_id, result.duration_ms);
460 }
461
462 match result.outcome {
464 TestOutcome::Passed => passed += 1,
465 TestOutcome::Failed => failed += 1,
466 TestOutcome::Skipped => skipped += 1,
467 TestOutcome::Error => errors += 1,
468 TestOutcome::Xfail => {
469 }
471 TestOutcome::Xpass => {
472 }
474 }
475 }
476
477 skipped += pre_skipped_count;
479
480 self.save_state()?;
482
483 let duration_ms = start_time.elapsed().unwrap_or(Duration::ZERO).as_millis() as u64;
484
485 Ok(RunSummary {
486 total: results.len() + pre_skipped_count,
487 passed,
488 failed,
489 skipped,
490 errors,
491 duration_ms,
492 })
493 }
494
495 fn save_state(&self) -> Result<()> {
497 if let Some(ref storage) = self.storage {
498 let mut tracker = self.flakiness_tracker.lock().unwrap();
500 tracker.flush_if_dirty()?;
501
502 let durations = self.duration_history.lock().unwrap();
504 let histories: Vec<(&str, &[u64])> = durations
505 .iter()
506 .map(|(id, d)| (id.as_str(), d.as_slice()))
507 .collect();
508 storage.save_duration_history_batch(&histories)?;
509 }
510 Ok(())
511 }
512
513 fn compute_hash(&self) -> String {
515 let inventory = self.inventory.lock().unwrap();
516 let mut ids: Vec<&String> = inventory.keys().collect();
517 ids.sort();
518
519 let mut hasher = Sha256::default();
520 for id in ids {
521 hasher.update(id.as_bytes());
522 }
523
524 hex::encode(hasher.finalize())
525 }
526
527 pub fn get_scheduler_status(&self) -> serde_json::Value {
529 let scheduler = self.scheduler.lock().unwrap();
530 serde_json::json!({
531 "tracked_tests": scheduler.tracked_count(),
532 "default_duration_ms": scheduler.default_duration_ms,
533 })
534 }
535
536 pub fn get_flakiness_report(&self) -> serde_json::Value {
538 let tracker = self.flakiness_tracker.lock().unwrap();
539 let flaky = tracker.get_flaky_tests();
540 let unstable = tracker.get_unstable_tests();
541
542 serde_json::json!({
543 "flaky_tests": flaky.iter().map(|r| self.serialize_flakiness_record(r)).collect::<Vec<_>>(),
544 "unstable_tests": unstable.iter().map(|r| self.serialize_flakiness_record(r)).collect::<Vec<_>>(),
545 "stable_count": tracker.stable_count(),
546 "total_tracked": tracker.total_tracked(),
547 })
548 }
549
550 fn serialize_flakiness_record(
551 &self,
552 record: &crate::models::FlakinessRecord,
553 ) -> serde_json::Value {
554 serde_json::json!({
555 "node_id": record.node_id,
556 "failure_rate": record.outcomes.iter().filter(|o| *o == "failed" || *o == "error").count() as f64 / record.outcomes.len() as f64,
557 "is_flaky": record.flaky_streak >= 2 && record.outcomes.iter().any(|o| *o == "passed"),
558 "flaky_streak": record.flaky_streak,
559 "consecutive_failures": record.consecutive_failures,
560 "consecutive_passes": record.consecutive_passes,
561 "total_runs": record.total_runs,
562 "recent_outcomes": record.outcomes.clone(),
563 })
564 }
565
566 pub fn execution_state(&self) -> &ExecutionState {
568 &self.execution_state
569 }
570
571 pub fn execution_mode(&self) -> ExecutionMode {
573 self.execution_state.current_mode()
574 }
575}