1use std::path::Path;
2use std::process::Command;
3use std::time::Duration;
4
5use anyhow::Result;
6
7use super::util::{combined_output, duration_from_secs_safe, has_marker_in_subdirs, truncate};
8use super::{
9 ConfidenceScore, DetectionResult, TestAdapter, TestCase, TestError, TestRunResult, TestStatus,
10 TestSuite,
11};
12
13pub struct CppAdapter;
14
15impl Default for CppAdapter {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl CppAdapter {
22 pub fn new() -> Self {
23 Self
24 }
25
26 fn detect_build_system(project_dir: &Path) -> Option<&'static str> {
28 let cmake = project_dir.join("CMakeLists.txt");
29 if cmake.exists() {
30 return Some("cmake");
31 }
32 if project_dir.join("meson.build").exists() {
33 return Some("meson");
34 }
35 if has_marker_in_subdirs(project_dir, 1, |name| name == "CMakeLists.txt") {
37 return Some("cmake");
38 }
39 if has_marker_in_subdirs(project_dir, 1, |name| name == "meson.build") {
40 return Some("meson");
41 }
42 None
43 }
44
45 fn find_build_dir(project_dir: &Path) -> Option<std::path::PathBuf> {
47 for name in &[
48 "build",
49 "cmake-build-debug",
50 "cmake-build-release",
51 "out/build",
52 ] {
53 let p = project_dir.join(name);
54 if p.is_dir() {
55 return Some(p);
56 }
57 }
58 None
59 }
60}
61
62impl TestAdapter for CppAdapter {
63 fn name(&self) -> &str {
64 "C/C++"
65 }
66
67 fn check_runner(&self) -> Option<String> {
68 if which::which("ctest").is_err() && which::which("meson").is_err() {
69 return Some("ctest or meson not found. Install CMake or Meson.".into());
70 }
71 None
72 }
73
74 fn detect(&self, project_dir: &Path) -> Option<DetectionResult> {
75 let build_system = Self::detect_build_system(project_dir)?;
76
77 let framework = match build_system {
78 "cmake" => "ctest",
79 "meson" => "meson test",
80 _ => "unknown",
81 };
82
83 let has_build_dir = Self::find_build_dir(project_dir).is_some();
84 let has_test_dir = project_dir.join("test").is_dir() || project_dir.join("tests").is_dir();
85 let has_runner = which::which("ctest").is_ok() || which::which("meson").is_ok();
86
87 let confidence = ConfidenceScore::base(0.50)
88 .signal(0.15, has_build_dir)
89 .signal(0.15, has_test_dir)
90 .signal(0.10, has_runner)
91 .finish();
92
93 Some(DetectionResult {
94 language: "C/C++".into(),
95 framework: framework.into(),
96 confidence,
97 })
98 }
99
100 fn build_command(&self, project_dir: &Path, extra_args: &[String]) -> Result<Command> {
101 let build_system = Self::detect_build_system(project_dir).unwrap_or("cmake");
102
103 let mut cmd;
104
105 match build_system {
106 "meson" => {
107 cmd = Command::new("meson");
108 cmd.arg("test");
109 cmd.arg("-C");
110 let build_dir = Self::find_build_dir(project_dir)
111 .unwrap_or_else(|| project_dir.join("builddir"));
112 cmd.arg(build_dir);
113 }
114 _ => {
115 cmd = Command::new("ctest");
117 cmd.arg("--output-on-failure");
118 cmd.arg("--test-dir");
119 let build_dir =
120 Self::find_build_dir(project_dir).unwrap_or_else(|| project_dir.join("build"));
121 cmd.arg(build_dir);
122 }
123 }
124
125 for arg in extra_args {
126 cmd.arg(arg);
127 }
128
129 cmd.current_dir(project_dir);
130 Ok(cmd)
131 }
132
133 fn filter_args(&self, pattern: &str) -> Vec<String> {
134 vec!["-R".to_string(), pattern.to_string()]
136 }
137
138 fn parse_output(&self, stdout: &str, stderr: &str, exit_code: i32) -> TestRunResult {
139 let combined = combined_output(stdout, stderr);
140
141 let mut suites = parse_ctest_output(&combined, exit_code);
142
143 let failures = parse_ctest_failures(&combined);
145 if !failures.is_empty() {
146 enrich_with_errors(&mut suites, &failures);
147 }
148
149 let gtest_suites = parse_gtest_output(&combined);
151 if !gtest_suites.is_empty() {
152 suites = gtest_suites;
154 }
155
156 let duration = parse_ctest_duration(&combined).unwrap_or(Duration::from_secs(0));
157
158 TestRunResult {
159 suites,
160 duration,
161 raw_exit_code: exit_code,
162 }
163 }
164}
165
166fn parse_ctest_output(output: &str, exit_code: i32) -> Vec<TestSuite> {
181 let mut tests = Vec::new();
182
183 for line in output.lines() {
184 let trimmed = line.trim();
185
186 if trimmed.contains("Test #")
188 && (trimmed.contains("Passed")
189 || trimmed.contains("Failed")
190 || trimmed.contains("Not Run"))
191 {
192 let (name, status, duration) = parse_ctest_line(trimmed);
193 tests.push(TestCase {
194 name,
195 status,
196 duration,
197 error: None,
198 });
199 }
200 }
201
202 if tests.is_empty()
204 && let Some((passed, failed)) = parse_ctest_summary(output)
205 {
206 for i in 0..passed {
207 tests.push(TestCase {
208 name: format!("test_{}", i + 1),
209 status: TestStatus::Passed,
210 duration: Duration::from_millis(0),
211 error: None,
212 });
213 }
214 for i in 0..failed {
215 tests.push(TestCase {
216 name: format!("failed_test_{}", i + 1),
217 status: TestStatus::Failed,
218 duration: Duration::from_millis(0),
219 error: None,
220 });
221 }
222 }
223
224 if tests.is_empty() {
225 tests.push(TestCase {
226 name: "test_suite".into(),
227 status: if exit_code == 0 {
228 TestStatus::Passed
229 } else {
230 TestStatus::Failed
231 },
232 duration: Duration::from_millis(0),
233 error: None,
234 });
235 }
236
237 vec![TestSuite {
238 name: "tests".into(),
239 tests,
240 }]
241}
242
243fn parse_ctest_line(line: &str) -> (String, TestStatus, Duration) {
244 let status = if line.contains("Passed") {
246 TestStatus::Passed
247 } else if line.contains("Not Run") {
248 TestStatus::Skipped
249 } else {
250 TestStatus::Failed
251 };
252
253 let name = if let Some(idx) = line.find(": ") {
255 let after = &line[idx + 2..];
256 let end = after
258 .find(" .")
259 .or_else(|| after.find(" "))
260 .unwrap_or(after.len());
261 after[..end].trim().to_string()
262 } else {
263 "unknown".into()
264 };
265
266 let duration = if let Some(idx) = line.rfind(" ") {
268 let after = line[idx..].trim();
269 let num_str: String = after
270 .chars()
271 .take_while(|c| c.is_ascii_digit() || *c == '.')
272 .collect();
273 num_str
274 .parse::<f64>()
275 .map(duration_from_secs_safe)
276 .unwrap_or(Duration::from_millis(0))
277 } else {
278 Duration::from_millis(0)
279 };
280
281 (name, status, duration)
282}
283
284fn parse_ctest_summary(output: &str) -> Option<(usize, usize)> {
285 for line in output.lines() {
287 let trimmed = line.trim();
288 if trimmed.contains("tests passed") && trimmed.contains("out of") {
289 let parts: Vec<&str> = trimmed.split_whitespace().collect();
290 let mut failed = 0usize;
291 let mut total = 0usize;
292 for (i, part) in parts.iter().enumerate() {
293 if *part == "failed" && i >= 2 {
295 failed = parts[i - 2].parse().unwrap_or(0);
296 }
297 if *part == "of" && i + 1 < parts.len() {
298 total = parts[i + 1].parse().unwrap_or(0);
299 }
300 }
301 if total > 0 {
302 return Some((total.saturating_sub(failed), failed));
303 }
304 }
305 }
306 None
307}
308
309fn parse_ctest_duration(output: &str) -> Option<Duration> {
310 for line in output.lines() {
312 if line.contains("Total Test time")
313 && let Some(idx) = line.find('=')
314 {
315 let after = line[idx + 1..].trim();
316 let num_str: String = after
317 .chars()
318 .take_while(|c| c.is_ascii_digit() || *c == '.')
319 .collect();
320 if let Ok(secs) = num_str.parse::<f64>() {
321 return Some(duration_from_secs_safe(secs));
322 }
323 }
324 }
325 None
326}
327
328#[derive(Debug, Clone)]
330struct CTestFailure {
331 test_name: String,
333 output: String,
335 error_line: Option<String>,
337}
338
339fn parse_ctest_failures(output: &str) -> Vec<CTestFailure> {
352 let mut failures = Vec::new();
353 let lines: Vec<&str> = output.lines().collect();
354 let mut i = 0;
355
356 while i < lines.len() {
357 let trimmed = lines[i].trim();
358
359 if trimmed.contains("Test #")
361 && (trimmed.contains("***Failed") || trimmed.contains("***Exception"))
362 {
363 let test_name = extract_ctest_name(trimmed);
364
365 let mut output_lines: Vec<String> = Vec::new();
367 let mut error_line = None;
368 i += 1;
369
370 while i < lines.len() {
372 let line = lines[i].trim();
373
374 if line == "Output:" || line.starts_with("---") {
375 i += 1;
376 continue;
377 }
378
379 if line.contains("Test #")
381 || line.contains("tests passed")
382 || line.contains("Total Test time")
383 || (line.starts_with("Start ") && line.contains(':'))
384 {
385 break;
386 }
387
388 if !line.is_empty() {
389 output_lines.push(line.to_string());
390
391 if error_line.is_none() && is_cpp_error_line(line) {
393 error_line = Some(line.to_string());
394 }
395 }
396
397 i += 1;
398 }
399
400 if !output_lines.is_empty() {
401 failures.push(CTestFailure {
402 test_name: test_name.clone(),
403 output: truncate(&output_lines.join("\n"), 800),
404 error_line,
405 });
406 }
407 continue;
408 }
409
410 i += 1;
411 }
412
413 failures
414}
415
416fn extract_ctest_name(line: &str) -> String {
418 if let Some(idx) = line.find(": ") {
419 let after = &line[idx + 2..];
420 let end = after
421 .find(" .")
422 .or_else(|| after.find(" "))
423 .unwrap_or(after.len());
424 after[..end].trim().to_string()
425 } else {
426 "unknown".into()
427 }
428}
429
430fn is_cpp_error_line(line: &str) -> bool {
432 let lower = line.to_lowercase();
433 lower.contains("assert")
434 || lower.contains("error:")
435 || lower.contains("failure")
436 || lower.contains("expected")
437 || lower.contains("actual")
438 || lower.contains("fatal")
439 || lower.contains("segfault")
440 || lower.contains("sigsegv")
441 || lower.contains("sigabrt")
442 || lower.contains("abort")
443}
444
445fn enrich_with_errors(suites: &mut [TestSuite], failures: &[CTestFailure]) {
447 for suite in suites.iter_mut() {
448 for test in suite.tests.iter_mut() {
449 if test.status != TestStatus::Failed || test.error.is_some() {
450 continue;
451 }
452 if let Some(failure) = find_matching_ctest_failure(&test.name, failures) {
453 let message = failure
454 .error_line
455 .clone()
456 .unwrap_or_else(|| first_meaningful_line(&failure.output));
457 test.error = Some(TestError {
458 message,
459 location: extract_cpp_location(&failure.output),
460 });
461 }
462 }
463 }
464}
465
466fn find_matching_ctest_failure<'a>(
468 test_name: &str,
469 failures: &'a [CTestFailure],
470) -> Option<&'a CTestFailure> {
471 for failure in failures {
472 if failure.test_name == test_name {
473 return Some(failure);
474 }
475 if test_name.contains(&failure.test_name) || failure.test_name.contains(test_name) {
477 return Some(failure);
478 }
479 }
480 if failures.len() == 1 {
481 return Some(&failures[0]);
482 }
483 None
484}
485
486fn first_meaningful_line(s: &str) -> String {
488 for line in s.lines() {
489 let trimmed = line.trim();
490 if !trimmed.is_empty() && !trimmed.starts_with("---") && !trimmed.starts_with("===") {
491 return trimmed.to_string();
492 }
493 }
494 s.lines().next().unwrap_or("unknown error").to_string()
495}
496
497fn extract_cpp_location(output: &str) -> Option<String> {
499 for line in output.lines() {
500 let trimmed = line.trim();
501 if let Some(loc) = extract_file_line_location(trimmed) {
504 return Some(loc);
505 }
506 if let Some(rest) = trimmed.strip_prefix("at ")
508 && rest.contains(':')
509 && (rest.contains(".cpp") || rest.contains(".c") || rest.contains(".h"))
510 {
511 return Some(rest.to_string());
512 }
513 }
514 None
515}
516
517fn extract_file_line_location(line: &str) -> Option<String> {
519 let extensions = [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp"];
521 for ext in &extensions {
522 if let Some(ext_pos) = line.find(ext) {
523 let after_ext = &line[ext_pos + ext.len()..];
524 if let Some(colon_after) = after_ext.strip_prefix(':') {
525 let num_end = colon_after
527 .find(|c: char| !c.is_ascii_digit())
528 .unwrap_or(colon_after.len());
529 if num_end > 0 {
530 let end = ext_pos + ext.len() + 1 + num_end;
531 return Some(line[..end].trim().to_string());
532 }
533 } else if after_ext.starts_with('(') {
534 if let Some(paren_close) = after_ext.find(')') {
536 let end = ext_pos + ext.len() + paren_close + 1;
537 return Some(line[..end].trim().to_string());
538 }
539 }
540 }
541 }
542 None
543}
544
545fn parse_gtest_output(output: &str) -> Vec<TestSuite> {
569 let mut suites_map: std::collections::HashMap<String, Vec<TestCase>> =
570 std::collections::HashMap::new();
571
572 let lines: Vec<&str> = output.lines().collect();
573 let mut i = 0;
574
575 while i < lines.len() {
576 let trimmed = lines[i].trim();
577
578 if trimmed.starts_with("[ RUN") {
580 let test_full = trimmed
581 .strip_prefix("[ RUN")
582 .unwrap_or("")
583 .trim()
584 .trim_start_matches(']')
585 .trim();
586
587 let (suite_name, test_name) = split_gtest_name(test_full);
588
589 let mut output_lines = Vec::new();
591 i += 1;
592
593 let mut status = TestStatus::Passed;
594 let mut duration = Duration::from_millis(0);
595
596 while i < lines.len() {
597 let line = lines[i].trim();
598
599 if line.starts_with("[ OK ]") || line.starts_with("[ FAILED ]") {
600 status = if line.starts_with("[ OK ]") {
601 TestStatus::Passed
602 } else {
603 TestStatus::Failed
604 };
605 duration = parse_gtest_duration(line);
606 break;
607 }
608
609 if !line.is_empty() && !line.starts_with("[") {
610 output_lines.push(line.to_string());
611 }
612
613 i += 1;
614 }
615
616 let error = if status == TestStatus::Failed && !output_lines.is_empty() {
617 let message = output_lines
618 .iter()
619 .find(|l| is_cpp_error_line(l))
620 .cloned()
621 .unwrap_or_else(|| output_lines[0].clone());
622 let location = output_lines
623 .iter()
624 .find_map(|l| extract_file_line_location(l));
625 Some(TestError { message, location })
626 } else {
627 None
628 };
629
630 suites_map.entry(suite_name).or_default().push(TestCase {
631 name: test_name,
632 status,
633 duration,
634 error,
635 });
636 }
637
638 i += 1;
639 }
640
641 let mut suites: Vec<TestSuite> = suites_map
642 .into_iter()
643 .map(|(name, tests)| TestSuite { name, tests })
644 .collect();
645 suites.sort_by(|a, b| a.name.cmp(&b.name));
646
647 suites
648}
649
650fn split_gtest_name(full_name: &str) -> (String, String) {
652 if let Some(dot) = full_name.find('.') {
653 (
654 full_name[..dot].to_string(),
655 full_name[dot + 1..].to_string(),
656 )
657 } else {
658 ("tests".into(), full_name.to_string())
659 }
660}
661
662fn parse_gtest_duration(line: &str) -> Duration {
664 if let Some(paren_start) = line.rfind('(') {
665 let inside = &line[paren_start + 1..line.len().saturating_sub(1)];
666 let inside = inside.trim();
667 if inside.ends_with("ms") {
668 let num_str = inside.strip_suffix("ms").unwrap_or("").trim();
669 if let Ok(ms) = num_str.parse::<u64>() {
670 return Duration::from_millis(ms);
671 }
672 }
673 }
674 Duration::from_millis(0)
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[test]
682 fn detect_cmake_project() {
683 let dir = tempfile::tempdir().unwrap();
684 std::fs::write(
685 dir.path().join("CMakeLists.txt"),
686 "cmake_minimum_required(VERSION 3.14)\nenable_testing()\n",
687 )
688 .unwrap();
689 let adapter = CppAdapter::new();
690 let det = adapter.detect(dir.path()).unwrap();
691 assert_eq!(det.language, "C/C++");
692 assert_eq!(det.framework, "ctest");
693 assert!(det.confidence > 0.4 && det.confidence < 1.0);
694 }
695
696 #[test]
697 fn detect_meson_project() {
698 let dir = tempfile::tempdir().unwrap();
699 std::fs::write(dir.path().join("meson.build"), "project('test', 'c')\n").unwrap();
700 let adapter = CppAdapter::new();
701 let det = adapter.detect(dir.path()).unwrap();
702 assert_eq!(det.framework, "meson test");
703 }
704
705 #[test]
706 fn detect_no_cpp() {
707 let dir = tempfile::tempdir().unwrap();
708 let adapter = CppAdapter::new();
709 assert!(adapter.detect(dir.path()).is_none());
710 }
711
712 #[test]
713 fn parse_ctest_detailed_output() {
714 let stdout = r#"
715Test project /home/user/project/build
716 Start 1: test_basic
7171/3 Test #1: test_basic ................... Passed 0.01 sec
718 Start 2: test_advanced
7192/3 Test #2: test_advanced ................ Passed 0.02 sec
720 Start 3: test_edge
7213/3 Test #3: test_edge ....................***Failed 0.01 sec
722
72367% tests passed, 1 tests failed out of 3
724
725Total Test time (real) = 0.04 sec
726"#;
727 let adapter = CppAdapter::new();
728 let result = adapter.parse_output(stdout, "", 1);
729
730 assert_eq!(result.total_tests(), 3);
731 assert_eq!(result.total_passed(), 2);
732 assert_eq!(result.total_failed(), 1);
733 assert!(!result.is_success());
734 }
735
736 #[test]
737 fn parse_ctest_all_pass() {
738 let stdout = r#"
739Test project /home/user/project/build
740 Start 1: test_one
7411/2 Test #1: test_one ..................... Passed 0.01 sec
742 Start 2: test_two
7432/2 Test #2: test_two ..................... Passed 0.01 sec
744
745100% tests passed, 0 tests failed out of 2
746
747Total Test time (real) = 0.02 sec
748"#;
749 let adapter = CppAdapter::new();
750 let result = adapter.parse_output(stdout, "", 0);
751
752 assert_eq!(result.total_tests(), 2);
753 assert_eq!(result.total_passed(), 2);
754 assert!(result.is_success());
755 }
756
757 #[test]
758 fn parse_ctest_summary_only() {
759 let stdout = "67% tests passed, 1 tests failed out of 3\n";
760 let adapter = CppAdapter::new();
761 let result = adapter.parse_output(stdout, "", 1);
762
763 assert_eq!(result.total_tests(), 3);
764 assert_eq!(result.total_passed(), 2);
765 assert_eq!(result.total_failed(), 1);
766 }
767
768 #[test]
769 fn parse_ctest_empty_output() {
770 let adapter = CppAdapter::new();
771 let result = adapter.parse_output("", "", 0);
772
773 assert_eq!(result.total_tests(), 1);
774 assert!(result.is_success());
775 }
776
777 #[test]
778 fn parse_ctest_duration_value() {
779 assert_eq!(
780 parse_ctest_duration("Total Test time (real) = 0.05 sec"),
781 Some(Duration::from_millis(50))
782 );
783 }
784
785 #[test]
786 fn find_build_dir_exists() {
787 let dir = tempfile::tempdir().unwrap();
788 std::fs::create_dir(dir.path().join("build")).unwrap();
789 assert!(CppAdapter::find_build_dir(dir.path()).is_some());
790 }
791
792 #[test]
793 fn find_build_dir_missing() {
794 let dir = tempfile::tempdir().unwrap();
795 assert!(CppAdapter::find_build_dir(dir.path()).is_none());
796 }
797
798 #[test]
799 fn parse_gtest_detailed_output() {
800 let stdout = r#"
801[==========] Running 3 tests from 1 test suite.
802[----------] 3 tests from MathTest
803[ RUN ] MathTest.TestAdd
804[ OK ] MathTest.TestAdd (0 ms)
805[ RUN ] MathTest.TestSub
806[ OK ] MathTest.TestSub (1 ms)
807[ RUN ] MathTest.TestDiv
808test_math.cpp:42: Failure
809Expected equality of these values:
810 divide(10, 3)
811 Which is: 3
812 4
813[ FAILED ] MathTest.TestDiv (0 ms)
814[----------] 3 tests from MathTest (1 ms total)
815[==========] 3 tests from 1 test suite ran. (1 ms total)
816[ PASSED ] 2 tests.
817[ FAILED ] 1 test, listed below:
818[ FAILED ] MathTest.TestDiv
819"#;
820 let suites = parse_gtest_output(stdout);
821 assert_eq!(suites.len(), 1);
822 assert_eq!(suites[0].name, "MathTest");
823 assert_eq!(suites[0].tests.len(), 3);
824 assert_eq!(suites[0].tests[0].name, "TestAdd");
825 assert_eq!(suites[0].tests[0].status, TestStatus::Passed);
826 assert_eq!(suites[0].tests[2].name, "TestDiv");
827 assert_eq!(suites[0].tests[2].status, TestStatus::Failed);
828 assert!(suites[0].tests[2].error.is_some());
829 }
830
831 #[test]
832 fn parse_gtest_all_pass() {
833 let stdout = r#"
834[==========] Running 2 tests from 1 test suite.
835[ RUN ] MathTest.TestAdd
836[ OK ] MathTest.TestAdd (0 ms)
837[ RUN ] MathTest.TestSub
838[ OK ] MathTest.TestSub (0 ms)
839[==========] 2 tests from 1 test suite ran. (0 ms total)
840[ PASSED ] 2 tests.
841"#;
842 let suites = parse_gtest_output(stdout);
843 assert_eq!(suites.len(), 1);
844 assert_eq!(suites[0].tests.len(), 2);
845 assert!(
846 suites[0]
847 .tests
848 .iter()
849 .all(|t| t.status == TestStatus::Passed)
850 );
851 }
852
853 #[test]
854 fn parse_gtest_multiple_suites() {
855 let stdout = r#"
856[ RUN ] MathTest.TestAdd
857[ OK ] MathTest.TestAdd (0 ms)
858[ RUN ] StringTest.TestUpper
859[ OK ] StringTest.TestUpper (0 ms)
860"#;
861 let suites = parse_gtest_output(stdout);
862 assert_eq!(suites.len(), 2);
863 }
864
865 #[test]
866 fn parse_gtest_failure_with_error_details() {
867 let stdout = r#"
868[ RUN ] MathTest.TestDiv
869test_math.cpp:42: Failure
870Expected: 4
871 Actual: 3
872[ FAILED ] MathTest.TestDiv (0 ms)
873"#;
874 let suites = parse_gtest_output(stdout);
875 let err = suites[0].tests[0].error.as_ref().unwrap();
876 assert!(err.location.is_some());
877 assert!(err.location.as_ref().unwrap().contains("test_math.cpp:42"));
878 }
879
880 #[test]
881 fn parse_gtest_duration_test() {
882 assert_eq!(
883 parse_gtest_duration("[ OK ] MathTest.TestAdd (123 ms)"),
884 Duration::from_millis(123)
885 );
886 assert_eq!(
887 parse_gtest_duration("[ FAILED ] MathTest.TestDiv (0 ms)"),
888 Duration::from_millis(0)
889 );
890 }
891
892 #[test]
893 fn split_gtest_name_test() {
894 assert_eq!(
895 split_gtest_name("MathTest.TestAdd"),
896 ("MathTest".into(), "TestAdd".into())
897 );
898 assert_eq!(
899 split_gtest_name("SimpleTest"),
900 ("tests".into(), "SimpleTest".into())
901 );
902 }
903
904 #[test]
905 fn is_cpp_error_line_test() {
906 assert!(is_cpp_error_line("ASSERT_EQ failed"));
907 assert!(is_cpp_error_line("error: expected 4"));
908 assert!(is_cpp_error_line("Failure"));
909 assert!(is_cpp_error_line("Segfault at 0x0"));
910 assert!(!is_cpp_error_line("Running tests..."));
911 }
912
913 #[test]
914 fn extract_file_line_location_test() {
915 assert_eq!(
916 extract_file_line_location("test.cpp:42: Failure"),
917 Some("test.cpp:42".into())
918 );
919 assert_eq!(
920 extract_file_line_location("main.c:10: error: boom"),
921 Some("main.c:10".into())
922 );
923 assert_eq!(
924 extract_file_line_location("test.hpp(15): fatal"),
925 Some("test.hpp(15)".into())
926 );
927 assert!(extract_file_line_location("no file here").is_none());
928 }
929
930 #[test]
931 fn extract_ctest_name_test() {
932 assert_eq!(
933 extract_ctest_name("1/3 Test #1: test_basic ................... Passed 0.01 sec"),
934 "test_basic"
935 );
936 }
937
938 #[test]
939 fn parse_ctest_failures_with_output() {
940 let output = r#"
9411/2 Test #1: test_pass ................... Passed 0.01 sec
9422/2 Test #2: test_edge ....................***Failed 0.01 sec
943ASSERT_EQ(4, result) failed
944 Expected: 4
945 Actual: 3
946test_edge.cpp:42: Failure
947
94867% tests passed, 1 tests failed out of 2
949"#;
950 let failures = parse_ctest_failures(output);
951 assert_eq!(failures.len(), 1);
952 assert_eq!(failures[0].test_name, "test_edge");
953 assert!(failures[0].error_line.is_some());
954 }
955
956 #[test]
957 fn truncate_test() {
958 assert_eq!(truncate("short", 100), "short");
959 let long = "x".repeat(1000);
960 let truncated = truncate(&long, 800);
961 assert!(truncated.ends_with("..."));
962 }
963
964 #[test]
965 fn first_meaningful_line_test() {
966 assert_eq!(
967 first_meaningful_line("---\n\nHello world\nmore"),
968 "Hello world"
969 );
970 assert_eq!(first_meaningful_line("first"), "first");
971 }
972
973 #[test]
974 fn parse_ctest_with_gtest_output() {
975 let stdout = r#"
976Test project /home/user/project/build
977 Start 1: gtest_math
9781/1 Test #1: gtest_math .....................***Failed 0.01 sec
979[==========] Running 2 tests from 1 test suite.
980[ RUN ] MathTest.TestAdd
981[ OK ] MathTest.TestAdd (0 ms)
982[ RUN ] MathTest.TestDiv
983test.cpp:10: Failure
984Expected: 4
985 Actual: 3
986[ FAILED ] MathTest.TestDiv (0 ms)
987[ PASSED ] 1 test.
988[ FAILED ] 1 test, listed below:
989[ FAILED ] MathTest.TestDiv
990
99167% tests passed, 1 tests failed out of 1
992
993Total Test time (real) = 0.01 sec
994"#;
995 let adapter = CppAdapter::new();
996 let result = adapter.parse_output(stdout, "", 1);
997
998 assert_eq!(result.suites.len(), 1);
1000 assert_eq!(result.suites[0].name, "MathTest");
1001 assert_eq!(result.total_tests(), 2);
1002 assert_eq!(result.total_passed(), 1);
1003 assert_eq!(result.total_failed(), 1);
1004 }
1005}