clnrm_core/cli/commands/
record.rs1use crate::cli::commands::run::run_tests_sequential_with_results;
15use crate::cli::types::{CliConfig, OutputFormat};
16use crate::cli::utils::discover_test_files;
17use crate::error::{CleanroomError, Result};
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20use tracing::{info, warn};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct BaselineRecord {
25 pub timestamp: String,
27 pub version: String,
29 pub test_results: Vec<BaselineTestResult>,
31 pub digest: String,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct BaselineTestResult {
38 pub name: String,
40 pub passed: bool,
42 pub duration_ms: u64,
44 pub file_path: String,
46}
47
48pub async fn run_record(paths: Option<Vec<PathBuf>>, output: Option<PathBuf>) -> Result<()> {
62 info!("Starting baseline recording");
64
65 let output_path = output.unwrap_or_else(|| PathBuf::from(".clnrm/baseline.json"));
66 let digest_path = output_path.with_extension("sha256");
67
68 if let Some(parent) = output_path.parent() {
70 std::fs::create_dir_all(parent).map_err(|e| {
71 CleanroomError::io_error(format!(
72 "Failed to create directory '{}': {}",
73 parent.display(),
74 e
75 ))
76 })?;
77 }
78
79 let test_paths = if let Some(paths) = paths {
81 paths
82 } else {
83 vec![PathBuf::from(".")]
84 };
85
86 let mut all_test_files = Vec::new();
87 for path in &test_paths {
88 let discovered = discover_test_files(path)?;
89 all_test_files.extend(discovered);
90 }
91
92 if all_test_files.is_empty() {
93 return Err(CleanroomError::validation_error(
94 "No test files found to record as baseline",
95 ));
96 }
97
98 info!("Found {} test file(s) to record", all_test_files.len());
99 println!(
100 "📹 Recording baseline from {} test file(s)...",
101 all_test_files.len()
102 );
103
104 let config = CliConfig {
106 parallel: false, jobs: 1,
108 format: OutputFormat::Auto,
109 fail_fast: false,
110 watch: false,
111 verbose: 0,
112 force: true, digest: true, validate: false, };
116
117 let results = run_tests_sequential_with_results(&all_test_files, &config).await?;
118
119 let baseline_results: Vec<BaselineTestResult> = results
121 .iter()
122 .map(|r| BaselineTestResult {
123 name: r.name.clone(),
124 passed: r.passed,
125 duration_ms: r.duration_ms,
126 file_path: extract_file_path(&r.name),
127 })
128 .collect();
129
130 let timestamp = chrono::Utc::now().to_rfc3339();
132 let version = env!("CARGO_PKG_VERSION").to_string();
133
134 let baseline_data_for_digest = serde_json::json!({
136 "timestamp": timestamp,
137 "version": version,
138 "test_results": baseline_results,
139 });
140
141 let digest = compute_sha256(&baseline_data_for_digest)?;
142
143 let baseline = BaselineRecord {
144 timestamp,
145 version,
146 test_results: baseline_results,
147 digest: digest.clone(),
148 };
149
150 let baseline_json = serde_json::to_string_pretty(&baseline).map_err(|e| {
152 CleanroomError::internal_error(format!("Failed to serialize baseline: {}", e))
153 })?;
154
155 std::fs::write(&output_path, &baseline_json).map_err(|e| {
156 CleanroomError::io_error(format!(
157 "Failed to write baseline to '{}': {}",
158 output_path.display(),
159 e
160 ))
161 })?;
162
163 std::fs::write(&digest_path, &digest).map_err(|e| {
165 CleanroomError::io_error(format!(
166 "Failed to write digest to '{}': {}",
167 digest_path.display(),
168 e
169 ))
170 })?;
171
172 let passed = baseline.test_results.iter().filter(|t| t.passed).count();
174 let failed = baseline.test_results.iter().filter(|t| !t.passed).count();
175
176 println!();
177 println!("✅ Baseline recorded successfully");
178 println!(" Tests: {} passed, {} failed", passed, failed);
179 println!(" Output: {}", output_path.display());
180 println!(" Digest: {}", digest_path.display());
181 println!(" SHA-256: {}", digest);
182
183 info!(
184 "Baseline recording completed: {} tests recorded",
185 baseline.test_results.len()
186 );
187
188 if failed > 0 {
189 warn!("Baseline contains {} failed test(s)", failed);
190 println!();
191 println!("⚠️ Warning: Baseline includes {} failed test(s)", failed);
192 println!(" Consider fixing failures before using this as a baseline.");
193 }
194
195 Ok(())
196}
197
198fn compute_sha256(data: &serde_json::Value) -> Result<String> {
209 use sha2::{Digest, Sha256};
210
211 let json_bytes = serde_json::to_vec(data).map_err(|e| {
212 CleanroomError::internal_error(format!("Failed to serialize data for hashing: {}", e))
213 })?;
214
215 let mut hasher = Sha256::new();
216 hasher.update(&json_bytes);
217 let result = hasher.finalize();
218
219 Ok(format!("{:x}", result))
220}
221
222fn extract_file_path(test_name: &str) -> String {
232 test_name.to_string()
236}