mirror of
https://github.com/karust/openserp.git
synced 2026-08-06 01:03:56 +08:00
175 lines
4.4 KiB
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, "_")
|
|
}
|