// Package devkit provides a developer toolkit for common automation commands. // LEK-1 | lthn.ai | EUPL-1.2 package devkit import ( "bufio" "encoding/json" "fmt" "math" "os" "regexp" "strconv" "strings" "time" ) // CoverageSnapshot represents a point-in-time coverage measurement. type CoverageSnapshot struct { Timestamp time.Time `json:"timestamp"` Packages map[string]float64 `json:"packages"` // package → coverage % Total float64 `json:"total"` // overall coverage % Meta map[string]string `json:"meta,omitempty"` // optional metadata (commit, branch, etc.) } // CoverageRegression flags a package whose coverage dropped between runs. type CoverageRegression struct { Package string Previous float64 Current float64 Delta float64 // Negative means regression } // CoverageComparison holds the result of comparing two snapshots. type CoverageComparison struct { Regressions []CoverageRegression Improvements []CoverageRegression NewPackages []string // Packages present in current but not previous Removed []string // Packages present in previous but not current TotalDelta float64 // Change in overall coverage } // CoverageStore persists coverage snapshots to a JSON file. type CoverageStore struct { Path string // File path for JSON storage } // NewCoverageStore creates a store backed by the given file path. func NewCoverageStore(path string) *CoverageStore { return &CoverageStore{Path: path} } // Append adds a snapshot to the store. func (s *CoverageStore) Append(snap CoverageSnapshot) error { snapshots, err := s.Load() if err != nil && !os.IsNotExist(err) { return fmt.Errorf("load snapshots: %w", err) } snapshots = append(snapshots, snap) data, err := json.MarshalIndent(snapshots, "", " ") if err != nil { return fmt.Errorf("marshal snapshots: %w", err) } if err := os.WriteFile(s.Path, data, 0644); err != nil { return fmt.Errorf("write %s: %w", s.Path, err) } return nil } // Load reads all snapshots from the store. func (s *CoverageStore) Load() ([]CoverageSnapshot, error) { data, err := os.ReadFile(s.Path) if err != nil { return nil, err } var snapshots []CoverageSnapshot if err := json.Unmarshal(data, &snapshots); err != nil { return nil, fmt.Errorf("parse %s: %w", s.Path, err) } return snapshots, nil } // Latest returns the most recent snapshot, or nil if the store is empty. func (s *CoverageStore) Latest() (*CoverageSnapshot, error) { snapshots, err := s.Load() if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } if len(snapshots) == 0 { return nil, nil } latest := &snapshots[0] for i := range snapshots { if snapshots[i].Timestamp.After(latest.Timestamp) { latest = &snapshots[i] } } return latest, nil } // ParseCoverProfile parses output from `go test -coverprofile=cover.out` format. // Each line is: mode: set/count/atomic (first line) or // package/file.go:startLine.startCol,endLine.endCol stmts count func ParseCoverProfile(data string) (CoverageSnapshot, error) { snap := CoverageSnapshot{ Timestamp: time.Now(), Packages: make(map[string]float64), } type pkgStats struct { covered int total int } packages := make(map[string]*pkgStats) scanner := bufio.NewScanner(strings.NewReader(data)) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "mode:") { continue } // Format: pkg/file.go:line.col,line.col numStmt count parts := strings.Fields(line) if len(parts) != 3 { continue } // Extract package from file path filePath := parts[0] colonIdx := strings.Index(filePath, ":") if colonIdx < 0 { continue } file := filePath[:colonIdx] // Package is everything up to the last / pkg := file if lastSlash := strings.LastIndex(file, "/"); lastSlash >= 0 { pkg = file[:lastSlash] } stmts, err := strconv.Atoi(parts[1]) if err != nil { continue } count, err := strconv.Atoi(parts[2]) if err != nil { continue } if _, ok := packages[pkg]; !ok { packages[pkg] = &pkgStats{} } packages[pkg].total += stmts if count > 0 { packages[pkg].covered += stmts } } totalCovered := 0 totalStmts := 0 for pkg, stats := range packages { if stats.total > 0 { snap.Packages[pkg] = math.Round(float64(stats.covered)/float64(stats.total)*1000) / 10 } else { snap.Packages[pkg] = 0 } totalCovered += stats.covered totalStmts += stats.total } if totalStmts > 0 { snap.Total = math.Round(float64(totalCovered)/float64(totalStmts)*1000) / 10 } return snap, nil } // ParseCoverOutput parses the human-readable `go test -cover ./...` output. // Extracts lines like: ok example.com/pkg 0.5s coverage: 85.0% of statements func ParseCoverOutput(output string) (CoverageSnapshot, error) { snap := CoverageSnapshot{ Timestamp: time.Now(), Packages: make(map[string]float64), } re := regexp.MustCompile(`ok\s+(\S+)\s+.*coverage:\s+([\d.]+)%`) scanner := bufio.NewScanner(strings.NewReader(output)) totalPct := 0.0 count := 0 for scanner.Scan() { matches := re.FindStringSubmatch(scanner.Text()) if len(matches) == 3 { pct, _ := strconv.ParseFloat(matches[2], 64) snap.Packages[matches[1]] = pct totalPct += pct count++ } } if count > 0 { snap.Total = math.Round(totalPct/float64(count)*10) / 10 } return snap, nil } // CompareCoverage computes the difference between two snapshots. func CompareCoverage(previous, current CoverageSnapshot) CoverageComparison { comp := CoverageComparison{ TotalDelta: math.Round((current.Total-previous.Total)*10) / 10, } // Check each current package against previous for pkg, curPct := range current.Packages { prevPct, existed := previous.Packages[pkg] if !existed { comp.NewPackages = append(comp.NewPackages, pkg) continue } delta := math.Round((curPct-prevPct)*10) / 10 if delta < 0 { comp.Regressions = append(comp.Regressions, CoverageRegression{ Package: pkg, Previous: prevPct, Current: curPct, Delta: delta, }) } else if delta > 0 { comp.Improvements = append(comp.Improvements, CoverageRegression{ Package: pkg, Previous: prevPct, Current: curPct, Delta: delta, }) } } // Check for removed packages for pkg := range previous.Packages { if _, exists := current.Packages[pkg]; !exists { comp.Removed = append(comp.Removed, pkg) } } return comp } // LEK-1 | lthn.ai | EUPL-1.2