1449 lines
48 KiB
C#
Executable File
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();
|
|
}
|
|
} |