1use crate::ast::Node;
7
8use crate::tdd_basic::{Diagnostic, DiagnosticSeverity};
10use crate::test_generator::{CoverageReport, TestResults, TestRunner};
11use crate::test_generator::{RefactoringSuggester, RefactoringSuggestion};
12use crate::test_generator::{TestCase, TestFramework, TestGenerator};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16
17pub struct TddWorkflow {
19 generator: TestGenerator,
21 runner: TestRunner,
23 suggester: RefactoringSuggester,
25 state: WorkflowState,
27 test_cache: HashMap<PathBuf, TestResults>,
29 coverage_tracker: CoverageTracker,
31 config: TddConfig,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum WorkflowState {
41 Red,
43 Green,
45 Refactor,
47 Idle,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TddConfig {
57 pub auto_generate_tests: bool,
59 pub test_on_save: bool,
61 pub show_inline_coverage: bool,
63 pub test_framework: String,
65 pub test_file_pattern: String,
67 pub coverage_threshold: f64,
69 pub continuous_testing: bool,
71 pub auto_suggest_refactorings: bool,
73}
74
75impl Default for TddConfig {
76 fn default() -> Self {
77 Self {
78 auto_generate_tests: true,
79 test_on_save: true,
80 show_inline_coverage: true,
81 test_framework: "Test::More".to_string(),
82 test_file_pattern: "t/{name}.t".to_string(),
83 coverage_threshold: 80.0,
84 continuous_testing: true,
85 auto_suggest_refactorings: true,
86 }
87 }
88}
89
90pub struct CoverageTracker {
92 line_coverage: HashMap<PathBuf, Vec<LineCoverage>>,
94 #[allow(dead_code)]
96 branch_coverage: HashMap<PathBuf, Vec<BranchCoverage>>,
97 total_coverage: f64,
99}
100
101#[derive(Debug, Clone)]
103pub struct LineCoverage {
104 pub line: usize,
106 pub hits: usize,
108 pub covered: bool,
110}
111
112#[derive(Debug, Clone)]
114pub struct BranchCoverage {
115 pub line: usize,
117 pub branch_id: usize,
119 pub taken: bool,
121 pub hits: usize,
123}
124
125impl TddWorkflow {
126 pub fn new(config: TddConfig) -> Self {
128 let framework = match config.test_framework.as_str() {
129 "Test2::V0" => TestFramework::Test2V0,
130 "Test::Simple" => TestFramework::TestSimple,
131 "Test::Class" => TestFramework::TestClass,
132 _ => TestFramework::TestMore,
133 };
134
135 Self {
136 generator: TestGenerator::new(framework),
137 runner: TestRunner::new(),
138 suggester: RefactoringSuggester::new(),
139 state: WorkflowState::Idle,
140 test_cache: HashMap::new(),
141 coverage_tracker: CoverageTracker::new(),
142 config,
143 }
144 }
145
146 pub fn start_cycle(&mut self, test_name: &str) -> TddCycleResult {
148 self.state = WorkflowState::Red;
149
150 TddCycleResult {
151 phase: "Red".to_string(),
152 message: format!("Starting TDD cycle for '{}'", test_name),
153 actions: vec![
154 TddAction::GenerateTest(test_name.to_string()),
155 TddAction::CreateTestFile(self.get_test_file_path(test_name)),
156 ],
157 }
158 }
159
160 pub fn generate_tests(&self, ast: &Node, source: &str) -> Vec<TestCase> {
162 self.generator.generate_tests(ast, source)
163 }
164
165 pub fn generate_test_for_function(
167 &self,
168 function_name: &str,
169 params: &[String],
170 test_type: TestType,
171 ) -> TestCase {
172 let test_name = format!("test_{}_{:?}", function_name, test_type);
173 let description = format!("{:?} test for {}", test_type, function_name);
174
175 let code = match test_type {
176 TestType::Basic => self.generate_basic_test(function_name, params),
177 TestType::EdgeCase => self.generate_edge_case_test(function_name, params),
178 TestType::ErrorHandling => self.generate_error_test(function_name, params),
179 TestType::Performance => self.generate_performance_test(function_name),
180 TestType::Integration => self.generate_integration_test(function_name, params),
181 };
182
183 TestCase {
184 name: test_name,
185 description,
186 code,
187 is_todo: matches!(test_type, TestType::Integration | TestType::Performance),
188 }
189 }
190
191 fn generate_basic_test(&self, name: &str, params: &[String]) -> String {
192 let args = params
193 .iter()
194 .enumerate()
195 .map(|(i, _)| format!("'test_value_{}'", i))
196 .collect::<Vec<_>>()
197 .join(", ");
198
199 format!(
200 "use Test::More;\n\n\
201 subtest '{}' => sub {{\n \
202 my $result = {}({});\n \
203 ok(defined $result, 'Returns defined value');\n \
204 # PENDING: Add specific assertions\n\
205 }};\n\n\
206 done_testing();\n",
207 name, name, args
208 )
209 }
210
211 fn generate_edge_case_test(&self, name: &str, _params: &[String]) -> String {
212 format!(
213 "use Test::More;\n\n\
214 subtest '{} edge cases' => sub {{\n \
215 # Test with undef\n \
216 eval {{ {}(undef) }};\n \
217 ok(!$@, 'Handles undef');\n \n \
218 # Test with empty values\n \
219 eval {{ {}('') }};\n \
220 ok(!$@, 'Handles empty string');\n \n \
221 # Test with special characters\n \
222 eval {{ {}(\"\\n\\t\\0\") }};\n \
223 ok(!$@, 'Handles special characters');\n\
224 }};\n\n\
225 done_testing();\n",
226 name, name, name, name
227 )
228 }
229
230 fn generate_error_test(&self, name: &str, _params: &[String]) -> String {
231 format!(
232 "use Test::More;\n\
233 use Test::Exception;\n\n\
234 subtest '{} error handling' => sub {{\n \
235 # Test that errors are caught\n \
236 dies_ok {{ {}(undef, undef, undef) }} 'Dies on invalid input';\n \n \
237 # Test error message\n \
238 throws_ok {{ {}() }} qr/required/, 'Correct error message';\n\
239 }};\n\n\
240 done_testing();\n",
241 name, name, name
242 )
243 }
244
245 fn generate_performance_test(&self, name: &str) -> String {
246 format!(
247 "use Test::More;\n\
248 use Benchmark qw(timethis);\n\n\
249 subtest '{} performance' => sub {{\n \
250 my $iterations = 10000;\n \
251 my $result = timethis($iterations, sub {{ {}() }});\n \
252 \n \
253 # Check performance threshold\n \
254 my $rate = $result->iters / $result->cpu_a;\n \
255 cmp_ok($rate, '>', 1000, 'Performance exceeds 1000 ops/sec');\n\
256 }};\n\n\
257 done_testing();\n",
258 name, name
259 )
260 }
261
262 fn generate_integration_test(&self, name: &str, _params: &[String]) -> String {
263 format!(
264 "use Test::More;\n\n\
265 subtest '{} integration' => sub {{\n \
266 # PENDING: Set up test environment\n \
267 # PENDING: Call {} with real dependencies\n \
268 # PENDING: Verify integration points\n \
269 pass('Integration test placeholder');\n\
270 }};\n\n\
271 done_testing();\n",
272 name, name
273 )
274 }
275
276 pub fn run_tests(&mut self, test_files: &[PathBuf]) -> TddCycleResult {
278 let file_strings: Vec<String> =
279 test_files.iter().map(|p| p.to_string_lossy().to_string()).collect();
280
281 let results = self.runner.run_tests(&file_strings);
282
283 for file in test_files {
285 self.test_cache.insert(file.clone(), results.clone());
286 }
287
288 let (new_state, message) = if results.failed > 0 {
290 (WorkflowState::Red, format!("{} tests failed", results.failed))
291 } else if results.todo > 0 {
292 (WorkflowState::Green, format!("All tests pass, {} TODOs remaining", results.todo))
293 } else {
294 (WorkflowState::Refactor, "All tests pass! Ready to refactor".to_string())
295 };
296
297 self.state = new_state.clone();
298
299 let mut actions = vec![];
300
301 if new_state == WorkflowState::Refactor && self.config.auto_suggest_refactorings {
303 actions.push(TddAction::SuggestRefactorings);
304 }
305
306 TddCycleResult { phase: format!("{:?}", new_state), message, actions }
307 }
308
309 pub fn get_refactoring_suggestions(
311 &mut self,
312 ast: &Node,
313 source: &str,
314 ) -> Vec<RefactoringSuggestion> {
315 self.suggester.analyze(ast, source)
316 }
317
318 pub fn get_coverage(&self) -> Option<CoverageReport> {
320 self.runner.get_coverage()
321 }
322
323 pub fn update_coverage(&mut self, file: PathBuf, coverage: Vec<LineCoverage>) {
325 self.coverage_tracker.line_coverage.insert(file, coverage);
326 self.coverage_tracker.calculate_total_coverage();
327 }
328
329 pub fn get_inline_coverage(&self, file: &Path) -> Vec<CoverageAnnotation> {
331 let mut annotations = Vec::new();
332
333 if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
334 for line_cov in coverage {
335 if !line_cov.covered {
336 annotations.push(CoverageAnnotation {
337 line: line_cov.line,
338 message: "Not covered by tests".to_string(),
339 severity: AnnotationSeverity::Warning,
340 });
341 } else if line_cov.hits == 0 {
342 annotations.push(CoverageAnnotation {
343 line: line_cov.line,
344 message: "Never executed".to_string(),
345 severity: AnnotationSeverity::Info,
346 });
347 }
348 }
349 }
350
351 annotations
352 }
353
354 pub fn check_coverage_threshold(&self) -> bool {
356 self.coverage_tracker.total_coverage >= self.config.coverage_threshold
357 }
358
359 fn get_test_file_path(&self, name: &str) -> PathBuf {
361 let pattern = &self.config.test_file_pattern;
362 let path_str = pattern.replace("{name}", name);
363 PathBuf::from(path_str)
364 }
365
366 pub fn get_status(&self) -> WorkflowStatus {
368 WorkflowStatus {
369 state: self.state.clone(),
370 coverage: self.coverage_tracker.total_coverage,
371 tests_passing: self.test_cache.values().all(|r| r.failed == 0),
372 suggestions_available: true, }
374 }
375
376 pub fn generate_coverage_diagnostics(&self, file: &Path) -> Vec<Diagnostic> {
378 let mut diagnostics = Vec::new();
379
380 if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
381 for line_cov in coverage {
382 if !line_cov.covered {
383 diagnostics.push(Diagnostic {
384 range: (line_cov.line, line_cov.line),
385 severity: DiagnosticSeverity::Warning,
386 code: Some("tdd.uncovered".to_string()),
387 message: "Line not covered by tests".to_string(),
388 related_information: vec![],
389 tags: vec![],
390 });
391 }
392 }
393 }
394
395 diagnostics
396 }
397}
398
399impl CoverageTracker {
400 fn new() -> Self {
401 Self { line_coverage: HashMap::new(), branch_coverage: HashMap::new(), total_coverage: 0.0 }
402 }
403
404 fn calculate_total_coverage(&mut self) {
405 let mut total_lines = 0;
406 let mut covered_lines = 0;
407
408 for coverage in self.line_coverage.values() {
409 for line in coverage {
410 total_lines += 1;
411 if line.covered {
412 covered_lines += 1;
413 }
414 }
415 }
416
417 if total_lines > 0 {
418 self.total_coverage = (covered_lines as f64 / total_lines as f64) * 100.0;
419 }
420 }
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct TddCycleResult {
426 pub phase: String,
428 pub message: String,
430 pub actions: Vec<TddAction>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub enum TddAction {
437 GenerateTest(String),
439 CreateTestFile(PathBuf),
441 RunTests,
443 SuggestRefactorings,
445 UpdateCoverage,
447 ShowFailures,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub enum TestType {
454 Basic,
456 EdgeCase,
458 ErrorHandling,
460 Performance,
462 Integration,
464}
465
466#[derive(Debug, Clone)]
468pub struct CoverageAnnotation {
469 pub line: usize,
471 pub message: String,
473 pub severity: AnnotationSeverity,
475}
476
477#[derive(Debug, Clone)]
479pub enum AnnotationSeverity {
480 Error,
482 Warning,
484 Info,
486 Hint,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct WorkflowStatus {
493 pub state: WorkflowState,
495 pub coverage: f64,
497 pub tests_passing: bool,
499 pub suggestions_available: bool,
501}
502
503#[cfg(feature = "lsp-compat")]
505pub mod lsp_integration {
506 use super::*;
507 use lsp_types::{
508 CodeAction, CodeActionKind, Command, Diagnostic as LspDiagnostic, DiagnosticSeverity,
509 Position, Range,
510 };
511
512 pub fn tdd_actions_to_code_actions(
514 actions: Vec<TddAction>,
515 _uri: &url::Url,
516 ) -> Vec<CodeAction> {
517 actions
518 .into_iter()
519 .map(|action| match action {
520 TddAction::GenerateTest(name) => CodeAction {
521 title: format!("Generate test for '{}'", name),
522 kind: Some(CodeActionKind::REFACTOR),
523 command: Some(Command {
524 title: "Generate Test".to_string(),
525 command: "perl.tdd.generateTest".to_string(),
526 arguments: Some(vec![serde_json::json!(name)]),
527 }),
528 ..Default::default()
529 },
530 TddAction::RunTests => CodeAction {
531 title: "Run tests".to_string(),
532 kind: Some(CodeActionKind::new("test.run")),
533 command: Some(Command {
534 title: "Run Tests".to_string(),
535 command: "perl.tdd.runTests".to_string(),
536 arguments: None,
537 }),
538 ..Default::default()
539 },
540 TddAction::SuggestRefactorings => CodeAction {
541 title: "Get refactoring suggestions".to_string(),
542 kind: Some(CodeActionKind::REFACTOR),
543 command: Some(Command {
544 title: "Suggest Refactorings".to_string(),
545 command: "perl.tdd.suggestRefactorings".to_string(),
546 arguments: None,
547 }),
548 ..Default::default()
549 },
550 _ => CodeAction {
551 title: format!("{:?}", action),
552 kind: Some(CodeActionKind::EMPTY),
553 ..Default::default()
554 },
555 })
556 .collect()
557 }
558
559 pub fn coverage_to_diagnostics(annotations: Vec<CoverageAnnotation>) -> Vec<LspDiagnostic> {
561 annotations
562 .into_iter()
563 .map(|ann| LspDiagnostic {
564 range: Range {
565 start: Position { line: ann.line as u32, character: 0 },
566 end: Position { line: ann.line as u32, character: 999 },
567 },
568 severity: Some(match ann.severity {
569 AnnotationSeverity::Error => DiagnosticSeverity::ERROR,
570 AnnotationSeverity::Warning => DiagnosticSeverity::WARNING,
571 AnnotationSeverity::Info => DiagnosticSeverity::INFORMATION,
572 AnnotationSeverity::Hint => DiagnosticSeverity::HINT,
573 }),
574 code: Some(lsp_types::NumberOrString::String("coverage".to_string())),
575 source: Some("TDD".to_string()),
576 message: ann.message,
577 ..Default::default()
578 })
579 .collect()
580 }
581
582 pub fn create_status_message(status: &WorkflowStatus) -> String {
584 format!(
585 "TDD: {:?} | Coverage: {:.1}% | Tests: {} | Refactor: {}",
586 status.state,
587 status.coverage,
588 if status.tests_passing { "✓" } else { "✗" },
589 if status.suggestions_available { "💡" } else { "" }
590 )
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::ast::NodeKind;
598 use crate::ast::SourceLocation;
599
600 #[test]
601 fn test_tdd_workflow_cycle() {
602 let config = TddConfig::default();
603 let mut workflow = TddWorkflow::new(config);
604
605 let result = workflow.start_cycle("calculate_sum");
607 assert_eq!(workflow.state, WorkflowState::Red);
608 assert!(result.message.contains("calculate_sum"));
609 }
610
611 #[test]
612 fn test_generate_tests() {
613 let config = TddConfig::default();
614 let workflow = TddWorkflow::new(config);
615
616 let ast = Node::new(
617 NodeKind::Subroutine {
618 name: Some("multiply".to_string()),
619 name_span: Some(SourceLocation { start: 4, end: 12 }),
620 signature: None,
621 body: Box::new(Node::new(
622 NodeKind::Block { statements: vec![] },
623 SourceLocation { start: 0, end: 0 },
624 )),
625 attributes: vec![],
626 prototype: None,
627 },
628 SourceLocation { start: 0, end: 0 },
629 );
630
631 let tests = workflow.generate_tests(&ast, "sub multiply { }");
632 assert!(!tests.is_empty());
633 }
634
635 #[test]
636 fn test_coverage_tracking() {
637 let config = TddConfig::default();
638 let mut workflow = TddWorkflow::new(config);
639
640 let coverage = vec![
641 LineCoverage { line: 1, hits: 5, covered: true },
642 LineCoverage { line: 2, hits: 0, covered: false },
643 LineCoverage { line: 3, hits: 10, covered: true },
644 ];
645
646 workflow.update_coverage(PathBuf::from("test.pl"), coverage);
647
648 let annotations = workflow.get_inline_coverage(&PathBuf::from("test.pl"));
649 assert_eq!(annotations.len(), 1); assert_eq!(annotations[0].line, 2);
651 }
652
653 #[test]
654 fn test_refactoring_suggestions() {
655 let config = TddConfig::default();
656 let mut workflow = TddWorkflow::new(config);
657
658 let parameters: Vec<Node> = (0..8)
660 .map(|i| {
661 Node::new(
662 NodeKind::MandatoryParameter {
663 variable: Box::new(Node::new(
664 NodeKind::Variable {
665 sigil: "$".to_string(),
666 name: format!("param{}", i),
667 },
668 SourceLocation { start: 0, end: 0 },
669 )),
670 },
671 SourceLocation { start: 0, end: 0 },
672 )
673 })
674 .collect();
675
676 let ast = Node::new(
677 NodeKind::Subroutine {
678 name: Some("complex_function".to_string()),
679 name_span: Some(SourceLocation { start: 4, end: 20 }),
680 signature: Some(Box::new(Node::new(
681 NodeKind::Signature { parameters },
682 SourceLocation { start: 0, end: 0 },
683 ))),
684 body: Box::new(Node::new(
685 NodeKind::Block { statements: vec![] },
686 SourceLocation { start: 0, end: 0 },
687 )),
688 attributes: vec![],
689 prototype: None,
690 },
691 SourceLocation { start: 0, end: 0 },
692 );
693
694 let suggestions = workflow.get_refactoring_suggestions(&ast, "sub complex_function { }");
695
696 assert!(
698 suggestions.iter().any(
699 |s| s.category == crate::test_generator::RefactoringCategory::TooManyParameters
700 )
701 );
702 }
703
704 #[test]
705 fn test_specific_test_generation() {
706 let config = TddConfig::default();
707 let workflow = TddWorkflow::new(config);
708
709 let test = workflow.generate_test_for_function(
710 "validate_email",
711 &["$email".to_string()],
712 TestType::EdgeCase,
713 );
714
715 assert!(test.code.contains("edge cases"));
716 assert!(test.code.contains("undef"));
717 assert!(test.code.contains("empty"));
718 }
719}