Files
fn_registry/functions/infra/audit_e2e_coverage.go
egutierrez 7b0b697b18 feat(0121b): audit_e2e_coverage_go_infra + fn doctor e2e-coverage subcmd
- Crea functions/infra/audit_e2e_coverage.go: AuditE2ECoverage(roots) escanea
  app.md recursivamente, detecta e2e_checks: en frontmatter, retorna
  E2ECoverageReport{total, with_checks, missing, coverage_pct}.
- Crea functions/infra/e2e_coverage_report.go: tipo E2ECoverageReport con
  JSON tags (total, with_checks, missing, coverage_pct).
- Crea types/infra/e2e_coverage_report.md: metadata del tipo para registry.
- Crea functions/infra/audit_e2e_coverage.md: documentacion self-contained
  con Ejemplo, Cuando usarla, Gotchas.
- Crea functions/infra/audit_e2e_coverage_test.go: 3 tests (empty, all-covered,
  partial) — todos pasan.
- Edita cmd/fn/doctor.go: agrega case "e2e-coverage" -> doctorE2ECoverage().
  Output text (tabla tabwriter + lista de apps missing) y --json (E2ECoverageReport).

Acceptance verificado:
  fn doctor e2e-coverage --json -> {total, with_checks, missing, coverage_pct} OK
  fn doctor e2e-coverage        -> tabla text OK
  go test ./functions/infra/... -> 3/3 PASS
  fn show audit_e2e_coverage_go_infra -> indexada OK

task_run: task_d285372493cce2e6 iter 1

Co-authored-by: fn-orquestador <noreply@fn-registry>
2026-05-19 01:45:54 +02:00

116 lines
3.1 KiB
Go

package infra
import (
"fmt"
"math"
"os"
"path/filepath"
"strings"
)
// AuditE2ECoverage walks each directory in roots looking for app.md files and
// reports which ones declare e2e_checks in their YAML frontmatter.
//
// For each app.md found, it reads the file and searches for the substring
// "e2e_checks:" inside the first frontmatter block (between the opening and
// closing "---" delimiters). Files that contain the key count as covered.
//
// Parameters:
// - roots: slice of directory paths to scan (e.g. ["apps", "cpp/apps", "projects"])
//
// Returns an E2ECoverageReport with total, with_checks, missing paths and
// coverage_pct rounded to 2 decimal places. Returns a non-nil error only when
// filesystem I/O fails for a reason other than a missing/empty root.
func AuditE2ECoverage(roots []string) (E2ECoverageReport, error) {
var report E2ECoverageReport
for _, root := range roots {
info, err := os.Stat(root)
if err != nil {
if os.IsNotExist(err) {
continue
}
return report, fmt.Errorf("audit_e2e_coverage: stat %q: %w", root, err)
}
if !info.IsDir() {
continue
}
walkErr := filepath.WalkDir(root, func(p string, d os.DirEntry, we error) error {
if we != nil {
// Skip unreadable entries without aborting the whole walk.
return nil
}
if d.IsDir() {
return nil
}
if filepath.Base(p) != "app.md" {
return nil
}
report.Total++
data, err := os.ReadFile(p)
if err != nil {
// Count but mark as missing — we cannot determine coverage.
report.Missing = append(report.Missing, p)
return nil
}
if hasE2EChecks(string(data)) {
report.WithChecks++
} else {
report.Missing = append(report.Missing, p)
}
return nil
})
if walkErr != nil {
return report, fmt.Errorf("audit_e2e_coverage: walk %q: %w", root, walkErr)
}
}
report.CoveragePct = computeCoveragePct(report.WithChecks, report.Total)
return report, nil
}
// hasE2EChecks returns true when the app.md content contains "e2e_checks:"
// inside the YAML frontmatter block (between the first pair of "---" lines).
// If the file has no frontmatter, it falls back to a whole-file scan so that
// non-standard formats are also handled gracefully.
func hasE2EChecks(content string) bool {
const key = "e2e_checks:"
if !strings.HasPrefix(content, "---") {
// No frontmatter delimiter — scan full content.
return strings.Contains(content, key)
}
// Skip the opening "---".
rest := content[3:]
if strings.HasPrefix(rest, "\r\n") {
rest = rest[2:]
} else if strings.HasPrefix(rest, "\n") {
rest = rest[1:]
}
// Find the closing "---".
end := strings.Index(rest, "\n---")
if end < 0 {
// Malformed frontmatter — scan full content.
return strings.Contains(content, key)
}
frontmatter := rest[:end]
return strings.Contains(frontmatter, key)
}
// computeCoveragePct returns (withChecks/total)*100 rounded to 2 decimal
// places. Returns 0.0 when total is zero to avoid division by zero.
func computeCoveragePct(withChecks, total int) float64 {
if total == 0 {
return 0.0
}
raw := float64(withChecks) / float64(total) * 100.0
return math.Round(raw*100) / 100
}