import { parseFlutterCode } from '../utils/parser.js';
export async function analyzeTestCoverage(args) {
const { projectPath, includeVisualReport = true, threshold = 80 } = args;
try {
// Simulate test coverage analysis
const coverageData = await gatherCoverageData(projectPath);
const analysis = analyzeCoverageGaps(coverageData);
const recommendations = generateCoverageRecommendations(analysis, threshold);
const visualReport = includeVisualReport ? generateVisualReport(coverageData) : null;
return {
content: [
{
type: 'text',
text: JSON.stringify({
summary: {
totalCoverage: coverageData.overall,
lineCoverage: coverageData.lines,
branchCoverage: coverageData.branches,
functionCoverage: coverageData.functions,
status: coverageData.overall >= threshold ? 'PASSING' : 'FAILING',
},
byFile: coverageData.files,
uncoveredCode: analysis.uncoveredAreas,
criticalGaps: analysis.criticalGaps,
recommendations,
visualReport,
testCommands: generateTestCommands(analysis),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error analyzing test coverage: ${error.message}`,
},
],
};
}
}
async function gatherCoverageData(projectPath) {
// Simulate gathering coverage data
// In real implementation, this would parse lcov.info or coverage reports
return {
overall: 75.5,
lines: 78.2,
branches: 68.4,
functions: 82.1,
files: [
{
path: 'lib/widgets/home_widget.dart',
coverage: 92.3,
lines: { covered: 120, total: 130 },
uncoveredLines: [45, 67, 89, 90, 91, 102, 103, 104, 115, 125],
},
{
path: 'lib/services/api_service.dart',
coverage: 65.4,
lines: { covered: 85, total: 130 },
uncoveredLines: generateUncoveredLines(45),
},
{
path: 'lib/models/user_model.dart',
coverage: 100,
lines: { covered: 45, total: 45 },
uncoveredLines: [],
},
{
path: 'lib/widgets/profile_widget.dart',
coverage: 45.2,
lines: { covered: 47, total: 104 },
uncoveredLines: generateUncoveredLines(57),
},
{
path: 'lib/utils/validators.dart',
coverage: 88.9,
lines: { covered: 40, total: 45 },
uncoveredLines: [12, 23, 34, 41, 42],
},
],
};
}
function generateUncoveredLines(count) {
const lines = [];
for (let i = 0; i < count; i++) {
lines.push(Math.floor(Math.random() * 200) + 1);
}
return lines.sort((a, b) => a - b);
}
function analyzeCoverageGaps(coverageData) {
const analysis = {
uncoveredAreas: [],
criticalGaps: [],
byCategory: {
widgets: [],
services: [],
models: [],
utils: [],
},
};
coverageData.files.forEach(file => {
const category = categorizeFile(file.path);
if (file.coverage < 80) {
analysis.uncoveredAreas.push({
file: file.path,
coverage: file.coverage,
missingLines: file.lines.total - file.lines.covered,
category,
severity: file.coverage < 50 ? 'critical' : 'warning',
});
if (file.coverage < 50) {
analysis.criticalGaps.push({
file: file.path,
coverage: file.coverage,
reason: determineCriticalReason(file, category),
impact: 'High risk of undetected bugs',
});
}
}
analysis.byCategory[category].push({
file: file.path,
coverage: file.coverage,
});
});
// Analyze patterns in uncovered code
analysis.patterns = detectCoveragePatterns(coverageData);
return analysis;
}
function categorizeFile(filePath) {
if (filePath.includes('/widgets/')) return 'widgets';
if (filePath.includes('/services/')) return 'services';
if (filePath.includes('/models/')) return 'models';
if (filePath.includes('/utils/')) return 'utils';
return 'other';
}
function determineCriticalReason(file, category) {
const reasons = {
widgets: 'UI components need testing to prevent visual bugs',
services: 'Business logic requires thorough testing',
models: 'Data models need validation testing',
utils: 'Utility functions are used across the app',
};
return reasons[category] || 'Core functionality lacks test coverage';
}
function detectCoveragePatterns(coverageData) {
const patterns = [];
// Check for common patterns
const errorHandlingGap = coverageData.files.some(f =>
f.path.includes('service') && f.coverage < 70
);
if (errorHandlingGap) {
patterns.push({
type: 'error_handling',
message: 'Error handling code appears to be under-tested',
recommendation: 'Add tests for error scenarios and edge cases',
});
}
const widgetTestingGap = coverageData.files.filter(f =>
f.path.includes('widget') && f.coverage < 80
).length > 2;
if (widgetTestingGap) {
patterns.push({
type: 'widget_testing',
message: 'Multiple widgets have low test coverage',
recommendation: 'Implement widget tests using flutter_test',
});
}
return patterns;
}
function generateCoverageRecommendations(analysis, threshold) {
const recommendations = [];
// Priority 1: Critical gaps
analysis.criticalGaps.forEach(gap => {
recommendations.push({
priority: 'critical',
file: gap.file,
action: `Increase coverage from ${gap.coverage}% to at least ${threshold}%`,
reason: gap.reason,
suggestedTests: generateSuggestedTests(gap.file),
});
});
// Priority 2: Below threshold files
analysis.uncoveredAreas
.filter(area => area.severity === 'warning')
.forEach(area => {
recommendations.push({
priority: 'high',
file: area.file,
action: `Add ${area.missingLines} more lines of test coverage`,
suggestedTests: generateSuggestedTests(area.file),
});
});
// Priority 3: Pattern-based recommendations
analysis.patterns.forEach(pattern => {
recommendations.push({
priority: 'medium',
category: pattern.type,
action: pattern.recommendation,
affectedFiles: analysis.uncoveredAreas
.filter(a => a.category === pattern.type.split('_')[0])
.map(a => a.file),
});
});
// General recommendations
if (analysis.byCategory.widgets.some(w => w.coverage < 90)) {
recommendations.push({
priority: 'medium',
category: 'widgets',
action: 'Implement golden tests for visual regression testing',
});
}
return recommendations;
}
function generateSuggestedTests(filePath) {
const suggestions = [];
const fileName = filePath.split('/').pop().replace('.dart', '');
if (filePath.includes('widget')) {
suggestions.push(
`Widget build test for ${fileName}`,
`Interaction tests for user inputs`,
`State management tests`,
`Edge case handling tests`
);
} else if (filePath.includes('service')) {
suggestions.push(
`Success response tests`,
`Error handling tests`,
`Network timeout tests`,
`Data transformation tests`
);
} else if (filePath.includes('model')) {
suggestions.push(
`Serialization/deserialization tests`,
`Validation tests`,
`Edge case data tests`
);
} else if (filePath.includes('utils')) {
suggestions.push(
`Input validation tests`,
`Boundary condition tests`,
`Null safety tests`
);
}
return suggestions;
}
function generateVisualReport(coverageData) {
const report = {
chart: {
type: 'coverage_sunburst',
data: {
name: 'Project',
coverage: coverageData.overall,
children: [],
},
},
heatmap: [],
trends: generateCoverageTrends(),
};
// Group files by directory for sunburst chart
const directories = {};
coverageData.files.forEach(file => {
const dir = file.path.split('/').slice(0, -1).join('/');
if (!directories[dir]) {
directories[dir] = {
name: dir,
files: [],
totalCoverage: 0,
};
}
directories[dir].files.push(file);
});
// Calculate directory coverage
Object.values(directories).forEach(dir => {
const totalLines = dir.files.reduce((sum, f) => sum + f.lines.total, 0);
const coveredLines = dir.files.reduce((sum, f) => sum + f.lines.covered, 0);
dir.totalCoverage = (coveredLines / totalLines) * 100;
report.chart.data.children.push({
name: dir.name,
coverage: dir.totalCoverage,
children: dir.files.map(f => ({
name: f.path.split('/').pop(),
coverage: f.coverage,
size: f.lines.total,
})),
});
});
// Generate heatmap data
coverageData.files.forEach(file => {
report.heatmap.push({
file: file.path,
coverage: file.coverage,
color: getCoverageColor(file.coverage),
uncoveredLines: file.uncoveredLines.length,
});
});
return report;
}
function getCoverageColor(coverage) {
if (coverage >= 90) return '#4CAF50'; // Green
if (coverage >= 80) return '#8BC34A'; // Light green
if (coverage >= 70) return '#FFC107'; // Amber
if (coverage >= 60) return '#FF9800'; // Orange
if (coverage >= 50) return '#FF5722'; // Deep orange
return '#F44336'; // Red
}
function generateCoverageTrends() {
// Simulate historical coverage data
return {
weekly: [
{ week: 'W1', coverage: 68.5 },
{ week: 'W2', coverage: 70.2 },
{ week: 'W3', coverage: 72.8 },
{ week: 'W4', coverage: 75.5 },
],
monthly: [
{ month: 'Jan', coverage: 65.0 },
{ month: 'Feb', coverage: 68.5 },
{ month: 'Mar', coverage: 75.5 },
],
};
}
function generateTestCommands(analysis) {
const commands = {
runAllTests: 'flutter test --coverage',
runSpecificFile: 'flutter test test/widget_test.dart --coverage',
generateReport: 'genhtml coverage/lcov.info -o coverage/html',
viewReport: 'open coverage/html/index.html',
focusedTesting: [],
};
// Generate focused test commands for critical files
analysis.criticalGaps.forEach(gap => {
const testFile = gap.file.replace('lib/', 'test/').replace('.dart', '_test.dart');
commands.focusedTesting.push({
file: gap.file,
command: `flutter test ${testFile} --coverage`,
});
});
return commands;
}