1use crate::ast::{Node, NodeKind};
2use serde_json::{Value, json};
3use std::path::Path;
4use std::process::{Command, Stdio};
5
6#[derive(Debug, Clone)]
8pub struct TestItem {
9 pub id: String,
11 pub label: String,
13 pub uri: String,
15 pub range: TestRange,
17 pub kind: TestKind,
19 pub children: Vec<TestItem>,
21}
22
23#[derive(Debug, Clone)]
25pub struct TestRange {
26 pub start_line: u32,
28 pub start_character: u32,
30 pub end_line: u32,
32 pub end_character: u32,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub enum TestKind {
39 File,
41 Suite,
43 Test,
45}
46
47#[derive(Debug, Clone)]
49pub struct TestResult {
50 pub test_id: String,
52 pub status: TestStatus,
54 pub message: Option<String>,
56 pub duration: Option<u64>,
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub enum TestStatus {
63 Passed,
65 Failed,
67 Skipped,
69 Errored,
71}
72
73impl TestStatus {
74 pub fn as_str(&self) -> &'static str {
76 match self {
77 TestStatus::Passed => "passed",
78 TestStatus::Failed => "failed",
79 TestStatus::Skipped => "skipped",
80 TestStatus::Errored => "errored",
81 }
82 }
83}
84
85pub struct TestRunner {
87 source: String,
89 uri: String,
91}
92
93impl TestRunner {
94 pub fn new(source: String, uri: String) -> Self {
96 Self { source, uri }
97 }
98
99 pub fn discover_tests(&self, ast: &Node) -> Vec<TestItem> {
101 let mut tests = Vec::new();
102
103 let mut test_functions = Vec::new();
105 self.find_test_functions_only(ast, &mut test_functions);
106
107 if self.is_test_file(&self.uri) {
109 let file_item = TestItem {
111 id: self.uri.clone(),
112 label: Path::new(&self.uri)
113 .file_name()
114 .and_then(|s| s.to_str())
115 .unwrap_or("test")
116 .to_string(),
117 uri: self.uri.clone(),
118 range: self.get_file_range(),
119 kind: TestKind::File,
120 children: test_functions,
121 };
122
123 tests.push(file_item);
124 } else {
125 tests.extend(test_functions);
127 }
128
129 tests
130 }
131
132 fn is_test_file(&self, uri: &str) -> bool {
134 let path = Path::new(uri);
135 let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
136
137 file_name.ends_with(".t")
139 || file_name.ends_with("_test.pl")
140 || file_name.ends_with("Test.pl")
141 || file_name.starts_with("test_")
142 || path.components().any(|c| c.as_os_str() == "t" || c.as_os_str() == "tests")
143 }
144
145 #[allow(dead_code)]
147 fn find_test_functions(&self, node: &Node) -> Vec<TestItem> {
148 let mut tests = Vec::new();
149 self.visit_node_for_tests(node, &mut tests);
150 tests
151 }
152
153 fn find_test_functions_only(&self, node: &Node, tests: &mut Vec<TestItem>) {
155 match &node.kind {
156 NodeKind::Program { statements } => {
157 for stmt in statements {
158 self.find_test_functions_only(stmt, tests);
159 }
160 }
161
162 NodeKind::Block { statements } => {
163 for stmt in statements {
164 self.find_test_functions_only(stmt, tests);
165 }
166 }
167
168 NodeKind::Subroutine { name, .. } => {
169 if let Some(func_name) = name {
170 if self.is_test_function(func_name) {
171 let test_item = TestItem {
172 id: format!("{}::{}", self.uri, func_name),
173 label: func_name.clone(),
174 uri: self.uri.clone(),
175 range: self.node_to_range(node),
176 kind: TestKind::Test,
177 children: vec![],
178 };
179 tests.push(test_item);
180 }
181 }
182 }
183
184 _ => {
185 self.visit_children_for_test_functions(node, tests);
187 }
188 }
189 }
190
191 fn visit_children_for_test_functions(&self, node: &Node, tests: &mut Vec<TestItem>) {
193 match &node.kind {
194 NodeKind::If { then_branch, elsif_branches, else_branch, .. } => {
195 self.find_test_functions_only(then_branch, tests);
196 for (_, body) in elsif_branches {
197 self.find_test_functions_only(body, tests);
198 }
199 if let Some(else_b) = else_branch {
200 self.find_test_functions_only(else_b, tests);
201 }
202 }
203 NodeKind::While { body, .. } => {
204 self.find_test_functions_only(body, tests);
205 }
206 NodeKind::For { body, .. } => {
207 self.find_test_functions_only(body, tests);
208 }
209 NodeKind::Foreach { body, .. } => {
210 self.find_test_functions_only(body, tests);
211 }
212 _ => {}
213 }
214 }
215
216 #[allow(dead_code)]
218 fn visit_node_for_tests(&self, node: &Node, tests: &mut Vec<TestItem>) {
219 match &node.kind {
220 NodeKind::Program { statements } => {
221 for stmt in statements {
222 self.visit_node_for_tests(stmt, tests);
223 }
224 }
225
226 NodeKind::Block { statements } => {
227 for stmt in statements {
228 self.visit_node_for_tests(stmt, tests);
229 }
230 }
231
232 NodeKind::Subroutine { name, body, .. } => {
233 if let Some(func_name) = name {
234 if self.is_test_function(func_name) {
235 let test_item = TestItem {
236 id: format!("{}::{}", self.uri, func_name),
237 label: func_name.clone(),
238 uri: self.uri.clone(),
239 range: self.node_to_range(node),
240 kind: TestKind::Test,
241 children: vec![],
242 };
243 tests.push(test_item);
244 }
245 }
246
247 self.visit_node_for_tests(body, tests);
249 }
250
251 NodeKind::FunctionCall { name, args } => {
253 if self.is_test_assertion(name) {
254 let description = self.extract_test_description(args);
256 let label = description.unwrap_or_else(|| name.clone());
257
258 let test_item = TestItem {
259 id: format!("{}::{}::{}", self.uri, name, node.location.start),
260 label,
261 uri: self.uri.clone(),
262 range: self.node_to_range(node),
263 kind: TestKind::Test,
264 children: vec![],
265 };
266 tests.push(test_item);
267 }
268
269 for arg in args {
271 self.visit_node_for_tests(arg, tests);
272 }
273 }
274
275 _ => {
276 self.visit_children_for_tests(node, tests);
278 }
279 }
280 }
281
282 fn is_test_function(&self, name: &str) -> bool {
284 name.starts_with("test_")
285 || name.ends_with("_test")
286 || name.starts_with("Test")
287 || name.ends_with("Test")
288 || name == "test"
289 }
290
291 #[allow(dead_code)]
293 fn is_test_assertion(&self, name: &str) -> bool {
294 matches!(
296 name,
297 "ok" | "is"
298 | "isnt"
299 | "like"
300 | "unlike"
301 | "is_deeply"
302 | "cmp_ok"
303 | "can_ok"
304 | "isa_ok"
305 | "pass"
306 | "fail"
307 | "dies_ok"
308 | "lives_ok"
309 | "throws_ok"
310 | "lives_and"
311 )
312 }
313
314 #[allow(dead_code)]
316 fn extract_test_description(&self, args: &[Node]) -> Option<String> {
317 args.last().and_then(|arg| match &arg.kind {
319 NodeKind::String { value, .. } => Some(value.clone()),
320 _ => None,
321 })
322 }
323
324 #[allow(dead_code)]
326 fn visit_children_for_tests(&self, node: &Node, tests: &mut Vec<TestItem>) {
327 match &node.kind {
328 NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
329 self.visit_node_for_tests(condition, tests);
330 self.visit_node_for_tests(then_branch, tests);
331 for (cond, body) in elsif_branches {
332 self.visit_node_for_tests(cond, tests);
333 self.visit_node_for_tests(body, tests);
334 }
335 if let Some(else_b) = else_branch {
336 self.visit_node_for_tests(else_b, tests);
337 }
338 }
339 NodeKind::While { condition, body, .. } => {
340 self.visit_node_for_tests(condition, tests);
341 self.visit_node_for_tests(body, tests);
342 }
343 NodeKind::For { init, condition, update, body, .. } => {
344 if let Some(i) = init {
345 self.visit_node_for_tests(i, tests);
346 }
347 if let Some(c) = condition {
348 self.visit_node_for_tests(c, tests);
349 }
350 if let Some(u) = update {
351 self.visit_node_for_tests(u, tests);
352 }
353 self.visit_node_for_tests(body, tests);
354 }
355 NodeKind::Foreach { variable, list, body, continue_block } => {
356 self.visit_node_for_tests(variable, tests);
357 self.visit_node_for_tests(list, tests);
358 self.visit_node_for_tests(body, tests);
359 if let Some(cb) = continue_block {
360 self.visit_node_for_tests(cb, tests);
361 }
362 }
363 _ => {}
364 }
365 }
366
367 fn node_to_range(&self, node: &Node) -> TestRange {
369 let (start_line, start_char) = self.offset_to_position(node.location.start);
370 let (end_line, end_char) = self.offset_to_position(node.location.end);
371
372 TestRange { start_line, start_character: start_char, end_line, end_character: end_char }
373 }
374
375 fn get_file_range(&self) -> TestRange {
377 let lines: Vec<&str> = self.source.lines().collect();
378 let last_line = lines.len().saturating_sub(1) as u32;
379 let last_char = lines.last().map(|l| l.len() as u32).unwrap_or(0);
380
381 TestRange {
382 start_line: 0,
383 start_character: 0,
384 end_line: last_line,
385 end_character: last_char,
386 }
387 }
388
389 fn offset_to_position(&self, offset: usize) -> (u32, u32) {
391 let mut line = 0;
392 let mut col = 0;
393
394 for (i, ch) in self.source.chars().enumerate() {
395 if i >= offset {
396 break;
397 }
398 if ch == '\n' {
399 line += 1;
400 col = 0;
401 } else {
402 col += 1;
403 }
404 }
405
406 (line, col)
407 }
408
409 pub fn run_test(&self, test_id: &str) -> Vec<TestResult> {
411 let mut results = Vec::new();
412
413 let file_path = test_id.split("::").next().unwrap_or(test_id);
415 let file_path = file_path.strip_prefix("file://").unwrap_or(file_path);
416
417 if file_path.ends_with(".t") {
419 results.extend(self.run_test_file(file_path));
421 } else {
422 results.extend(self.run_perl_test(file_path));
424 }
425
426 results
427 }
428
429 fn run_test_file(&self, file_path: &str) -> Vec<TestResult> {
431 let start_time = std::time::Instant::now();
432
433 let safe_prove_path = if file_path.starts_with('-') {
438 format!("./{}", file_path)
439 } else {
440 file_path.to_string()
441 };
442
443 let output = Command::new("prove")
445 .arg("-v")
446 .arg(&safe_prove_path)
447 .stdout(Stdio::piped())
448 .stderr(Stdio::piped())
449 .output();
450
451 let output = match output {
452 Ok(out) => out,
453 Err(_) => {
454 match Command::new("perl")
457 .arg("--")
458 .arg(file_path)
459 .stdout(Stdio::piped())
460 .stderr(Stdio::piped())
461 .output()
462 {
463 Ok(out) => out,
464 Err(e) => {
465 return vec![TestResult {
466 test_id: file_path.to_string(),
467 status: TestStatus::Errored,
468 message: Some(format!("Failed to run test: {}", e)),
469 duration: Some(start_time.elapsed().as_millis() as u64),
470 }];
471 }
472 }
473 }
474 };
475
476 let duration = start_time.elapsed().as_millis() as u64;
477
478 self.parse_tap_output(
480 &String::from_utf8_lossy(&output.stdout),
481 &String::from_utf8_lossy(&output.stderr),
482 output.status.success(),
483 duration,
484 file_path,
485 )
486 }
487
488 fn run_perl_test(&self, file_path: &str) -> Vec<TestResult> {
490 let start_time = std::time::Instant::now();
491
492 let output = match Command::new("perl")
494 .arg("-Ilib")
495 .arg("--")
496 .arg(file_path)
497 .stdout(Stdio::piped())
498 .stderr(Stdio::piped())
499 .output()
500 {
501 Ok(out) => out,
502 Err(e) => {
503 return vec![TestResult {
504 test_id: file_path.to_string(),
505 status: TestStatus::Errored,
506 message: Some(format!("Failed to run test: {}", e)),
507 duration: Some(start_time.elapsed().as_millis() as u64),
508 }];
509 }
510 };
511
512 let duration = start_time.elapsed().as_millis() as u64;
513 let stdout = String::from_utf8_lossy(&output.stdout);
514 let stderr = String::from_utf8_lossy(&output.stderr);
515
516 vec![TestResult {
517 test_id: file_path.to_string(),
518 status: if output.status.success() { TestStatus::Passed } else { TestStatus::Failed },
519 message: if !stderr.is_empty() {
520 Some(stderr.to_string())
521 } else if !stdout.is_empty() {
522 Some(stdout.to_string())
523 } else {
524 None
525 },
526 duration: Some(duration),
527 }]
528 }
529
530 fn parse_tap_output(
532 &self,
533 stdout: &str,
534 stderr: &str,
535 success: bool,
536 duration: u64,
537 test_id: &str,
538 ) -> Vec<TestResult> {
539 let mut results = Vec::new();
540 let mut _test_count = 0;
541
542 for line in stdout.lines() {
544 if line.starts_with("ok ") {
545 _test_count += 1;
546 let test_name = line.splitn(3, ' ').nth(2).unwrap_or("test");
547 results.push(TestResult {
548 test_id: format!("{}::{}", test_id, test_name),
549 status: TestStatus::Passed,
550 message: None,
551 duration: None,
552 });
553 } else if line.starts_with("not ok ") {
554 _test_count += 1;
555 let test_name = line.splitn(3, ' ').nth(2).unwrap_or("test");
556 results.push(TestResult {
557 test_id: format!("{}::{}", test_id, test_name),
558 status: TestStatus::Failed,
559 message: Some(line.to_string()),
560 duration: None,
561 });
562 }
563 }
564
565 if results.is_empty() {
567 results.push(TestResult {
568 test_id: test_id.to_string(),
569 status: if success { TestStatus::Passed } else { TestStatus::Failed },
570 message: if !stderr.is_empty() { Some(stderr.to_string()) } else { None },
571 duration: Some(duration),
572 });
573 }
574
575 results
576 }
577}
578
579impl TestItem {
581 pub fn to_json(&self) -> Value {
583 json!({
584 "id": self.id,
585 "label": self.label,
586 "uri": self.uri,
587 "range": {
588 "start": {
589 "line": self.range.start_line,
590 "character": self.range.start_character
591 },
592 "end": {
593 "line": self.range.end_line,
594 "character": self.range.end_character
595 }
596 },
597 "canResolveChildren": !self.children.is_empty(),
598 "children": self.children.iter().map(|c| c.to_json()).collect::<Vec<_>>()
599 })
600 }
601}
602
603impl TestResult {
605 pub fn to_json(&self) -> Value {
607 let mut result = json!({
608 "testId": self.test_id,
609 "state": match self.status {
610 TestStatus::Passed => "passed",
611 TestStatus::Failed => "failed",
612 TestStatus::Skipped => "skipped",
613 TestStatus::Errored => "errored",
614 }
615 });
616
617 if let Some(message) = &self.message {
618 result["message"] = json!({
619 "message": message
620 });
621 }
622
623 if let Some(duration) = self.duration {
624 result["duration"] = json!(duration);
625 }
626
627 result
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634 use crate::parser::Parser;
635
636 #[test]
637 fn test_discover_test_functions() {
638 let code = r#"
639sub test_basic {
640 ok(1, "Basic test");
641}
642
643sub helper_function {
644 # Not a test
645}
646
647sub test_another_thing {
648 is($result, 42, "The answer");
649}
650"#;
651
652 let mut parser = Parser::new(code);
653 if let Ok(ast) = parser.parse() {
654 let runner = TestRunner::new(code.to_string(), "file:///test.pl".to_string());
655 let tests = runner.discover_tests(&ast);
656
657 eprintln!("Found {} tests", tests.len());
659 for test in &tests {
660 eprintln!("Test: {} (kind: {:?})", test.label, test.kind);
661 for child in &test.children {
662 eprintln!(" Child: {}", child.label);
663 }
664 }
665
666 assert!(!tests.is_empty());
668
669 let test_functions: Vec<&str> = tests
671 .iter()
672 .filter(|t| t.kind == TestKind::Test && t.label.starts_with("test_"))
673 .map(|t| t.label.as_str())
674 .collect();
675
676 eprintln!("Test functions: {:?}", test_functions);
677 assert!(test_functions.contains(&"test_basic"));
678 assert!(test_functions.contains(&"test_another_thing"));
679 }
680 }
681
682 #[test]
683 fn test_discover_test_assertions() {
684 let code = r#"
685use Test::More;
686
687ok(1, "First test");
688is($x, 5, "X should be 5");
689like($string, qr/pattern/, "String matches");
690
691done_testing();
692"#;
693
694 let mut parser = Parser::new(code);
695 if let Ok(ast) = parser.parse() {
696 let runner = TestRunner::new(code.to_string(), "file:///test.t".to_string());
697 let tests = runner.discover_tests(&ast);
698
699 assert!(!tests.is_empty());
701
702 let all_tests: Vec<&TestItem> = tests
704 .iter()
705 .flat_map(|t| {
706 let mut items = vec![t];
707 items.extend(&t.children);
708 items
709 })
710 .collect();
711
712 eprintln!("All tests found:");
714 for test in &all_tests {
715 eprintln!(" Test: {} (kind: {:?})", test.label, test.kind);
716 }
717
718 assert!(!tests.is_empty());
720 assert_eq!(tests[0].kind, TestKind::File);
721 }
722 }
723
724 #[test]
725 fn test_is_test_file() {
726 let runner = TestRunner::new("".to_string(), "".to_string());
727
728 assert!(runner.is_test_file("file:///t/basic.t"));
729 assert!(runner.is_test_file("file:///tests/foo_test.pl"));
730 assert!(runner.is_test_file("file:///MyTest.pl"));
731 assert!(runner.is_test_file("file:///test_something.pl"));
732
733 assert!(!runner.is_test_file("file:///lib/Module.pm"));
734 assert!(!runner.is_test_file("file:///script.pl"));
735 }
736}