MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Analysis/TestAnalysisPlugin.cs

1449 lines
48 KiB
C#
Executable File

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<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["projectPath"] = typeof(string),
["calculateCoverage"] = typeof(bool),
["identifyUntested"] = typeof(bool),
["analyzeTestQuality"] = typeof(bool),
["generateTestStubs"] = typeof(bool),
["suggestAdvancedTesting"] = typeof(bool),
["checkRedundantTests"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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<UntestedFunction>(),
TestQualityIssues = new List<TestQualityIssue>(),
GeneratedTestStubs = new List<TestStub>(),
AdvancedTestingSuggestions = new List<AdvancedTestingSuggestion>(),
RedundantTests = new List<RedundantTest>()
};
// 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<SourceMethod>();
analysis.TestMethods = new List<TestMethod>();
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<string>();
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<string>();
// 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<TestQualityIssue>();
// 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<TestMethod> 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<string> { $"{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<List<SourceMethod>> ExtractSourceMethods(string filePath)
{
var methods = new List<SourceMethod>();
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methodDeclarations = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
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<List<TestMethod>> ExtractTestMethods(string filePath)
{
var methods = new List<TestMethod>();
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methodDeclarations = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
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<SourceMethod> FindPotentialTestTargets(TestMethod testMethod, List<SourceMethod> sourceMethods)
{
var targets = new List<SourceMethod>();
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<string> SuggestTestTypes(SourceMethod method)
{
var testTypes = new List<string>();
// 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<string>();
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<string>();
// 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<string>();
// 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<IDependency> _mockDependency;");
code.Add(" private SampleClass _sut;");
code.Add("");
code.Add(" [SetUp]");
code.Add(" public void SetUp()");
code.Add(" {");
code.Add(" _mockDependency = new Mock<IDependency>();");
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<TestImprovementAction> GenerateTestImprovementPlan(TestAnalysis analysis)
{
var plan = new List<TestImprovementAction>();
// 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<string>()
});
}
// 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<string> { "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<string> { "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<string>()
});
}
// 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<string> { "Core test coverage completed" }
});
}
return plan;
}
// Utility methods
private string GetContainingClassName(SyntaxNode node)
{
var classDeclaration = node.Ancestors().OfType<ClassDeclarationSyntax>().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<IfStatementSyntax>().Count();
complexity += descendants.OfType<WhileStatementSyntax>().Count();
complexity += descendants.OfType<ForStatementSyntax>().Count();
complexity += descendants.OfType<ForEachStatementSyntax>().Count();
complexity += descendants.OfType<SwitchStatementSyntax>().Count();
complexity += descendants.OfType<CatchClauseSyntax>().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<LiteralExpressionSyntax>().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<string> GetTopTestRecommendations(TestAnalysis analysis)
{
var recommendations = new List<string>();
// 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<string> SourceFiles { get; set; } = new();
public List<string> TestFiles { get; set; } = new();
public int TotalSourceFiles { get; set; }
public int TotalTestFiles { get; set; }
public List<SourceMethod> SourceMethods { get; set; } = new();
public List<TestMethod> TestMethods { get; set; } = new();
public int TotalSourceMethods { get; set; }
public int TotalTestMethods { get; set; }
public CoverageMetrics CoverageMetrics { get; set; } = new();
public List<UntestedFunction> UntestedFunctions { get; set; } = new();
public List<TestQualityIssue> TestQualityIssues { get; set; } = new();
public List<TestStub> GeneratedTestStubs { get; set; } = new();
public List<AdvancedTestingSuggestion> AdvancedTestingSuggestions { get; set; } = new();
public List<RedundantTest> 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<string> 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<string> 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<string> 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<string> Dependencies { get; set; } = new();
}
}