mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
170 lines
4.8 KiB
Go
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
|
|
}
|