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 declarator: None,
655 signature: None,
656 body: Box::new(Node::new(
657 NodeKind::Block { statements: vec![] },
658 SourceLocation { start: 0, end: 0 },
659 )),
660 attributes: vec![],
661 prototype: None,
662 },
663 SourceLocation { start: 0, end: 0 },
664 );
665
666 let tests = workflow.generate_tests(&ast, "sub multiply { }");
667 assert!(!tests.is_empty());
668 }
669
670 #[test]
671 fn test_coverage_tracking() {
672 let config = TddConfig::default();
673 let mut workflow = TddWorkflow::new(config);
674
675 let coverage = vec![
676 LineCoverage { line: 1, hits: 5, covered: true },
677 LineCoverage { line: 2, hits: 0, covered: false },
678 LineCoverage { line: 3, hits: 10, covered: true },
679 ];
680
681 workflow.update_coverage(PathBuf::from("test.pl"), coverage);
682
683 let annotations = workflow.get_inline_coverage(&PathBuf::from("test.pl"));
684 assert_eq!(annotations.len(), 1); assert_eq!(annotations[0].line, 2);
686 }
687
688 #[test]
689 fn test_coverage_resets_to_zero_when_all_inputs_empty() -> Result<(), Box<dyn std::error::Error>>
690 {
691 let config = TddConfig::default();
692 let mut workflow = TddWorkflow::new(config);
693
694 workflow.update_coverage(
695 PathBuf::from("a.pl"),
696 vec![LineCoverage { line: 1, hits: 1, covered: true }],
697 );
698 assert!(workflow.check_coverage_threshold());
699
700 workflow.update_coverage(PathBuf::from("a.pl"), vec![]);
701
702 assert!(!workflow.check_coverage_threshold());
703 assert_eq!(workflow.get_status().coverage, 0.0);
704 Ok(())
705 }
706
707 #[test]
708 fn test_refactoring_suggestions() {
709 let config = TddConfig::default();
710 let mut workflow = TddWorkflow::new(config);
711
712 let parameters: Vec<Node> = (0..8)
714 .map(|i| {
715 Node::new(
716 NodeKind::MandatoryParameter {
717 variable: Box::new(Node::new(
718 NodeKind::Variable {
719 sigil: "$".to_string(),
720 name: format!("param{}", i),
721 },
722 SourceLocation { start: 0, end: 0 },
723 )),
724 },
725 SourceLocation { start: 0, end: 0 },
726 )
727 })
728 .collect();
729
730 let ast = Node::new(
731 NodeKind::Subroutine {
732 name: Some("complex_function".to_string()),
733 name_span: Some(SourceLocation { start: 4, end: 20 }),
734 declarator: None,
735 signature: Some(Box::new(Node::new(
736 NodeKind::Signature { parameters },
737 SourceLocation { start: 0, end: 0 },
738 ))),
739 body: Box::new(Node::new(
740 NodeKind::Block { statements: vec![] },
741 SourceLocation { start: 0, end: 0 },
742 )),
743 attributes: vec![],
744 prototype: None,
745 },
746 SourceLocation { start: 0, end: 0 },
747 );
748
749 let suggestions = workflow.get_refactoring_suggestions(&ast, "sub complex_function { }");
750
751 assert!(
753 suggestions.iter().any(
754 |s| s.category == crate::test_generator::RefactoringCategory::TooManyParameters
755 )
756 );
757 }
758
759 #[test]
760 fn test_specific_test_generation() {
761 let config = TddConfig::default();
762 let workflow = TddWorkflow::new(config);
763
764 let test = workflow.generate_test_for_function(
765 "validate_email",
766 &["$email".to_string()],
767 TestType::EdgeCase,
768 );
769
770 assert!(test.code.contains("edge cases"));
771 assert!(test.code.contains("undef"));
772 assert!(test.code.contains("empty"));
773 }
774
775 #[test]
776 fn test_edge_case_generation_preserves_signature_arity() {
777 let config = TddConfig::default();
778 let workflow = TddWorkflow::new(config);
779
780 let test = workflow.generate_test_for_function(
781 "validate_contact",
782 &["$name".to_string(), "$email".to_string()],
783 TestType::EdgeCase,
784 );
785
786 assert!(test.code.contains("validate_contact(undef, undef)"));
787 assert!(test.code.contains("validate_contact('', '')"));
788 }
789}