Skip to main content
Glama
coverage_reporter.go8.24 kB
// Package metrics provides coverage reporting utilities for MCP tests. package metrics import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) // CoverageData represents code coverage data for a package. type CoverageData struct { Package string `json:"package"` TotalLines int `json:"totalLines"` CoveredLines int `json:"coveredLines"` Coverage float64 `json:"coverage"` Timestamp time.Time `json:"timestamp"` Files []FileCoverage `json:"files"` } // FileCoverage represents code coverage data for a specific file. type FileCoverage struct { Filename string `json:"filename"` TotalLines int `json:"totalLines"` CoveredLines int `json:"coveredLines"` Coverage float64 `json:"coverage"` } // CoverageReport represents a comprehensive coverage report. type CoverageReport struct { Timestamp time.Time `json:"timestamp"` TotalCoverage float64 `json:"totalCoverage"` TotalLines int `json:"totalLines"` CoveredLines int `json:"coveredLines"` PackageCoverage []CoverageData `json:"packageCoverage"` } // GenerateCoverageReport generates a coverage report for the specified packages. // If no packages are specified, all packages are included. func GenerateCoverageReport(coverageFile string, packages ...string) (*CoverageReport, error) { // Ensure the coverage file exists if _, err := os.Stat(coverageFile); os.IsNotExist(err) { return nil, fmt.Errorf("coverage file not found: %s", coverageFile) } // Parse the coverage file packageCoverage, err := parseCoverageFile(coverageFile) if err != nil { return nil, err } // Filter packages if specified if len(packages) > 0 { filteredCoverage := make([]CoverageData, 0) for _, pkg := range packageCoverage { for _, requestedPkg := range packages { if strings.Contains(pkg.Package, requestedPkg) { filteredCoverage = append(filteredCoverage, pkg) break } } } packageCoverage = filteredCoverage } // Calculate total coverage totalLines := 0 coveredLines := 0 for _, pkg := range packageCoverage { totalLines += pkg.TotalLines coveredLines += pkg.CoveredLines } totalCoverage := 0.0 if totalLines > 0 { totalCoverage = float64(coveredLines) * 100.0 / float64(totalLines) } // Create the report report := &CoverageReport{ Timestamp: time.Now(), TotalCoverage: totalCoverage, TotalLines: totalLines, CoveredLines: coveredLines, PackageCoverage: packageCoverage, } return report, nil } // parseCoverageFile parses the coverage file output by "go test -coverprofile". func parseCoverageFile(coverageFile string) ([]CoverageData, error) { cmd := exec.Command("go", "tool", "cover", "-func", coverageFile) output, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("failed to run go tool cover: %w", err) } // Process output by package packageMap := make(map[string]*CoverageData) lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.TrimSpace(line) == "" { continue } // Split the line into parts parts := strings.Fields(line) if len(parts) < 3 { continue } // Extract package and file fileInfo := strings.Split(parts[0], ":") if len(fileInfo) < 2 { continue } fullPath := fileInfo[0] pkgPath := filepath.Dir(fullPath) fileName := filepath.Base(fullPath) // Extract coverage percentage if parts[len(parts)-1] == "(statements)" { // Total coverage line coverageStr := strings.TrimSuffix(parts[len(parts)-2], "%") coverage, err := strconv.ParseFloat(coverageStr, 64) if err != nil { return nil, fmt.Errorf("failed to parse coverage percentage: %w", err) } // Create or update package data pkg, ok := packageMap[pkgPath] if !ok { pkg = &CoverageData{ Package: pkgPath, Files: make([]FileCoverage, 0), Timestamp: time.Now(), Coverage: coverage, } packageMap[pkgPath] = pkg } else { pkg.Coverage = coverage } } else { // Line for a specific function // Format: path/to/file.go:line.column function coverage% // We need to calculate file coverage separately // For now, we just count this as a covered line // Create or update package data pkg, ok := packageMap[pkgPath] if !ok { pkg = &CoverageData{ Package: pkgPath, Files: make([]FileCoverage, 0), Timestamp: time.Now(), } packageMap[pkgPath] = pkg } // Find or create file coverage var fileCov *FileCoverage for i, fc := range pkg.Files { if fc.Filename == fileName { fileCov = &pkg.Files[i] break } } if fileCov == nil { pkg.Files = append(pkg.Files, FileCoverage{ Filename: fileName, TotalLines: 1, CoveredLines: 0, // We'll update this below }) fileCov = &pkg.Files[len(pkg.Files)-1] } else { fileCov.TotalLines++ } // Check if this line is covered if strings.HasPrefix(parts[len(parts)-1], "100.0%") { fileCov.CoveredLines++ pkg.CoveredLines++ } pkg.TotalLines++ } } // Convert map to slice and calculate file coverage percentages result := make([]CoverageData, 0, len(packageMap)) for _, pkg := range packageMap { // Calculate file coverage percentages for i := range pkg.Files { if pkg.Files[i].TotalLines > 0 { pkg.Files[i].Coverage = float64(pkg.Files[i].CoveredLines) * 100.0 / float64(pkg.Files[i].TotalLines) } } result = append(result, *pkg) } return result, nil } // GenerateHTMLCoverageReport generates an HTML coverage report. func GenerateHTMLCoverageReport(coverageFile, outputFile string) error { cmd := exec.Command("go", "tool", "cover", "-html", coverageFile, "-o", outputFile) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to generate HTML coverage report: %w\nOutput: %s", err, string(output)) } return nil } // GenerateCoverageReportFiles generates and saves coverage report files. func GenerateCoverageReportFiles(coverageFile, outputDir string, packages ...string) error { // Ensure the coverage file exists if _, err := os.Stat(coverageFile); os.IsNotExist(err) { return fmt.Errorf("coverage file not found: %s", coverageFile) } // Create the output directory if it doesn't exist if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } // Generate the coverage report report, err := GenerateCoverageReport(coverageFile, packages...) if err != nil { return err } // Save the report as JSON jsonPath := filepath.Join(outputDir, "coverage_report.json") jsonData, err := json.MarshalIndent(report, "", " ") if err != nil { return fmt.Errorf("failed to marshal coverage report to JSON: %w", err) } if err := os.WriteFile(jsonPath, jsonData, 0644); err != nil { return fmt.Errorf("failed to write JSON report: %w", err) } // Save the report as text textPath := filepath.Join(outputDir, "coverage_report.txt") var sb strings.Builder sb.WriteString(fmt.Sprintf("Coverage Report - Generated at %s\n", report.Timestamp.Format(time.RFC3339))) sb.WriteString("=====================================\n\n") sb.WriteString(fmt.Sprintf("Total Coverage: %.2f%% (%d of %d lines covered)\n\n", report.TotalCoverage, report.CoveredLines, report.TotalLines)) sb.WriteString("Package Coverage:\n") for _, pkg := range report.PackageCoverage { sb.WriteString(fmt.Sprintf(" %s: %.2f%% (%d of %d lines)\n", pkg.Package, pkg.Coverage, pkg.CoveredLines, pkg.TotalLines)) sb.WriteString(" Files:\n") for _, file := range pkg.Files { sb.WriteString(fmt.Sprintf(" %s: %.2f%% (%d of %d lines)\n", file.Filename, file.Coverage, file.CoveredLines, file.TotalLines)) } sb.WriteString("\n") } if err := os.WriteFile(textPath, []byte(sb.String()), 0644); err != nil { return fmt.Errorf("failed to write text report: %w", err) } // Generate HTML coverage report htmlPath := filepath.Join(outputDir, "coverage_report.html") if err := GenerateHTMLCoverageReport(coverageFile, htmlPath); err != nil { return err } return nil }

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/MrFixit96/go-dev-mcp'

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