mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
update
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user