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>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user