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 let undef_args = repeated_edge_case_args(params, "undef");
213 let empty_args = repeated_edge_case_args(params, "''");
214 let special_args = repeated_edge_case_args(params, "\"\\n\\t\\0\"");
215
216 format!(
217 "use Test::More;\n\n\
218 subtest '{} edge cases' => sub {{\n \
219 # Test with undef\n \
220 eval {{ {}({}) }};\n \
221 ok(!$@, 'Handles undef');\n \n \
222 # Test with empty values\n \
223 eval {{ {}({}) }};\n \
224 ok(!$@, 'Handles empty string');\n \n \
225 # Test with special characters\n \
226 eval {{ {}({}) }};\n \
227 ok(!$@, 'Handles special characters');\n\
228 }};\n\n\
229 done_testing();\n",
230 name, name, undef_args, name, empty_args, name, special_args
231 )
232 }
233
234 fn generate_error_test(&self, name: &str, _params: &[String]) -> String {
235 format!(
236 "use Test::More;\n\
237 use Test::Exception;\n\n\
238 subtest '{} error handling' => sub {{\n \
239 # Test that errors are caught\n \
240 dies_ok {{ {}(undef, undef, undef) }} 'Dies on invalid input';\n \n \
241 # Test error message\n \
242 throws_ok {{ {}() }} qr/required/, 'Correct error message';\n\
243 }};\n\n\
244 done_testing();\n",
245 name, name, name
246 )
247 }
248
249 fn generate_performance_test(&self, name: &str) -> String {
250 format!(
251 "use Test::More;\n\
252 use Benchmark qw(timethis);\n\n\
253 subtest '{} performance' => sub {{\n \
254 my $iterations = 10000;\n \
255 my $result = timethis($iterations, sub {{ {}() }});\n \
256 \n \
257 # Check performance threshold\n \
258 my $rate = $result->iters / $result->cpu_a;\n \
259 cmp_ok($rate, '>', 1000, 'Performance exceeds 1000 ops/sec');\n\
260 }};\n\n\
261 done_testing();\n",
262 name, name
263 )
264 }
265
266 fn generate_integration_test(&self, name: &str, _params: &[String]) -> String {
267 format!(
268 "use Test::More;\n\n\
269 subtest '{} integration' => sub {{\n \
270 # PENDING: Set up test environment\n \
271 # PENDING: Call {} with real dependencies\n \
272 # PENDING: Verify integration points\n \
273 pass('Integration test placeholder');\n\
274 }};\n\n\
275 done_testing();\n",
276 name, name
277 )
278 }
279
280 pub fn run_tests(&mut self, test_files: &[PathBuf]) -> TddCycleResult {
282 let file_strings: Vec<String> =
283 test_files.iter().map(|p| p.to_string_lossy().to_string()).collect();
284
285 let results = self.runner.run_tests(&file_strings);
286
287 for file in test_files {
289 self.test_cache.insert(file.clone(), results.clone());
290 }
291
292 let (new_state, message) = if results.failed > 0 {
296 (WorkflowState::Red, format!("{} tests failed", results.failed))
297 } else if results.todo > 0 {
298 (WorkflowState::Green, format!("All tests pass, {} TODOs remaining", results.todo))
299 } else {
300 (WorkflowState::Green, "All tests pass! Ready to refactor".to_string())
301 };
302
303 self.state = new_state.clone();
304
305 let mut actions = vec![];
306
307 if new_state == WorkflowState::Green && self.config.auto_suggest_refactorings {
309 actions.push(TddAction::SuggestRefactorings);
310 }
311
312 TddCycleResult { phase: format!("{:?}", new_state), message, actions }
313 }
314
315 pub fn start_refactor(&mut self) -> TddCycleResult {
317 self.state = WorkflowState::Refactor;
318
319 TddCycleResult {
320 phase: "Refactor".to_string(),
321 message: "Refactoring phase - improve code while keeping tests green".to_string(),
322 actions: vec![TddAction::SuggestRefactorings, TddAction::UpdateCoverage],
323 }
324 }
325
326 pub fn complete_cycle(&mut self) -> TddCycleResult {
328 self.state = WorkflowState::Idle;
329
330 TddCycleResult {
331 phase: "Idle".to_string(),
332 message: "TDD cycle complete".to_string(),
333 actions: vec![TddAction::RunTests],
334 }
335 }
336
337 pub fn get_refactoring_suggestions(
339 &mut self,
340 ast: &Node,
341 source: &str,
342 ) -> Vec<RefactoringSuggestion> {
343 self.suggester.analyze(ast, source)
344 }
345
346 pub fn get_coverage(&self) -> Option<CoverageReport> {
348 self.runner.get_coverage()
349 }
350
351 pub fn update_coverage(&mut self, file: PathBuf, coverage: Vec<LineCoverage>) {
353 self.coverage_tracker.line_coverage.insert(file, coverage);
354 self.coverage_tracker.calculate_total_coverage();
355 }
356
357 pub fn get_inline_coverage(&self, file: &Path) -> Vec<CoverageAnnotation> {
359 let mut annotations = Vec::new();
360
361 if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
362 for line_cov in coverage {
363 if !line_cov.covered {
364 annotations.push(CoverageAnnotation {
365 line: line_cov.line,
366 message: "Not covered by tests".to_string(),
367 severity: AnnotationSeverity::Warning,
368 });
369 } else if line_cov.hits == 0 {
370 annotations.push(CoverageAnnotation {
371 line: line_cov.line,
372 message: "Never executed".to_string(),
373 severity: AnnotationSeverity::Info,
374 });
375 }
376 }
377 }
378
379 annotations
380 }
381
382 pub fn check_coverage_threshold(&self) -> bool {
384 self.coverage_tracker.total_coverage >= self.config.coverage_threshold
385 }
386
387 fn get_test_file_path(&self, name: &str) -> PathBuf {
389 let pattern = &self.config.test_file_pattern;
390 let path_str = pattern.replace("{name}", name);
391 PathBuf::from(path_str)
392 }
393
394 pub fn get_status(&self) -> WorkflowStatus {
396 WorkflowStatus {
397 state: self.state.clone(),
398 coverage: self.coverage_tracker.total_coverage,
399 tests_passing: self.test_cache.values().all(|r| r.failed == 0),
400 suggestions_available: true, }
402 }
403
404 pub fn generate_coverage_diagnostics(&self, file: &Path) -> Vec<Diagnostic> {
406 let mut diagnostics = Vec::new();
407
408 if let Some(coverage) = self.coverage_tracker.line_coverage.get(file) {
409 for line_cov in coverage {
410 if !line_cov.covered {
411 diagnostics.push(Diagnostic {
412 range: (line_cov.line, line_cov.line),
413 severity: DiagnosticSeverity::Warning,
414 code: Some("tdd.uncovered".to_string()),
415 message: "Line not covered by tests".to_string(),
416 related_information: vec![],
417 tags: vec![],
418 });
419 }
420 }
421 }
422
423 diagnostics
424 }
425}
426
427fn repeated_edge_case_args(params: &[String], edge_case: &str) -> String {
428 match params.len() {
429 0 => String::new(),
430 len => std::iter::repeat_n(edge_case, len).collect::<Vec<_>>().join(", "),
431 }
432}
433
434impl CoverageTracker {
435 fn new() -> Self {
436 Self { line_coverage: HashMap::new(), branch_coverage: HashMap::new(), total_coverage: 0.0 }
437 }
438
439 fn calculate_total_coverage(&mut self) {
440 let mut total_lines = 0;
441 let mut covered_lines = 0;
442
443 for coverage in self.line_coverage.values() {
444 for line in coverage {
445 total_lines += 1;
446 if line.covered {
447 covered_lines += 1;
448 }
449 }
450 }
451
452 self.total_coverage =
453 if total_lines > 0 { (covered_lines as f64 / total_lines as f64) * 100.0 } else { 0.0 };
454 }
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct TddCycleResult {
460 pub phase: String,
462 pub message: String,
464 pub actions: Vec<TddAction>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub enum TddAction {
471 GenerateTest(String),
473 CreateTestFile(PathBuf),
475 RunTests,
477 SuggestRefactorings,
479 UpdateCoverage,
481 ShowFailures,
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
487pub enum TestType {
488 Basic,
490 EdgeCase,
492 ErrorHandling,
494 Performance,
496 Integration,
498}
499
500#[derive(Debug, Clone)]
502pub struct CoverageAnnotation {
503 pub line: usize,
505 pub message: String,
507 pub severity: AnnotationSeverity,
509}
510
511#[derive(Debug, Clone)]
513pub enum AnnotationSeverity {
514 Error,
516 Warning,
518 Info,
520 Hint,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct WorkflowStatus {
527 pub state: WorkflowState,
529 pub coverage: f64,
531 pub tests_passing: bool,
533 pub suggestions_available: bool,
535}
536
537#[cfg(feature = "lsp-compat")]
539pub mod lsp_integration {
540 use super::*;
541 use lsp_types::{
542 CodeAction, CodeActionKind, Command, Diagnostic as LspDiagnostic, DiagnosticSeverity,
543 Position, Range,
544 };
545
546 pub fn tdd_actions_to_code_actions(
548 actions: Vec<TddAction>,
549 _uri: &url::Url,
550 ) -> Vec<CodeAction> {
551 actions
552 .into_iter()
553 .map(|action| match action {
554 TddAction::GenerateTest(name) => CodeAction {
555 title: format!("Generate test for '{}'", name),
556 kind: Some(CodeActionKind::REFACTOR),
557 command: Some(Command {
558 title: "Generate Test".to_string(),
559 command: "perl.tdd.generateTest".to_string(),
560 arguments: Some(vec![serde_json::json!(name)]),
561 }),
562 ..Default::default()
563 },
564 TddAction::RunTests => CodeAction {
565 title: "Run tests".to_string(),
566 kind: Some(CodeActionKind::new("test.run")),
567 command: Some(Command {
568 title: "Run Tests".to_string(),
569 command: "perl.tdd.runTests".to_string(),
570 arguments: None,
571 }),
572 ..Default::default()
573 },
574 TddAction::SuggestRefactorings => CodeAction {
575 title: "Get refactoring suggestions".to_string(),
576 kind: Some(CodeActionKind::REFACTOR),
577 command: Some(Command {
578 title: "Suggest Refactorings".to_string(),
579 command: "perl.tdd.suggestRefactorings".to_string(),
580 arguments: None,
581 }),
582 ..Default::default()
583 },
584 _ => CodeAction {
585 title: format!("{:?}", action),
586 kind: Some(CodeActionKind::EMPTY),
587 ..Default::default()
588 },
589 })
590 .collect()
591 }
592
593 pub fn coverage_to_diagnostics(annotations: Vec<CoverageAnnotation>) -> Vec<LspDiagnostic> {
595 annotations
596 .into_iter()
597 .map(|ann| LspDiagnostic {
598 range: Range {
599 start: Position { line: ann.line as u32, character: 0 },
600 end: Position { line: ann.line as u32, character: 999 },
601 },
602 severity: Some(match ann.severity {
603 AnnotationSeverity::Error => DiagnosticSeverity::ERROR,
604 AnnotationSeverity::Warning => DiagnosticSeverity::WARNING,
605 AnnotationSeverity::Info => DiagnosticSeverity::INFORMATION,
606 AnnotationSeverity::Hint => DiagnosticSeverity::HINT,
607 }),
608 code: Some(lsp_types::NumberOrString::String("coverage".to_string())),
609 source: Some("TDD".to_string()),
610 message: ann.message,
611 ..Default::default()
612 })
613 .collect()
614 }
615
616 pub fn create_status_message(status: &WorkflowStatus) -> String {
618 format!(
619 "TDD: {:?} | Coverage: {:.1}% | Tests: {} | Refactor: {}",
620 status.state,
621 status.coverage,
622 if status.tests_passing { "✓" } else { "✗" },
623 if status.suggestions_available { "💡" } else { "" }
624 )
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use crate::ast::NodeKind;
632 use crate::ast::SourceLocation;
633
634 #[test]
635 fn test_tdd_workflow_cycle() {
636 let config = TddConfig::default();
637 let mut workflow = TddWorkflow::new(config);
638
639 let result = workflow.start_cycle("calculate_sum");
641 assert_eq!(workflow.state, WorkflowState::Red);
642 assert!(result.message.contains("calculate_sum"));
643 }
644
645 #[test]
646 fn test_generate_tests() {
647 let config = TddConfig::default();
648 let workflow = TddWorkflow::new(config);
649
650 let ast = Node::new(
651 NodeKind::Subroutine {
652 name: Some("multiply".to_string()),
653 name_span: Some(SourceLocation { start: 4, end: 12 }),
654 signature: None,
655 body: Box::new(Node::new(
656 NodeKind::Block { statements: vec![] },
657 SourceLocation { start: 0, end: 0 },
658 )),
659 attributes: vec![],
660 prototype: None,
661 },
662 SourceLocation { start: 0, end: 0 },
663 );
664
665 let tests = workflow.generate_tests(&ast, "sub multiply { }");
666 assert!(!tests.is_empty());
667 }
668
669 #[test]
670 fn test_coverage_tracking() {
671 let config = TddConfig::default();
672 let mut workflow = TddWorkflow::new(config);
673
674 let coverage = vec![
675 LineCoverage { line: 1, hits: 5, covered: true },
676 LineCoverage { line: 2, hits: 0, covered: false },
677 LineCoverage { line: 3, hits: 10, covered: true },
678 ];
679
680 workflow.update_coverage(PathBuf::from("test.pl"), coverage);
681
682 let annotations = workflow.get_inline_coverage(&PathBuf::from("test.pl"));
683 assert_eq!(annotations.len(), 1); assert_eq!(annotations[0].line, 2);
685 }
686
687 #[test]
688 fn test_coverage_resets_to_zero_when_all_inputs_empty() -> Result<(), Box<dyn std::error::Error>>
689 {
690 let config = TddConfig::default();
691 let mut workflow = TddWorkflow::new(config);
692
693 workflow.update_coverage(
694 PathBuf::from("a.pl"),
695 vec![LineCoverage { line: 1, hits: 1, covered: true }],
696 );
697 assert!(workflow.check_coverage_threshold());
698
699 workflow.update_coverage(PathBuf::from("a.pl"), vec![]);
700
701 assert!(!workflow.check_coverage_threshold());
702 assert_eq!(workflow.get_status().coverage, 0.0);
703 Ok(())
704 }
705
706 #[test]
707 fn test_refactoring_suggestions() {
708 let config = TddConfig::default();
709 let mut workflow = TddWorkflow::new(config);
710
711 let parameters: Vec<Node> = (0..8)
713 .map(|i| {
714 Node::new(
715 NodeKind::MandatoryParameter {
716 variable: Box::new(Node::new(
717 NodeKind::Variable {
718 sigil: "$".to_string(),
719 name: format!("param{}", i),
720 },
721 SourceLocation { start: 0, end: 0 },
722 )),
723 },
724 SourceLocation { start: 0, end: 0 },
725 )
726 })
727 .collect();
728
729 let ast = Node::new(
730 NodeKind::Subroutine {
731 name: Some("complex_function".to_string()),
732 name_span: Some(SourceLocation { start: 4, end: 20 }),
733 signature: Some(Box::new(Node::new(
734 NodeKind::Signature { parameters },
735 SourceLocation { start: 0, end: 0 },
736 ))),
737 body: Box::new(Node::new(
738 NodeKind::Block { statements: vec![] },
739 SourceLocation { start: 0, end: 0 },
740 )),
741 attributes: vec![],
742 prototype: None,
743 },
744 SourceLocation { start: 0, end: 0 },
745 );
746
747 let suggestions = workflow.get_refactoring_suggestions(&ast, "sub complex_function { }");
748
749 assert!(
751 suggestions.iter().any(
752 |s| s.category == crate::test_generator::RefactoringCategory::TooManyParameters
753 )
754 );
755 }
756
757 #[test]
758 fn test_specific_test_generation() {
759 let config = TddConfig::default();
760 let workflow = TddWorkflow::new(config);
761
762 let test = workflow.generate_test_for_function(
763 "validate_email",
764 &["$email".to_string()],
765 TestType::EdgeCase,
766 );
767
768 assert!(test.code.contains("edge cases"));
769 assert!(test.code.contains("undef"));
770 assert!(test.code.contains("empty"));
771 }
772
773 #[test]
774 fn test_edge_case_generation_preserves_signature_arity() {
775 let config = TddConfig::default();
776 let workflow = TddWorkflow::new(config);
777
778 let test = workflow.generate_test_for_function(
779 "validate_contact",
780 &["$name".to_string(), "$email".to_string()],
781 TestType::EdgeCase,
782 );
783
784 assert!(test.code.contains("validate_contact(undef, undef)"));
785 assert!(test.code.contains("validate_contact('', '')"));
786 }
787}