1use std::path::{Path, PathBuf};
13use std::process::Stdio;
14use std::sync::Arc;
15use std::time::Instant;
16use tokio::process::Command;
17use tokio::sync::RwLock;
18
19use super::blueprint_manager::BlueprintManager;
20use super::task_tree_manager::TaskTreeManager;
21use super::types::{AcceptanceTest, TaskNode};
22
23#[derive(Debug, Clone)]
29pub struct AcceptanceTestRunResult {
30 pub test_id: String,
32 pub test_name: String,
34 pub passed: bool,
36 pub output: String,
38 pub duration: u64,
40 pub error_message: Option<String>,
42}
43
44#[derive(Debug, Clone)]
46pub struct AcceptanceTestRunnerConfig {
47 pub project_root: PathBuf,
49 pub test_timeout: u64,
51 pub debug: bool,
53 pub parallel_count: usize,
55}
56
57impl Default for AcceptanceTestRunnerConfig {
58 fn default() -> Self {
59 Self {
60 project_root: std::env::current_dir().unwrap_or_default(),
61 test_timeout: 60000,
62 debug: false,
63 parallel_count: 1,
64 }
65 }
66}
67
68pub struct AcceptanceTestRunner {
74 config: AcceptanceTestRunnerConfig,
75 task_tree_manager: Arc<RwLock<TaskTreeManager>>,
76 blueprint_manager: Arc<RwLock<BlueprintManager>>,
77}
78
79impl AcceptanceTestRunner {
80 pub fn new(
82 config: AcceptanceTestRunnerConfig,
83 task_tree_manager: Arc<RwLock<TaskTreeManager>>,
84 blueprint_manager: Arc<RwLock<BlueprintManager>>,
85 ) -> Self {
86 Self {
87 config,
88 task_tree_manager,
89 blueprint_manager,
90 }
91 }
92
93 pub async fn run_tests_for_file(&self, file_path: &str) -> Vec<AcceptanceTestRunResult> {
95 let tree_manager = self.task_tree_manager.read().await;
96
97 let tree = match tree_manager.get_current_task_tree().await {
99 Some(t) => t,
100 None => {
101 self.log("[AcceptanceTestRunner] 没有活跃的任务树");
102 return vec![];
103 }
104 };
105
106 let relevant_tests = self.find_relevant_tests(file_path, &tree.root).await;
108 if relevant_tests.is_empty() {
109 self.log(&format!(
110 "[AcceptanceTestRunner] 没有找到与 {} 相关的验收测试",
111 file_path
112 ));
113 return vec![];
114 }
115
116 self.log(&format!(
117 "[AcceptanceTestRunner] 找到 {} 个相关测试",
118 relevant_tests.len()
119 ));
120
121 let mut results = Vec::new();
122
123 if self.config.parallel_count > 1 {
125 let batches = self.create_batches(&relevant_tests, self.config.parallel_count);
127 for batch in batches {
128 let mut handles = Vec::new();
129 for test in batch {
130 let test_clone = test.clone();
131 let config = self.config.clone();
132 handles.push(tokio::spawn(async move {
133 Self::run_single_test_static(&config, &test_clone).await
134 }));
135 }
136 for handle in handles {
137 if let Ok(result) = handle.await {
138 results.push(result);
139 }
140 }
141 }
142 } else {
143 for test in &relevant_tests {
145 let result = self.run_single_test(test).await;
146 results.push(result);
147 }
148 }
149
150 drop(tree_manager);
152 self.record_results(&tree.id, &results).await;
153
154 self.print_summary(&results);
156
157 results
158 }
159
160 pub async fn run_acceptance_test(&self, test: &AcceptanceTest) -> AcceptanceTestRunResult {
162 self.run_single_test(test).await
163 }
164
165 async fn run_single_test(&self, test: &AcceptanceTest) -> AcceptanceTestRunResult {
167 Self::run_single_test_static(&self.config, test).await
168 }
169
170 async fn run_single_test_static(
172 config: &AcceptanceTestRunnerConfig,
173 test: &AcceptanceTest,
174 ) -> AcceptanceTestRunResult {
175 let start_time = Instant::now();
176
177 if config.debug {
178 println!("[AcceptanceTestRunner] 运行测试: {}", test.name);
179 }
180
181 match Self::execute_test_command(config, &test.test_command, Some(&test.test_file_path))
182 .await
183 {
184 Ok(output) => {
185 let duration = start_time.elapsed().as_millis() as u64;
186 let passed = Self::parse_test_success(&output);
187
188 let result = AcceptanceTestRunResult {
189 test_id: test.id.clone(),
190 test_name: test.name.clone(),
191 passed,
192 output: output.clone(),
193 duration,
194 error_message: if passed {
195 None
196 } else {
197 Some(Self::extract_error_message(&output))
198 },
199 };
200
201 if passed {
202 println!("✅ 验收测试通过: {} ({}ms)", test.name, duration);
203 } else {
204 eprintln!("❌ 验收测试失败: {}", test.name);
205 if let Some(ref err) = result.error_message {
206 if let Some(first_line) = err.lines().next() {
207 eprintln!(" 错误: {}", first_line);
208 }
209 }
210 }
211
212 result
213 }
214 Err(e) => {
215 let duration = start_time.elapsed().as_millis() as u64;
216 eprintln!("❌ 验收测试执行失败: {}", test.name);
217 eprintln!(" {}", e);
218
219 AcceptanceTestRunResult {
220 test_id: test.id.clone(),
221 test_name: test.name.clone(),
222 passed: false,
223 output: String::new(),
224 duration,
225 error_message: Some(e),
226 }
227 }
228 }
229 }
230
231 async fn find_relevant_tests(
233 &self,
234 file_path: &str,
235 root_task: &TaskNode,
236 ) -> Vec<AcceptanceTest> {
237 let mut tests = Vec::new();
238 let normalized_path = Path::new(file_path).to_string_lossy().to_lowercase();
239
240 self.traverse_for_tests(root_task, &normalized_path, &mut tests)
241 .await;
242 tests
243 }
244
245 async fn traverse_for_tests(
247 &self,
248 task: &TaskNode,
249 normalized_file_path: &str,
250 tests: &mut Vec<AcceptanceTest>,
251 ) {
252 for test in &task.acceptance_tests {
253 if self
254 .is_test_relevant(test, normalized_file_path, task)
255 .await
256 {
257 tests.push(test.clone());
258 }
259 }
260
261 for child in &task.children {
262 Box::pin(self.traverse_for_tests(child, normalized_file_path, tests)).await;
263 }
264 }
265
266 async fn is_test_relevant(
268 &self,
269 _test: &AcceptanceTest,
270 normalized_file_path: &str,
271 task: &TaskNode,
272 ) -> bool {
273 for artifact in &task.code_artifacts {
275 if let Some(ref artifact_path) = artifact.file_path {
276 let artifact_normalized = artifact_path.to_lowercase();
277 if normalized_file_path.contains(&artifact_normalized)
278 || artifact_normalized.contains(normalized_file_path)
279 {
280 return true;
281 }
282 }
283 }
284
285 if let Some(ref module_id) = task.blueprint_module_id {
287 let bp_manager = self.blueprint_manager.read().await;
288 if let Some(blueprint) = bp_manager.get_current_blueprint().await {
289 if let Some(module) = blueprint.modules.iter().find(|m| &m.id == module_id) {
290 let default_path = format!("src/{}", module.name.to_lowercase());
291 let module_path = module.root_path.as_deref().unwrap_or(&default_path);
292 if normalized_file_path.contains(&module_path.to_lowercase()) {
293 return true;
294 }
295 }
296 }
297 }
298
299 let file_name = Path::new(normalized_file_path)
301 .file_name()
302 .and_then(|n| n.to_str())
303 .unwrap_or("");
304 let task_name_lower = task.name.to_lowercase();
305
306 let file_base_name = file_name
308 .trim_end_matches(".ts")
309 .trim_end_matches(".tsx")
310 .trim_end_matches(".js")
311 .trim_end_matches(".jsx")
312 .trim_end_matches(".rs");
313
314 if task_name_lower.contains(file_base_name)
315 || file_base_name.contains(&task_name_lower.replace(' ', "-"))
316 {
317 return true;
318 }
319
320 false
321 }
322
323 async fn execute_test_command(
325 config: &AcceptanceTestRunnerConfig,
326 command: &str,
327 test_file_path: Option<&str>,
328 ) -> Result<String, String> {
329 let full_command = if let Some(path) = test_file_path {
331 if !command.contains(path) {
332 format!("{} {}", command, path)
333 } else {
334 command.to_string()
335 }
336 } else {
337 command.to_string()
338 };
339
340 if config.debug {
341 println!("[AcceptanceTestRunner] 执行命令: {}", full_command);
342 }
343
344 let parts: Vec<&str> = full_command.split_whitespace().collect();
345 if parts.is_empty() {
346 return Err("空命令".to_string());
347 }
348
349 let cmd = parts[0];
350 let args = &parts[1..];
351
352 let output = Command::new(cmd)
353 .args(args)
354 .current_dir(&config.project_root)
355 .stdout(Stdio::piped())
356 .stderr(Stdio::piped())
357 .output()
358 .await
359 .map_err(|e| format!("执行命令失败: {}", e))?;
360
361 let stdout = String::from_utf8_lossy(&output.stdout);
362 let stderr = String::from_utf8_lossy(&output.stderr);
363 let combined = format!("{}{}", stdout, stderr);
364
365 if output.status.success() {
366 Ok(combined)
367 } else {
368 Err(format!(
369 "测试命令退出码: {:?}\n{}",
370 output.status.code(),
371 combined
372 ))
373 }
374 }
375
376 fn parse_test_success(output: &str) -> bool {
378 if output.contains("Test Files") && output.contains("passed") {
380 return !output.contains("failed");
381 }
382
383 if output.contains("Tests:") && output.contains("passed") {
385 return !output.contains("failed");
386 }
387
388 if output.contains("passing") {
390 return !output.contains("failing");
391 }
392
393 if output.contains("passed") || output.contains("PASSED") {
395 return !output.contains("failed") && !output.contains("FAILED");
396 }
397
398 if output.contains("test result: ok") {
400 return true;
401 }
402 if output.contains("test result: FAILED") {
403 return false;
404 }
405
406 true
408 }
409
410 fn extract_error_message(output: &str) -> String {
412 let mut error_lines = Vec::new();
413 let mut in_error = false;
414
415 for line in output.lines() {
416 if line.contains("Error:")
417 || line.contains("FAIL")
418 || line.contains("✖")
419 || line.contains("AssertionError")
420 || line.contains("panicked")
421 {
422 in_error = true;
423 }
424
425 if in_error {
426 error_lines.push(line);
427 if error_lines.len() >= 15 {
428 break;
429 }
430 }
431 }
432
433 if !error_lines.is_empty() {
434 error_lines.join("\n")
435 } else {
436 output.chars().take(500).collect()
437 }
438 }
439
440 async fn record_results(&self, _tree_id: &str, results: &[AcceptanceTestRunResult]) {
442 for result in results {
445 if result.passed {
446 tracing::info!("验收测试通过: {} ({}ms)", result.test_name, result.duration);
447 } else {
448 tracing::warn!(
449 "验收测试失败: {} - {:?}",
450 result.test_name,
451 result.error_message
452 );
453 }
454 }
455 }
456
457 #[allow(dead_code)]
459 fn find_task_id_for_test(root_task: &TaskNode, test_id: &str) -> Option<String> {
460 for test in &root_task.acceptance_tests {
461 if test.id == test_id {
462 return Some(root_task.id.clone());
463 }
464 }
465
466 for child in &root_task.children {
467 if let Some(found) = Self::find_task_id_for_test(child, test_id) {
468 return Some(found);
469 }
470 }
471
472 None
473 }
474
475 fn print_summary(&self, results: &[AcceptanceTestRunResult]) {
477 if results.is_empty() {
478 return;
479 }
480
481 let passed = results.iter().filter(|r| r.passed).count();
482 let failed = results.len() - passed;
483 let total_duration: u64 = results.iter().map(|r| r.duration).sum();
484
485 println!("\n📊 验收测试汇总:");
486 println!(
487 " 通过: {}, 失败: {}, 总耗时: {}ms",
488 passed, failed, total_duration
489 );
490
491 if failed > 0 {
492 println!("\n⚠️ 失败的测试:");
493 for result in results.iter().filter(|r| !r.passed) {
494 println!(" - {}", result.test_name);
495 }
496 }
497 }
498
499 fn create_batches<T: Clone>(&self, items: &[T], batch_size: usize) -> Vec<Vec<T>> {
501 items.chunks(batch_size).map(|c| c.to_vec()).collect()
502 }
503
504 fn log(&self, message: &str) {
506 if self.config.debug {
507 println!("{}", message);
508 }
509 }
510
511 pub fn set_project_root(&mut self, project_root: PathBuf) {
517 self.config.project_root = project_root;
518 }
519
520 pub fn set_test_timeout(&mut self, timeout: u64) {
522 self.config.test_timeout = timeout;
523 }
524
525 pub fn set_debug(&mut self, debug: bool) {
527 self.config.debug = debug;
528 }
529}
530
531pub fn create_acceptance_test_runner(
537 config: AcceptanceTestRunnerConfig,
538 task_tree_manager: Arc<RwLock<TaskTreeManager>>,
539 blueprint_manager: Arc<RwLock<BlueprintManager>>,
540) -> AcceptanceTestRunner {
541 AcceptanceTestRunner::new(config, task_tree_manager, blueprint_manager)
542}