clnrm_core/cli/commands/
dev.rs

1//! Development mode command with file watching
2//!
3//! Provides hot reload functionality for `.toml.tera` template files,
4//! enabling instant feedback (<3s) when developers save changes.
5//!
6//! Core Team Compliance:
7//! - ✅ Async functions for I/O operations
8//! - ✅ Proper error handling with CleanroomError
9//! - ✅ No unwrap() or expect() calls
10//! - ✅ Use tracing for structured logging
11
12use crate::cli::types::CliConfig;
13use crate::error::{CleanroomError, Result};
14use crate::watch::WatchConfig;
15use std::path::PathBuf;
16use tracing::{info, warn};
17
18/// Re-export DevConfig and DevWatcher from watch module for backward compatibility
19pub use crate::watch::WatchConfig as DevConfig;
20
21/// Re-export for compatibility
22pub struct DevWatcher;
23
24/// Run development mode with file watching and optional filtering/timeboxing
25///
26/// Watches `.toml.tera` files for changes and automatically re-runs tests
27/// when modifications are detected. Provides instant feedback for iterative
28/// test development.
29///
30/// # Arguments
31///
32/// * `paths` - Directories or files to watch (default: current directory)
33/// * `debounce_ms` - Debounce delay in milliseconds (default: 300ms)
34/// * `clear_screen` - Clear terminal before each test run
35/// * `only_pattern` - Optional pattern to filter scenarios (substring match on path)
36/// * `timebox_ms` - Optional maximum execution time per scenario in milliseconds
37/// * `cli_config` - CLI configuration for test execution
38///
39/// # Performance
40///
41/// Target: <3s from file save to test result display
42///
43/// # Example
44///
45/// ```no_run
46/// use clnrm_core::cli::commands::dev::run_dev_mode_with_filters;
47/// use clnrm_core::cli::types::CliConfig;
48/// use std::path::PathBuf;
49///
50/// # async fn example() -> clnrm_core::error::Result<()> {
51/// let paths = vec![PathBuf::from("tests/")];
52/// let config = CliConfig::default();
53///
54/// run_dev_mode_with_filters(Some(paths), 300, true, None, None, config).await?;
55/// # Ok(())
56/// # }
57/// ```
58pub async fn run_dev_mode_with_filters(
59    paths: Option<Vec<PathBuf>>,
60    debounce_ms: u64,
61    clear_screen: bool,
62    only_pattern: Option<String>,
63    timebox_ms: Option<u64>,
64    cli_config: CliConfig,
65) -> Result<()> {
66    info!("🚀 Starting development mode with file watching");
67
68    // Log filtering options if provided
69    if let Some(ref pattern) = only_pattern {
70        info!("🔍 Filtering scenarios matching pattern: {}", pattern);
71    }
72    if let Some(timeout) = timebox_ms {
73        info!("⏱️  Timeboxing scenarios to {}ms", timeout);
74    }
75
76    // Determine paths to watch
77    let watch_paths = match paths {
78        Some(paths) if !paths.is_empty() => paths,
79        _ => {
80            // Default: watch current directory
81            info!("No paths specified, watching current directory");
82            vec![PathBuf::from(".")]
83        }
84    };
85
86    // Validate all paths exist
87    for path in &watch_paths {
88        if !path.exists() {
89            return Err(CleanroomError::validation_error(format!(
90                "Path does not exist: {}",
91                path.display()
92            ))
93            .with_context("Cannot watch non-existent path"));
94        }
95
96        if !path.is_dir() && !path.is_file() {
97            return Err(CleanroomError::validation_error(format!(
98                "Path is not a file or directory: {}",
99                path.display()
100            ))
101            .with_context("Watch path must be a file or directory"));
102        }
103    }
104
105    // Display configuration
106    info!("Watch configuration:");
107    info!("  Paths: {:?}", watch_paths);
108    info!("  Debounce: {}ms", debounce_ms);
109    info!("  Clear screen: {}", clear_screen);
110    if let Some(ref pattern) = only_pattern {
111        info!("  Filter pattern: {}", pattern);
112    }
113    if let Some(timeout) = timebox_ms {
114        info!("  Timebox: {}ms", timeout);
115    }
116    info!("  Parallel: {}", cli_config.parallel);
117    info!("  Jobs: {}", cli_config.jobs);
118
119    // Validate debounce delay is reasonable
120    if debounce_ms < 50 {
121        warn!(
122            "⚠️  Debounce delay is very low ({}ms), may cause excessive runs",
123            debounce_ms
124        );
125    } else if debounce_ms > 2000 {
126        warn!(
127            "⚠️  Debounce delay is very high ({}ms), may feel sluggish",
128            debounce_ms
129        );
130    }
131
132    // Create watch configuration with filters
133    let mut watch_config =
134        WatchConfig::new(watch_paths, debounce_ms, clear_screen).with_cli_config(cli_config);
135
136    // Apply filters if provided
137    if let Some(pattern) = only_pattern {
138        watch_config = watch_config.with_filter_pattern(pattern);
139    }
140    if let Some(timeout) = timebox_ms {
141        watch_config = watch_config.with_timebox(timeout);
142    }
143
144    // Start watching
145    info!("📁 Watching for .toml.tera file changes...");
146    if watch_config.has_filter_pattern() {
147        info!("🔍 Filtering scenarios by pattern");
148    }
149    if watch_config.has_timebox() {
150        info!("⏱️  Timeboxing enabled");
151    }
152    info!("Press Ctrl+C to stop");
153
154    // Delegate to watch module
155    crate::watch::watch_and_run(watch_config).await?;
156
157    Ok(())
158}
159
160/// Legacy function for backward compatibility
161///
162/// Calls the new `run_dev_mode_with_filters` with no filtering or timeboxing
163pub async fn run_dev_mode(
164    paths: Option<Vec<PathBuf>>,
165    debounce_ms: u64,
166    clear_screen: bool,
167    cli_config: CliConfig,
168) -> Result<()> {
169    run_dev_mode_with_filters(paths, debounce_ms, clear_screen, None, None, cli_config).await
170}