Files
Devour/cmd/verify.go
Tomas Dvorak 898a3c303f update
2026-02-24 10:33:59 +01:00

170 lines
4.8 KiB
Go

package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/yourorg/devour/internal/scraper"
)
var (
verifyFormat string
verifyTimeout int
)
var verifyCmd = &cobra.Command{
Use: "verify",
Short: "Run Devour verification suites",
Long: `Run deterministic and live verification suites for Devour commands and scrapers.`,
}
var verifySmokeCmd = &cobra.Command{
Use: "smoke",
Short: "Run live docs scraping smoke checks",
Long: `Run an opt-in live network smoke suite and persist a machine-readable report under devour_data/verify/.`,
RunE: runVerifySmoke,
}
func init() {
verifyCmd.AddCommand(verifySmokeCmd)
verifySmokeCmd.Flags().StringVar(&verifyFormat, "format", "text", "output format (text, json)")
verifySmokeCmd.Flags().IntVar(&verifyTimeout, "timeout", 90, "timeout per smoke case in seconds")
}
type verifyCase struct {
Name string `json:"name"`
Type scraper.SourceType `json:"type"`
URL string `json:"url"`
Passed bool `json:"passed"`
Docs int `json:"docs"`
Error string `json:"error,omitempty"`
TookMs int64 `json:"took_ms"`
}
type verifyReport struct {
CreatedAt time.Time `json:"created_at"`
Duration string `json:"duration"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Cases []verifyCase `json:"cases"`
}
func runVerifySmoke(cmd *cobra.Command, args []string) error {
cfg, err := loadAppConfig()
if err != nil {
return err
}
if verifyTimeout <= 0 {
verifyTimeout = 90
}
cases := []verifyCase{
{Name: "Go net/http", Type: scraper.SourceTypeGoDocs, URL: "https://pkg.go.dev/net/http"},
{Name: "Python asyncio", Type: scraper.SourceTypePythonDocs, URL: "https://docs.python.org/3/library/asyncio.html"},
{Name: "React reference", Type: scraper.SourceTypeReactDocs, URL: "https://react.dev/reference/react"},
{Name: "TypeScript handbook", Type: scraper.SourceTypeTSDocs, URL: "https://www.typescriptlang.org/docs/handbook/2/basic-types.html"},
{Name: "Next.js docs", Type: scraper.SourceTypeWeb, URL: "https://nextjs.org/docs"},
{Name: "Svelte docs", Type: scraper.SourceTypeWeb, URL: "https://svelte.dev/docs/kit"},
{Name: "Angular guide", Type: scraper.SourceTypeWeb, URL: "https://angular.dev/guide/http"},
{Name: "Remix docs", Type: scraper.SourceTypeWeb, URL: "https://v2.remix.run/docs"},
{Name: "Solid docs repo", Type: scraper.SourceTypeGitHub, URL: "https://github.com/solidjs/solid-docs"},
{Name: "Express guide", Type: scraper.SourceTypeWeb, URL: "https://expressjs.com/en/guide/routing.html"},
}
startAll := time.Now()
passed := 0
failed := 0
for i := range cases {
c := &cases[i]
caseStart := time.Now()
s := scraper.NewScraper(c.Type, toScraperConfig(cfg, 4))
if s == nil {
c.Error = "scraper not available"
c.Passed = false
failed++
continue
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(verifyTimeout)*time.Second)
docs, err := s.Scrape(ctx, &scraper.Source{Name: c.Name, Type: c.Type, URL: c.URL})
cancel()
c.TookMs = time.Since(caseStart).Milliseconds()
if err != nil {
c.Error = err.Error()
c.Passed = false
failed++
continue
}
c.Docs = len(docs)
if len(docs) == 0 {
c.Error = "0 documents"
c.Passed = false
failed++
continue
}
c.Passed = true
passed++
}
report := verifyReport{
CreatedAt: time.Now(),
Duration: time.Since(startAll).String(),
Passed: passed,
Failed: failed,
Cases: cases,
}
rootDataDir := filepath.Dir(cfg.Storage.DocsDir)
verifyDir := filepath.Join(rootDataDir, "verify")
if err := os.MkdirAll(verifyDir, 0o755); err != nil {
return err
}
filename := fmt.Sprintf("smoke-%s.json", time.Now().Format("20060102-150405"))
reportPath := filepath.Join(verifyDir, filename)
b, err := json.MarshalIndent(report, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(reportPath, b, 0o644); err != nil {
return err
}
switch verifyFormat {
case "json":
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
if err := enc.Encode(report); err != nil {
return err
}
default:
fmt.Fprintf(cmd.OutOrStdout(), "Smoke verification complete\n")
fmt.Fprintf(cmd.OutOrStdout(), " Passed: %d\n", report.Passed)
fmt.Fprintf(cmd.OutOrStdout(), " Failed: %d\n", report.Failed)
fmt.Fprintf(cmd.OutOrStdout(), " Report: %s\n", reportPath)
for _, c := range report.Cases {
status := "PASS"
if !c.Passed {
status = "FAIL"
}
fmt.Fprintf(cmd.OutOrStdout(), " - [%s] %s (%d docs, %dms)", status, c.Name, c.Docs, c.TookMs)
if c.Error != "" {
fmt.Fprintf(cmd.OutOrStdout(), " error=%s", c.Error)
}
fmt.Fprintln(cmd.OutOrStdout())
}
}
if report.Failed > 0 {
return fmt.Errorf("smoke verification completed with failures")
}
return nil
}