Skip to main content
Glama
testCoverageAnalyzer.js10.7 kB
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; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dvillegastech/flutter_mcp_2'

If you have feedback or need assistance with the MCP directory API, please join our Discord server