using MarketAlly.AIPlugin; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; [assembly: DoNotParallelize] namespace MarketAlly.AIPlugin.Analysis.Plugins { [AIPlugin("TestAnalysis", "Analyzes test coverage, quality, and generates test improvement suggestions")] public class TestAnalysisPlugin : IAIPlugin { [AIParameter("Full path to the project or test directory", required: true)] public string ProjectPath { get; set; } = string.Empty; [AIParameter("Calculate code coverage metrics", required: false)] public bool CalculateCoverage { get; set; } = true; [AIParameter("Identify untested functions and classes", required: false)] public bool IdentifyUntested { get; set; } = true; [AIParameter("Analyze test quality and maintainability", required: false)] public bool AnalyzeTestQuality { get; set; } = true; [AIParameter("Generate test stubs for untested code", required: false)] public bool GenerateTestStubs { get; set; } = false; [AIParameter("Suggest property-based and fuzz testing opportunities", required: false)] public bool SuggestAdvancedTesting { get; set; } = true; [AIParameter("Check for redundant or fragile tests", required: false)] public bool CheckRedundantTests { get; set; } = true; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["projectPath"] = typeof(string), ["calculateCoverage"] = typeof(bool), ["identifyUntested"] = typeof(bool), ["analyzeTestQuality"] = typeof(bool), ["generateTestStubs"] = typeof(bool), ["suggestAdvancedTesting"] = typeof(bool), ["checkRedundantTests"] = typeof(bool) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { // Properties are auto-populated by the AIPlugin system from AI's tool call try { // Validate required parameters if (string.IsNullOrEmpty(ProjectPath)) { return new AIPluginResult( new ArgumentException("ProjectPath is required"), "ProjectPath parameter is required" ); } // Validate path if (!Directory.Exists(ProjectPath)) { return new AIPluginResult( new DirectoryNotFoundException($"Directory not found: {ProjectPath}"), "Directory not found" ); } // Initialize test analysis var analysis = new TestAnalysis { ProjectPath = ProjectPath, AnalysisDate = DateTime.UtcNow, CoverageMetrics = new CoverageMetrics(), UntestedFunctions = new List(), TestQualityIssues = new List(), GeneratedTestStubs = new List(), AdvancedTestingSuggestions = new List(), RedundantTests = new List() }; // Discover source and test files await DiscoverProjectFiles(ProjectPath, analysis); // Calculate coverage metrics if (CalculateCoverage) { await CalculateCoverageMetrics(analysis); } // Identify untested functions if (IdentifyUntested) { await IdentifyUntestedFunctions(analysis); } // Analyze test quality if (AnalyzeTestQuality) { await AnalyzeTestQualityMethod(analysis); } // Generate test stubs if (GenerateTestStubs) { await GenerateTestStubsMethod(analysis); } // Suggest advanced testing if (SuggestAdvancedTesting) { await SuggestAdvancedTestingMethod(analysis); } // Check for redundant tests if (CheckRedundantTests) { await CheckRedundantTestsMethod(analysis); } // Calculate test quality score var testQualityScore = CalculateTestQualityScore(analysis); // Generate improvement plan var improvementPlan = GenerateTestImprovementPlan(analysis); var result = new { ProjectPath = ProjectPath, AnalysisDate = analysis.AnalysisDate, TestQualityScore = testQualityScore, ProjectOverview = new { analysis.TotalSourceFiles, analysis.TotalTestFiles, analysis.TotalSourceMethods, analysis.TotalTestMethods, TestToSourceRatio = analysis.TotalSourceMethods > 0 ? Math.Round((double)analysis.TotalTestMethods / analysis.TotalSourceMethods, 2) : 0 }, CoverageMetrics = CalculateCoverage ? new { LineCoverage = analysis.CoverageMetrics.LineCoverage, BranchCoverage = analysis.CoverageMetrics.BranchCoverage, MethodCoverage = analysis.CoverageMetrics.MethodCoverage, ClassCoverage = analysis.CoverageMetrics.ClassCoverage, TestedMethods = analysis.CoverageMetrics.TestedMethods, UntestedMethods = analysis.CoverageMetrics.UntestedMethods, CoverageLevel = GetCoverageLevel(analysis.CoverageMetrics.MethodCoverage) } : null, UntestedFunctions = IdentifyUntested ? analysis.UntestedFunctions.Select(u => new { u.ClassName, u.MethodName, u.FilePath, u.LineNumber, u.Visibility, u.Complexity, u.Priority, u.Rationale, u.SuggestedTestTypes }).OrderByDescending(u => u.Priority).ToList() : null, TestQualityIssues = AnalyzeTestQuality ? analysis.TestQualityIssues.Select(q => new { q.TestClass, q.TestMethod, q.IssueType, q.Severity, q.Description, q.FilePath, q.LineNumber, q.Recommendation, q.Impact }).OrderByDescending(q => q.Severity == "High" ? 3 : q.Severity == "Medium" ? 2 : 1).ToList() : null, GeneratedTestStubs = GenerateTestStubs ? analysis.GeneratedTestStubs.Select(s => new { s.TargetClass, s.TargetMethod, s.TestClassName, s.TestMethodName, s.TestFramework, s.GeneratedCode, s.TestScenarios }).ToList() : null, AdvancedTestingSuggestions = SuggestAdvancedTesting ? analysis.AdvancedTestingSuggestions.Select(a => new { a.TestingType, a.TargetClass, a.TargetMethod, a.Rationale, a.Benefit, a.Implementation, a.Priority, a.EstimatedEffort }).OrderByDescending(a => a.Priority).ToList() : null, RedundantTests = CheckRedundantTests ? analysis.RedundantTests.Select(r => new { r.TestClass, r.TestMethod, r.RedundancyType, r.Description, r.RelatedTests, r.Recommendation, r.FilePath, r.LineNumber }).ToList() : null, ImprovementPlan = improvementPlan.Select(i => new { i.Phase, i.Priority, i.Title, i.Description, i.EstimatedHours, i.ExpectedBenefit, i.Dependencies }).ToList(), Summary = new { OverallTestHealth = GetTestHealth(testQualityScore), CriticalGaps = analysis.UntestedFunctions.Count(u => u.Priority >= 8), QualityIssues = analysis.TestQualityIssues.Count(q => q.Severity == "High"), CoverageGap = analysis.CoverageMetrics.MethodCoverage < 80 ? $"{80 - analysis.CoverageMetrics.MethodCoverage:F1}% to reach 80% coverage" : "Coverage target met", TopRecommendations = GetTopTestRecommendations(analysis), EstimatedEffortToImprove = improvementPlan.Sum(p => p.EstimatedHours) } }; return new AIPluginResult(result, $"Test analysis completed. Quality Score: {testQualityScore}/100, Coverage: {analysis.CoverageMetrics.MethodCoverage:F1}%. " + $"Found {analysis.UntestedFunctions.Count} untested functions and {analysis.TestQualityIssues.Count} quality issues."); } catch (Exception ex) { return new AIPluginResult(ex, "Failed to analyze tests"); } } private async Task DiscoverProjectFiles(string projectPath, TestAnalysis analysis) { var allCsFiles = Directory.GetFiles(projectPath, "*.cs", SearchOption.AllDirectories) .Where(f => !f.Contains("\\bin\\") && !f.Contains("\\obj\\") && !f.EndsWith(".Designer.cs") && !f.EndsWith(".g.cs")) .ToList(); // Separate source and test files var testFiles = allCsFiles.Where(f => IsTestFile(f)).ToList(); var sourceFiles = allCsFiles.Except(testFiles).ToList(); analysis.SourceFiles = sourceFiles; analysis.TestFiles = testFiles; analysis.TotalSourceFiles = sourceFiles.Count; analysis.TotalTestFiles = testFiles.Count; // Parse files to extract method information analysis.SourceMethods = new List(); analysis.TestMethods = new List(); foreach (var sourceFile in sourceFiles) { var methods = await ExtractSourceMethods(sourceFile); analysis.SourceMethods.AddRange(methods); } foreach (var testFile in testFiles) { var methods = await ExtractTestMethods(testFile); analysis.TestMethods.AddRange(methods); } analysis.TotalSourceMethods = analysis.SourceMethods.Count; analysis.TotalTestMethods = analysis.TestMethods.Count; } private Task CalculateCoverageMetrics(TestAnalysis analysis) { var testedMethods = new HashSet(); var totalMethods = analysis.SourceMethods.Count; // Simple heuristic-based coverage calculation foreach (var testMethod in analysis.TestMethods) { var potentialTargets = FindPotentialTestTargets(testMethod, analysis.SourceMethods); foreach (var target in potentialTargets) { testedMethods.Add($"{target.ClassName}.{target.MethodName}"); } } var methodCoverage = totalMethods > 0 ? (double)testedMethods.Count / totalMethods * 100 : 100; // Estimate other coverage metrics based on method coverage analysis.CoverageMetrics.MethodCoverage = methodCoverage; analysis.CoverageMetrics.LineCoverage = Math.Max(0, methodCoverage - 5); // Usually slightly lower analysis.CoverageMetrics.BranchCoverage = Math.Max(0, methodCoverage - 10); // Usually significantly lower analysis.CoverageMetrics.ClassCoverage = Math.Min(100, methodCoverage + 10); // Usually higher analysis.CoverageMetrics.TestedMethods = testedMethods.Count; analysis.CoverageMetrics.UntestedMethods = totalMethods - testedMethods.Count; return Task.CompletedTask; } private Task IdentifyUntestedFunctions(TestAnalysis analysis) { var testedMethodSignatures = new HashSet(); // Build set of tested method signatures foreach (var testMethod in analysis.TestMethods) { var targets = FindPotentialTestTargets(testMethod, analysis.SourceMethods); foreach (var target in targets) { testedMethodSignatures.Add($"{target.ClassName}.{target.MethodName}"); } } // Identify untested methods foreach (var sourceMethod in analysis.SourceMethods) { var signature = $"{sourceMethod.ClassName}.{sourceMethod.MethodName}"; if (!testedMethodSignatures.Contains(signature)) { var priority = CalculateTestPriority(sourceMethod); var suggestedTestTypes = SuggestTestTypes(sourceMethod); analysis.UntestedFunctions.Add(new UntestedFunction { ClassName = sourceMethod.ClassName, MethodName = sourceMethod.MethodName, FilePath = sourceMethod.FilePath, LineNumber = sourceMethod.LineNumber, Visibility = sourceMethod.IsPublic ? "Public" : sourceMethod.IsPrivate ? "Private" : "Internal", Complexity = sourceMethod.CyclomaticComplexity, Priority = priority, Rationale = GetTestRationale(sourceMethod, priority), SuggestedTestTypes = suggestedTestTypes }); } } return Task.CompletedTask; } private Task AnalyzeTestQualityMethod(TestAnalysis analysis) { foreach (var testMethod in analysis.TestMethods) { AnalyzeTestMethodQuality(testMethod, analysis); } // Analyze test class quality var testClasses = analysis.TestMethods.GroupBy(t => t.ClassName); foreach (var testClass in testClasses) { AnalyzeTestClassQuality(testClass.Key, testClass.ToList(), analysis); } return Task.CompletedTask; } private Task AnalyzeTestMethodQuality(TestMethod testMethod, TestAnalysis analysis) { var issues = new List(); // Check for missing assertions if (!testMethod.HasAssertions) { issues.Add(new TestQualityIssue { TestClass = testMethod.ClassName, TestMethod = testMethod.MethodName, IssueType = "Missing Assertions", Severity = "High", Description = "Test method has no assertions", FilePath = testMethod.FilePath, LineNumber = testMethod.LineNumber, Recommendation = "Add appropriate assertions to verify expected behavior", Impact = "Test provides no validation" }); } // Check for overly complex tests if (testMethod.LinesOfCode > 50) { issues.Add(new TestQualityIssue { TestClass = testMethod.ClassName, TestMethod = testMethod.MethodName, IssueType = "Complex Test", Severity = "Medium", Description = $"Test method is too long ({testMethod.LinesOfCode} lines)", FilePath = testMethod.FilePath, LineNumber = testMethod.LineNumber, Recommendation = "Break down into smaller, focused test methods", Impact = "Difficult to understand and maintain" }); } // Check for poor naming if (!IsGoodTestName(testMethod.MethodName)) { issues.Add(new TestQualityIssue { TestClass = testMethod.ClassName, TestMethod = testMethod.MethodName, IssueType = "Poor Naming", Severity = "Low", Description = "Test method name is not descriptive", FilePath = testMethod.FilePath, LineNumber = testMethod.LineNumber, Recommendation = "Use descriptive names that explain what is being tested", Impact = "Reduces test readability and maintenance" }); } // Check for hardcoded values if (testMethod.HasHardcodedValues) { issues.Add(new TestQualityIssue { TestClass = testMethod.ClassName, TestMethod = testMethod.MethodName, IssueType = "Hardcoded Values", Severity = "Medium", Description = "Test contains hardcoded values that reduce maintainability", FilePath = testMethod.FilePath, LineNumber = testMethod.LineNumber, Recommendation = "Use test data builders or parameterized tests", Impact = "Tests become brittle and hard to maintain" }); } // Check for missing test categories/attributes if (!testMethod.HasTestAttributes) { issues.Add(new TestQualityIssue { TestClass = testMethod.ClassName, TestMethod = testMethod.MethodName, IssueType = "Missing Test Attributes", Severity = "Low", Description = "Test method lacks proper test framework attributes", FilePath = testMethod.FilePath, LineNumber = testMethod.LineNumber, Recommendation = "Add appropriate test attributes (e.g., [Test], [Fact], [TestMethod])", Impact = "Test may not be executed by test runner" }); } analysis.TestQualityIssues.AddRange(issues); return Task.CompletedTask; } private Task AnalyzeTestClassQuality(string className, List testMethods, TestAnalysis analysis) { // Check for test class size if (testMethods.Count > 20) { analysis.TestQualityIssues.Add(new TestQualityIssue { TestClass = className, TestMethod = "N/A", IssueType = "Large Test Class", Severity = "Medium", Description = $"Test class has {testMethods.Count} test methods", FilePath = testMethods.First().FilePath, LineNumber = 1, Recommendation = "Split into smaller, focused test classes", Impact = "Difficult to navigate and maintain" }); } // Check for missing setup/teardown var hasSetup = testMethods.Any(t => IsSetupMethod(t.MethodName)); var hasTeardown = testMethods.Any(t => IsTeardownMethod(t.MethodName)); if (testMethods.Count > 5 && !hasSetup) { analysis.TestQualityIssues.Add(new TestQualityIssue { TestClass = className, TestMethod = "N/A", IssueType = "Missing Test Setup", Severity = "Low", Description = "Large test class lacks setup methods", FilePath = testMethods.First().FilePath, LineNumber = 1, Recommendation = "Consider adding setup methods for common test initialization", Impact = "Code duplication in test methods" }); } return Task.CompletedTask; } private Task GenerateTestStubsMethod(TestAnalysis analysis) { var highPriorityUntested = analysis.UntestedFunctions .Where(u => u.Priority >= 7) .Take(10) // Limit to prevent overwhelming output .ToList(); foreach (var untested in highPriorityUntested) { var sourceMethod = analysis.SourceMethods .FirstOrDefault(s => s.ClassName == untested.ClassName && s.MethodName == untested.MethodName); if (sourceMethod != null) { var testStub = GenerateTestStub(sourceMethod); analysis.GeneratedTestStubs.Add(testStub); } } return Task.CompletedTask; } private Task SuggestAdvancedTestingMethod(TestAnalysis analysis) { foreach (var sourceMethod in analysis.SourceMethods) { // Property-based testing suggestions if (IsSuitableForPropertyBasedTesting(sourceMethod)) { analysis.AdvancedTestingSuggestions.Add(new AdvancedTestingSuggestion { TestingType = "Property-Based Testing", TargetClass = sourceMethod.ClassName, TargetMethod = sourceMethod.MethodName, Rationale = "Method has mathematical properties that can be verified with random inputs", Benefit = "Discovers edge cases and increases confidence in correctness", Implementation = "Use FsCheck or similar property-based testing framework", Priority = 7, EstimatedEffort = 4 }); } // Fuzz testing suggestions if (IsSuitableForFuzzTesting(sourceMethod)) { analysis.AdvancedTestingSuggestions.Add(new AdvancedTestingSuggestion { TestingType = "Fuzz Testing", TargetClass = sourceMethod.ClassName, TargetMethod = sourceMethod.MethodName, Rationale = "Method processes external input and could benefit from fuzz testing", Benefit = "Discovers security vulnerabilities and crash scenarios", Implementation = "Generate random/malformed inputs to test robustness", Priority = 8, EstimatedEffort = 3 }); } // Performance testing suggestions if (IsSuitableForPerformanceTesting(sourceMethod)) { analysis.AdvancedTestingSuggestions.Add(new AdvancedTestingSuggestion { TestingType = "Performance Testing", TargetClass = sourceMethod.ClassName, TargetMethod = sourceMethod.MethodName, Rationale = "Method appears to be performance-critical", Benefit = "Ensures performance requirements are met and regression is detected", Implementation = "Use BenchmarkDotNet or similar performance testing framework", Priority = 6, EstimatedEffort = 6 }); } // Integration testing suggestions if (IsSuitableForIntegrationTesting(sourceMethod)) { analysis.AdvancedTestingSuggestions.Add(new AdvancedTestingSuggestion { TestingType = "Integration Testing", TargetClass = sourceMethod.ClassName, TargetMethod = sourceMethod.MethodName, Rationale = "Method interacts with external systems", Benefit = "Validates end-to-end functionality and system interactions", Implementation = "Create integration tests with test containers or mocked services", Priority = 8, EstimatedEffort = 8 }); } } return Task.CompletedTask; } private Task CheckRedundantTestsMethod(TestAnalysis analysis) { var testsByTarget = analysis.TestMethods .GroupBy(t => GetTestTarget(t)) .Where(g => g.Count() > 1); foreach (var group in testsByTarget) { var tests = group.ToList(); for (int i = 0; i < tests.Count; i++) { for (int j = i + 1; j < tests.Count; j++) { var similarity = CalculateTestSimilarity(tests[i], tests[j]); if (similarity > 0.8) // 80% similar { analysis.RedundantTests.Add(new RedundantTest { TestClass = tests[i].ClassName, TestMethod = tests[i].MethodName, RedundancyType = "Duplicate Test Logic", Description = $"Very similar to {tests[j].ClassName}.{tests[j].MethodName}", RelatedTests = new List { $"{tests[j].ClassName}.{tests[j].MethodName}" }, Recommendation = "Consolidate similar tests or ensure they test different scenarios", FilePath = tests[i].FilePath, LineNumber = tests[i].LineNumber }); } } } } return Task.CompletedTask; } // Helper methods for test analysis private bool IsTestFile(string filePath) { var fileName = Path.GetFileName(filePath).ToLowerInvariant(); var directory = Path.GetDirectoryName(filePath)?.ToLowerInvariant() ?? string.Empty; return fileName.Contains("test") || fileName.Contains("spec") || directory.Contains("test") || directory.Contains("spec") || fileName.EndsWith("tests.cs") || fileName.EndsWith("test.cs"); } private async Task> ExtractSourceMethods(string filePath) { var methods = new List(); var sourceCode = await File.ReadAllTextAsync(filePath); var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath); var root = await syntaxTree.GetRootAsync(); var methodDeclarations = root.DescendantNodes().OfType(); foreach (var method in methodDeclarations) { var className = GetContainingClassName(method); methods.Add(new SourceMethod { ClassName = className, MethodName = method.Identifier.ValueText, FilePath = filePath, LineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1, IsPublic = method.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)), IsPrivate = method.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)), IsStatic = method.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)), ReturnType = method.ReturnType.ToString(), ParameterCount = method.ParameterList.Parameters.Count, LinesOfCode = CalculateMethodLines(method), CyclomaticComplexity = CalculateMethodComplexity(method), HasBusinessLogic = HasBusinessLogic(method), AccessesDatabase = AccessesDatabase(method), AccessesExternalServices = AccessesExternalServices(method) }); } return methods; } private async Task> ExtractTestMethods(string filePath) { var methods = new List(); var sourceCode = await File.ReadAllTextAsync(filePath); var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath); var root = await syntaxTree.GetRootAsync(); var methodDeclarations = root.DescendantNodes().OfType(); foreach (var method in methodDeclarations) { var className = GetContainingClassName(method); methods.Add(new TestMethod { ClassName = className, MethodName = method.Identifier.ValueText, FilePath = filePath, LineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1, LinesOfCode = CalculateMethodLines(method), HasTestAttributes = HasTestAttributes(method), HasAssertions = HasAssertions(method), HasHardcodedValues = HasHardcodedValues(method), TestFramework = DetectTestFramework(method), TestType = DetectTestType(method) }); } return methods; } private List FindPotentialTestTargets(TestMethod testMethod, List sourceMethods) { var targets = new List(); var testName = testMethod.MethodName.ToLowerInvariant(); var testClass = testMethod.ClassName.ToLowerInvariant(); foreach (var sourceMethod in sourceMethods) { var sourceName = sourceMethod.MethodName.ToLowerInvariant(); var sourceClass = sourceMethod.ClassName.ToLowerInvariant(); // Check if test name contains source method name if (testName.Contains(sourceName) || testName.Contains(sourceClass)) { targets.Add(sourceMethod); continue; } // Check if test class targets source class if (testClass.Replace("test", "").Replace("tests", "") == sourceClass) { targets.Add(sourceMethod); continue; } // Check for conventional naming patterns if (IsConventionalTestNaming(testMethod, sourceMethod)) { targets.Add(sourceMethod); } } return targets; } private int CalculateTestPriority(SourceMethod method) { var priority = 5; // Base priority // Higher priority for public methods if (method.IsPublic) priority += 2; // Higher priority for complex methods if (method.CyclomaticComplexity > 5) priority += 2; // Higher priority for business logic if (method.HasBusinessLogic) priority += 2; // Higher priority for database/external service access if (method.AccessesDatabase || method.AccessesExternalServices) priority += 1; // Lower priority for getters/setters if (IsPropertyAccessor(method.MethodName)) priority -= 3; return Math.Max(1, Math.Min(10, priority)); } private List SuggestTestTypes(SourceMethod method) { var testTypes = new List(); // Unit tests for most methods testTypes.Add("Unit Test"); // Integration tests for database/external service methods if (method.AccessesDatabase || method.AccessesExternalServices) { testTypes.Add("Integration Test"); } // Performance tests for complex algorithms if (method.CyclomaticComplexity > 10) { testTypes.Add("Performance Test"); } // Property-based tests for mathematical functions if (IsMathematicalFunction(method)) { testTypes.Add("Property-Based Test"); } // Security tests for input validation methods if (IsInputValidationMethod(method)) { testTypes.Add("Security Test"); } return testTypes; } private string GetTestRationale(SourceMethod method, int priority) { var reasons = new List(); if (method.IsPublic) reasons.Add("public API"); if (method.CyclomaticComplexity > 5) reasons.Add($"complex logic (complexity: {method.CyclomaticComplexity})"); if (method.HasBusinessLogic) reasons.Add("business logic"); if (method.AccessesDatabase) reasons.Add("database access"); if (method.AccessesExternalServices) reasons.Add("external service calls"); var rationale = reasons.Any() ? $"Critical to test due to: {string.Join(", ", reasons)}" : "Should be tested for completeness"; return $"{rationale} (Priority: {priority}/10)"; } private TestStub GenerateTestStub(SourceMethod method) { var testClassName = $"{method.ClassName}Tests"; var testMethodName = $"{method.MethodName}_Should_ReturnExpectedResult"; var testFramework = "NUnit"; // Default framework var scenarios = new List(); // Generate basic test scenarios scenarios.Add("Valid input returns expected result"); if (method.ParameterCount > 0) { scenarios.Add("Null input throws ArgumentNullException"); scenarios.Add("Invalid input throws ArgumentException"); } if (method.ReturnType != "void") { scenarios.Add("Boundary values return correct results"); } // Generate test code stub var testCode = GenerateTestCode(method, testClassName, testMethodName, testFramework); return new TestStub { TargetClass = method.ClassName, TargetMethod = method.MethodName, TestClassName = testClassName, TestMethodName = testMethodName, TestFramework = testFramework, GeneratedCode = testCode, TestScenarios = scenarios }; } private string GenerateTestCode(SourceMethod method, string testClassName, string testMethodName, string framework) { var code = new List(); // Add usings code.Add("using NUnit.Framework;"); code.Add("using System;"); code.Add(""); // Add test class code.Add($"[TestFixture]"); code.Add($"public class {testClassName}"); code.Add("{"); // Add setup if needed if (method.AccessesDatabase || method.AccessesExternalServices) { code.Add(" private Mock _mockDependency;"); code.Add(" private SampleClass _sut;"); code.Add(""); code.Add(" [SetUp]"); code.Add(" public void SetUp()"); code.Add(" {"); code.Add(" _mockDependency = new Mock();"); code.Add(" _sut = new SampleClass(_mockDependency.Object);"); code.Add(" }"); code.Add(""); } // Add test method code.Add(" [Test]"); code.Add($" public void {testMethodName}()"); code.Add(" {"); code.Add(" // Arrange"); if (method.ParameterCount > 0) { code.Add(" var input = /* TODO: Provide test input */;"); } code.Add(" var expected = /* TODO: Define expected result */;"); code.Add(""); code.Add(" // Act"); var methodCall = method.ParameterCount > 0 ? $"var result = sut.{method.MethodName}(input);" : $"var result = sut.{method.MethodName}();"; code.Add($" {methodCall}"); code.Add(""); code.Add(" // Assert"); if (method.ReturnType != "void") { code.Add(" Assert.That(result, Is.EqualTo(expected));"); } else { code.Add(" Assert.That(() => /* TODO: Add appropriate assertion */, Throws.Nothing);"); } code.Add(" }"); code.Add("}"); return string.Join(Environment.NewLine, code); } private int CalculateTestQualityScore(TestAnalysis analysis) { var score = 100; // Coverage impact (40%) var coverageScore = analysis.CoverageMetrics.MethodCoverage; score = (int)(score * 0.6 + coverageScore * 0.4); // Quality issues impact (30%) var highIssues = analysis.TestQualityIssues.Count(i => i.Severity == "High"); var mediumIssues = analysis.TestQualityIssues.Count(i => i.Severity == "Medium"); var lowIssues = analysis.TestQualityIssues.Count(i => i.Severity == "Low"); var qualityPenalty = highIssues * 10 + mediumIssues * 5 + lowIssues * 2; score -= Math.Min(40, qualityPenalty); // Test-to-source ratio impact (20%) var testRatio = analysis.TotalSourceMethods > 0 ? (double)analysis.TotalTestMethods / analysis.TotalSourceMethods : 0; if (testRatio >= 1.0) score += 10; // Bonus for good test ratio else if (testRatio < 0.5) score -= 10; // Penalty for low test ratio // Redundancy penalty (10%) var redundancyPenalty = analysis.RedundantTests.Count * 2; score -= Math.Min(10, redundancyPenalty); return Math.Max(0, Math.Min(100, score)); } private List GenerateTestImprovementPlan(TestAnalysis analysis) { var plan = new List(); // Phase 1: Critical quality issues var criticalIssues = analysis.TestQualityIssues.Where(i => i.Severity == "High").ToList(); if (criticalIssues.Any()) { plan.Add(new TestImprovementAction { Phase = 1, Priority = "Critical", Title = "Fix Critical Test Quality Issues", Description = $"Address {criticalIssues.Count} high-severity test quality issues", EstimatedHours = criticalIssues.Count * 0.5, ExpectedBenefit = "Ensure tests provide reliable validation", Dependencies = new List() }); } // Phase 2: High-priority untested functions var highPriorityUntested = analysis.UntestedFunctions.Where(u => u.Priority >= 8).ToList(); if (highPriorityUntested.Any()) { plan.Add(new TestImprovementAction { Phase = 2, Priority = "High", Title = "Test Critical Untested Functions", Description = $"Add tests for {highPriorityUntested.Count} high-priority untested functions", EstimatedHours = highPriorityUntested.Count * 2, ExpectedBenefit = "Cover most critical business logic and public APIs", Dependencies = new List { "Test infrastructure setup" } }); } // Phase 3: Improve coverage to 80% if (analysis.CoverageMetrics.MethodCoverage < 80) { var methodsToTest = (int)((80 - analysis.CoverageMetrics.MethodCoverage) / 100 * analysis.TotalSourceMethods); plan.Add(new TestImprovementAction { Phase = 3, Priority = "High", Title = "Achieve 80% Test Coverage", Description = $"Add tests for approximately {methodsToTest} additional methods", EstimatedHours = methodsToTest * 1.5, ExpectedBenefit = "Reach industry standard test coverage levels", Dependencies = new List { "High-priority tests completed" } }); } // Phase 4: Remove redundant tests if (analysis.RedundantTests.Any()) { plan.Add(new TestImprovementAction { Phase = 4, Priority = "Medium", Title = "Remove Redundant Tests", Description = $"Consolidate or remove {analysis.RedundantTests.Count} redundant tests", EstimatedHours = analysis.RedundantTests.Count * 0.25, ExpectedBenefit = "Improve test maintainability and execution speed", Dependencies = new List() }); } // Phase 5: Advanced testing var advancedSuggestions = analysis.AdvancedTestingSuggestions.Where(a => a.Priority >= 7).ToList(); if (advancedSuggestions.Any()) { plan.Add(new TestImprovementAction { Phase = 5, Priority = "Medium", Title = "Implement Advanced Testing", Description = $"Add {advancedSuggestions.Count} advanced testing scenarios (property-based, fuzz, etc.)", EstimatedHours = advancedSuggestions.Sum(a => a.EstimatedEffort), ExpectedBenefit = "Discover edge cases and improve test robustness", Dependencies = new List { "Core test coverage completed" } }); } return plan; } // Utility methods private string GetContainingClassName(SyntaxNode node) { var classDeclaration = node.Ancestors().OfType().FirstOrDefault(); return classDeclaration?.Identifier.ValueText ?? "Unknown"; } private int CalculateMethodLines(MethodDeclarationSyntax method) { var span = method.GetLocation().GetLineSpan(); return span.EndLinePosition.Line - span.StartLinePosition.Line + 1; } private int CalculateMethodComplexity(MethodDeclarationSyntax method) { var complexity = 1; var descendants = method.DescendantNodes(); complexity += descendants.OfType().Count(); complexity += descendants.OfType().Count(); complexity += descendants.OfType().Count(); complexity += descendants.OfType().Count(); complexity += descendants.OfType().Count(); complexity += descendants.OfType().Count(); return complexity; } private bool HasBusinessLogic(MethodDeclarationSyntax method) { var methodName = method.Identifier.ValueText.ToLowerInvariant(); var businessKeywords = new[] { "calculate", "process", "validate", "execute", "handle", "manage", "transform" }; return businessKeywords.Any(keyword => methodName.Contains(keyword)); } private bool AccessesDatabase(MethodDeclarationSyntax method) { var methodBody = method.Body?.ToString() ?? ""; var dbKeywords = new[] { "connection", "command", "query", "sql", "database", "repository", "entity" }; return dbKeywords.Any(keyword => methodBody.ToLowerInvariant().Contains(keyword)); } private bool AccessesExternalServices(MethodDeclarationSyntax method) { var methodBody = method.Body?.ToString() ?? ""; var serviceKeywords = new[] { "httpclient", "webclient", "api", "service", "rest", "soap", "endpoint" }; return serviceKeywords.Any(keyword => methodBody.ToLowerInvariant().Contains(keyword)); } private bool HasTestAttributes(MethodDeclarationSyntax method) { var attributes = method.AttributeLists.SelectMany(al => al.Attributes); var testAttributes = new[] { "test", "testmethod", "fact", "theory", "testcase" }; return attributes.Any(attr => testAttributes.Any(ta => attr.Name.ToString().ToLowerInvariant().Contains(ta))); } private bool HasAssertions(MethodDeclarationSyntax method) { var methodBody = method.Body?.ToString() ?? ""; var assertKeywords = new[] { "assert", "should", "expect", "verify" }; return assertKeywords.Any(keyword => methodBody.ToLowerInvariant().Contains(keyword)); } private bool HasHardcodedValues(MethodDeclarationSyntax method) { var methodBody = method.Body?.ToString() ?? ""; var literals = method.DescendantNodes().OfType().Count(); return literals > 3; // Simple heuristic } private string DetectTestFramework(MethodDeclarationSyntax method) { var attributes = method.AttributeLists.SelectMany(al => al.Attributes) .Select(a => a.Name.ToString().ToLowerInvariant()); if (attributes.Any(a => a.Contains("test") && !a.Contains("method"))) return "NUnit"; if (attributes.Any(a => a.Contains("testmethod"))) return "MSTest"; if (attributes.Any(a => a.Contains("fact") || a.Contains("theory"))) return "xUnit"; return "Unknown"; } private string DetectTestType(MethodDeclarationSyntax method) { var methodName = method.Identifier.ValueText.ToLowerInvariant(); if (methodName.Contains("integration")) return "Integration"; if (methodName.Contains("performance") || methodName.Contains("benchmark")) return "Performance"; if (methodName.Contains("security")) return "Security"; return "Unit"; } private bool IsGoodTestName(string methodName) { // Check for descriptive test naming patterns var goodPatterns = new[] { "should", "when", "given", "returns", "throws", "validates" }; var name = methodName.ToLowerInvariant(); return goodPatterns.Any(pattern => name.Contains(pattern)) && name.Length > 10 && !name.Equals("test"); } private bool IsSetupMethod(string methodName) { var setupNames = new[] { "setup", "init", "arrange", "beforeeach", "beforetest" }; return setupNames.Any(name => methodName.ToLowerInvariant().Contains(name)); } private bool IsTeardownMethod(string methodName) { var teardownNames = new[] { "teardown", "cleanup", "dispose", "aftereach", "aftertest" }; return teardownNames.Any(name => methodName.ToLowerInvariant().Contains(name)); } private bool IsPropertyAccessor(string methodName) { return methodName.StartsWith("get_") || methodName.StartsWith("set_") || methodName.Equals("ToString") || methodName.Equals("GetHashCode"); } private bool IsMathematicalFunction(SourceMethod method) { var mathKeywords = new[] { "calculate", "compute", "sum", "average", "min", "max", "sqrt", "pow" }; return mathKeywords.Any(keyword => method.MethodName.ToLowerInvariant().Contains(keyword)); } private bool IsInputValidationMethod(SourceMethod method) { var validationKeywords = new[] { "validate", "verify", "check", "parse", "sanitize" }; return validationKeywords.Any(keyword => method.MethodName.ToLowerInvariant().Contains(keyword)); } private bool IsSuitableForPropertyBasedTesting(SourceMethod method) { return IsMathematicalFunction(method) && method.ParameterCount > 0 && method.ReturnType != "void"; } private bool IsSuitableForFuzzTesting(SourceMethod method) { return method.ParameterCount > 0 && (IsInputValidationMethod(method) || method.MethodName.ToLowerInvariant().Contains("parse")); } private bool IsSuitableForPerformanceTesting(SourceMethod method) { return method.CyclomaticComplexity > 5 || method.MethodName.ToLowerInvariant().Contains("process") || method.MethodName.ToLowerInvariant().Contains("calculate"); } private bool IsSuitableForIntegrationTesting(SourceMethod method) { return method.AccessesDatabase || method.AccessesExternalServices; } private bool IsConventionalTestNaming(TestMethod testMethod, SourceMethod sourceMethod) { var testName = testMethod.MethodName.ToLowerInvariant(); var sourceName = sourceMethod.MethodName.ToLowerInvariant(); // Check for conventional patterns like Test_MethodName, MethodName_Test, etc. return testName.Contains($"test{sourceName}") || testName.Contains($"{sourceName}test") || testName.StartsWith(sourceName) || testName.EndsWith(sourceName); } private string GetTestTarget(TestMethod testMethod) { var name = testMethod.MethodName.ToLowerInvariant(); // Extract likely target method name from test name name = name.Replace("test", "").Replace("should", "").Replace("when", "").Replace("_", ""); return $"{testMethod.ClassName.Replace("Test", "").Replace("Tests", "")}.{name}"; } private double CalculateTestSimilarity(TestMethod test1, TestMethod test2) { // Simple similarity calculation based on method content analysis // In a real implementation, this would analyze the actual test logic var name1 = test1.MethodName.ToLowerInvariant(); var name2 = test2.MethodName.ToLowerInvariant(); // Calculate string similarity var commonWords = name1.Split('_').Intersect(name2.Split('_')).Count(); var totalWords = name1.Split('_').Union(name2.Split('_')).Count(); return totalWords > 0 ? (double)commonWords / totalWords : 0; } private string GetCoverageLevel(double coverage) { return coverage switch { >= 90 => "Excellent", >= 80 => "Good", >= 70 => "Fair", >= 50 => "Poor", _ => "Critical" }; } private string GetTestHealth(int score) { return score switch { >= 80 => "Excellent", >= 60 => "Good", >= 40 => "Fair", >= 20 => "Poor", _ => "Critical" }; } private List GetTopTestRecommendations(TestAnalysis analysis) { var recommendations = new List(); // Coverage recommendations if (analysis.CoverageMetrics.MethodCoverage < 80) { recommendations.Add($"Increase test coverage from {analysis.CoverageMetrics.MethodCoverage:F1}% to 80%"); } // Quality recommendations var highQualityIssues = analysis.TestQualityIssues.Count(i => i.Severity == "High"); if (highQualityIssues > 0) { recommendations.Add($"Fix {highQualityIssues} high-severity test quality issues"); } // Untested critical functions var criticalUntested = analysis.UntestedFunctions.Count(u => u.Priority >= 8); if (criticalUntested > 0) { recommendations.Add($"Add tests for {criticalUntested} critical untested functions"); } // Advanced testing var advancedOpportunities = analysis.AdvancedTestingSuggestions.Count(a => a.Priority >= 7); if (advancedOpportunities > 0) { recommendations.Add($"Consider {advancedOpportunities} advanced testing opportunities"); } // Redundancy cleanup if (analysis.RedundantTests.Any()) { recommendations.Add($"Remove or consolidate {analysis.RedundantTests.Count} redundant tests"); } if (!recommendations.Any()) { recommendations.Add("Test suite is in good shape - continue maintaining quality standards"); } return recommendations.Take(5).ToList(); } } // Supporting data structures for test analysis public class TestAnalysis { public string ProjectPath { get; set; } = string.Empty; public DateTime AnalysisDate { get; set; } public List SourceFiles { get; set; } = new(); public List TestFiles { get; set; } = new(); public int TotalSourceFiles { get; set; } public int TotalTestFiles { get; set; } public List SourceMethods { get; set; } = new(); public List TestMethods { get; set; } = new(); public int TotalSourceMethods { get; set; } public int TotalTestMethods { get; set; } public CoverageMetrics CoverageMetrics { get; set; } = new(); public List UntestedFunctions { get; set; } = new(); public List TestQualityIssues { get; set; } = new(); public List GeneratedTestStubs { get; set; } = new(); public List AdvancedTestingSuggestions { get; set; } = new(); public List RedundantTests { get; set; } = new(); } public class CoverageMetrics { public double LineCoverage { get; set; } public double BranchCoverage { get; set; } public double MethodCoverage { get; set; } public double ClassCoverage { get; set; } public int TestedMethods { get; set; } public int UntestedMethods { get; set; } } public class SourceMethod { public string ClassName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public int LineNumber { get; set; } public bool IsPublic { get; set; } public bool IsPrivate { get; set; } public bool IsStatic { get; set; } public string ReturnType { get; set; } = string.Empty; public int ParameterCount { get; set; } public int LinesOfCode { get; set; } public int CyclomaticComplexity { get; set; } public bool HasBusinessLogic { get; set; } public bool AccessesDatabase { get; set; } public bool AccessesExternalServices { get; set; } } public class TestMethod { public string ClassName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public int LineNumber { get; set; } public int LinesOfCode { get; set; } public bool HasTestAttributes { get; set; } public bool HasAssertions { get; set; } public bool HasHardcodedValues { get; set; } public string TestFramework { get; set; } = string.Empty; public string TestType { get; set; } = string.Empty; } public class UntestedFunction { public string ClassName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public int LineNumber { get; set; } public string Visibility { get; set; } = string.Empty; public int Complexity { get; set; } public int Priority { get; set; } public string Rationale { get; set; } = string.Empty; public List SuggestedTestTypes { get; set; } = new(); } public class TestQualityIssue { public string TestClass { get; set; } = string.Empty; public string TestMethod { get; set; } = string.Empty; public string IssueType { get; set; } = string.Empty; public string Severity { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public int LineNumber { get; set; } public string Recommendation { get; set; } = string.Empty; public string Impact { get; set; } = string.Empty; } public class TestStub { public string TargetClass { get; set; } = string.Empty; public string TargetMethod { get; set; } = string.Empty; public string TestClassName { get; set; } = string.Empty; public string TestMethodName { get; set; } = string.Empty; public string TestFramework { get; set; } = string.Empty; public string GeneratedCode { get; set; } = string.Empty; public List TestScenarios { get; set; } = new(); } public class AdvancedTestingSuggestion { public string TestingType { get; set; } = string.Empty; public string TargetClass { get; set; } = string.Empty; public string TargetMethod { get; set; } = string.Empty; public string Rationale { get; set; } = string.Empty; public string Benefit { get; set; } = string.Empty; public string Implementation { get; set; } = string.Empty; public int Priority { get; set; } public int EstimatedEffort { get; set; } } public class RedundantTest { public string TestClass { get; set; } = string.Empty; public string TestMethod { get; set; } = string.Empty; public string RedundancyType { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public List RelatedTests { get; set; } = new(); public string Recommendation { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public int LineNumber { get; set; } } public class TestImprovementAction { public int Phase { get; set; } public string Priority { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public double EstimatedHours { get; set; } public string ExpectedBenefit { get; set; } = string.Empty; public List Dependencies { get; set; } = new(); } }