Files
openserp/core/fpcheck/runner.go

175 lines
4.4 KiB
Go

package fpcheck
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
// RunOptions controls detector run behavior.
type RunOptions struct {
ArtifactDir string
WaitBeforeExtract time.Duration
WaitBeforeClose time.Duration
}
// Run navigates the given browser to detector URL, extracts verdicts,
// captures a screenshot artifact, and returns a normalized report.
func Run(ctx context.Context, browser BrowserNavigator, detector Detector, artifactDir string) (Report, error) {
return RunWithOptions(ctx, browser, detector, RunOptions{
ArtifactDir: artifactDir,
})
}
// RunWithOptions navigates the given browser to detector URL, extracts
// verdicts, captures a screenshot artifact, optionally waits, and returns a
// normalized report.
func RunWithOptions(ctx context.Context, browser BrowserNavigator, detector Detector, options RunOptions) (Report, error) {
report := Report{
DetectorName: detector.Name(),
URL: detector.URL(),
Detections: map[string]Detection{},
}
artifactDir := strings.TrimSpace(options.ArtifactDir)
if artifactDir == "" {
artifactDir = "testdata"
}
screenshotPath := filepath.Join(artifactDir, fmt.Sprintf("fpcheck_%s.png", sanitizeFilePart(detector.Name())))
report.Screenshot = filepath.ToSlash(screenshotPath)
page, err := browser.Navigate(ctx, detector.URL())
if err != nil {
return report, fmt.Errorf("navigate %s: %w", detector.Name(), err)
}
defer func() {
if options.WaitBeforeClose > 0 {
_ = sleepWithContext(ctx, options.WaitBeforeClose)
}
closePageWithTimeout(context.Background(), browser, page, time.Second)
}()
if options.WaitBeforeExtract > 0 {
_ = sleepWithContext(ctx, options.WaitBeforeExtract)
}
detections, rawNotes, err := detector.Extract(ctx, page)
if err != nil {
_ = saveScreenshot(page, screenshotPath)
return report, fmt.Errorf("extract %s: %w", detector.Name(), err)
}
if err := saveScreenshot(page, screenshotPath); err != nil {
return report, fmt.Errorf("capture screenshot %s: %w", detector.Name(), err)
}
report.CapturedAtUTC = time.Now().UTC().Format(time.RFC3339)
report.Detections = detections
report.Summary = summarize(detections)
report.RawNotes = strings.TrimSpace(rawNotes)
return report, nil
}
func sleepWithContext(ctx context.Context, d time.Duration) error {
if d <= 0 {
return nil
}
if ctx == nil {
ctx = context.Background()
}
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func summarize(detections map[string]Detection) Summary {
summary := Summary{}
for key, detection := range detections {
if detection.Detected {
summary.Failed++
if strings.EqualFold(strings.TrimSpace(detection.Severity), "critical") {
summary.Critical = append(summary.Critical, key)
}
continue
}
summary.Passed++
}
sort.Strings(summary.Critical)
return summary
}
func saveScreenshot(page *rod.Page, path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create screenshot directory: %w", err)
}
bytes, err := page.Screenshot(true, nil)
if err != nil {
return fmt.Errorf("capture screenshot: %w", err)
}
if err := os.WriteFile(path, bytes, 0o644); err != nil {
return fmt.Errorf("write screenshot file %s: %w", path, err)
}
return nil
}
type pageCloser interface {
ClosePage(context.Context, *rod.Page, time.Duration) error
}
func closePageWithTimeout(ctx context.Context, browser BrowserNavigator, page *rod.Page, timeout time.Duration) {
if page == nil {
return
}
if closer, ok := browser.(pageCloser); ok {
_ = closer.ClosePage(ctx, page, timeout)
return
}
if timeout <= 0 {
timeout = time.Second
}
closeCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
pageWithTimeout := page.Context(closeCtx)
info, _ := pageWithTimeout.Info()
_ = pageWithTimeout.Close()
if info != nil && info.BrowserContextID != "" {
_ = (proto.TargetDisposeBrowserContext{BrowserContextID: info.BrowserContextID}).Call(page.Browser().Context(closeCtx))
}
}
func sanitizeFilePart(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
if value == "" {
return "detector"
}
parts := strings.FieldsFunc(value, func(r rune) bool {
return !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9')
})
if len(parts) == 0 {
return "detector"
}
return strings.Join(parts, "_")
}