1use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use crate::adapters::util::duration_from_secs_safe;
10use crate::adapters::{TestCase, TestError, TestRunResult, TestStatus, TestSuite};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum OutputParser {
15 Json,
17 Junit,
19 Tap,
21 Lines,
23 Regex(RegexParserConfig),
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct RegexParserConfig {
30 pub pass_pattern: String,
32 pub fail_pattern: String,
34 pub skip_pattern: Option<String>,
36 pub name_group: usize,
38 pub duration_group: Option<usize>,
40}
41
42#[derive(Debug, Clone)]
44pub struct ScriptAdapterConfig {
45 pub name: String,
47 pub detect_file: String,
49 pub detect_pattern: Option<String>,
51 pub command: String,
53 pub args: Vec<String>,
55 pub parser: OutputParser,
57 pub working_dir: Option<String>,
59 pub env: Vec<(String, String)>,
61}
62
63impl ScriptAdapterConfig {
64 pub fn new(name: &str, detect_file: &str, command: &str) -> Self {
66 Self {
67 name: name.to_string(),
68 detect_file: detect_file.to_string(),
69 detect_pattern: None,
70 command: command.to_string(),
71 args: Vec::new(),
72 parser: OutputParser::Lines,
73 working_dir: None,
74 env: Vec::new(),
75 }
76 }
77
78 pub fn with_parser(mut self, parser: OutputParser) -> Self {
80 self.parser = parser;
81 self
82 }
83
84 pub fn with_args(mut self, args: Vec<String>) -> Self {
86 self.args = args;
87 self
88 }
89
90 pub fn with_working_dir(mut self, dir: &str) -> Self {
92 self.working_dir = Some(dir.to_string());
93 self
94 }
95
96 pub fn with_env(mut self, key: &str, value: &str) -> Self {
98 self.env.push((key.to_string(), value.to_string()));
99 self
100 }
101
102 pub fn detect(&self, project_dir: &Path) -> bool {
104 let detect_path = project_dir.join(&self.detect_file);
105 if detect_path.exists() {
106 return true;
107 }
108
109 if let Some(ref pattern) = self.detect_pattern {
111 return glob_detect(project_dir, pattern);
112 }
113
114 false
115 }
116
117 pub fn effective_working_dir(&self, project_dir: &Path) -> PathBuf {
119 match &self.working_dir {
120 Some(dir) => {
121 let dir_path = std::path::Path::new(dir);
122 if dir_path.is_absolute()
125 || dir_path
126 .components()
127 .any(|c| matches!(c, std::path::Component::ParentDir))
128 {
129 return project_dir.to_path_buf();
130 }
131 let candidate = project_dir.join(dir);
132 if let (Ok(resolved), Ok(base)) =
135 (candidate.canonicalize(), project_dir.canonicalize())
136 {
137 if resolved.starts_with(&base) {
138 return resolved;
139 }
140 return base;
141 }
142 candidate
146 }
147 None => project_dir.to_path_buf(),
148 }
149 }
150
151 pub fn full_command(&self) -> String {
153 let mut parts = vec![self.command.clone()];
154 parts.extend(self.args.clone());
155 parts.join(" ")
156 }
157}
158
159fn glob_detect(project_dir: &Path, pattern: &str) -> bool {
161 if pattern.contains('*') {
163 if let Some(base) = pattern.split('*').next() {
165 let base = base.trim_end_matches('/');
166 if !base.is_empty() {
167 return project_dir.join(base).exists();
168 }
169 }
170 project_dir.join(pattern).exists()
172 } else {
173 project_dir.join(pattern).exists()
174 }
175}
176
177pub fn parse_script_output(
181 parser: &OutputParser,
182 stdout: &str,
183 stderr: &str,
184 exit_code: i32,
185) -> TestRunResult {
186 match parser {
187 OutputParser::Json => parse_json_output(stdout, stderr, exit_code),
188 OutputParser::Junit => parse_junit_output(stdout, exit_code),
189 OutputParser::Tap => parse_tap_output(stdout, exit_code),
190 OutputParser::Lines => parse_lines_output(stdout, exit_code),
191 OutputParser::Regex(config) => parse_regex_output(stdout, config, exit_code),
192 }
193}
194
195fn parse_json_output(stdout: &str, _stderr: &str, exit_code: i32) -> TestRunResult {
197 if let Ok(result) = serde_json::from_str::<serde_json::Value>(stdout) {
199 let suites = parse_json_suites(&result);
200 if !suites.is_empty() {
201 return TestRunResult {
202 suites,
203 duration: Duration::ZERO,
204 raw_exit_code: exit_code,
205 };
206 }
207 }
208
209 fallback_result(stdout, exit_code, "json")
211}
212
213fn parse_json_suites(value: &serde_json::Value) -> Vec<TestSuite> {
215 let mut suites = Vec::new();
216
217 if let Some(arr) = value.get("suites").and_then(|v| v.as_array()) {
219 for suite_val in arr {
220 if let Some(suite) = parse_json_suite(suite_val) {
221 suites.push(suite);
222 }
223 }
224 }
225
226 if suites.is_empty()
228 && let Some(arr) = value.get("tests").and_then(|v| v.as_array())
229 {
230 let name = value
231 .get("name")
232 .and_then(|v| v.as_str())
233 .unwrap_or("tests");
234 let tests: Vec<TestCase> = arr.iter().filter_map(parse_json_test).collect();
235 if !tests.is_empty() {
236 suites.push(TestSuite {
237 name: name.to_string(),
238 tests,
239 });
240 }
241 }
242
243 if suites.is_empty()
245 && let Some(arr) = value.as_array()
246 {
247 let tests: Vec<TestCase> = arr.iter().filter_map(parse_json_test).collect();
248 if !tests.is_empty() {
249 suites.push(TestSuite {
250 name: "tests".to_string(),
251 tests,
252 });
253 }
254 }
255
256 suites
257}
258
259fn parse_json_suite(value: &serde_json::Value) -> Option<TestSuite> {
260 let name = value.get("name").and_then(|v| v.as_str())?;
261 let tests_arr = value.get("tests").and_then(|v| v.as_array())?;
262 let tests: Vec<TestCase> = tests_arr.iter().filter_map(parse_json_test).collect();
263 Some(TestSuite {
264 name: name.to_string(),
265 tests,
266 })
267}
268
269fn parse_json_test(value: &serde_json::Value) -> Option<TestCase> {
270 let name = value.get("name").and_then(|v| v.as_str())?;
271 let status_str = value.get("status").and_then(|v| v.as_str())?;
272
273 let status = match status_str.to_lowercase().as_str() {
274 "passed" | "pass" | "ok" | "success" => TestStatus::Passed,
275 "failed" | "fail" | "error" | "failure" => TestStatus::Failed,
276 "skipped" | "skip" | "pending" | "ignored" => TestStatus::Skipped,
277 _ => return None,
278 };
279
280 let duration = value
281 .get("duration")
282 .and_then(|v| v.as_f64())
283 .map(|ms| duration_from_secs_safe(ms / 1000.0))
284 .unwrap_or(Duration::ZERO);
285
286 let error = value.get("error").and_then(|v| {
287 let message = v.as_str().map(|s| s.to_string()).or_else(|| {
288 v.get("message")
289 .and_then(|m| m.as_str().map(|s| s.to_string()))
290 })?;
291 let location = v
292 .get("location")
293 .and_then(|l| l.as_str().map(|s| s.to_string()));
294 Some(TestError { message, location })
295 });
296
297 Some(TestCase {
298 name: name.to_string(),
299 status,
300 duration,
301 error,
302 })
303}
304
305fn parse_junit_output(stdout: &str, exit_code: i32) -> TestRunResult {
307 let mut suites = Vec::new();
308
309 for line in stdout.lines() {
311 let trimmed = line.trim();
312 if trimmed.starts_with("<testsuite")
313 && !trimmed.starts_with("<testsuites")
314 && let Some(suite) = parse_junit_suite_tag(trimmed, stdout)
315 {
316 suites.push(suite);
317 }
318 }
319
320 if suites.is_empty() {
322 let tests = parse_junit_testcases(stdout);
323 if !tests.is_empty() {
324 suites.push(TestSuite {
325 name: "tests".to_string(),
326 tests,
327 });
328 }
329 }
330
331 if suites.is_empty() {
332 return fallback_result(stdout, exit_code, "junit");
333 }
334
335 TestRunResult {
336 suites,
337 duration: Duration::ZERO,
338 raw_exit_code: exit_code,
339 }
340}
341
342fn parse_junit_suite_tag(tag: &str, full_output: &str) -> Option<TestSuite> {
343 let name = extract_xml_attr(tag, "name").unwrap_or_else(|| "tests".to_string());
344 let tests = parse_junit_testcases(full_output);
345 if tests.is_empty() {
346 return None;
347 }
348 Some(TestSuite { name, tests })
349}
350
351fn parse_junit_testcases(xml: &str) -> Vec<TestCase> {
352 let mut tests = Vec::new();
353 let lines: Vec<&str> = xml.lines().collect();
354
355 let mut i = 0;
356 while i < lines.len() {
357 let trimmed = lines[i].trim();
358 if trimmed.starts_with("<testcase") {
359 let name = extract_xml_attr(trimmed, "name").unwrap_or_else(|| "unknown".to_string());
360 let time = extract_xml_attr(trimmed, "time")
361 .and_then(|t| t.parse::<f64>().ok())
362 .map(duration_from_secs_safe)
363 .unwrap_or(Duration::ZERO);
364
365 let mut status = TestStatus::Passed;
367 let mut error = None;
368
369 if trimmed.ends_with("/>") {
370 if trimmed.contains("<skipped") {
372 status = TestStatus::Skipped;
373 }
374 } else {
375 let mut j = i + 1;
377 while j < lines.len() {
378 let inner = lines[j].trim();
379 if inner.starts_with("</testcase") {
380 break;
381 }
382 if inner.starts_with("<failure") || inner.starts_with("<error") {
383 status = TestStatus::Failed;
384 let message = extract_xml_attr(inner, "message")
385 .unwrap_or_else(|| "Test failed".to_string());
386 error = Some(TestError {
387 message,
388 location: None,
389 });
390 }
391 if inner.starts_with("<skipped") {
392 status = TestStatus::Skipped;
393 }
394 j += 1;
395 }
396 }
397
398 tests.push(TestCase {
399 name,
400 status,
401 duration: time,
402 error,
403 });
404 }
405 i += 1;
406 }
407
408 tests
409}
410
411fn extract_xml_attr(tag: &str, attr: &str) -> Option<String> {
413 let search = format!("{attr}=\"");
414 let start = tag.find(&search)? + search.len();
415 let rest = &tag[start..];
416 let end = rest.find('"')?;
417 Some(rest[..end].to_string())
418}
419
420fn parse_tap_output(stdout: &str, exit_code: i32) -> TestRunResult {
422 let mut tests = Vec::new();
423 let mut _plan_count = 0;
424
425 for line in stdout.lines() {
426 let trimmed = line.trim();
427
428 if let Some(rest) = trimmed.strip_prefix("1..") {
430 if let Ok(n) = rest.parse::<usize>() {
431 _plan_count = n;
432 }
433 continue;
434 }
435
436 if let Some(rest) = trimmed.strip_prefix("ok ") {
438 let (name, is_skip) = parse_tap_description(rest);
439 tests.push(TestCase {
440 name,
441 status: if is_skip {
442 TestStatus::Skipped
443 } else {
444 TestStatus::Passed
445 },
446 duration: Duration::ZERO,
447 error: None,
448 });
449 continue;
450 }
451
452 if let Some(rest) = trimmed.strip_prefix("not ok ") {
454 let (name, is_skip) = parse_tap_description(rest);
455 let is_todo = trimmed.contains("# TODO");
456 tests.push(TestCase {
457 name,
458 status: if is_skip || is_todo {
459 TestStatus::Skipped
460 } else {
461 TestStatus::Failed
462 },
463 duration: Duration::ZERO,
464 error: if !is_skip && !is_todo {
465 Some(TestError {
466 message: "Test failed".to_string(),
467 location: None,
468 })
469 } else {
470 None
471 },
472 });
473 }
474 }
475
476 if tests.is_empty() {
477 return fallback_result(stdout, exit_code, "tap");
478 }
479
480 TestRunResult {
481 suites: vec![TestSuite {
482 name: "tests".to_string(),
483 tests,
484 }],
485 duration: Duration::ZERO,
486 raw_exit_code: exit_code,
487 }
488}
489
490fn parse_tap_description(rest: &str) -> (String, bool) {
492 let after_num = rest
494 .find(|c: char| !c.is_ascii_digit())
495 .map(|i| rest[i..].trim_start())
496 .unwrap_or(rest);
497
498 let desc = after_num.strip_prefix("- ").unwrap_or(after_num);
500
501 let is_skip = desc.contains("# SKIP") || desc.contains("# skip");
503
504 let name = if let Some(idx) = desc.find(" # ") {
506 desc[..idx].to_string()
507 } else {
508 desc.to_string()
509 };
510
511 (name, is_skip)
512}
513
514fn parse_lines_output(stdout: &str, exit_code: i32) -> TestRunResult {
519 let mut tests = Vec::new();
520
521 for line in stdout.lines() {
522 let trimmed = line.trim();
523 if trimmed.is_empty() {
524 continue;
525 }
526
527 if let Some(test) = parse_status_line(trimmed) {
528 tests.push(test);
529 }
530 }
531
532 if tests.is_empty() {
533 return fallback_result(stdout, exit_code, "lines");
534 }
535
536 TestRunResult {
537 suites: vec![TestSuite {
538 name: "tests".to_string(),
539 tests,
540 }],
541 duration: Duration::ZERO,
542 raw_exit_code: exit_code,
543 }
544}
545
546fn parse_status_line(line: &str) -> Option<TestCase> {
548 let (status, rest) = parse_status_prefix(line)?;
549 let name = rest.trim().to_string();
550 if name.is_empty() {
551 return None;
552 }
553
554 let failed = status == TestStatus::Failed;
555 Some(TestCase {
556 name,
557 status,
558 duration: Duration::ZERO,
559 error: if failed {
560 Some(TestError {
561 message: "Test failed".into(),
562 location: None,
563 })
564 } else {
565 None
566 },
567 })
568}
569
570fn parse_status_prefix(line: &str) -> Option<(TestStatus, &str)> {
572 let patterns: &[(&str, TestStatus)] = &[
573 ("ok ", TestStatus::Passed),
574 ("pass ", TestStatus::Passed),
575 ("passed ", TestStatus::Passed),
576 ("PASS ", TestStatus::Passed),
577 ("PASSED ", TestStatus::Passed),
578 ("OK ", TestStatus::Passed),
579 ("✓ ", TestStatus::Passed),
580 ("✔ ", TestStatus::Passed),
581 ("fail ", TestStatus::Failed),
582 ("failed ", TestStatus::Failed),
583 ("error ", TestStatus::Failed),
584 ("FAIL ", TestStatus::Failed),
585 ("FAILED ", TestStatus::Failed),
586 ("ERROR ", TestStatus::Failed),
587 ("✗ ", TestStatus::Failed),
588 ("✘ ", TestStatus::Failed),
589 ("skip ", TestStatus::Skipped),
590 ("skipped ", TestStatus::Skipped),
591 ("pending ", TestStatus::Skipped),
592 ("SKIP ", TestStatus::Skipped),
593 ("SKIPPED ", TestStatus::Skipped),
594 ("PENDING ", TestStatus::Skipped),
595 ];
596
597 for (prefix, status) in patterns {
598 if let Some(rest) = line.strip_prefix(prefix) {
599 return Some((status.clone(), rest));
600 }
601 }
602
603 let colon_patterns: &[(&str, TestStatus)] = &[
605 ("ok:", TestStatus::Passed),
606 ("pass:", TestStatus::Passed),
607 ("fail:", TestStatus::Failed),
608 ("error:", TestStatus::Failed),
609 ("skip:", TestStatus::Skipped),
610 ];
611
612 for (prefix, status) in colon_patterns {
613 if let Some(rest) = line.to_lowercase().strip_prefix(prefix) {
614 let idx = prefix.len();
615 let _ = rest; return Some((status.clone(), line[idx..].trim_start()));
617 }
618 }
619
620 None
621}
622
623fn parse_regex_output(stdout: &str, config: &RegexParserConfig, exit_code: i32) -> TestRunResult {
625 let mut tests = Vec::new();
626
627 for line in stdout.lines() {
628 let trimmed = line.trim();
629 if trimmed.is_empty() {
630 continue;
631 }
632
633 if let Some(test) =
634 try_regex_match(trimmed, &config.pass_pattern, TestStatus::Passed, config)
635 {
636 tests.push(test);
637 } else if let Some(test) =
638 try_regex_match(trimmed, &config.fail_pattern, TestStatus::Failed, config)
639 {
640 tests.push(test);
641 } else if let Some(ref skip_pattern) = config.skip_pattern
642 && let Some(test) = try_regex_match(trimmed, skip_pattern, TestStatus::Skipped, config)
643 {
644 tests.push(test);
645 }
646 }
647
648 if tests.is_empty() {
649 return fallback_result(stdout, exit_code, "regex");
650 }
651
652 TestRunResult {
653 suites: vec![TestSuite {
654 name: "tests".to_string(),
655 tests,
656 }],
657 duration: Duration::ZERO,
658 raw_exit_code: exit_code,
659 }
660}
661
662fn try_regex_match(
667 line: &str,
668 pattern: &str,
669 status: TestStatus,
670 config: &RegexParserConfig,
671) -> Option<TestCase> {
672 let captures = simple_pattern_match(pattern, line)?;
673
674 let name = captures.get(config.name_group.saturating_sub(1))?.clone();
675 if name.is_empty() {
676 return None;
677 }
678
679 let duration = config
680 .duration_group
681 .and_then(|g| captures.get(g.saturating_sub(1)))
682 .and_then(|d| d.parse::<f64>().ok())
683 .map(|ms| duration_from_secs_safe(ms / 1000.0))
684 .unwrap_or(Duration::ZERO);
685
686 Some(TestCase {
687 name,
688 status: status.clone(),
689 duration,
690 error: if status == TestStatus::Failed {
691 Some(TestError {
692 message: "Test failed".into(),
693 location: None,
694 })
695 } else {
696 None
697 },
698 })
699}
700
701fn simple_pattern_match(pattern: &str, input: &str) -> Option<Vec<String>> {
706 let mut captures = Vec::new();
707 let mut pat_idx = 0;
708 let mut inp_idx = 0;
709 let pat_bytes = pattern.as_bytes();
710 let inp_bytes = input.as_bytes();
711
712 while pat_idx < pat_bytes.len() && inp_idx <= inp_bytes.len() {
713 if pat_idx + 4 <= pat_bytes.len() && &pat_bytes[pat_idx..pat_idx + 4] == b"(.*)" {
714 pat_idx += 4;
716
717 let next_literal = find_next_literal(pattern, pat_idx);
719
720 match next_literal {
721 Some(lit) => {
722 let remaining = &input[inp_idx..];
724 if let Some(pos) = remaining.find(&lit) {
725 captures.push(remaining[..pos].to_string());
726 inp_idx += pos;
727 } else {
728 return None;
729 }
730 }
731 None => {
732 captures.push(input[inp_idx..].to_string());
734 inp_idx = inp_bytes.len();
735 }
736 }
737 } else if pat_idx + 1 < pat_bytes.len()
738 && pat_bytes[pat_idx] == b'.'
739 && pat_bytes[pat_idx + 1] == b'*'
740 {
741 pat_idx += 2;
743 let next_literal = find_next_literal(pattern, pat_idx);
744 match next_literal {
745 Some(lit) => {
746 let remaining = &input[inp_idx..];
747 if let Some(pos) = remaining.find(&lit) {
748 inp_idx += pos;
749 } else {
750 return None;
751 }
752 }
753 None => {
754 inp_idx = inp_bytes.len();
755 }
756 }
757 } else if inp_idx < inp_bytes.len() && pat_bytes[pat_idx] == inp_bytes[inp_idx] {
758 pat_idx += 1;
759 inp_idx += 1;
760 } else {
761 return None;
762 }
763 }
764
765 if pat_idx == pat_bytes.len() && inp_idx == inp_bytes.len() {
767 Some(captures)
768 } else {
769 None
770 }
771}
772
773fn find_next_literal(pattern: &str, from: usize) -> Option<String> {
775 let rest = &pattern[from..];
776 if rest.is_empty() {
777 return None;
778 }
779
780 let mut lit = String::new();
781 let bytes = rest.as_bytes();
782 let mut i = 0;
783 while i < bytes.len() {
784 if i + 1 < bytes.len() && bytes[i] == b'.' && bytes[i + 1] == b'*' {
785 break;
786 }
787 if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"(.*)" {
788 break;
789 }
790 lit.push(bytes[i] as char);
791 i += 1;
792 }
793
794 if lit.is_empty() { None } else { Some(lit) }
795}
796
797fn fallback_result(stdout: &str, exit_code: i32, parser_name: &str) -> TestRunResult {
799 let status = if exit_code == 0 {
800 TestStatus::Passed
801 } else {
802 TestStatus::Failed
803 };
804
805 TestRunResult {
806 suites: vec![TestSuite {
807 name: format!("{parser_name}-output"),
808 tests: vec![TestCase {
809 name: format!("test run ({parser_name} parser)"),
810 status,
811 duration: Duration::ZERO,
812 error: if exit_code != 0 {
813 Some(TestError {
814 message: stdout.lines().next().unwrap_or("Test failed").to_string(),
815 location: None,
816 })
817 } else {
818 None
819 },
820 }],
821 }],
822 duration: Duration::ZERO,
823 raw_exit_code: exit_code,
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use std::path::PathBuf;
831
832 #[test]
835 fn config_new() {
836 let config = ScriptAdapterConfig::new("mytest", "Makefile", "make test");
837 assert_eq!(config.name, "mytest");
838 assert_eq!(config.detect_file, "Makefile");
839 assert_eq!(config.command, "make test");
840 assert_eq!(config.parser, OutputParser::Lines);
841 }
842
843 #[test]
844 fn config_builder() {
845 let config = ScriptAdapterConfig::new("mytest", "Makefile", "make test")
846 .with_parser(OutputParser::Tap)
847 .with_args(vec!["--verbose".into()])
848 .with_working_dir("src")
849 .with_env("CI", "true");
850
851 assert_eq!(config.parser, OutputParser::Tap);
852 assert_eq!(config.args, vec!["--verbose"]);
853 assert_eq!(config.working_dir, Some("src".into()));
854 assert_eq!(config.env, vec![("CI".into(), "true".into())]);
855 }
856
857 #[test]
858 fn config_full_command() {
859 let config = ScriptAdapterConfig::new("test", "f", "make test")
860 .with_args(vec!["--verbose".into(), "--color".into()]);
861 assert_eq!(config.full_command(), "make test --verbose --color");
862 }
863
864 #[test]
865 fn config_effective_working_dir() {
866 let base = PathBuf::from("/project");
867
868 let config = ScriptAdapterConfig::new("test", "f", "cmd");
869 assert_eq!(
870 config.effective_working_dir(&base),
871 PathBuf::from("/project")
872 );
873
874 let config = config.with_working_dir("src");
875 assert_eq!(
876 config.effective_working_dir(&base),
877 PathBuf::from("/project/src")
878 );
879 }
880
881 #[test]
884 fn parse_tap_basic() {
885 let output = "1..3\nok 1 - first test\nok 2 - second test\nnot ok 3 - third test\n";
886 let result = parse_tap_output(output, 1);
887 assert_eq!(result.total_tests(), 3);
888 assert_eq!(result.total_passed(), 2);
889 assert_eq!(result.total_failed(), 1);
890 }
891
892 #[test]
893 fn parse_tap_skip() {
894 let output = "1..2\nok 1 - test one\nok 2 - test two # SKIP not ready\n";
895 let result = parse_tap_output(output, 0);
896 assert_eq!(result.total_tests(), 2);
897 assert_eq!(result.total_passed(), 1);
898 assert_eq!(result.total_skipped(), 1);
899 }
900
901 #[test]
902 fn parse_tap_todo() {
903 let output = "1..1\nnot ok 1 - todo test # TODO implement later\n";
904 let result = parse_tap_output(output, 0);
905 assert_eq!(result.total_tests(), 1);
906 assert_eq!(result.total_skipped(), 1);
907 }
908
909 #[test]
910 fn parse_tap_empty() {
911 let result = parse_tap_output("", 0);
912 assert_eq!(result.total_tests(), 1); }
914
915 #[test]
916 fn parse_tap_no_plan() {
917 let output = "ok 1 - works\nnot ok 2 - broken\n";
918 let result = parse_tap_output(output, 1);
919 assert_eq!(result.total_tests(), 2);
920 }
921
922 #[test]
925 fn parse_lines_basic() {
926 let output = "ok test_one\nfail test_two\nskip test_three\n";
927 let result = parse_lines_output(output, 1);
928 assert_eq!(result.total_tests(), 3);
929 assert_eq!(result.total_passed(), 1);
930 assert_eq!(result.total_failed(), 1);
931 assert_eq!(result.total_skipped(), 1);
932 }
933
934 #[test]
935 fn parse_lines_uppercase() {
936 let output = "PASS test_one\nFAIL test_two\nSKIP test_three\n";
937 let result = parse_lines_output(output, 1);
938 assert_eq!(result.total_tests(), 3);
939 }
940
941 #[test]
942 fn parse_lines_unicode() {
943 let output = "✓ test_one\n✗ test_two\n";
944 let result = parse_lines_output(output, 1);
945 assert_eq!(result.total_tests(), 2);
946 assert_eq!(result.total_passed(), 1);
947 assert_eq!(result.total_failed(), 1);
948 }
949
950 #[test]
951 fn parse_lines_empty() {
952 let result = parse_lines_output("", 0);
953 assert_eq!(result.total_tests(), 1); }
955
956 #[test]
957 fn parse_lines_ignores_non_matching() {
958 let output = "running tests...\nok test_one\nsome other output\nfail test_two\ndone";
959 let result = parse_lines_output(output, 1);
960 assert_eq!(result.total_tests(), 2);
961 }
962
963 #[test]
966 fn parse_json_suites_format() {
967 let json = r#"{
968 "suites": [
969 {
970 "name": "math",
971 "tests": [
972 {"name": "test_add", "status": "passed", "duration": 10},
973 {"name": "test_sub", "status": "failed", "duration": 5}
974 ]
975 }
976 ]
977 }"#;
978 let result = parse_json_output(json, "", 1);
979 assert_eq!(result.total_tests(), 2);
980 assert_eq!(result.total_passed(), 1);
981 assert_eq!(result.total_failed(), 1);
982 }
983
984 #[test]
985 fn parse_json_flat_tests() {
986 let json = r#"{"tests": [
987 {"name": "test1", "status": "pass"},
988 {"name": "test2", "status": "skip"}
989 ]}"#;
990 let result = parse_json_output(json, "", 0);
991 assert_eq!(result.total_tests(), 2);
992 assert_eq!(result.total_passed(), 1);
993 assert_eq!(result.total_skipped(), 1);
994 }
995
996 #[test]
997 fn parse_json_array_format() {
998 let json = r#"[
999 {"name": "test1", "status": "ok"},
1000 {"name": "test2", "status": "error"}
1001 ]"#;
1002 let result = parse_json_output(json, "", 1);
1003 assert_eq!(result.total_tests(), 2);
1004 }
1005
1006 #[test]
1007 fn parse_json_with_errors() {
1008 let json = r#"{"tests": [
1009 {"name": "test1", "status": "failed", "error": {"message": "expected 1 got 2", "location": "test.rs:10"}}
1010 ]}"#;
1011 let result = parse_json_output(json, "", 1);
1012 assert_eq!(result.total_failed(), 1);
1013 let test = &result.suites[0].tests[0];
1014 assert!(test.error.is_some());
1015 assert_eq!(test.error.as_ref().unwrap().message, "expected 1 got 2");
1016 }
1017
1018 #[test]
1019 fn parse_json_invalid() {
1020 let result = parse_json_output("not json {{{", "", 1);
1021 assert_eq!(result.total_tests(), 1); }
1023
1024 #[test]
1027 fn parse_junit_basic() {
1028 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1029<testsuite name="math" tests="2" failures="1">
1030 <testcase name="test_add" classname="Math" time="0.01"/>
1031 <testcase name="test_div" classname="Math" time="0.02">
1032 <failure message="division by zero"/>
1033 </testcase>
1034</testsuite>"#;
1035 let result = parse_junit_output(xml, 1);
1036 assert_eq!(result.total_tests(), 2);
1037 assert_eq!(result.total_passed(), 1);
1038 assert_eq!(result.total_failed(), 1);
1039 }
1040
1041 #[test]
1042 fn parse_junit_skipped() {
1043 let xml = r#"<testsuite name="t" tests="1">
1044 <testcase name="test_skip" time="0.0">
1045 <skipped/>
1046 </testcase>
1047</testsuite>"#;
1048 let result = parse_junit_output(xml, 0);
1049 assert_eq!(result.total_skipped(), 1);
1050 }
1051
1052 #[test]
1053 fn parse_junit_empty() {
1054 let result = parse_junit_output("", 0);
1055 assert_eq!(result.total_tests(), 1); }
1057
1058 #[test]
1061 fn parse_regex_basic() {
1062 let config = RegexParserConfig {
1063 pass_pattern: "PASS: (.*)".to_string(),
1064 fail_pattern: "FAIL: (.*)".to_string(),
1065 skip_pattern: None,
1066 name_group: 1,
1067 duration_group: None,
1068 };
1069 let output = "PASS: test_one\nFAIL: test_two\nsome output\n";
1070 let result = parse_regex_output(output, &config, 1);
1071 assert_eq!(result.total_tests(), 2);
1072 assert_eq!(result.total_passed(), 1);
1073 assert_eq!(result.total_failed(), 1);
1074 }
1075
1076 #[test]
1077 fn parse_regex_with_skip() {
1078 let config = RegexParserConfig {
1079 pass_pattern: "[OK] (.*)".to_string(),
1080 fail_pattern: "[ERR] (.*)".to_string(),
1081 skip_pattern: Some("[SKIP] (.*)".to_string()),
1082 name_group: 1,
1083 duration_group: None,
1084 };
1085 let output = "[OK] test_one\n[SKIP] test_two\n";
1086 let result = parse_regex_output(output, &config, 0);
1087 assert_eq!(result.total_tests(), 2);
1088 }
1089
1090 #[test]
1093 fn simple_match_literal() {
1094 let result = simple_pattern_match("hello world", "hello world");
1095 assert!(result.is_some());
1096 assert!(result.unwrap().is_empty());
1097 }
1098
1099 #[test]
1100 fn simple_match_capture() {
1101 let result = simple_pattern_match("PASS: (.*)", "PASS: test_one");
1102 assert!(result.is_some());
1103 assert_eq!(result.unwrap(), vec!["test_one"]);
1104 }
1105
1106 #[test]
1107 fn simple_match_multiple_captures() {
1108 let result = simple_pattern_match("(.*)=(.*)", "key=value");
1109 assert!(result.is_some());
1110 assert_eq!(result.unwrap(), vec!["key", "value"]);
1111 }
1112
1113 #[test]
1114 fn simple_match_wildcard() {
1115 let result = simple_pattern_match("hello .*!", "hello world!");
1116 assert!(result.is_some());
1117 }
1118
1119 #[test]
1120 fn simple_match_no_match() {
1121 let result = simple_pattern_match("hello", "world");
1122 assert!(result.is_none());
1123 }
1124
1125 #[test]
1126 fn simple_match_capture_with_context() {
1127 let result = simple_pattern_match("test (.*) in (.*)ms", "test add in 50ms");
1128 assert!(result.is_some());
1129 let caps = result.unwrap();
1130 assert_eq!(caps, vec!["add", "50"]);
1131 }
1132
1133 #[test]
1136 fn tap_description_basic() {
1137 let (name, skip) = parse_tap_description("1 - my test");
1138 assert_eq!(name, "my test");
1139 assert!(!skip);
1140 }
1141
1142 #[test]
1143 fn tap_description_skip() {
1144 let (name, skip) = parse_tap_description("1 - my test # SKIP not implemented");
1145 assert_eq!(name, "my test");
1146 assert!(skip);
1147 }
1148
1149 #[test]
1150 fn tap_description_no_dash() {
1151 let (name, skip) = parse_tap_description("1 test name");
1152 assert_eq!(name, "test name");
1153 assert!(!skip);
1154 }
1155
1156 #[test]
1159 fn status_line_pass() {
1160 let tc = parse_status_line("ok test_one").unwrap();
1161 assert_eq!(tc.name, "test_one");
1162 assert_eq!(tc.status, TestStatus::Passed);
1163 }
1164
1165 #[test]
1166 fn status_line_fail() {
1167 let tc = parse_status_line("fail test_two").unwrap();
1168 assert_eq!(tc.name, "test_two");
1169 assert_eq!(tc.status, TestStatus::Failed);
1170 }
1171
1172 #[test]
1173 fn status_line_skip() {
1174 let tc = parse_status_line("skip test_three").unwrap();
1175 assert_eq!(tc.name, "test_three");
1176 assert_eq!(tc.status, TestStatus::Skipped);
1177 }
1178
1179 #[test]
1180 fn status_line_no_match() {
1181 assert!(parse_status_line("some random text").is_none());
1182 }
1183
1184 #[test]
1185 fn status_line_empty_name() {
1186 assert!(parse_status_line("ok ").is_none());
1187 }
1188
1189 #[test]
1192 fn xml_attr_basic() {
1193 assert_eq!(
1194 extract_xml_attr(r#"<test name="hello" time="1.5">"#, "name"),
1195 Some("hello".into())
1196 );
1197 }
1198
1199 #[test]
1200 fn xml_attr_missing() {
1201 assert_eq!(extract_xml_attr("<test>", "name"), None);
1202 }
1203
1204 #[test]
1207 fn fallback_pass() {
1208 let result = fallback_result("all good", 0, "test");
1209 assert_eq!(result.total_passed(), 1);
1210 assert_eq!(result.raw_exit_code, 0);
1211 }
1212
1213 #[test]
1214 fn fallback_fail() {
1215 let result = fallback_result("something failed", 1, "test");
1216 assert_eq!(result.total_failed(), 1);
1217 assert!(result.suites[0].tests[0].error.is_some());
1218 }
1219
1220 #[test]
1223 fn script_output_delegates_to_tap() {
1224 let output = "1..2\nok 1 - a\nnot ok 2 - b\n";
1225 let result = parse_script_output(&OutputParser::Tap, output, "", 1);
1226 assert_eq!(result.total_tests(), 2);
1227 }
1228
1229 #[test]
1230 fn script_output_delegates_to_lines() {
1231 let output = "PASS test1\nFAIL test2\n";
1232 let result = parse_script_output(&OutputParser::Lines, output, "", 1);
1233 assert_eq!(result.total_tests(), 2);
1234 }
1235
1236 #[test]
1237 fn script_output_delegates_to_json() {
1238 let output = r#"[{"name": "t1", "status": "passed"}]"#;
1239 let result = parse_script_output(&OutputParser::Json, output, "", 0);
1240 assert_eq!(result.total_tests(), 1);
1241 assert_eq!(result.total_passed(), 1);
1242 }
1243
1244 #[test]
1249 fn config_detect_nonexistent_dir() {
1250 let config = ScriptAdapterConfig::new("test", "Makefile", "make");
1251 assert!(!config.detect(&PathBuf::from("/nonexistent/path/xyz")));
1252 }
1253
1254 #[test]
1255 fn config_detect_with_pattern_nonexistent() {
1256 let mut config = ScriptAdapterConfig::new("test", "nonexistent.xyz", "cmd");
1257 config.detect_pattern = Some("src/*.test".into());
1258 assert!(!config.detect(&PathBuf::from("/nonexistent/path")));
1259 }
1260
1261 #[test]
1262 fn config_empty_args() {
1263 let config = ScriptAdapterConfig::new("test", "f", "cmd");
1264 assert_eq!(config.full_command(), "cmd");
1265 }
1266
1267 #[test]
1268 fn config_multiple_env_vars() {
1269 let config = ScriptAdapterConfig::new("test", "f", "cmd")
1270 .with_env("A", "1")
1271 .with_env("B", "2")
1272 .with_env("C", "3");
1273 assert_eq!(config.env.len(), 3);
1274 }
1275
1276 #[test]
1277 fn config_chained_builders() {
1278 let config = ScriptAdapterConfig::new("test", "f", "cmd")
1279 .with_parser(OutputParser::Json)
1280 .with_args(vec!["--a".into()])
1281 .with_working_dir("build")
1282 .with_env("X", "Y")
1283 .with_parser(OutputParser::Tap); assert_eq!(config.parser, OutputParser::Tap);
1285 assert_eq!(config.working_dir, Some("build".into()));
1286 }
1287
1288 #[test]
1291 fn parse_json_empty_object() {
1292 let result = parse_json_output("{}", "", 0);
1293 assert_eq!(result.total_tests(), 1); assert_eq!(result.total_passed(), 1);
1295 }
1296
1297 #[test]
1298 fn parse_json_empty_suites_array() {
1299 let result = parse_json_output(r#"{"suites": []}"#, "", 0);
1300 assert_eq!(result.total_tests(), 1); }
1302
1303 #[test]
1304 fn parse_json_empty_tests_array() {
1305 let result = parse_json_output(r#"{"tests": []}"#, "", 0);
1306 assert_eq!(result.total_tests(), 1); }
1308
1309 #[test]
1310 fn parse_json_empty_flat_array() {
1311 let result = parse_json_output("[]", "", 0);
1312 assert_eq!(result.total_tests(), 1); }
1314
1315 #[test]
1316 fn parse_json_unknown_status() {
1317 let json = r#"[{"name": "t1", "status": "wonky"}]"#;
1318 let result = parse_json_output(json, "", 0);
1319 assert_eq!(result.total_tests(), 1);
1321 }
1322
1323 #[test]
1324 fn parse_json_missing_name() {
1325 let json = r#"[{"status": "passed"}]"#;
1326 let result = parse_json_output(json, "", 0);
1327 assert_eq!(result.total_tests(), 1); }
1329
1330 #[test]
1331 fn parse_json_missing_status() {
1332 let json = r#"[{"name": "t1"}]"#;
1333 let result = parse_json_output(json, "", 0);
1334 assert_eq!(result.total_tests(), 1); }
1336
1337 #[test]
1338 fn parse_json_all_status_synonyms() {
1339 let json = r#"[
1340 {"name": "t1", "status": "pass"},
1341 {"name": "t2", "status": "ok"},
1342 {"name": "t3", "status": "success"},
1343 {"name": "t4", "status": "fail"},
1344 {"name": "t5", "status": "error"},
1345 {"name": "t6", "status": "failure"},
1346 {"name": "t7", "status": "skip"},
1347 {"name": "t8", "status": "pending"},
1348 {"name": "t9", "status": "ignored"}
1349 ]"#;
1350 let result = parse_json_output(json, "", 1);
1351 assert_eq!(result.total_tests(), 9);
1352 assert_eq!(result.total_passed(), 3);
1353 assert_eq!(result.total_failed(), 3);
1354 assert_eq!(result.total_skipped(), 3);
1355 }
1356
1357 #[test]
1358 fn parse_json_with_duration_ms() {
1359 let json = r#"[{"name": "slow", "status": "passed", "duration": 1500}]"#;
1360 let result = parse_json_output(json, "", 0);
1361 let test = &result.suites[0].tests[0];
1362 assert!(test.duration >= Duration::from_millis(1400)); }
1364
1365 #[test]
1366 fn parse_json_error_as_string() {
1367 let json = r#"[{"name": "t1", "status": "failed", "error": "boom"}]"#;
1368 let result = parse_json_output(json, "", 1);
1369 let test = &result.suites[0].tests[0];
1370 assert!(test.error.is_some());
1371 assert_eq!(test.error.as_ref().unwrap().message, "boom");
1372 }
1373
1374 #[test]
1375 fn parse_json_error_as_object() {
1376 let json = r#"[{"name": "t1", "status": "failed", "error": {"message": "bad", "location": "foo.rs:5"}}]"#;
1377 let result = parse_json_output(json, "", 1);
1378 let test = &result.suites[0].tests[0];
1379 let err = test.error.as_ref().unwrap();
1380 assert_eq!(err.message, "bad");
1381 assert_eq!(err.location.as_deref(), Some("foo.rs:5"));
1382 }
1383
1384 #[test]
1385 fn parse_json_nested_suites_with_names() {
1386 let json = r#"{"suites": [
1387 {"name": "s1", "tests": [{"name": "t1", "status": "passed"}]},
1388 {"name": "s2", "tests": [{"name": "t2", "status": "failed"}]}
1389 ]}"#;
1390 let result = parse_json_output(json, "", 1);
1391 assert_eq!(result.suites.len(), 2);
1392 assert_eq!(result.suites[0].name, "s1");
1393 assert_eq!(result.suites[1].name, "s2");
1394 }
1395
1396 #[test]
1397 fn parse_json_tests_with_custom_suite_name() {
1398 let json = r#"{"name": "my-suite", "tests": [{"name": "t1", "status": "passed"}]}"#;
1399 let result = parse_json_output(json, "", 0);
1400 assert_eq!(result.suites[0].name, "my-suite");
1401 }
1402
1403 #[test]
1404 fn parse_json_stderr_ignored() {
1405 let json = r#"[{"name": "t", "status": "passed"}]"#;
1406 let result = parse_json_output(json, "STDERR NOISE", 0);
1407 assert_eq!(result.total_passed(), 1);
1408 }
1409
1410 #[test]
1413 fn parse_tap_only_plan_no_tests() {
1414 let result = parse_tap_output("1..0\n", 0);
1415 assert_eq!(result.total_tests(), 1); }
1417
1418 #[test]
1419 fn parse_tap_plan_at_end() {
1420 let output = "ok 1 - first\nok 2 - second\n1..2\n";
1421 let result = parse_tap_output(output, 0);
1422 assert_eq!(result.total_tests(), 2);
1423 assert_eq!(result.total_passed(), 2);
1424 }
1425
1426 #[test]
1427 fn parse_tap_diagnostic_lines_ignored() {
1428 let output = "# running tests\nok 1 - test\n# end\n";
1429 let result = parse_tap_output(output, 0);
1430 assert_eq!(result.total_tests(), 1);
1431 }
1432
1433 #[test]
1434 fn parse_tap_mixed_pass_fail_skip() {
1435 let output =
1436 "1..4\nok 1 - a\nnot ok 2 - b\nok 3 - c # SKIP reason\nnot ok 4 - d # TODO later\n";
1437 let result = parse_tap_output(output, 1);
1438 assert_eq!(result.total_passed(), 1);
1439 assert_eq!(result.total_failed(), 1);
1440 assert_eq!(result.total_skipped(), 2); }
1442
1443 #[test]
1444 fn parse_tap_lowercase_skip() {
1445 let output = "ok 1 - t # skip not ready\n";
1446 let result = parse_tap_output(output, 0);
1447 assert_eq!(result.total_skipped(), 1);
1448 }
1449
1450 #[test]
1451 fn parse_tap_no_description() {
1452 let output = "ok 1\nnot ok 2\n";
1453 let result = parse_tap_output(output, 1);
1454 assert_eq!(result.total_tests(), 2);
1455 }
1456
1457 #[test]
1458 fn parse_tap_large_test_numbers() {
1459 let output = "ok 999 - big number test\n";
1460 let result = parse_tap_output(output, 0);
1461 assert_eq!(result.total_tests(), 1);
1462 }
1463
1464 #[test]
1465 fn parse_tap_failed_test_has_error() {
1466 let output = "not ok 1 - broken\n";
1467 let result = parse_tap_output(output, 1);
1468 let test = &result.suites[0].tests[0];
1469 assert_eq!(test.status, TestStatus::Failed);
1470 assert!(test.error.is_some());
1471 assert_eq!(test.error.as_ref().unwrap().message, "Test failed");
1472 }
1473
1474 #[test]
1477 fn parse_lines_blank_lines_ignored() {
1478 let output = "\n\nok test1\n\n\nfail test2\n\n";
1479 let result = parse_lines_output(output, 1);
1480 assert_eq!(result.total_tests(), 2);
1481 }
1482
1483 #[test]
1484 fn parse_lines_colon_format() {
1485 let output = "ok: test1\nfail: test2\nskip: test3\n";
1486 let result = parse_lines_output(output, 1);
1487 assert_eq!(result.total_tests(), 3);
1488 }
1489
1490 #[test]
1491 fn parse_lines_all_pass_variants() {
1492 let output = "ok t1\npass t2\npassed t3\nPASS t4\nPASSED t5\nOK t6\n✓ t7\n✔ t8\n";
1493 let result = parse_lines_output(output, 0);
1494 assert_eq!(result.total_passed(), 8);
1495 }
1496
1497 #[test]
1498 fn parse_lines_all_fail_variants() {
1499 let output = "fail t1\nfailed t2\nerror t3\nFAIL t4\nFAILED t5\nERROR t6\n✗ t7\n✘ t8\n";
1500 let result = parse_lines_output(output, 1);
1501 assert_eq!(result.total_failed(), 8);
1502 }
1503
1504 #[test]
1505 fn parse_lines_all_skip_variants() {
1506 let output = "skip t1\nskipped t2\npending t3\nSKIP t4\nSKIPPED t5\nPENDING t6\n";
1507 let result = parse_lines_output(output, 0);
1508 assert_eq!(result.total_skipped(), 6);
1509 }
1510
1511 #[test]
1512 fn parse_lines_failed_has_error() {
1513 let output = "fail broken_test\n";
1514 let result = parse_lines_output(output, 1);
1515 let test = &result.suites[0].tests[0];
1516 assert!(test.error.is_some());
1517 }
1518
1519 #[test]
1520 fn parse_lines_passed_has_no_error() {
1521 let output = "ok good_test\n";
1522 let result = parse_lines_output(output, 0);
1523 let test = &result.suites[0].tests[0];
1524 assert!(test.error.is_none());
1525 }
1526
1527 #[test]
1528 fn parse_lines_only_noise() {
1529 let output = "compiling...\nrunning tests...\ndone\n";
1530 let result = parse_lines_output(output, 0);
1531 assert_eq!(result.total_tests(), 1); }
1533
1534 #[test]
1537 fn parse_junit_multiple_suites() {
1538 let xml = r#"<testsuites>
1539 <testsuite name="s1" tests="1">
1540 <testcase name="t1" time="0.01"/>
1541 </testsuite>
1542 <testsuite name="s2" tests="1">
1543 <testcase name="t2" time="0.02"/>
1544 </testsuite>
1545</testsuites>"#;
1546 let result = parse_junit_output(xml, 0);
1547 assert!(result.total_tests() >= 2);
1549 }
1550
1551 #[test]
1552 fn parse_junit_self_closing_testcase() {
1553 let xml = r#"<testsuite name="t" tests="1">
1554 <testcase name="fast" classname="Test" time="0.001"/>
1555</testsuite>"#;
1556 let result = parse_junit_output(xml, 0);
1557 assert_eq!(result.total_passed(), 1);
1558 }
1559
1560 #[test]
1561 fn parse_junit_error_element() {
1562 let xml = r#"<testsuite name="t" tests="1">
1563 <testcase name="crasher" time="0.01">
1564 <error message="segfault"/>
1565 </testcase>
1566</testsuite>"#;
1567 let result = parse_junit_output(xml, 1);
1568 assert_eq!(result.total_failed(), 1);
1569 let test = &result.suites[0].tests[0];
1570 assert_eq!(test.error.as_ref().unwrap().message, "segfault");
1571 }
1572
1573 #[test]
1574 fn parse_junit_no_time_attribute() {
1575 let xml = r#"<testsuite name="t" tests="1">
1576 <testcase name="notime"/>
1577</testsuite>"#;
1578 let result = parse_junit_output(xml, 0);
1579 assert_eq!(result.total_tests(), 1);
1580 assert_eq!(result.suites[0].tests[0].duration, Duration::ZERO);
1581 }
1582
1583 #[test]
1584 fn parse_junit_invalid_xml() {
1585 let result = parse_junit_output("not xml at all <<<>>>", 1);
1586 assert_eq!(result.total_tests(), 1); }
1588
1589 #[test]
1590 fn parse_junit_testcases_without_testsuite() {
1591 let xml = r#"<testcase name="orphan1" time="0.1"/>
1592<testcase name="orphan2" time="0.2"/>"#;
1593 let result = parse_junit_output(xml, 0);
1594 assert_eq!(result.total_tests(), 2);
1595 }
1596
1597 #[test]
1600 fn parse_regex_no_matches() {
1601 let config = RegexParserConfig {
1602 pass_pattern: "PASS: (.*)".into(),
1603 fail_pattern: "FAIL: (.*)".into(),
1604 skip_pattern: None,
1605 name_group: 1,
1606 duration_group: None,
1607 };
1608 let result = parse_regex_output("no matching lines here", &config, 0);
1609 assert_eq!(result.total_tests(), 1); }
1611
1612 #[test]
1613 fn parse_regex_with_duration() {
1614 let config = RegexParserConfig {
1615 pass_pattern: "PASS (.*) (.*)ms".into(),
1616 fail_pattern: "FAIL (.*) (.*)ms".into(),
1617 skip_pattern: None,
1618 name_group: 1,
1619 duration_group: Some(2),
1620 };
1621 let output = "PASS test_one 150ms\nFAIL test_two 50ms\n";
1622 let result = parse_regex_output(output, &config, 1);
1623 assert_eq!(result.total_tests(), 2);
1624 let pass_test = &result.suites[0].tests[0];
1626 assert!(pass_test.duration > Duration::ZERO);
1627 }
1628
1629 #[test]
1630 fn parse_regex_empty_capture_filtered() {
1631 let config = RegexParserConfig {
1632 pass_pattern: "PASS:(.*)".into(),
1633 fail_pattern: "FAIL:(.*)".into(),
1634 skip_pattern: None,
1635 name_group: 1,
1636 duration_group: None,
1637 };
1638 let result = parse_regex_output("PASS:", &config, 0);
1640 assert_eq!(result.total_tests(), 1); }
1642
1643 #[test]
1646 fn simple_match_empty_pattern_empty_input() {
1647 let result = simple_pattern_match("", "");
1648 assert!(result.is_some());
1649 assert!(result.unwrap().is_empty());
1650 }
1651
1652 #[test]
1653 fn simple_match_empty_pattern_nonempty_input() {
1654 let result = simple_pattern_match("", "hello");
1655 assert!(result.is_none());
1656 }
1657
1658 #[test]
1659 fn simple_match_nonempty_pattern_empty_input() {
1660 let result = simple_pattern_match("hello", "");
1661 assert!(result.is_none());
1662 }
1663
1664 #[test]
1665 fn simple_match_capture_at_start() {
1666 let result = simple_pattern_match("(.*) done", "testing done");
1667 assert!(result.is_some());
1668 assert_eq!(result.unwrap(), vec!["testing"]);
1669 }
1670
1671 #[test]
1672 fn simple_match_capture_in_middle() {
1673 let result = simple_pattern_match("start (.*) end", "start middle end");
1674 assert!(result.is_some());
1675 assert_eq!(result.unwrap(), vec!["middle"]);
1676 }
1677
1678 #[test]
1679 fn simple_match_adjacent_groups() {
1680 let result = simple_pattern_match("(.*):(.*)!", "key:value!");
1682 assert!(result.is_some());
1683 let caps = result.unwrap();
1684 assert_eq!(caps[0], "key");
1685 assert_eq!(caps[1], "value");
1686 }
1687
1688 #[test]
1689 fn simple_match_wildcard_at_end() {
1690 let result = simple_pattern_match("hello .*", "hello world more stuff");
1691 assert!(result.is_some());
1692 }
1693
1694 #[test]
1695 fn simple_match_partial_mismatch() {
1696 let result = simple_pattern_match("abc", "abx");
1697 assert!(result.is_none());
1698 }
1699
1700 #[test]
1701 fn simple_match_pattern_longer_than_input() {
1702 let result = simple_pattern_match("hello world", "hello");
1703 assert!(result.is_none());
1704 }
1705
1706 #[test]
1709 fn fallback_empty_stdout() {
1710 let result = fallback_result("", 1, "test");
1711 assert_eq!(result.total_failed(), 1);
1712 assert_eq!(
1714 result.suites[0].tests[0].error.as_ref().unwrap().message,
1715 "Test failed"
1716 );
1717 }
1718
1719 #[test]
1720 fn fallback_multiline_takes_first() {
1721 let result = fallback_result("first line\nsecond line", 1, "test");
1722 assert_eq!(
1723 result.suites[0].tests[0].error.as_ref().unwrap().message,
1724 "first line"
1725 );
1726 }
1727
1728 #[test]
1729 fn fallback_parser_name_in_suite() {
1730 let result = fallback_result("", 0, "myparser");
1731 assert_eq!(result.suites[0].name, "myparser-output");
1732 assert!(result.suites[0].tests[0].name.contains("myparser"));
1733 }
1734
1735 #[test]
1736 fn fallback_exit_zero_is_pass() {
1737 let result = fallback_result("anything", 0, "x");
1738 assert_eq!(result.total_passed(), 1);
1739 assert!(result.suites[0].tests[0].error.is_none());
1740 }
1741
1742 #[test]
1743 fn fallback_exit_nonzero_is_fail() {
1744 let result = fallback_result("anything", 42, "x");
1745 assert_eq!(result.total_failed(), 1);
1746 assert!(result.suites[0].tests[0].error.is_some());
1747 }
1748
1749 #[test]
1752 fn script_output_delegates_to_junit() {
1753 let xml = r#"<testsuite name="t" tests="1">
1754 <testcase name="t1" time="0.01"/>
1755</testsuite>"#;
1756 let result = parse_script_output(&OutputParser::Junit, xml, "", 0);
1757 assert_eq!(result.total_passed(), 1);
1758 }
1759
1760 #[test]
1761 fn script_output_delegates_to_regex() {
1762 let config = RegexParserConfig {
1763 pass_pattern: "PASS (.*)".into(),
1764 fail_pattern: "FAIL (.*)".into(),
1765 skip_pattern: None,
1766 name_group: 1,
1767 duration_group: None,
1768 };
1769 let result = parse_script_output(
1770 &OutputParser::Regex(config),
1771 "PASS test1\nFAIL test2\n",
1772 "",
1773 1,
1774 );
1775 assert_eq!(result.total_tests(), 2);
1776 }
1777
1778 #[test]
1781 fn xml_attr_empty_value() {
1782 assert_eq!(
1783 extract_xml_attr(r#"<test name="">"#, "name"),
1784 Some("".into())
1785 );
1786 }
1787
1788 #[test]
1789 fn xml_attr_with_spaces() {
1790 assert_eq!(
1791 extract_xml_attr(r#"<test name="hello world">"#, "name"),
1792 Some("hello world".into())
1793 );
1794 }
1795
1796 #[test]
1797 fn xml_attr_multiple_attrs() {
1798 let tag = r#"<testcase name="add" classname="Math" time="1.5"/>"#;
1799 assert_eq!(extract_xml_attr(tag, "name"), Some("add".into()));
1800 assert_eq!(extract_xml_attr(tag, "classname"), Some("Math".into()));
1801 assert_eq!(extract_xml_attr(tag, "time"), Some("1.5".into()));
1802 }
1803
1804 #[test]
1807 fn output_parser_equality() {
1808 assert_eq!(OutputParser::Json, OutputParser::Json);
1809 assert_eq!(OutputParser::Tap, OutputParser::Tap);
1810 assert_ne!(OutputParser::Json, OutputParser::Tap);
1811 }
1812
1813 #[test]
1814 fn output_parser_debug() {
1815 let dbg = format!("{:?}", OutputParser::Lines);
1816 assert_eq!(dbg, "Lines");
1817 }
1818
1819 #[test]
1820 fn regex_parser_config_clone() {
1821 let config = RegexParserConfig {
1822 pass_pattern: "P (.*)".into(),
1823 fail_pattern: "F (.*)".into(),
1824 skip_pattern: Some("S (.*)".into()),
1825 name_group: 1,
1826 duration_group: Some(2),
1827 };
1828 let cloned = config.clone();
1829 assert_eq!(cloned, config);
1830 }
1831}