7b0b697b18
- 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>
116 lines
3.1 KiB
Go
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
|
|
}
|