From 889ea86d692db40298a528210fe0df5abeeb9c3e Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 18 May 2026 15:37:37 +0800 Subject: [PATCH 01/21] feat(dialtesting): add browser test --- dialtesting/browser.go | 493 ++++++++++++++++++++++++++++++++++++ dialtesting/browser_test.go | 228 +++++++++++++++++ dialtesting/task.go | 2 +- go.mod | 2 +- 4 files changed, 723 insertions(+), 2 deletions(-) create mode 100644 dialtesting/browser.go create mode 100644 dialtesting/browser_test.go diff --git a/dialtesting/browser.go b/dialtesting/browser.go new file mode 100644 index 00000000..b78af159 --- /dev/null +++ b/dialtesting/browser.go @@ -0,0 +1,493 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package dialtesting + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "text/template" + "time" + + "gopkg.in/yaml.v3" +) + +var ( + _ TaskChild = (*BrowserTask)(nil) + _ ITask = (*BrowserTask)(nil) +) + +const ( + defaultBrowserDialPath = "browser-dial" + defaultBrowserDialTimeout = 30_000 + + optionBrowserDialPath = "browser_dial_path" + optionBrowserDialPathCamel = "browserDialPath" +) + +type BrowserTask struct { + *Task + BrowserConfig string `json:"browser_config"` + + duration time.Duration + result browserDialRun + exitCode int + reqError string + stderr string + rawTask *BrowserTask +} + +type browserConfig struct { + Name string `yaml:"name"` + Target string `yaml:"target"` + TimeoutMS int `yaml:"timeout_ms"` + Tags map[string]string `yaml:"tags"` + ConfigVars []ConfigVar `yaml:"config_vars"` + Steps []browserConfigStep `yaml:"steps"` +} + +type browserConfigStep struct { + Action string `yaml:"action"` + URL string `yaml:"url"` +} + +type browserDialOutput struct { + ExitCode int `json:"exit_code"` + Run browserDialRun `json:"run"` +} + +type browserDialRun struct { + RunID string `json:"run_id"` + Name string `json:"name"` + Target string `json:"target,omitempty"` + Status string `json:"status"` + Success bool `json:"success"` + DurationUS int64 `json:"duration_us"` + Steps []browserDialStep `json:"steps"` + TraceIDs []string `json:"trace_ids,omitempty"` + Error *browserDialError `json:"error,omitempty"` + FailReason string `json:"fail_reason,omitempty"` +} + +type browserDialStep struct { + Seq int `json:"seq"` + Name string `json:"name"` + Status string `json:"status"` + DurationUS int64 `json:"duration_us"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Error *browserDialError `json:"error,omitempty"` +} + +type browserDialError struct { + Name string `json:"name"` + Message string `json:"message"` + Stack string `json:"stack,omitempty"` +} + +func (t *BrowserTask) clear() { + t.duration = 0 + t.result = browserDialRun{} + t.exitCode = 0 + t.reqError = "" + t.stderr = "" +} + +func (t *BrowserTask) stop() {} + +func (t *BrowserTask) class() string { + return ClassHeadless +} + +func (t *BrowserTask) metricName() string { + return "browser_dial_testing" +} + +func (t *BrowserTask) run() error { + start := time.Now() + path, err := t.writeScriptFile() + if err != nil { + t.reqError = err.Error() + return nil + } + defer os.Remove(path) //nolint:errcheck + + timeoutMS := t.effectiveTimeoutMS() + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) + defer cancel() + + args := []string{ + "run", path, + "--dry-run", + "--skip-token-check", + "--json", + } + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmdArgs := executableArgs(args) + cmd := exec.CommandContext(ctx, t.executablePath(), cmdArgs...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + t.duration = time.Since(start) + t.stderr = strings.TrimSpace(stderr.String()) + if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { + t.exitCode = exitErr.ExitCode() + } else if err != nil { + t.reqError = err.Error() + return nil + } + + var output browserDialOutput + if decodeErr := json.Unmarshal(stdout.Bytes(), &output); decodeErr != nil { + t.reqError = fmt.Sprintf("parse browser-dial output failed: %s", decodeErr) + if t.stderr != "" { + t.reqError += ": " + t.stderr + } + return nil + } + t.exitCode = output.ExitCode + t.result = output.Run + return nil +} + +func executableArgs(args []string) []string { + if os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") == "" { + return args + } + return append([]string{"-test.run=TestBrowserDialHelperProcess", "--"}, args...) +} + +func (t *BrowserTask) writeScriptFile() (string, error) { + file, err := os.CreateTemp("", "dialtesting-browser-*.yaml") + if err != nil { + return "", err + } + defer file.Close() //nolint:errcheck + + if _, err := file.WriteString(t.BrowserConfig); err != nil { + os.Remove(file.Name()) //nolint:errcheck + return "", err + } + return file.Name(), nil +} + +func (t *BrowserTask) executablePath() string { + if value := t.GetOption()[optionBrowserDialPath]; value != "" { + return value + } + if value := t.GetOption()[optionBrowserDialPathCamel]; value != "" { + return value + } + if value := os.Getenv("BROWSER_DIAL_PATH"); value != "" { + return value + } + return defaultBrowserDialPath +} + +func (t *BrowserTask) effectiveTimeoutMS() int { + cfg, err := t.parseBrowserConfig() + if err == nil && cfg.TimeoutMS > 0 { + return cfg.TimeoutMS + } + return defaultBrowserDialTimeout +} + +func (t *BrowserTask) checkResult() (reasons []string, succFlag bool) { + if t.reqError != "" { + return []string{t.reqError}, false + } + if t.result.Success { + return nil, true + } + if t.result.FailReason != "" { + reasons = append(reasons, t.result.FailReason) + } + if t.result.Error != nil && t.result.Error.Message != "" { + reasons = append(reasons, t.result.Error.Message) + } + if len(reasons) == 0 && t.stderr != "" { + reasons = append(reasons, t.stderr) + } + if len(reasons) == 0 { + reasons = append(reasons, "browser dial failed") + } + return reasons, false +} + +func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]interface{}) { + cfg, _ := t.parseBrowserConfig() + name := firstNonEmpty(t.result.Name, cfg.Name, t.Name) + target := firstNonEmpty(t.result.Target, cfg.Target) + tags = map[string]string{ + "name": name, + "url": target, + "status": "FAIL", + "runner": "browser-dial", + "browser_engine": "lightpanda", + } + for k, v := range cfg.Tags { + tags[k] = v + } + for k, v := range t.Tags { + tags[k] = v + } + + responseTime := t.result.DurationUS + if responseTime == 0 { + responseTime = int64(t.duration) / 1000 + } + fields = map[string]interface{}{ + "response_time": responseTime, + "success": int64(-1), + "last_step": int64(lastBrowserStep(t.result.Steps)), + "browser_run_id": t.result.RunID, + "exit_code": int64(t.exitCode), + } + + if t.reqError == "" && t.result.Success { + tags["status"] = "OK" + fields["success"] = int64(1) + fields["message"] = "success" + } else { + reasons, _ := t.checkResult() + fields["fail_reason"] = strings.Join(reasons, ";") + fields["message"] = strings.Join(reasons, ";") + } + if len(t.result.Steps) > 0 { + last := t.result.Steps[len(t.result.Steps)-1] + fields["page_url"] = last.URL + fields["page_title"] = last.Title + } + if len(t.result.TraceIDs) > 0 { + fields["trace_id"] = t.result.TraceIDs[0] + if traceIDs, err := json.Marshal(t.result.TraceIDs); err == nil { + fields["trace_ids"] = string(traceIDs) + } + } + if steps, err := json.Marshal(t.result.Steps); err == nil { + fields["steps"] = string(steps) + } + + return tags, fields +} + +func lastBrowserStep(steps []browserDialStep) int { + if len(steps) == 0 { + return 0 + } + return steps[len(steps)-1].Seq +} + +func (t *BrowserTask) check() error { + if strings.TrimSpace(t.BrowserConfig) == "" { + return errors.New("browser_config should not be empty") + } + cfg, err := t.parseBrowserConfig() + if err != nil { + return fmt.Errorf("parse browser_config failed: %w", err) + } + if len(cfg.Steps) == 0 { + return errors.New("browser_config steps should not be empty") + } + return nil +} + +func (t *BrowserTask) init() error { + return nil +} + +func (t *BrowserTask) getHostName() ([]string, error) { + cfg, err := t.parseBrowserConfig() + if err != nil { + return nil, err + } + hosts := make([]string, 0, 1) + if strings.TrimSpace(cfg.Target) != "" { + host, err := getHostName(cfg.Target) + if err != nil { + return nil, err + } + hosts = append(hosts, host) + } + for _, step := range cfg.Steps { + if step.Action != "goto" || strings.TrimSpace(step.URL) == "" { + continue + } + host, err := getHostName(step.URL) + if err != nil { + return nil, err + } + hosts = append(hosts, host) + } + if len(hosts) == 0 { + return nil, errors.New("browser_config target or goto url should not be empty") + } + return dedupBrowserHostNames(hosts), nil +} + +func (t *BrowserTask) getVariableValue(variable Variable) (string, error) { + return "", errors.New("not support") +} + +func (t *BrowserTask) getRawTask(taskString string) (string, error) { + task := BrowserTask{} + if err := json.Unmarshal([]byte(taskString), &task); err != nil { + return "", fmt.Errorf("unmarshal browser task failed: %w", err) + } + task.Task = nil + task.BrowserConfig = sanitizeBrowserConfig(task.BrowserConfig) + bytes, _ := json.Marshal(task) + return string(bytes), nil +} + +func (t *BrowserTask) renderTemplate(fm template.FuncMap) error { + if t.rawTask == nil { + task := &BrowserTask{} + if err := t.NewRawTask(task); err != nil { + return fmt.Errorf("new raw task failed: %w", err) + } + t.rawTask = task + } + if t.rawTask == nil { + return errors.New("raw task is nil") + } + + browserConfig, err := t.GetParsedString(t.rawTask.BrowserConfig, fm) + if err != nil { + return fmt.Errorf("render browser_config failed: %w", err) + } + t.BrowserConfig = browserConfig + return nil +} + +func (t *BrowserTask) initTask() { + if t.Task == nil { + t.Task = &Task{} + } +} + +func (t *BrowserTask) setReqError(err string) { + t.reqError = err +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} + +func dedupBrowserHostNames(hosts []string) []string { + seen := map[string]struct{}{} + out := make([]string, 0, len(hosts)) + for _, host := range hosts { + if host == "" { + continue + } + if _, ok := seen[host]; ok { + continue + } + seen[host] = struct{}{} + out = append(out, host) + } + return out +} + +func (t *BrowserTask) parseBrowserConfig() (browserConfig, error) { + var cfg browserConfig + if strings.TrimSpace(t.BrowserConfig) == "" { + return cfg, errors.New("browser_config is empty") + } + if err := yaml.Unmarshal([]byte(t.BrowserConfig), &cfg); err != nil { + return cfg, err + } + return cfg, nil +} + +func sanitizeBrowserConfig(config string) string { + if strings.TrimSpace(config) == "" { + return config + } + var node yaml.Node + if err := yaml.Unmarshal([]byte(config), &node); err != nil { + return config + } + sanitizeYAMLConfigVars(&node) + data, err := yaml.Marshal(&node) + if err != nil { + return config + } + return string(data) +} + +func sanitizeYAMLConfigVars(node *yaml.Node) { + if node == nil { + return + } + if node.Kind == yaml.MappingNode { + for i := 0; i+1 < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + if key.Value == "config_vars" { + sanitizeConfigVarSequence(value) + continue + } + sanitizeYAMLConfigVars(value) + } + return + } + for _, child := range node.Content { + sanitizeYAMLConfigVars(child) + } +} + +func sanitizeConfigVarSequence(node *yaml.Node) { + if node == nil || node.Kind != yaml.SequenceNode { + return + } + for _, item := range node.Content { + if item == nil || item.Kind != yaml.MappingNode { + continue + } + if yamlMapBoolValue(item, "secure") { + setYAMLMapValue(item, "value", "") + } + } +} + +func yamlMapBoolValue(node *yaml.Node, key string) bool { + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + return strings.EqualFold(node.Content[i+1].Value, "true") + } + } + return false +} + +func setYAMLMapValue(node *yaml.Node, key string, value string) { + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + node.Content[i+1].Kind = yaml.ScalarNode + node.Content[i+1].Tag = "!!str" + node.Content[i+1].Value = value + return + } + } + node.Content = append(node.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value}, + ) +} diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go new file mode 100644 index 00000000..171d0458 --- /dev/null +++ b/dialtesting/browser_test.go @@ -0,0 +1,228 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package dialtesting + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateBrowserTaskChild(t *testing.T) { + ct, err := CreateTaskChild(ClassHeadless) + require.NoError(t, err) + require.IsType(t, &BrowserTask{}, ct) +} + +func TestBrowserTaskRunExternalProcess(t *testing.T) { + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + err = task.Run() + require.NoError(t, err) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, ClassHeadless, task.Class()) + assert.Equal(t, "browser_dial_testing", task.MetricName()) + assert.Equal(t, int64(1), fields["success"]) + assert.Equal(t, int64(12345), fields["response_time"]) + assert.Equal(t, int64(1), fields["last_step"]) + assert.Equal(t, "trace-1", fields["trace_id"]) + assert.Contains(t, fields["steps"], "open") + assert.Equal(t, "https://example.com", tags["url"]) + assert.Equal(t, "platform", tags["owner"]) +} + +func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "failure") + err = task.Run() + require.NoError(t, err) + + tags, fields := task.GetResults() + assert.Equal(t, "FAIL", tags["status"]) + assert.Equal(t, int64(-1), fields["success"]) + assert.Contains(t, fields["fail_reason"], "step_error") + assert.Contains(t, fields["message"], "title mismatch") + assert.Equal(t, int64(2), fields["last_step"]) + assert.Contains(t, fields["steps"], "title") +} + +func TestBrowserTaskRenderTemplate(t *testing.T) { + task := &BrowserTask{ + Task: &Task{ + Name: "browser", + ConfigVars: []*ConfigVar{ + {Name: "host", Value: "example.com"}, + {Name: "title", Value: "Example"}, + }, + }, + BrowserConfig: "name: browser\ntarget: https://{{host}}\nsteps:\n - action: goto\n - action: assert_title\n contains: {{title}}\n", + } + itask, err := NewTask("", task) + require.NoError(t, err) + require.NoError(t, itask.RenderTemplateAndInit(nil)) + + assert.Contains(t, task.BrowserConfig, "target: https://example.com") + assert.Contains(t, task.BrowserConfig, "contains: Example") +} + +func TestBrowserTaskCheckBrowserConfig(t *testing.T) { + task := newBrowserTaskForTest() + require.NoError(t, task.check()) + + task.BrowserConfig = "" + assert.EqualError(t, task.check(), "browser_config should not be empty") + + task.BrowserConfig = "name: homepage\ntarget: https://example.com\n" + assert.EqualError(t, task.check(), "browser_config steps should not be empty") +} + +func TestBrowserTaskGetHostNameFromGotoURL(t *testing.T) { + task := &BrowserTask{ + BrowserConfig: `name: homepage +steps: + - action: goto + url: https://example.com + - action: goto + url: https://example.com/path + - action: goto + url: https://docs.example.com +`, + } + hosts, err := task.getHostName() + require.NoError(t, err) + assert.Equal(t, []string{"example.com", "docs.example.com"}, hosts) +} + +func TestBrowserTaskIgnoresOuterSuccessWhen(t *testing.T) { + taskJSON := `{ + "external_id": "bd-homepage", + "name": "homepage", + "frequency": "1m", + "success_when": [{"response_time": "1ms"}], + "success_when_logic": "and", + "browser_config": "name: homepage\ntarget: https://example.com\nsteps:\n - action: goto\n" + }` + child, err := CreateTaskChild(ClassHeadless) + require.NoError(t, err) + task, err := NewTask(taskJSON, child) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + require.NoError(t, task.Check()) + require.NoError(t, task.Run()) + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, int64(1), fields["success"]) +} + +func TestBrowserTaskSanitizeRawTask(t *testing.T) { + task := &BrowserTask{ + Task: &Task{Name: "homepage", Frequency: "1m"}, + BrowserConfig: `name: homepage +target: https://example.com +config_vars: + - name: LOGIN_USER + value: user@example.com + secure: false + - name: LOGIN_PASSWORD + value: secret + secure: true +steps: + - action: goto +`, + } + raw, err := task.getRawTask(mustJSON(t, task)) + require.NoError(t, err) + assert.Contains(t, raw, "LOGIN_PASSWORD") + assert.NotContains(t, raw, "secret") + assert.Contains(t, raw, "user@example.com") +} + +func TestBrowserDialHelperProcess(t *testing.T) { + mode := os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") + if mode == "" { + return + } + if len(os.Args) == 0 || !strings.Contains(strings.Join(os.Args, " "), "run") { + os.Exit(2) + } + + output := map[string]interface{}{ + "exit_code": 0, + "run": map[string]interface{}{ + "run_id": "run-1", + "name": "homepage", + "target": "https://example.com", + "status": "OK", + "success": true, + "duration_us": 12345, + "steps": []map[string]interface{}{ + {"seq": 1, "name": "open", "status": "OK", "duration_us": 1000, "url": "https://example.com", "title": "Example"}, + }, + "trace_ids": []string{"trace-1"}, + }, + } + exitCode := 0 + if mode == "failure" { + output["exit_code"] = 1 + output["run"] = map[string]interface{}{ + "run_id": "run-2", + "name": "homepage", + "target": "https://example.com", + "status": "FAIL", + "success": false, + "duration_us": 54321, + "fail_reason": "step_error", + "error": map[string]interface{}{ + "name": "assertion", + "message": "title mismatch", + }, + "steps": []map[string]interface{}{ + {"seq": 1, "name": "open", "status": "OK", "duration_us": 1000}, + {"seq": 2, "name": "title", "status": "FAIL", "duration_us": 1000}, + }, + } + exitCode = 1 + } + + _ = json.NewEncoder(os.Stdout).Encode(output) + os.Exit(exitCode) +} + +func newBrowserTaskForTest() *BrowserTask { + task := &BrowserTask{ + Task: &Task{ + Name: "homepage", + Frequency: "1m", + Tags: map[string]string{"owner": "platform"}, + }, + BrowserConfig: "name: homepage\ntarget: https://example.com\ntimeout_ms: 1000\ntags:\n owner: platform\nsteps:\n - action: goto\n - action: assert_title\n contains: Example\n", + } + task.initTask() + return task +} + +func mustJSON(t *testing.T, value interface{}) string { + t.Helper() + data, err := json.Marshal(value) + require.NoError(t, err) + return string(data) +} diff --git a/dialtesting/task.go b/dialtesting/task.go index b73dcb39..5f25c1d6 100644 --- a/dialtesting/task.go +++ b/dialtesting/task.go @@ -230,7 +230,7 @@ func CreateTaskChild(taskType string) (TaskChild, error) { ct = &MultiTask{} case "headless", "browser", ClassHeadless: - return nil, errors.New("headless task deprecated") + ct = &BrowserTask{} case "tcp", ClassTCP: ct = &TCPTask{} diff --git a/go.mod b/go.mod index 554a93e9..a0adc2ea 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( gopkg.in/CodapeWild/dd-trace-go.v1 v1.35.17 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -113,7 +114,6 @@ require ( golang.org/x/tools v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect From 80f50be6acf3e10bc3fd4433fcfbdbd8231182a4 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 18 May 2026 16:26:45 +0800 Subject: [PATCH 02/21] save --- dialtesting/README.md | 139 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 dialtesting/README.md diff --git a/dialtesting/README.md b/dialtesting/README.md new file mode 100644 index 00000000..291059e7 --- /dev/null +++ b/dialtesting/README.md @@ -0,0 +1,139 @@ + + +# Dialtesting Browser Tasks + +`BROWSER` tasks run browser synthetic checks through an external `browser-dial` +process. The `dialtesting` package keeps the same task lifecycle as other dial +types: + +```text +task JSON -> NewTask -> Run -> GetResults -> report +``` + +`browser-dial` executes the browser script and returns JSON. `dialtesting` +parses that JSON and reports the final `browser_dial_testing` point through the +existing reporting path. + +## Runtime Dependencies + +Browser tasks require these binaries on the dial node: + +- `browser-dial` +- Lightpanda, resolved by `browser-dial` + +`dialtesting` resolves `browser-dial` in this order: + +1. `Task.SetOption()["browser_dial_path"]` +2. `Task.SetOption()["browserDialPath"]` +3. `BROWSER_DIAL_PATH` +4. `browser-dial` from `PATH` + +Lightpanda is not configured in task JSON. Configure it in the node environment, +for example with `LIGHTPANDA_EXECUTABLE_PATH`, or make it available from `PATH`. + +## Task Fields + +Browser tasks use the normal common task fields, including: + +- `external_id` +- `name` +- `status` +- `frequency` +- `schedule_type` +- `crontab` +- `post_url` +- `tags` +- `config_vars` +- `owner_external_id` + +The browser-specific task field is: + +```json +{ + "browser_config": "" +} +``` + +`browser_config` is a YAML string using the native `browser-dial` script format. +It can contain `name`, `target`, `timeout_ms`, `tags`, `metadata`, `auth`, +`config_vars`, and `steps`. + +Do not put these browser script fields at the outer task level: + +- `target` +- `steps` +- `auth` +- `success_when` +- `success_when_logic` +- `browser_dial_path` +- `lightpanda_path` + +Success rules belong in `browser_config.steps` as browser assertions such as +`assert_title`, `assert_url`, and `assert_text`. + +## Example + +```json +{ + "external_id": "bd-homepage", + "name": "homepage", + "status": "OK", + "frequency": "1m", + "schedule_type": "frequency", + "tags": { + "owner": "platform" + }, + "browser_config": "name: homepage\ntarget: https://example.com\ntimeout_ms: 30000\nsteps:\n - name: open homepage\n action: goto\n - name: check title\n action: assert_title\n contains: Example\n - name: check body\n action: assert_text\n selector: body\n contains: Example Domain\n" +} +``` + +The generated command is equivalent to: + +```bash +browser-dial run --dry-run --skip-token-check --json +``` + +## Host Validation + +`GetHostName()` returns every explicitly configured browser destination so the +caller can reject illegal dial addresses before execution. + +It collects hostnames from: + +- top-level `browser_config.target` +- `browser_config.steps` entries where `action: goto` and `url` is set + +The returned list is de-duplicated. Runtime redirects, JavaScript navigation, +third-party page assets, and `eval`-generated requests are not statically +expanded. + +## Result Mapping + +When `browser-dial` returns `run.success=true`: + +- tag `status=OK` +- field `success=1` + +When `browser-dial` returns `run.success=false`: + +- tag `status=FAIL` +- field `success=-1` +- field `fail_reason` from `run.fail_reason` and the error message + +Common result fields include: + +- `response_time` +- `last_step` +- `browser_run_id` +- `exit_code` +- `message` +- `page_url` +- `page_title` +- `trace_id` +- `trace_ids` +- `steps` From 7e2e8153f47d0afe322ba3824e34e92aaf957e5d Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 21 May 2026 14:21:14 +0800 Subject: [PATCH 03/21] save --- dialtesting/browser.go | 50 +++++++++++++++++++++++- dialtesting/browser_test.go | 76 +++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index b78af159..f6239b22 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "strings" + "sync" "text/template" "time" @@ -31,6 +32,8 @@ const ( optionBrowserDialPath = "browser_dial_path" optionBrowserDialPathCamel = "browserDialPath" + optionLightpandaPath = "lightpanda_path" + optionLightpandaPathCamel = "lightpandaPath" ) type BrowserTask struct { @@ -43,6 +46,13 @@ type BrowserTask struct { reqError string stderr string rawTask *BrowserTask + + cancelMu sync.Mutex + cancel *browserTaskCancel +} + +type browserTaskCancel struct { + cancel context.CancelFunc } type browserConfig struct { @@ -101,7 +111,14 @@ func (t *BrowserTask) clear() { t.stderr = "" } -func (t *BrowserTask) stop() {} +func (t *BrowserTask) stop() { + t.cancelMu.Lock() + cancel := t.cancel + t.cancelMu.Unlock() + if cancel != nil && cancel.cancel != nil { + cancel.cancel() + } +} func (t *BrowserTask) class() string { return ClassHeadless @@ -123,6 +140,8 @@ func (t *BrowserTask) run() error { timeoutMS := t.effectiveTimeoutMS() ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) defer cancel() + cancelState := t.setCancel(cancel) + defer t.clearCancel(cancelState) args := []string{ "run", path, @@ -137,6 +156,9 @@ func (t *BrowserTask) run() error { cmd := exec.CommandContext(ctx, t.executablePath(), cmdArgs...) cmd.Stdout = &stdout cmd.Stderr = &stderr + if lightpandaPath := t.lightpandaPath(); lightpandaPath != "" { + cmd.Env = append(os.Environ(), "LIGHTPANDA_EXECUTABLE_PATH="+lightpandaPath) + } err = cmd.Run() t.duration = time.Since(start) @@ -161,6 +183,22 @@ func (t *BrowserTask) run() error { return nil } +func (t *BrowserTask) setCancel(cancel context.CancelFunc) *browserTaskCancel { + cancelState := &browserTaskCancel{cancel: cancel} + t.cancelMu.Lock() + t.cancel = cancelState + t.cancelMu.Unlock() + return cancelState +} + +func (t *BrowserTask) clearCancel(cancelState *browserTaskCancel) { + t.cancelMu.Lock() + if t.cancel == cancelState { + t.cancel = nil + } + t.cancelMu.Unlock() +} + func executableArgs(args []string) []string { if os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") == "" { return args @@ -195,6 +233,16 @@ func (t *BrowserTask) executablePath() string { return defaultBrowserDialPath } +func (t *BrowserTask) lightpandaPath() string { + if value := t.GetOption()[optionLightpandaPath]; value != "" { + return value + } + if value := t.GetOption()[optionLightpandaPathCamel]; value != "" { + return value + } + return "" +} + func (t *BrowserTask) effectiveTimeoutMS() int { cfg, err := t.parseBrowserConfig() if err == nil && cfg.TimeoutMS > 0 { diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 171d0458..9a13cc48 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -10,6 +10,7 @@ import ( "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,6 +45,24 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, "platform", tags["owner"]) } +func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{ + optionBrowserDialPath: os.Args[0], + optionLightpandaPath: "/opt/datakit/lightpanda", + }) + + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "check-lightpanda") + err = task.Run() + require.NoError(t, err) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, int64(1), fields["success"]) +} + func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) @@ -63,6 +82,41 @@ func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { assert.Contains(t, fields["steps"], "title") } +func TestBrowserTaskStopCancelsExternalProcess(t *testing.T) { + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + startedPath := t.TempDir() + "/started" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "sleep") + t.Setenv("BROWSER_DIAL_HELPER_STARTED", startedPath) + + done := make(chan error, 1) + go func() { + done <- task.Run() + }() + + require.Eventually(t, func() bool { + _, err := os.Stat(startedPath) + return err == nil + }, 2*time.Second, 10*time.Millisecond) + + task.Stop() + task.Stop() + + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(3 * time.Second): + t.Fatal("browser task did not stop external process") + } + + _, fields := task.GetResults() + assert.Equal(t, int64(-1), fields["success"]) + assert.NotEmpty(t, fields["message"]) +} + func TestBrowserTaskRenderTemplate(t *testing.T) { task := &BrowserTask{ Task: &Task{ @@ -156,6 +210,18 @@ steps: assert.Contains(t, raw, "user@example.com") } +func TestBrowserTaskLightpandaPathOption(t *testing.T) { + task := &BrowserTask{Task: &Task{}} + task.SetOption(map[string]string{optionLightpandaPath: "/opt/lightpanda"}) + assert.Equal(t, "/opt/lightpanda", task.lightpandaPath()) + + task.SetOption(map[string]string{optionLightpandaPathCamel: "/opt/lightpanda-camel"}) + assert.Equal(t, "/opt/lightpanda-camel", task.lightpandaPath()) + + task.SetOption(map[string]string{}) + assert.Empty(t, task.lightpandaPath()) +} + func TestBrowserDialHelperProcess(t *testing.T) { mode := os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") if mode == "" { @@ -164,6 +230,16 @@ func TestBrowserDialHelperProcess(t *testing.T) { if len(os.Args) == 0 || !strings.Contains(strings.Join(os.Args, " "), "run") { os.Exit(2) } + if mode == "check-lightpanda" && os.Getenv("LIGHTPANDA_EXECUTABLE_PATH") != "/opt/datakit/lightpanda" { + os.Exit(3) + } + if mode == "sleep" { + if startedPath := os.Getenv("BROWSER_DIAL_HELPER_STARTED"); startedPath != "" { + _ = os.WriteFile(startedPath, []byte("started"), 0o600) + } + time.Sleep(10 * time.Second) + os.Exit(0) + } output := map[string]interface{}{ "exit_code": 0, From a4ebd3eeb889f528b39d257a4811fda24bddcc63 Mon Sep 17 00:00:00 2001 From: zhengb Date: Thu, 28 May 2026 15:18:05 +0800 Subject: [PATCH 04/21] save --- dialtesting/browser.go | 291 ++++++++++++++++++++++++++++++++++-- dialtesting/browser_test.go | 219 +++++++++++++++++++++++++++ 2 files changed, 495 insertions(+), 15 deletions(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index f6239b22..e86d4fa5 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -29,6 +29,8 @@ var ( const ( defaultBrowserDialPath = "browser-dial" defaultBrowserDialTimeout = 30_000 + defaultBrowserWidth = 1920 + defaultBrowserHeight = 1080 optionBrowserDialPath = "browser_dial_path" optionBrowserDialPathCamel = "browserDialPath" @@ -40,21 +42,67 @@ type BrowserTask struct { *Task BrowserConfig string `json:"browser_config"` + BrowserWindow *BrowserWindowOption `json:"browser_window,omitempty"` + AdvanceOptions *BrowserAdvanceOption `json:"advance_options,omitempty"` + RetryOptions *BrowserRetryOption `json:"retry_options,omitempty"` + duration time.Duration result browserDialRun exitCode int reqError string stderr string rawTask *BrowserTask + results []browserViewportResult cancelMu sync.Mutex cancel *browserTaskCancel } +type BrowserWindowOption struct { + Viewports []BrowserViewport `json:"viewports,omitempty"` +} + +type BrowserViewport struct { + Width int `json:"width"` + Height int `json:"height"` +} + +type BrowserAdvanceOption struct { + Engine string `json:"engine,omitempty"` + + ScreenshotOnFailure bool `json:"screenshot_on_failure,omitempty"` + + Headers map[string]string `json:"headers,omitempty"` + Cookies []BrowserCookie `json:"cookies,omitempty"` + IgnoreHTTPSErrors bool `json:"ignore_https_errors,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` +} + +type BrowserCookie struct { + Name string `json:"name"` + Value string `json:"value,omitempty"` +} + +type BrowserRetryOption struct { + Enabled bool `json:"enabled,omitempty"` + Count int `json:"count,omitempty"` + IntervalSec int `json:"interval_sec,omitempty"` +} + type browserTaskCancel struct { cancel context.CancelFunc } +type browserViewportResult struct { + viewport BrowserViewport + duration time.Duration + result browserDialRun + exitCode int + reqError string + stderr string + attempts int +} + type browserConfig struct { Name string `yaml:"name"` Target string `yaml:"target"` @@ -109,6 +157,7 @@ func (t *BrowserTask) clear() { t.exitCode = 0 t.reqError = "" t.stderr = "" + t.results = nil } func (t *BrowserTask) stop() { @@ -129,7 +178,11 @@ func (t *BrowserTask) metricName() string { } func (t *BrowserTask) run() error { - start := time.Now() + if _, err := t.parseBrowserConfig(); err != nil { + t.reqError = fmt.Sprintf("parse browser_config failed: %s", err) + return nil + } + path, err := t.writeScriptFile() if err != nil { t.reqError = err.Error() @@ -137,18 +190,52 @@ func (t *BrowserTask) run() error { } defer os.Remove(path) //nolint:errcheck + result := t.runViewport(path, t.effectiveViewports()[0]) + t.results = append(t.results, result) + t.setLastResult(result) + + return nil +} + +func (t *BrowserTask) runViewport(path string, viewport BrowserViewport) browserViewportResult { + maxAttempts := 1 + if t.RetryOptions != nil && t.RetryOptions.Enabled { + maxAttempts += clampBrowserRetryCount(t.RetryOptions.Count) + } + + var result browserViewportResult + for attempt := 1; attempt <= maxAttempts; attempt++ { + result = t.runBrowserDial(path, viewport) + result.attempts = attempt + if result.reqError == "" && result.result.Success { + return result + } + if isBrowserParseFailure(result) || attempt == maxAttempts { + return result + } + if interval := t.retryInterval(); interval > 0 { + browserRetrySleep(interval) + } + } + return result +} + +func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) browserViewportResult { + start := time.Now() timeoutMS := t.effectiveTimeoutMS() ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) defer cancel() cancelState := t.setCancel(cancel) defer t.clearCancel(cancelState) + result := browserViewportResult{viewport: viewport} args := []string{ "run", path, "--dry-run", "--skip-token-check", "--json", } + args = append(args, t.browserDialOptions(viewport)...) var stdout bytes.Buffer var stderr bytes.Buffer @@ -160,27 +247,64 @@ func (t *BrowserTask) run() error { cmd.Env = append(os.Environ(), "LIGHTPANDA_EXECUTABLE_PATH="+lightpandaPath) } - err = cmd.Run() - t.duration = time.Since(start) - t.stderr = strings.TrimSpace(stderr.String()) + err := cmd.Run() + result.duration = time.Since(start) + result.stderr = strings.TrimSpace(stderr.String()) if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { - t.exitCode = exitErr.ExitCode() + result.exitCode = exitErr.ExitCode() } else if err != nil { - t.reqError = err.Error() - return nil + result.reqError = err.Error() + return result } var output browserDialOutput if decodeErr := json.Unmarshal(stdout.Bytes(), &output); decodeErr != nil { - t.reqError = fmt.Sprintf("parse browser-dial output failed: %s", decodeErr) - if t.stderr != "" { - t.reqError += ": " + t.stderr + result.reqError = fmt.Sprintf("parse browser-dial output failed: %s", decodeErr) + if result.stderr != "" { + result.reqError += ": " + result.stderr } - return nil + return result } - t.exitCode = output.ExitCode - t.result = output.Run - return nil + result.exitCode = output.ExitCode + result.result = output.Run + return result +} + +func (t *BrowserTask) setLastResult(result browserViewportResult) { + t.duration = result.duration + t.result = result.result + t.exitCode = result.exitCode + t.reqError = result.reqError + t.stderr = result.stderr +} + +func (t *BrowserTask) browserDialOptions(viewport BrowserViewport) []string { + args := []string{ + "--viewport-width", fmt.Sprintf("%d", viewport.Width), + "--viewport-height", fmt.Sprintf("%d", viewport.Height), + } + if t.AdvanceOptions == nil { + return args + } + if t.AdvanceOptions.Engine != "" { + args = append(args, "--engine", t.AdvanceOptions.Engine) + } + if t.AdvanceOptions.ScreenshotOnFailure { + args = append(args, "--screenshot-on-failure") + } + for key, value := range t.AdvanceOptions.Headers { + args = append(args, "--header", key+"="+value) + } + for _, cookie := range t.AdvanceOptions.Cookies { + args = append(args, "--cookie", cookie.Name+"="+cookie.Value) + } + if t.AdvanceOptions.IgnoreHTTPSErrors { + args = append(args, "--ignore-https-errors") + } + if t.AdvanceOptions.ProxyURL != "" { + args = append(args, "--proxy-url", t.AdvanceOptions.ProxyURL) + } + return args } func (t *BrowserTask) setCancel(cancel context.CancelFunc) *browserTaskCancel { @@ -275,6 +399,7 @@ func (t *BrowserTask) checkResult() (reasons []string, succFlag bool) { func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]interface{}) { cfg, _ := t.parseBrowserConfig() + result := t.lastViewportResult() name := firstNonEmpty(t.result.Name, cfg.Name, t.Name) target := firstNonEmpty(t.result.Target, cfg.Target) tags = map[string]string{ @@ -282,7 +407,7 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in "url": target, "status": "FAIL", "runner": "browser-dial", - "browser_engine": "lightpanda", + "browser_engine": t.effectiveEngine(), } for k, v := range cfg.Tags { tags[k] = v @@ -290,6 +415,9 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in for k, v := range t.Tags { tags[k] = v } + if result.viewport.Width > 0 && result.viewport.Height > 0 { + tags["viewport"] = fmt.Sprintf("%dx%d", result.viewport.Width, result.viewport.Height) + } responseTime := t.result.DurationUS if responseTime == 0 { @@ -302,6 +430,13 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in "browser_run_id": t.result.RunID, "exit_code": int64(t.exitCode), } + if result.viewport.Width > 0 && result.viewport.Height > 0 { + fields["viewport_width"] = int64(result.viewport.Width) + fields["viewport_height"] = int64(result.viewport.Height) + } + if result.attempts > 0 { + fields["retry_count"] = int64(result.attempts - 1) + } if t.reqError == "" && t.result.Success { tags["status"] = "OK" @@ -348,6 +483,16 @@ func (t *BrowserTask) check() error { if len(cfg.Steps) == 0 { return errors.New("browser_config steps should not be empty") } + t.applyDefaultBrowserWindow() + if err := t.checkBrowserWindow(); err != nil { + return err + } + if err := t.checkBrowserAdvanceOptions(); err != nil { + return err + } + if err := t.checkBrowserRetryOptions(); err != nil { + return err + } return nil } @@ -423,6 +568,7 @@ func (t *BrowserTask) initTask() { if t.Task == nil { t.Task = &Task{} } + t.applyDefaultBrowserWindow() } func (t *BrowserTask) setReqError(err string) { @@ -465,6 +611,121 @@ func (t *BrowserTask) parseBrowserConfig() (browserConfig, error) { return cfg, nil } +func (t *BrowserTask) applyDefaultBrowserWindow() { + if t.BrowserWindow == nil { + t.BrowserWindow = &BrowserWindowOption{} + } + if len(t.BrowserWindow.Viewports) == 0 { + t.BrowserWindow.Viewports = []BrowserViewport{{Width: defaultBrowserWidth, Height: defaultBrowserHeight}} + } +} + +func (t *BrowserTask) effectiveViewports() []BrowserViewport { + t.applyDefaultBrowserWindow() + return t.BrowserWindow.Viewports +} + +func (t *BrowserTask) checkBrowserWindow() error { + viewports := t.effectiveViewports() + if len(viewports) > 1 { + return fmt.Errorf("browser_window.viewports currently supports at most one viewport") + } + for _, viewport := range viewports { + if viewport.Width <= 0 || viewport.Height <= 0 { + return fmt.Errorf("browser_window viewport width and height should be greater than 0") + } + } + return nil +} + +func (t *BrowserTask) checkBrowserAdvanceOptions() error { + if t.AdvanceOptions == nil { + return nil + } + switch t.AdvanceOptions.Engine { + case "", "chrome", "lightpanda": + default: + return fmt.Errorf("advance_options engine should be chrome or lightpanda") + } + for key := range t.AdvanceOptions.Headers { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("advance_options headers key should not be empty") + } + } + for _, cookie := range t.AdvanceOptions.Cookies { + if strings.TrimSpace(cookie.Name) == "" { + return fmt.Errorf("advance_options cookie name should not be empty") + } + } + return nil +} + +func (t *BrowserTask) effectiveEngine() string { + if t.AdvanceOptions != nil && t.AdvanceOptions.Engine != "" { + return t.AdvanceOptions.Engine + } + return "chrome" +} + +func (t *BrowserTask) checkBrowserRetryOptions() error { + if t.RetryOptions == nil { + return nil + } + if t.RetryOptions.Count < 0 || t.RetryOptions.Count > 3 { + return fmt.Errorf("retry_options count should be between 0 and 3") + } + if t.RetryOptions.Enabled && t.RetryOptions.Count > 0 && + (t.RetryOptions.IntervalSec < 5 || t.RetryOptions.IntervalSec > 300) { + return fmt.Errorf("retry_options interval_sec should be between 5 and 300") + } + return nil +} + +func (t *BrowserTask) retryInterval() time.Duration { + if t.RetryOptions == nil || !t.RetryOptions.Enabled || t.RetryOptions.Count <= 0 { + return 0 + } + interval := t.RetryOptions.IntervalSec + if interval < 5 { + interval = 5 + } + if interval > 300 { + interval = 300 + } + return time.Duration(interval) * time.Second +} + +func (t *BrowserTask) lastViewportResult() browserViewportResult { + if len(t.results) == 0 { + return browserViewportResult{} + } + return t.results[len(t.results)-1] +} + +func clampBrowserRetryCount(count int) int { + if count < 0 { + return 0 + } + if count > 3 { + return 3 + } + return count +} + +func isBrowserParseFailure(result browserViewportResult) bool { + text := strings.ToLower(strings.Join([]string{ + result.reqError, + result.stderr, + result.result.FailReason, + }, " ")) + if result.result.Error != nil { + text += " " + strings.ToLower(result.result.Error.Name+" "+result.result.Error.Message) + } + return strings.Contains(text, "parse") +} + +var browserRetrySleep = time.Sleep + func sanitizeBrowserConfig(config string) string { if strings.TrimSpace(config) == "" { return config diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 9a13cc48..08ca21e3 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -8,6 +8,7 @@ package dialtesting import ( "encoding/json" "os" + "strconv" "strings" "testing" "time" @@ -43,6 +44,10 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Contains(t, fields["steps"], "open") assert.Equal(t, "https://example.com", tags["url"]) assert.Equal(t, "platform", tags["owner"]) + assert.Equal(t, "1920x1080", tags["viewport"]) + assert.Equal(t, int64(1920), fields["viewport_width"]) + assert.Equal(t, int64(1080), fields["viewport_height"]) + assert.Equal(t, int64(0), fields["retry_count"]) } func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { @@ -147,6 +152,189 @@ func TestBrowserTaskCheckBrowserConfig(t *testing.T) { assert.EqualError(t, task.check(), "browser_config steps should not be empty") } +func TestBrowserTaskParseNewFields(t *testing.T) { + taskJSON := `{ + "external_id": "bd-homepage", + "name": "homepage", + "frequency": "1m", + "browser_config": "name: homepage\ntarget: https://example.com\nsteps:\n - action: goto\n", + "browser_window": {"viewports": [{"width": 1920, "height": 1080}]}, + "advance_options": { + "engine": "chrome", + "screenshot_on_failure": true, + "headers": {"X-Test": "ok"}, + "cookies": [{"name": "sid", "value": "abc"}], + "ignore_https_errors": true, + "proxy_url": "http://127.0.0.1:7897" + }, + "retry_options": {"enabled": true, "count": 2, "interval_sec": 10} + }` + child, err := CreateTaskChild(ClassHeadless) + require.NoError(t, err) + task, err := NewTask(taskJSON, child) + require.NoError(t, err) + browserTask := task.(*BrowserTask) //nolint:forcetypeassert + require.NoError(t, task.Check()) + assert.Len(t, browserTask.BrowserWindow.Viewports, 1) + assert.Equal(t, "chrome", browserTask.AdvanceOptions.Engine) + assert.True(t, browserTask.AdvanceOptions.ScreenshotOnFailure) + assert.Equal(t, "ok", browserTask.AdvanceOptions.Headers["X-Test"]) + assert.Equal(t, "sid", browserTask.AdvanceOptions.Cookies[0].Name) + assert.True(t, browserTask.AdvanceOptions.IgnoreHTTPSErrors) + assert.Equal(t, "http://127.0.0.1:7897", browserTask.AdvanceOptions.ProxyURL) + assert.Equal(t, 2, browserTask.RetryOptions.Count) +} + +func TestBrowserTaskCheckInvalidEngine(t *testing.T) { + task := newBrowserTaskForTest() + task.AdvanceOptions = &BrowserAdvanceOption{Engine: "firefox"} + assert.EqualError(t, task.check(), "advance_options engine should be chrome or lightpanda") +} + +func TestBrowserTaskCheckInvalidViewport(t *testing.T) { + task := newBrowserTaskForTest() + task.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 0, Height: 1080}}} + assert.EqualError(t, task.check(), "browser_window viewport width and height should be greater than 0") +} + +func TestBrowserTaskCheckMultipleViewports(t *testing.T) { + task := newBrowserTaskForTest() + task.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{ + {Width: 1920, Height: 1080}, + {Width: 1366, Height: 768}, + }} + assert.EqualError(t, task.check(), "browser_window.viewports currently supports at most one viewport") +} + +func TestBrowserTaskDefaultViewport(t *testing.T) { + task := newBrowserTaskForTest() + task.BrowserWindow = nil + require.NoError(t, task.check()) + require.NotNil(t, task.BrowserWindow) + require.Len(t, task.BrowserWindow.Viewports, 1) + assert.Equal(t, BrowserViewport{Width: 1920, Height: 1080}, task.BrowserWindow.Viewports[0]) + + task.BrowserWindow = &BrowserWindowOption{} + require.NoError(t, task.check()) + require.Len(t, task.BrowserWindow.Viewports, 1) + assert.Equal(t, BrowserViewport{Width: 1920, Height: 1080}, task.BrowserWindow.Viewports[0]) +} + +func TestBrowserTaskDefaultEngine(t *testing.T) { + task := newBrowserTaskForTest() + assert.Equal(t, "chrome", task.effectiveEngine()) +} + +func TestBrowserTaskCheckInvalidRetry(t *testing.T) { + task := newBrowserTaskForTest() + task.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 4, IntervalSec: 10} + assert.EqualError(t, task.check(), "retry_options count should be between 0 and 3") + + task.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 1, IntervalSec: 4} + assert.EqualError(t, task.check(), "retry_options interval_sec should be between 5 and 300") +} + +func TestBrowserTaskCheckInvalidHeaderAndCookie(t *testing.T) { + task := newBrowserTaskForTest() + task.AdvanceOptions = &BrowserAdvanceOption{Headers: map[string]string{"": "value"}} + assert.EqualError(t, task.check(), "advance_options headers key should not be empty") + + task.AdvanceOptions = &BrowserAdvanceOption{Cookies: []BrowserCookie{{Value: "value"}}} + assert.EqualError(t, task.check(), "advance_options cookie name should not be empty") +} + +func TestBrowserTaskRunSingleViewport(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 1366, Height: 768}}} + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + argsPath := t.TempDir() + "/args.log" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + require.NoError(t, task.Run()) + + lines := readHelperArgs(t, argsPath) + require.Len(t, lines, 1) + assert.Contains(t, lines[0], "--viewport-width 1366 --viewport-height 768") + + tags, fields := task.GetResults() + assert.Equal(t, "1366x768", tags["viewport"]) + assert.Equal(t, int64(1366), fields["viewport_width"]) + assert.Equal(t, int64(768), fields["viewport_height"]) +} + +func TestBrowserTaskRunAdvanceOptions(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.AdvanceOptions = &BrowserAdvanceOption{ + Engine: "chrome", + ScreenshotOnFailure: true, + Headers: map[string]string{"X-Test": "ok"}, + Cookies: []BrowserCookie{{Name: "sid", Value: "abc"}}, + IgnoreHTTPSErrors: true, + ProxyURL: "http://127.0.0.1:7897", + } + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + argsPath := t.TempDir() + "/args.log" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + require.NoError(t, task.Run()) + + line := readHelperArgs(t, argsPath)[0] + assert.Contains(t, line, "--engine chrome") + assert.Contains(t, line, "--screenshot-on-failure") + assert.Contains(t, line, "--header X-Test=ok") + assert.Contains(t, line, "--cookie sid=abc") + assert.Contains(t, line, "--ignore-https-errors") + assert.Contains(t, line, "--proxy-url http://127.0.0.1:7897") +} + +func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { + oldSleep := browserRetrySleep + browserRetrySleep = func(time.Duration) {} + defer func() { browserRetrySleep = oldSleep }() + + browserTask := newBrowserTaskForTest() + browserTask.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 2, IntervalSec: 5} + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + dir := t.TempDir() + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "fail-once") + t.Setenv("BROWSER_DIAL_HELPER_COUNT", dir+"/count") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", dir+"/args.log") + require.NoError(t, task.Run()) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, int64(1), fields["retry_count"]) + assert.Len(t, readHelperArgs(t, dir+"/args.log"), 2) +} + +func TestBrowserTaskParseFailureDoesNotRetry(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.BrowserConfig = "name: [" + browserTask.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 2, IntervalSec: 5} + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + argsPath := t.TempDir() + "/args.log" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + require.NoError(t, task.Run()) + + _, err = os.Stat(argsPath) + assert.True(t, os.IsNotExist(err)) + _, fields := task.GetResults() + assert.Contains(t, fields["message"], "parse browser_config failed") +} + func TestBrowserTaskGetHostNameFromGotoURL(t *testing.T) { task := &BrowserTask{ BrowserConfig: `name: homepage @@ -230,6 +418,14 @@ func TestBrowserDialHelperProcess(t *testing.T) { if len(os.Args) == 0 || !strings.Contains(strings.Join(os.Args, " "), "run") { os.Exit(2) } + if argsPath := os.Getenv("BROWSER_DIAL_HELPER_ARGS"); argsPath != "" { + file, err := os.OpenFile(argsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + os.Exit(4) + } + _, _ = file.WriteString(strings.Join(os.Args, " ") + "\n") + _ = file.Close() + } if mode == "check-lightpanda" && os.Getenv("LIGHTPANDA_EXECUTABLE_PATH") != "/opt/datakit/lightpanda" { os.Exit(3) } @@ -257,6 +453,9 @@ func TestBrowserDialHelperProcess(t *testing.T) { }, } exitCode := 0 + if mode == "fail-once" && incrementHelperCount(os.Getenv("BROWSER_DIAL_HELPER_COUNT")) == 1 { + mode = "failure" + } if mode == "failure" { output["exit_code"] = 1 output["run"] = map[string]interface{}{ @@ -302,3 +501,23 @@ func mustJSON(t *testing.T, value interface{}) string { require.NoError(t, err) return string(data) } + +func readHelperArgs(t *testing.T, path string) []string { + t.Helper() + data, err := os.ReadFile(path) + require.NoError(t, err) + return strings.Split(strings.TrimSpace(string(data)), "\n") +} + +func incrementHelperCount(path string) int { + if path == "" { + return 1 + } + count := 0 + if data, err := os.ReadFile(path); err == nil { + count, _ = strconv.Atoi(strings.TrimSpace(string(data))) + } + count++ + _ = os.WriteFile(path, []byte(strconv.Itoa(count)), 0o600) + return count +} From 521570d09bc0aa62f714e54062cbfc94240778fe Mon Sep 17 00:00:00 2001 From: zhengb Date: Thu, 28 May 2026 15:45:42 +0800 Subject: [PATCH 05/21] save --- dialtesting/browser_test.go | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 08ca21e3..ad1483b2 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -410,6 +410,96 @@ func TestBrowserTaskLightpandaPathOption(t *testing.T) { assert.Empty(t, task.lightpandaPath()) } +func TestBrowserTaskExecutablePathOption(t *testing.T) { + task := &BrowserTask{Task: &Task{}} + task.SetOption(map[string]string{optionBrowserDialPath: "/opt/browser-dial"}) + assert.Equal(t, "/opt/browser-dial", task.executablePath()) + + task.SetOption(map[string]string{optionBrowserDialPathCamel: "/opt/browser-dial-camel"}) + assert.Equal(t, "/opt/browser-dial-camel", task.executablePath()) + + task.SetOption(map[string]string{}) + t.Setenv("BROWSER_DIAL_PATH", "/env/browser-dial") + assert.Equal(t, "/env/browser-dial", task.executablePath()) + + t.Setenv("BROWSER_DIAL_PATH", "") + assert.Equal(t, defaultBrowserDialPath, task.executablePath()) +} + +func TestBrowserTaskResultFallbacks(t *testing.T) { + task := newBrowserTaskForTest() + task.stderr = "stderr failure" + reasons, ok := task.checkResult() + assert.False(t, ok) + assert.Equal(t, []string{"stderr failure"}, reasons) + + task.stderr = "" + reasons, ok = task.checkResult() + assert.False(t, ok) + assert.Equal(t, []string{"browser dial failed"}, reasons) + + task.setReqError("before run failed") + reasons, ok = task.checkResult() + assert.False(t, ok) + assert.Equal(t, []string{"before run failed"}, reasons) +} + +func TestBrowserTaskHostNameErrors(t *testing.T) { + task := &BrowserTask{BrowserConfig: "name: homepage\nsteps:\n - action: click\n"} + _, err := task.getHostName() + assert.EqualError(t, err, "browser_config target or goto url should not be empty") + + task.BrowserConfig = "name: homepage\ntarget: http://[::1\nsteps:\n - action: goto\n" + _, err = task.getHostName() + assert.Error(t, err) +} + +func TestBrowserTaskRetryHelpers(t *testing.T) { + task := &BrowserTask{RetryOptions: &BrowserRetryOption{Enabled: true, Count: 1, IntervalSec: 1}} + assert.Equal(t, 5*time.Second, task.retryInterval()) + + task.RetryOptions.IntervalSec = 301 + assert.Equal(t, 300*time.Second, task.retryInterval()) + + task.RetryOptions.Enabled = false + assert.Equal(t, time.Duration(0), task.retryInterval()) + + assert.Equal(t, 0, clampBrowserRetryCount(-1)) + assert.Equal(t, 3, clampBrowserRetryCount(4)) +} + +func TestBrowserTaskSanitizeBrowserConfigFallbacks(t *testing.T) { + assert.Equal(t, "", sanitizeBrowserConfig("")) + + invalid := "name: [" + assert.Equal(t, invalid, sanitizeBrowserConfig(invalid)) + + sanitized := sanitizeBrowserConfig(`name: homepage +target: https://example.com +config_vars: + - name: LOGIN_PASSWORD + secure: true +steps: + - action: goto +`) + assert.Contains(t, sanitized, "LOGIN_PASSWORD") + assert.Contains(t, sanitized, "value: \"\"") +} + +func TestBrowserTaskSmallHelpers(t *testing.T) { + task := &BrowserTask{} + _, err := task.getVariableValue(Variable{}) + assert.EqualError(t, err, "not support") + + task.initTask() + require.NotNil(t, task.Task) + assert.Equal(t, []BrowserViewport{{Width: 1920, Height: 1080}}, task.BrowserWindow.Viewports) + + assert.Equal(t, []string{"example.com"}, dedupBrowserHostNames([]string{"example.com", "", "example.com"})) + assert.Equal(t, []string{}, dedupBrowserHostNames(nil)) + assert.Equal(t, []string{}, dedupBrowserHostNames([]string{"", ""})) +} + func TestBrowserDialHelperProcess(t *testing.T) { mode := os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") if mode == "" { From bd9c65b71c9b97a91f8fdd991da901e4671fa47e Mon Sep 17 00:00:00 2001 From: zhengb Date: Thu, 28 May 2026 18:11:04 +0800 Subject: [PATCH 06/21] save --- dialtesting/browser.go | 130 ++++++++++++++++++++++++++++++++++-- dialtesting/browser_test.go | 64 +++++++++++++++++- 2 files changed, 189 insertions(+), 5 deletions(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index e86d4fa5..832041ec 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -28,9 +28,11 @@ var ( const ( defaultBrowserDialPath = "browser-dial" - defaultBrowserDialTimeout = 30_000 + defaultBrowserDialTimeout = 300_000 defaultBrowserWidth = 1920 defaultBrowserHeight = 1080 + maxBrowserTotalTimeoutMS = 300_000 + maxBrowserStepTimeoutMS = 60_000 optionBrowserDialPath = "browser_dial_path" optionBrowserDialPathCamel = "browserDialPath" @@ -109,12 +111,18 @@ type browserConfig struct { TimeoutMS int `yaml:"timeout_ms"` Tags map[string]string `yaml:"tags"` ConfigVars []ConfigVar `yaml:"config_vars"` + Auth browserConfigAuth `yaml:"auth"` Steps []browserConfigStep `yaml:"steps"` } +type browserConfigAuth struct { + Steps []browserConfigStep `yaml:"steps"` +} + type browserConfigStep struct { - Action string `yaml:"action"` - URL string `yaml:"url"` + Action string `yaml:"action"` + URL string `yaml:"url"` + TimeoutMS int `yaml:"timeout_ms"` } type browserDialOutput struct { @@ -142,6 +150,7 @@ type browserDialStep struct { DurationUS int64 `json:"duration_us"` URL string `json:"url,omitempty"` Title string `json:"title,omitempty"` + Screenshot string `json:"screenshot,omitempty"` Error *browserDialError `json:"error,omitempty"` } @@ -337,7 +346,12 @@ func (t *BrowserTask) writeScriptFile() (string, error) { } defer file.Close() //nolint:errcheck - if _, err := file.WriteString(t.BrowserConfig); err != nil { + config, err := normalizeBrowserConfigTimeouts(t.BrowserConfig) + if err != nil { + os.Remove(file.Name()) //nolint:errcheck + return "", err + } + if _, err := file.WriteString(config); err != nil { os.Remove(file.Name()) //nolint:errcheck return "", err } @@ -483,6 +497,9 @@ func (t *BrowserTask) check() error { if len(cfg.Steps) == 0 { return errors.New("browser_config steps should not be empty") } + if err := checkBrowserConfigTimeouts(cfg); err != nil { + return err + } t.applyDefaultBrowserWindow() if err := t.checkBrowserWindow(); err != nil { return err @@ -611,6 +628,68 @@ func (t *BrowserTask) parseBrowserConfig() (browserConfig, error) { return cfg, nil } +func checkBrowserConfigTimeouts(cfg browserConfig) error { + if cfg.TimeoutMS > maxBrowserTotalTimeoutMS { + return fmt.Errorf("browser_config timeout_ms should not exceed %d", maxBrowserTotalTimeoutMS) + } + for index, step := range cfg.Auth.Steps { + if step.TimeoutMS > maxBrowserStepTimeoutMS { + return fmt.Errorf("browser_config auth.steps %d timeout_ms should not exceed %d", index+1, maxBrowserStepTimeoutMS) + } + } + for index, step := range cfg.Steps { + if step.TimeoutMS > maxBrowserStepTimeoutMS { + return fmt.Errorf("browser_config steps %d timeout_ms should not exceed %d", index+1, maxBrowserStepTimeoutMS) + } + } + return nil +} + +func normalizeBrowserConfigTimeouts(config string) (string, error) { + var node yaml.Node + if err := yaml.Unmarshal([]byte(config), &node); err != nil { + return "", err + } + if len(node.Content) == 0 || node.Content[0].Kind != yaml.MappingNode { + return config, nil + } + root := node.Content[0] + if timeout, ok := getYAMLMapInt(root, "timeout_ms"); ok && timeout > maxBrowserTotalTimeoutMS { + return "", fmt.Errorf("browser_config timeout_ms should not exceed %d", maxBrowserTotalTimeoutMS) + } + setYAMLMapIntIfMissingOrZero(root, "timeout_ms", maxBrowserTotalTimeoutMS) + if err := normalizeBrowserStepTimeouts(root, "steps", "browser_config steps"); err != nil { + return "", err + } + if auth := yamlMapValue(root, "auth"); auth != nil && auth.Kind == yaml.MappingNode { + if err := normalizeBrowserStepTimeouts(auth, "steps", "browser_config auth.steps"); err != nil { + return "", err + } + } + data, err := yaml.Marshal(&node) + if err != nil { + return "", err + } + return string(data), nil +} + +func normalizeBrowserStepTimeouts(parent *yaml.Node, key string, label string) error { + steps := yamlMapValue(parent, key) + if steps == nil || steps.Kind != yaml.SequenceNode { + return nil + } + for index, step := range steps.Content { + if step == nil || step.Kind != yaml.MappingNode { + continue + } + if timeout, ok := getYAMLMapInt(step, "timeout_ms"); ok && timeout > maxBrowserStepTimeoutMS { + return fmt.Errorf("%s %d timeout_ms should not exceed %d", label, index+1, maxBrowserStepTimeoutMS) + } + setYAMLMapIntIfMissingOrZero(step, "timeout_ms", maxBrowserStepTimeoutMS) + } + return nil +} + func (t *BrowserTask) applyDefaultBrowserWindow() { if t.BrowserWindow == nil { t.BrowserWindow = &BrowserWindowOption{} @@ -786,6 +865,49 @@ func yamlMapBoolValue(node *yaml.Node, key string) bool { return false } +func yamlMapValue(node *yaml.Node, key string) *yaml.Node { + if node == nil || node.Kind != yaml.MappingNode { + return nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + return node.Content[i+1] + } + } + return nil +} + +func getYAMLMapInt(node *yaml.Node, key string) (int, bool) { + value := yamlMapValue(node, key) + if value == nil { + return 0, false + } + var out int + if err := value.Decode(&out); err != nil { + return 0, false + } + return out, true +} + +func setYAMLMapIntIfMissingOrZero(node *yaml.Node, key string, value int) { + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + current, ok := getYAMLMapInt(node, key) + if ok && current > 0 { + return + } + node.Content[i+1].Kind = yaml.ScalarNode + node.Content[i+1].Tag = "!!int" + node.Content[i+1].Value = fmt.Sprintf("%d", value) + return + } + } + node.Content = append(node.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%d", value)}, + ) +} + func setYAMLMapValue(node *yaml.Node, key string, value string) { for i := 0; i+1 < len(node.Content); i += 2 { if node.Content[i].Value == key { diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index ad1483b2..5c9de9b8 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -42,6 +42,7 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, int64(1), fields["last_step"]) assert.Equal(t, "trace-1", fields["trace_id"]) assert.Contains(t, fields["steps"], "open") + assert.Contains(t, fields["steps"], "/tmp/browser-dial/run-1-step-1.png") assert.Equal(t, "https://example.com", tags["url"]) assert.Equal(t, "platform", tags["owner"]) assert.Equal(t, "1920x1080", tags["viewport"]) @@ -152,6 +153,61 @@ func TestBrowserTaskCheckBrowserConfig(t *testing.T) { assert.EqualError(t, task.check(), "browser_config steps should not be empty") } +func TestBrowserTaskCheckBrowserConfigTimeoutLimits(t *testing.T) { + task := newBrowserTaskForTest() + task.BrowserConfig = "name: homepage\ntarget: https://example.com\ntimeout_ms: 300001\nsteps:\n - action: goto\n" + assert.EqualError(t, task.check(), "browser_config timeout_ms should not exceed 300000") + + task.BrowserConfig = "name: homepage\ntarget: https://example.com\nsteps:\n - action: goto\n timeout_ms: 60001\n" + assert.EqualError(t, task.check(), "browser_config steps 1 timeout_ms should not exceed 60000") + + task.BrowserConfig = "name: homepage\ntarget: https://example.com\nauth:\n mode: form\n steps:\n - action: goto\n timeout_ms: 60001\nsteps:\n - action: goto\n" + assert.EqualError(t, task.check(), "browser_config auth.steps 1 timeout_ms should not exceed 60000") +} + +func TestBrowserTaskNormalizeBrowserConfigTimeouts(t *testing.T) { + config := `name: homepage +target: https://example.com +auth: + mode: form + steps: + - action: goto +steps: + - action: goto + - action: assert_title + timeout_ms: 1000 +` + normalized, err := normalizeBrowserConfigTimeouts(config) + require.NoError(t, err) + assert.Contains(t, normalized, "timeout_ms: 300000") + assert.Contains(t, normalized, "timeout_ms: 60000") + assert.Contains(t, normalized, "timeout_ms: 1000") + + task := &BrowserTask{BrowserConfig: config} + path, err := task.writeScriptFile() + require.NoError(t, err) + defer os.Remove(path) //nolint:errcheck + data, err := os.ReadFile(path) + require.NoError(t, err) + assert.Contains(t, string(data), "timeout_ms: 300000") + assert.Contains(t, string(data), "timeout_ms: 60000") +} + +func TestBrowserTaskNormalizeBrowserConfigTimeoutErrors(t *testing.T) { + _, err := normalizeBrowserConfigTimeouts("name: [") + assert.Error(t, err) + + normalized, err := normalizeBrowserConfigTimeouts("- item") + require.NoError(t, err) + assert.Equal(t, "- item", normalized) + + _, err = normalizeBrowserConfigTimeouts("name: homepage\ntimeout_ms: 300001\nsteps:\n - action: goto\n") + assert.EqualError(t, err, "browser_config timeout_ms should not exceed 300000") + + _, err = normalizeBrowserConfigTimeouts("name: homepage\nsteps:\n - action: goto\n timeout_ms: 60001\n") + assert.EqualError(t, err, "browser_config steps 1 timeout_ms should not exceed 60000") +} + func TestBrowserTaskParseNewFields(t *testing.T) { taskJSON := `{ "external_id": "bd-homepage", @@ -223,6 +279,9 @@ func TestBrowserTaskDefaultViewport(t *testing.T) { func TestBrowserTaskDefaultEngine(t *testing.T) { task := newBrowserTaskForTest() assert.Equal(t, "chrome", task.effectiveEngine()) + + task.AdvanceOptions = &BrowserAdvanceOption{Engine: "lightpanda"} + assert.Equal(t, "lightpanda", task.effectiveEngine()) } func TestBrowserTaskCheckInvalidRetry(t *testing.T) { @@ -537,7 +596,10 @@ func TestBrowserDialHelperProcess(t *testing.T) { "success": true, "duration_us": 12345, "steps": []map[string]interface{}{ - {"seq": 1, "name": "open", "status": "OK", "duration_us": 1000, "url": "https://example.com", "title": "Example"}, + { + "seq": 1, "name": "open", "status": "OK", "duration_us": 1000, + "url": "https://example.com", "title": "Example", "screenshot": "/tmp/browser-dial/run-1-step-1.png", + }, }, "trace_ids": []string{"trace-1"}, }, From b038ef53f79507ddb4a6759ca4640e4f000371c2 Mon Sep 17 00:00:00 2001 From: zhengb Date: Fri, 29 May 2026 10:11:52 +0800 Subject: [PATCH 07/21] feat(dialtesting): pass through browser result fields --- dialtesting/README.md | 11 +++++ dialtesting/browser.go | 92 +++++++++++++++++++++++++++++-------- dialtesting/browser_test.go | 42 +++++++++++++---- 3 files changed, 116 insertions(+), 29 deletions(-) diff --git a/dialtesting/README.md b/dialtesting/README.md index 291059e7..edc53cb1 100644 --- a/dialtesting/README.md +++ b/dialtesting/README.md @@ -119,6 +119,12 @@ When `browser-dial` returns `run.success=true`: - tag `status=OK` - field `success=1` +When `browser-dial` succeeds after retry: + +- tag `status=RETRY_OK` +- field `success=1` +- field `retry_count` greater than 0 + When `browser-dial` returns `run.success=false`: - tag `status=FAIL` @@ -132,8 +138,13 @@ Common result fields include: - `browser_run_id` - `exit_code` - `message` +- `failure_type` - `page_url` - `page_title` - `trace_id` - `trace_ids` +- `ttfb` +- `loading_time` +- `lcp` +- `cls` - `steps` diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 832041ec..65a29b20 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -131,27 +131,48 @@ type browserDialOutput struct { } type browserDialRun struct { - RunID string `json:"run_id"` - Name string `json:"name"` - Target string `json:"target,omitempty"` - Status string `json:"status"` - Success bool `json:"success"` - DurationUS int64 `json:"duration_us"` - Steps []browserDialStep `json:"steps"` - TraceIDs []string `json:"trace_ids,omitempty"` - Error *browserDialError `json:"error,omitempty"` - FailReason string `json:"fail_reason,omitempty"` + RunID string `json:"run_id"` + Name string `json:"name"` + Target string `json:"target,omitempty"` + Status string `json:"status"` + Success bool `json:"success"` + DurationUS int64 `json:"duration_us"` + Steps []browserDialStep `json:"steps"` + TraceIDs []string `json:"trace_ids,omitempty"` + Performance *browserPerformanceMetrics `json:"performance,omitempty"` + Error *browserDialError `json:"error,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + FailureType string `json:"failure_type,omitempty"` } type browserDialStep struct { - Seq int `json:"seq"` - Name string `json:"name"` - Status string `json:"status"` - DurationUS int64 `json:"duration_us"` - URL string `json:"url,omitempty"` - Title string `json:"title,omitempty"` - Screenshot string `json:"screenshot,omitempty"` - Error *browserDialError `json:"error,omitempty"` + Seq int `json:"seq"` + Name string `json:"name"` + Action string `json:"action,omitempty"` + Selector string `json:"selector,omitempty"` + InputDisplay string `json:"input_display,omitempty"` + ValueFrom string `json:"value_from,omitempty"` + Expected string `json:"expected,omitempty"` + TimeoutMS int `json:"timeout_ms,omitempty"` + Auth bool `json:"auth,omitempty"` + Status string `json:"status"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Screenshot string `json:"screenshot,omitempty"` + SkipReason string `json:"skip_reason,omitempty"` + Error *browserDialError `json:"error,omitempty"` +} + +type browserPerformanceMetrics struct { + TTFBMS int64 `json:"ttfb_ms,omitempty"` + LoadingTimeMS int64 `json:"loading_time_ms,omitempty"` + LCPMS int64 `json:"lcp_ms,omitempty"` + CLS float64 `json:"cls,omitempty"` + DOMContentLoadedMS int64 `json:"dom_content_loaded_ms,omitempty"` + LoadEventEndMS int64 `json:"load_event_end_ms,omitempty"` } type browserDialError struct { @@ -453,13 +474,18 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in } if t.reqError == "" && t.result.Success { - tags["status"] = "OK" + if result.attempts > 1 { + tags["status"] = "RETRY_OK" + } else { + tags["status"] = "OK" + } fields["success"] = int64(1) fields["message"] = "success" } else { reasons, _ := t.checkResult() fields["fail_reason"] = strings.Join(reasons, ";") fields["message"] = strings.Join(reasons, ";") + fields["failure_type"] = t.result.FailureType } if len(t.result.Steps) > 0 { last := t.result.Steps[len(t.result.Steps)-1] @@ -472,6 +498,7 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in fields["trace_ids"] = string(traceIDs) } } + addBrowserPerformanceFields(fields, t.result.Performance) if steps, err := json.Marshal(t.result.Steps); err == nil { fields["steps"] = string(steps) } @@ -479,6 +506,33 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in return tags, fields } +func addBrowserPerformanceFields(fields map[string]interface{}, metrics *browserPerformanceMetrics) { + if metrics == nil { + return + } + if metrics.TTFBMS > 0 { + fields["ttfb"] = metrics.TTFBMS + fields["ttfb_ms"] = metrics.TTFBMS + } + if metrics.LoadingTimeMS > 0 { + fields["loading_time"] = metrics.LoadingTimeMS + fields["loading_time_ms"] = metrics.LoadingTimeMS + } + if metrics.LCPMS > 0 { + fields["lcp"] = metrics.LCPMS + fields["lcp_ms"] = metrics.LCPMS + } + if metrics.CLS > 0 { + fields["cls"] = metrics.CLS + } + if metrics.DOMContentLoadedMS > 0 { + fields["dom_content_loaded_ms"] = metrics.DOMContentLoadedMS + } + if metrics.LoadEventEndMS > 0 { + fields["load_event_end_ms"] = metrics.LoadEventEndMS + } +} + func lastBrowserStep(steps []browserDialStep) int { if len(steps) == 0 { return 0 diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 5c9de9b8..3752d3eb 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -39,9 +39,12 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, "browser_dial_testing", task.MetricName()) assert.Equal(t, int64(1), fields["success"]) assert.Equal(t, int64(12345), fields["response_time"]) - assert.Equal(t, int64(1), fields["last_step"]) + assert.Equal(t, int64(2), fields["last_step"]) assert.Equal(t, "trace-1", fields["trace_id"]) assert.Contains(t, fields["steps"], "open") + assert.Contains(t, fields["steps"], "assert_text") + assert.Contains(t, fields["steps"], "main") + assert.Contains(t, fields["steps"], "Example Domain") assert.Contains(t, fields["steps"], "/tmp/browser-dial/run-1-step-1.png") assert.Equal(t, "https://example.com", tags["url"]) assert.Equal(t, "platform", tags["owner"]) @@ -49,6 +52,10 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, int64(1920), fields["viewport_width"]) assert.Equal(t, int64(1080), fields["viewport_height"]) assert.Equal(t, int64(0), fields["retry_count"]) + assert.Equal(t, int64(12), fields["ttfb"]) + assert.Equal(t, int64(123), fields["loading_time"]) + assert.Equal(t, int64(98), fields["lcp"]) + assert.Equal(t, 0.03, fields["cls"]) } func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { @@ -83,6 +90,7 @@ func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { assert.Equal(t, "FAIL", tags["status"]) assert.Equal(t, int64(-1), fields["success"]) assert.Contains(t, fields["fail_reason"], "step_error") + assert.Equal(t, "assertion_failed", fields["failure_type"]) assert.Contains(t, fields["message"], "title mismatch") assert.Equal(t, int64(2), fields["last_step"]) assert.Contains(t, fields["steps"], "title") @@ -370,8 +378,9 @@ func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { require.NoError(t, task.Run()) tags, fields := task.GetResults() - assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, "RETRY_OK", tags["status"]) assert.Equal(t, int64(1), fields["retry_count"]) + assert.Equal(t, int64(1), fields["success"]) assert.Len(t, readHelperArgs(t, dir+"/args.log"), 2) } @@ -597,9 +606,21 @@ func TestBrowserDialHelperProcess(t *testing.T) { "duration_us": 12345, "steps": []map[string]interface{}{ { - "seq": 1, "name": "open", "status": "OK", "duration_us": 1000, + "seq": 1, "name": "open", "action": "goto", "status": "OK", "duration_us": 1000, "url": "https://example.com", "title": "Example", "screenshot": "/tmp/browser-dial/run-1-step-1.png", }, + { + "seq": 2, "name": "assert body", "action": "assert_text", "selector": "main", "expected": "Example Domain", + "status": "OK", "duration_us": 2000, "url": "https://example.com", "title": "Example", + }, + }, + "performance": map[string]interface{}{ + "ttfb_ms": 12, + "loading_time_ms": 123, + "lcp_ms": 98, + "cls": 0.03, + "dom_content_loaded_ms": 88, + "load_event_end_ms": 123, }, "trace_ids": []string{"trace-1"}, }, @@ -611,13 +632,14 @@ func TestBrowserDialHelperProcess(t *testing.T) { if mode == "failure" { output["exit_code"] = 1 output["run"] = map[string]interface{}{ - "run_id": "run-2", - "name": "homepage", - "target": "https://example.com", - "status": "FAIL", - "success": false, - "duration_us": 54321, - "fail_reason": "step_error", + "run_id": "run-2", + "name": "homepage", + "target": "https://example.com", + "status": "FAIL", + "success": false, + "duration_us": 54321, + "fail_reason": "step_error", + "failure_type": "assertion_failed", "error": map[string]interface{}{ "name": "assertion", "message": "title mismatch", From 487576cb9f2318d07b71ad283d952d35af99a188 Mon Sep 17 00:00:00 2001 From: zhengb Date: Fri, 29 May 2026 14:05:14 +0800 Subject: [PATCH 08/21] feat(dialtesting): report browser config vars --- dialtesting/browser.go | 35 +++++++++++++++++++++++++++++++++++ dialtesting/browser_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 65a29b20..7af2ab57 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -175,6 +175,13 @@ type browserPerformanceMetrics struct { LoadEventEndMS int64 `json:"load_event_end_ms,omitempty"` } +type browserConfigResultVar struct { + Name string `json:"name"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + Secure bool `json:"secure"` +} + type browserDialError struct { Name string `json:"name"` Message string `json:"message"` @@ -502,10 +509,38 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in if steps, err := json.Marshal(t.result.Steps); err == nil { fields["steps"] = string(steps) } + if vars := browserConfigResultVars(cfg.ConfigVars); len(vars) > 0 { + if data, err := json.Marshal(vars); err == nil { + fields["browser_config_vars"] = string(data) + } + } return tags, fields } +func browserConfigResultVars(configVars []ConfigVar) []browserConfigResultVar { + vars := make([]browserConfigResultVar, 0, len(configVars)) + for _, v := range configVars { + result := browserConfigResultVar{ + Name: v.Name, + Type: v.Type, + Secure: v.Secure, + } + if result.Type == "" { + if v.Secure { + result.Type = "secret" + } else { + result.Type = "text" + } + } + if !v.Secure { + result.Value = v.Value + } + vars = append(vars, result) + } + return vars +} + func addBrowserPerformanceFields(fields map[string]interface{}, metrics *browserPerformanceMetrics) { if metrics == nil { return diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 3752d3eb..7a1f6232 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -150,6 +150,43 @@ func TestBrowserTaskRenderTemplate(t *testing.T) { assert.Contains(t, task.BrowserConfig, "contains: Example") } +func TestBrowserTaskResultIncludesBrowserConfigVars(t *testing.T) { + task := &BrowserTask{ + Task: &Task{Name: "browser"}, + BrowserConfig: `name: browser +config_vars: + - name: username + value: admin@example.com + - name: password + value: secret-value + secure: true +steps: + - action: goto +`, + } + itask, err := NewTask("", task) + require.NoError(t, err) + + _, fields := itask.GetResults() + raw, ok := fields["browser_config_vars"].(string) + require.True(t, ok) + + var vars []browserConfigResultVar + require.NoError(t, json.Unmarshal([]byte(raw), &vars)) + require.Len(t, vars, 2) + + assert.Equal(t, "username", vars[0].Name) + assert.Equal(t, "text", vars[0].Type) + assert.Equal(t, "admin@example.com", vars[0].Value) + assert.False(t, vars[0].Secure) + + assert.Equal(t, "password", vars[1].Name) + assert.Equal(t, "secret", vars[1].Type) + assert.Empty(t, vars[1].Value) + assert.True(t, vars[1].Secure) + assert.NotContains(t, raw, "secret-value") +} + func TestBrowserTaskCheckBrowserConfig(t *testing.T) { task := newBrowserTaskForTest() require.NoError(t, task.check()) From c62e90df9428437c5c8a245ebafb9eec0d1cad05 Mon Sep 17 00:00:00 2001 From: zhengb Date: Fri, 29 May 2026 14:44:46 +0800 Subject: [PATCH 09/21] feat(dialtesting): pass chrome path to browser dial --- dialtesting/README.md | 6 +++++- dialtesting/browser.go | 30 ++++++++++++++++++++++++++- dialtesting/browser_test.go | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/dialtesting/README.md b/dialtesting/README.md index edc53cb1..ac9ede7a 100644 --- a/dialtesting/README.md +++ b/dialtesting/README.md @@ -24,7 +24,8 @@ existing reporting path. Browser tasks require these binaries on the dial node: - `browser-dial` -- Lightpanda, resolved by `browser-dial` +- Chrome/Chromium, resolved by `browser-dial` when `engine=chrome` +- Lightpanda, resolved by `browser-dial` when `engine=lightpanda` `dialtesting` resolves `browser-dial` in this order: @@ -35,6 +36,8 @@ Browser tasks require these binaries on the dial node: Lightpanda is not configured in task JSON. Configure it in the node environment, for example with `LIGHTPANDA_EXECUTABLE_PATH`, or make it available from `PATH`. +Chrome can be configured by `Task.SetOption()["chrome_path"]`; `dialtesting` +passes it to `browser-dial` as `CHROME_EXECUTABLE_PATH`. ## Task Fields @@ -71,6 +74,7 @@ Do not put these browser script fields at the outer task level: - `success_when` - `success_when_logic` - `browser_dial_path` +- `chrome_path` - `lightpanda_path` Success rules belong in `browser_config.steps` as browser assertions such as diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 7af2ab57..ba9c0ec2 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -38,6 +38,8 @@ const ( optionBrowserDialPathCamel = "browserDialPath" optionLightpandaPath = "lightpanda_path" optionLightpandaPathCamel = "lightpandaPath" + optionChromePath = "chrome_path" + optionChromePathCamel = "chromePath" ) type BrowserTask struct { @@ -280,9 +282,14 @@ func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) brow cmd := exec.CommandContext(ctx, t.executablePath(), cmdArgs...) cmd.Stdout = &stdout cmd.Stderr = &stderr + env := os.Environ() if lightpandaPath := t.lightpandaPath(); lightpandaPath != "" { - cmd.Env = append(os.Environ(), "LIGHTPANDA_EXECUTABLE_PATH="+lightpandaPath) + env = setEnv(env, "LIGHTPANDA_EXECUTABLE_PATH", lightpandaPath) } + if chromePath := t.chromePath(); chromePath != "" { + env = setEnv(env, "CHROME_EXECUTABLE_PATH", chromePath) + } + cmd.Env = env err := cmd.Run() result.duration = time.Since(start) @@ -367,6 +374,17 @@ func executableArgs(args []string) []string { return append([]string{"-test.run=TestBrowserDialHelperProcess", "--"}, args...) } +func setEnv(env []string, key string, value string) []string { + prefix := key + "=" + for index, item := range env { + if strings.HasPrefix(item, prefix) { + env[index] = prefix + value + return env + } + } + return append(env, prefix+value) +} + func (t *BrowserTask) writeScriptFile() (string, error) { file, err := os.CreateTemp("", "dialtesting-browser-*.yaml") if err != nil { @@ -409,6 +427,16 @@ func (t *BrowserTask) lightpandaPath() string { return "" } +func (t *BrowserTask) chromePath() string { + if value := t.GetOption()[optionChromePath]; value != "" { + return value + } + if value := t.GetOption()[optionChromePathCamel]; value != "" { + return value + } + return "" +} + func (t *BrowserTask) effectiveTimeoutMS() int { cfg, err := t.parseBrowserConfig() if err == nil && cfg.TimeoutMS > 0 { diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 7a1f6232..caffc7ae 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -76,6 +76,24 @@ func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { assert.Equal(t, int64(1), fields["success"]) } +func TestBrowserTaskRunSetsChromePath(t *testing.T) { + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{ + optionBrowserDialPath: os.Args[0], + optionChromePath: "/opt/datakit-browser/chrome/chrome", + }) + + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "check-chrome") + err = task.Run() + require.NoError(t, err) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, int64(1), fields["success"]) +} + func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) @@ -515,6 +533,26 @@ func TestBrowserTaskLightpandaPathOption(t *testing.T) { assert.Empty(t, task.lightpandaPath()) } +func TestBrowserTaskChromePathOption(t *testing.T) { + task := &BrowserTask{Task: &Task{}} + task.SetOption(map[string]string{optionChromePath: "/opt/chrome"}) + assert.Equal(t, "/opt/chrome", task.chromePath()) + + task.SetOption(map[string]string{optionChromePathCamel: "/opt/chrome-camel"}) + assert.Equal(t, "/opt/chrome-camel", task.chromePath()) + + task.SetOption(map[string]string{}) + assert.Empty(t, task.chromePath()) +} + +func TestSetEnvOverridesExistingValue(t *testing.T) { + env := setEnv([]string{"A=1", "CHROME_EXECUTABLE_PATH=/old/chrome"}, "CHROME_EXECUTABLE_PATH", "/new/chrome") + assert.Equal(t, []string{"A=1", "CHROME_EXECUTABLE_PATH=/new/chrome"}, env) + + env = setEnv([]string{"A=1"}, "CHROME_EXECUTABLE_PATH", "/new/chrome") + assert.Equal(t, []string{"A=1", "CHROME_EXECUTABLE_PATH=/new/chrome"}, env) +} + func TestBrowserTaskExecutablePathOption(t *testing.T) { task := &BrowserTask{Task: &Task{}} task.SetOption(map[string]string{optionBrowserDialPath: "/opt/browser-dial"}) @@ -624,6 +662,9 @@ func TestBrowserDialHelperProcess(t *testing.T) { if mode == "check-lightpanda" && os.Getenv("LIGHTPANDA_EXECUTABLE_PATH") != "/opt/datakit/lightpanda" { os.Exit(3) } + if mode == "check-chrome" && os.Getenv("CHROME_EXECUTABLE_PATH") != "/opt/datakit-browser/chrome/chrome" { + os.Exit(3) + } if mode == "sleep" { if startedPath := os.Getenv("BROWSER_DIAL_HELPER_STARTED"); startedPath != "" { _ = os.WriteFile(startedPath, []byte("started"), 0o600) From 4c1e523e28f178a5062a7ca03888f288ea1112fb Mon Sep 17 00:00:00 2001 From: zhengb Date: Fri, 29 May 2026 16:53:45 +0800 Subject: [PATCH 10/21] feat(dialtesting): support browser display url --- dialtesting/browser.go | 9 ++++++++- dialtesting/browser_test.go | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index ba9c0ec2..930d3f7f 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -44,6 +44,7 @@ const ( type BrowserTask struct { *Task + URL string `json:"url,omitempty"` BrowserConfig string `json:"browser_config"` BrowserWindow *BrowserWindowOption `json:"browser_window,omitempty"` @@ -471,7 +472,7 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in cfg, _ := t.parseBrowserConfig() result := t.lastViewportResult() name := firstNonEmpty(t.result.Name, cfg.Name, t.Name) - target := firstNonEmpty(t.result.Target, cfg.Target) + target := firstNonEmpty(t.URL, t.result.Target, cfg.Target) tags = map[string]string{ "name": name, "url": target, @@ -695,6 +696,12 @@ func (t *BrowserTask) renderTemplate(fm template.FuncMap) error { return fmt.Errorf("render browser_config failed: %w", err) } t.BrowserConfig = browserConfig + + url, err := t.GetParsedString(t.rawTask.URL, fm) + if err != nil { + return fmt.Errorf("render url failed: %w", err) + } + t.URL = url return nil } diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index caffc7ae..67fd5464 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -158,16 +158,37 @@ func TestBrowserTaskRenderTemplate(t *testing.T) { {Name: "title", Value: "Example"}, }, }, + URL: "https://{{host}}/display", BrowserConfig: "name: browser\ntarget: https://{{host}}\nsteps:\n - action: goto\n - action: assert_title\n contains: {{title}}\n", } itask, err := NewTask("", task) require.NoError(t, err) require.NoError(t, itask.RenderTemplateAndInit(nil)) + assert.Equal(t, "https://example.com/display", task.URL) assert.Contains(t, task.BrowserConfig, "target: https://example.com") assert.Contains(t, task.BrowserConfig, "contains: Example") } +func TestBrowserTaskURLIsDisplayOnlyResultTag(t *testing.T) { + task := &BrowserTask{ + Task: &Task{Name: "browser"}, + URL: "https://display.example.com", + BrowserConfig: strings.Join([]string{ + "name: browser", + "target: https://runtime.example.com", + "steps:", + " - action: goto", + "", + }, "\n"), + } + itask, err := NewTask("", task) + require.NoError(t, err) + + tags, _ := itask.GetResults() + assert.Equal(t, "https://display.example.com", tags["url"]) +} + func TestBrowserTaskResultIncludesBrowserConfigVars(t *testing.T) { task := &BrowserTask{ Task: &Task{Name: "browser"}, From 3ca85c1812c248ae07f12282bfbfebcb6ecdc904 Mon Sep 17 00:00:00 2001 From: zhengb Date: Sat, 30 May 2026 16:43:42 +0800 Subject: [PATCH 11/21] feat(dialtesting): support browser daemon mode --- dialtesting/browser.go | 137 ++++++++++++++++++++++++++++++++++++ dialtesting/browser_test.go | 115 ++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 930d3f7f..1c5d14e8 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -11,6 +11,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "os" "os/exec" "strings" @@ -36,6 +38,10 @@ const ( optionBrowserDialPath = "browser_dial_path" optionBrowserDialPathCamel = "browserDialPath" + optionBrowserDialMode = "browser_dial_mode" + optionBrowserDialModeCamel = "browserDialMode" + optionBrowserDialURL = "browser_dial_url" + optionBrowserDialURLCamel = "browserDialURL" optionLightpandaPath = "lightpanda_path" optionLightpandaPathCamel = "lightpandaPath" optionChromePath = "chrome_path" @@ -133,6 +139,22 @@ type browserDialOutput struct { Run browserDialRun `json:"run"` } +type browserDialDaemonRequest struct { + Script string `json:"script"` + Name string `json:"name,omitempty"` + Engine string `json:"engine,omitempty"` + TimeoutMS int `json:"timeout_ms,omitempty"` + ChromePath string `json:"chrome_path,omitempty"` + LightpandaPath string `json:"lightpanda_path,omitempty"` + ScreenshotOnFailure bool `json:"screenshot_on_failure,omitempty"` + ViewportWidth int `json:"viewport_width,omitempty"` + ViewportHeight int `json:"viewport_height,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Cookies []BrowserCookie `json:"cookies,omitempty"` + IgnoreHTTPSErrors bool `json:"ignore_https_errors,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` +} + type browserDialRun struct { RunID string `json:"run_id"` Name string `json:"name"` @@ -261,6 +283,10 @@ func (t *BrowserTask) runViewport(path string, viewport BrowserViewport) browser } func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) browserViewportResult { + if t.useBrowserDialDaemon() { + return t.runBrowserDialDaemon(path, viewport) + } + start := time.Now() timeoutMS := t.effectiveTimeoutMS() ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) @@ -315,6 +341,84 @@ func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) brow return result } +func (t *BrowserTask) runBrowserDialDaemon(path string, viewport BrowserViewport) browserViewportResult { + start := time.Now() + timeoutMS := t.effectiveTimeoutMS() + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) + defer cancel() + cancelState := t.setCancel(cancel) + defer t.clearCancel(cancelState) + + result := browserViewportResult{viewport: viewport} + if t.browserDialURL() == "" { + result.reqError = "browser_dial_url should not be empty when browser_dial_mode is daemon" + return result + } + script, err := os.ReadFile(path) + if err != nil { + result.reqError = err.Error() + return result + } + + payload := browserDialDaemonRequest{ + Script: string(script), + Name: t.Name, + Engine: t.effectiveEngine(), + TimeoutMS: timeoutMS, + ChromePath: t.chromePath(), + LightpandaPath: t.lightpandaPath(), + ViewportWidth: viewport.Width, + ViewportHeight: viewport.Height, + } + if t.AdvanceOptions != nil { + payload.ScreenshotOnFailure = t.AdvanceOptions.ScreenshotOnFailure + payload.Headers = t.AdvanceOptions.Headers + payload.Cookies = t.AdvanceOptions.Cookies + payload.IgnoreHTTPSErrors = t.AdvanceOptions.IgnoreHTTPSErrors + payload.ProxyURL = t.AdvanceOptions.ProxyURL + } + + body, err := json.Marshal(payload) + if err != nil { + result.reqError = err.Error() + return result + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(t.browserDialURL(), "/")+"/v1/run", bytes.NewReader(body)) + if err != nil { + result.reqError = err.Error() + return result + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + result.duration = time.Since(start) + if err != nil { + result.reqError = err.Error() + return result + } + defer resp.Body.Close() //nolint:errcheck + + respBody, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + if err != nil { + result.reqError = err.Error() + return result + } + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + result.exitCode = 1 + result.reqError = fmt.Sprintf("browser-dial daemon returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + return result + } + + var output browserDialOutput + if decodeErr := json.Unmarshal(respBody, &output); decodeErr != nil { + result.reqError = fmt.Sprintf("parse browser-dial daemon output failed: %s", decodeErr) + return result + } + result.exitCode = output.ExitCode + result.result = output.Run + return result +} + func (t *BrowserTask) setLastResult(result browserViewportResult) { t.duration = result.duration t.result = result.result @@ -418,6 +522,30 @@ func (t *BrowserTask) executablePath() string { return defaultBrowserDialPath } +func (t *BrowserTask) browserDialMode() string { + if value := t.GetOption()[optionBrowserDialMode]; value != "" { + return strings.ToLower(strings.TrimSpace(value)) + } + if value := t.GetOption()[optionBrowserDialModeCamel]; value != "" { + return strings.ToLower(strings.TrimSpace(value)) + } + return "exec" +} + +func (t *BrowserTask) browserDialURL() string { + if value := t.GetOption()[optionBrowserDialURL]; value != "" { + return strings.TrimSpace(value) + } + if value := t.GetOption()[optionBrowserDialURLCamel]; value != "" { + return strings.TrimSpace(value) + } + return "" +} + +func (t *BrowserTask) useBrowserDialDaemon() bool { + return t.browserDialMode() == "daemon" || t.browserDialURL() != "" +} + func (t *BrowserTask) lightpandaPath() string { if value := t.GetOption()[optionLightpandaPath]; value != "" { return value @@ -628,6 +756,15 @@ func (t *BrowserTask) check() error { if err := t.checkBrowserRetryOptions(); err != nil { return err } + switch t.browserDialMode() { + case "exec": + case "daemon": + if t.browserDialURL() == "" { + return errors.New("browser_dial_url should not be empty when browser_dial_mode is daemon") + } + default: + return errors.New("browser_dial_mode should be exec or daemon") + } return nil } diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 67fd5464..20b3bc04 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -7,6 +7,8 @@ package dialtesting import ( "encoding/json" + "net/http" + "net/http/httptest" "os" "strconv" "strings" @@ -58,6 +60,95 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, 0.03, fields["cls"]) } +func TestBrowserTaskRunDaemon(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 1366, Height: 768}}} + browserTask.AdvanceOptions = &BrowserAdvanceOption{ + Engine: "chrome", + ScreenshotOnFailure: true, + Headers: map[string]string{"X-Test": "ok"}, + Cookies: []BrowserCookie{{Name: "sid", Value: "abc"}}, + IgnoreHTTPSErrors: true, + ProxyURL: "http://127.0.0.1:7897", + } + + var got browserDialDaemonRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/v1/run", r.URL.Path) + require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) + assert.Contains(t, got.Script, "name: homepage") + assert.Equal(t, "homepage", got.Name) + assert.Equal(t, "chrome", got.Engine) + assert.Equal(t, 1000, got.TimeoutMS) + assert.Equal(t, "/opt/datakit-browser/chrome/chrome", got.ChromePath) + assert.Equal(t, 1366, got.ViewportWidth) + assert.Equal(t, 768, got.ViewportHeight) + assert.True(t, got.ScreenshotOnFailure) + assert.Equal(t, "ok", got.Headers["X-Test"]) + assert.Equal(t, []BrowserCookie{{Name: "sid", Value: "abc"}}, got.Cookies) + assert.True(t, got.IgnoreHTTPSErrors) + assert.Equal(t, "http://127.0.0.1:7897", got.ProxyURL) + + _ = json.NewEncoder(w).Encode(browserDialOutput{ + ExitCode: 0, + Run: browserDialRun{ + RunID: "run-daemon", + Name: "homepage", + Target: "https://example.com", + Status: "OK", + Success: true, + DurationUS: 23456, + Steps: []browserDialStep{{ + Seq: 1, + Name: "open", + Action: "goto", + Status: "OK", + DurationUS: 1000, + }}, + }, + }) + })) + defer server.Close() + + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{ + optionBrowserDialMode: "daemon", + optionBrowserDialURL: server.URL, + optionChromePath: "/opt/datakit-browser/chrome/chrome", + }) + + require.NoError(t, task.Run()) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, "1366x768", tags["viewport"]) + assert.Equal(t, int64(1), fields["success"]) + assert.Equal(t, int64(23456), fields["response_time"]) + assert.Equal(t, "run-daemon", fields["browser_run_id"]) +} + +func TestBrowserTaskRunDaemonFailure(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "busy", http.StatusServiceUnavailable) + })) + defer server.Close() + + browserTask := newBrowserTaskForTest() + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{ + optionBrowserDialMode: "daemon", + optionBrowserDialURL: server.URL, + }) + + require.NoError(t, task.Run()) + _, fields := task.GetResults() + assert.Equal(t, int64(-1), fields["success"]) + assert.Contains(t, fields["message"], "browser-dial daemon returned HTTP 503") +} + func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) @@ -590,6 +681,30 @@ func TestBrowserTaskExecutablePathOption(t *testing.T) { assert.Equal(t, defaultBrowserDialPath, task.executablePath()) } +func TestBrowserTaskDaemonOptions(t *testing.T) { + task := &BrowserTask{Task: &Task{}} + task.SetOption(map[string]string{ + optionBrowserDialMode: "daemon", + optionBrowserDialURL: "http://127.0.0.1:18080", + }) + assert.Equal(t, "daemon", task.browserDialMode()) + assert.Equal(t, "http://127.0.0.1:18080", task.browserDialURL()) + assert.True(t, task.useBrowserDialDaemon()) + + task.SetOption(map[string]string{ + optionBrowserDialModeCamel: "daemon", + optionBrowserDialURLCamel: "http://127.0.0.1:18081", + }) + assert.Equal(t, "daemon", task.browserDialMode()) + assert.Equal(t, "http://127.0.0.1:18081", task.browserDialURL()) + assert.True(t, task.useBrowserDialDaemon()) + + task.SetOption(map[string]string{}) + assert.Equal(t, "exec", task.browserDialMode()) + assert.Empty(t, task.browserDialURL()) + assert.False(t, task.useBrowserDialDaemon()) +} + func TestBrowserTaskResultFallbacks(t *testing.T) { task := newBrowserTaskForTest() task.stderr = "stderr failure" From b54709ed3d44273146165e48d362ec7afbd3c772 Mon Sep 17 00:00:00 2001 From: zhengb Date: Sun, 31 May 2026 00:30:24 +0800 Subject: [PATCH 12/21] fix(dialtesting): validate browser runtime config --- dialtesting/browser.go | 21 +++++++- dialtesting/browser_test.go | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 1c5d14e8..e60b9a22 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -240,8 +240,8 @@ func (t *BrowserTask) metricName() string { } func (t *BrowserTask) run() error { - if _, err := t.parseBrowserConfig(); err != nil { - t.reqError = fmt.Sprintf("parse browser_config failed: %s", err) + if err := t.check(); err != nil { + t.setConfigError(err) return nil } @@ -300,6 +300,7 @@ func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) brow "--dry-run", "--skip-token-check", "--json", + "--timeout", fmt.Sprintf("%d", timeoutMS), } args = append(args, t.browserDialOptions(viewport)...) @@ -616,6 +617,9 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in } if result.viewport.Width > 0 && result.viewport.Height > 0 { tags["viewport"] = fmt.Sprintf("%dx%d", result.viewport.Width, result.viewport.Height) + } else if viewports := t.effectiveViewports(); len(viewports) > 0 { + result.viewport = viewports[0] + tags["viewport"] = fmt.Sprintf("%dx%d", result.viewport.Width, result.viewport.Height) } responseTime := t.result.DurationUS @@ -635,6 +639,8 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in } if result.attempts > 0 { fields["retry_count"] = int64(result.attempts - 1) + } else { + fields["retry_count"] = int64(0) } if t.reqError == "" && t.result.Success { @@ -650,6 +656,9 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in fields["fail_reason"] = strings.Join(reasons, ";") fields["message"] = strings.Join(reasons, ";") fields["failure_type"] = t.result.FailureType + if fields["failure_type"] == "" && t.reqError != "" { + fields["failure_type"] = "config_error" + } } if len(t.result.Steps) > 0 { last := t.result.Steps[len(t.result.Steps)-1] @@ -853,6 +862,14 @@ func (t *BrowserTask) setReqError(err string) { t.reqError = err } +func (t *BrowserTask) setConfigError(err error) { + if err == nil { + return + } + t.reqError = err.Error() + t.result.FailureType = "config_error" +} + func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 20b3bc04..fe1524e0 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -492,6 +492,7 @@ func TestBrowserTaskRunSingleViewport(t *testing.T) { lines := readHelperArgs(t, argsPath) require.Len(t, lines, 1) assert.Contains(t, lines[0], "--viewport-width 1366 --viewport-height 768") + assert.Contains(t, lines[0], "--timeout 1000") tags, fields := task.GetResults() assert.Equal(t, "1366x768", tags["viewport"]) @@ -499,6 +500,102 @@ func TestBrowserTaskRunSingleViewport(t *testing.T) { assert.Equal(t, int64(768), fields["viewport_height"]) } +func TestBrowserTaskRunRejectsInvalidRetryOptionsAtRuntime(t *testing.T) { + cases := []struct { + name string + options *BrowserRetryOption + want string + }{ + { + name: "count", + options: &BrowserRetryOption{Enabled: true, Count: 4, IntervalSec: 5}, + want: "retry_options count should be between 0 and 3", + }, + { + name: "interval", + options: &BrowserRetryOption{Enabled: true, Count: 1, IntervalSec: 4}, + want: "retry_options interval_sec should be between 5 and 300", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.RetryOptions = tc.options + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + argsPath := t.TempDir() + "/args.log" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + require.NoError(t, task.Run()) + + _, err = os.Stat(argsPath) + assert.True(t, os.IsNotExist(err)) + + tags, fields := task.GetResults() + assert.Equal(t, "FAIL", tags["status"]) + assert.Equal(t, tc.want, fields["message"]) + assert.Equal(t, "config_error", fields["failure_type"]) + assert.Equal(t, int64(0), fields["retry_count"]) + assert.Equal(t, "1920x1080", tags["viewport"]) + assert.Equal(t, int64(1920), fields["viewport_width"]) + assert.Equal(t, int64(1080), fields["viewport_height"]) + }) + } +} + +func TestBrowserTaskConfigErrorsReportConsistentFields(t *testing.T) { + cases := []struct { + name string + config string + want string + }{ + { + name: "total-timeout", + config: "name: homepage\ntarget: https://example.com\ntimeout_ms: 300001\nsteps:\n - action: goto\n", + want: "browser_config timeout_ms should not exceed 300000", + }, + { + name: "step-timeout", + config: "name: homepage\ntarget: https://example.com\nsteps:\n - action: goto\n timeout_ms: 60001\n", + want: "browser_config steps 1 timeout_ms should not exceed 60000", + }, + { + name: "parse", + config: "name: [", + want: "parse browser_config failed", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.BrowserConfig = tc.config + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + + argsPath := t.TempDir() + "/args.log" + t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + require.NoError(t, task.Run()) + + _, err = os.Stat(argsPath) + assert.True(t, os.IsNotExist(err)) + + tags, fields := task.GetResults() + assert.Equal(t, "FAIL", tags["status"]) + assert.Contains(t, fields["message"], tc.want) + assert.Equal(t, "config_error", fields["failure_type"]) + assert.Equal(t, int64(-1), fields["success"]) + assert.Equal(t, int64(0), fields["retry_count"]) + assert.Equal(t, "1920x1080", tags["viewport"]) + assert.Equal(t, int64(1920), fields["viewport_width"]) + assert.Equal(t, int64(1080), fields["viewport_height"]) + }) + } +} + func TestBrowserTaskRunAdvanceOptions(t *testing.T) { browserTask := newBrowserTaskForTest() browserTask.AdvanceOptions = &BrowserAdvanceOption{ From bf9eee52cde065a3aaeb51bfa76f177f37f4584b Mon Sep 17 00:00:00 2001 From: zhengb Date: Sun, 31 May 2026 21:37:11 +0800 Subject: [PATCH 13/21] fix dialtesting browser result fields --- dialtesting/browser.go | 73 +++++++++++++++++++++++----------- dialtesting/browser_test.go | 79 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 24 deletions(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index e60b9a22..0e94eb6d 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -171,24 +171,25 @@ type browserDialRun struct { } type browserDialStep struct { - Seq int `json:"seq"` - Name string `json:"name"` - Action string `json:"action,omitempty"` - Selector string `json:"selector,omitempty"` - InputDisplay string `json:"input_display,omitempty"` - ValueFrom string `json:"value_from,omitempty"` - Expected string `json:"expected,omitempty"` - TimeoutMS int `json:"timeout_ms,omitempty"` - Auth bool `json:"auth,omitempty"` - Status string `json:"status"` - StartedAt string `json:"started_at,omitempty"` - EndedAt string `json:"ended_at,omitempty"` - DurationUS int64 `json:"duration_us"` - URL string `json:"url,omitempty"` - Title string `json:"title,omitempty"` - Screenshot string `json:"screenshot,omitempty"` - SkipReason string `json:"skip_reason,omitempty"` - Error *browserDialError `json:"error,omitempty"` + Seq int `json:"seq"` + Name string `json:"name"` + Action string `json:"action,omitempty"` + Selector string `json:"selector,omitempty"` + InputDisplay string `json:"input_display,omitempty"` + ValueFrom string `json:"value_from,omitempty"` + Expected string `json:"expected,omitempty"` + TimeoutMS int `json:"timeout_ms,omitempty"` + Auth bool `json:"auth,omitempty"` + Status string `json:"status"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Performance *browserPerformanceMetrics `json:"performance,omitempty"` + Screenshot string `json:"screenshot,omitempty"` + SkipReason string `json:"skip_reason,omitempty"` + Error *browserDialError `json:"error,omitempty"` } type browserPerformanceMetrics struct { @@ -660,9 +661,8 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in fields["failure_type"] = "config_error" } } - if len(t.result.Steps) > 0 { - last := t.result.Steps[len(t.result.Steps)-1] - fields["page_url"] = last.URL + if last, ok := lastExecutedBrowserStep(t.result.Steps); ok { + fields["page_url"] = firstNonEmpty(last.URL, t.result.Target, cfg.Target, t.URL) fields["page_title"] = last.Title } if len(t.result.TraceIDs) > 0 { @@ -672,7 +672,7 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in } } addBrowserPerformanceFields(fields, t.result.Performance) - if steps, err := json.Marshal(t.result.Steps); err == nil { + if steps, err := json.Marshal(compactBrowserSteps(t.result.Steps)); err == nil { fields["steps"] = string(steps) } if vars := browserConfigResultVars(cfg.ConfigVars); len(vars) > 0 { @@ -735,10 +735,35 @@ func addBrowserPerformanceFields(fields map[string]interface{}, metrics *browser } func lastBrowserStep(steps []browserDialStep) int { + if last, ok := lastExecutedBrowserStep(steps); ok { + return last.Seq + } + return 0 +} + +func lastExecutedBrowserStep(steps []browserDialStep) (browserDialStep, bool) { + for i := len(steps) - 1; i >= 0; i-- { + if !strings.EqualFold(steps[i].Status, "SKIP") { + return steps[i], true + } + } + return browserDialStep{}, false +} + +func compactBrowserSteps(steps []browserDialStep) []browserDialStep { if len(steps) == 0 { - return 0 + return nil + } + compact := make([]browserDialStep, len(steps)) + copy(compact, steps) + for i := range compact { + if compact[i].Error != nil { + errInfo := *compact[i].Error + errInfo.Stack = "" + compact[i].Error = &errInfo + } } - return steps[len(steps)-1].Seq + return compact } func (t *BrowserTask) check() error { diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index fe1524e0..25d0b7f1 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -820,6 +820,77 @@ func TestBrowserTaskResultFallbacks(t *testing.T) { assert.Equal(t, []string{"before run failed"}, reasons) } +func TestBrowserTaskResultUsesLastExecutedStep(t *testing.T) { + task := newBrowserTaskForTest() + task.exitCode = 1 + task.result = browserDialRun{ + RunID: "run-failed", + Name: "homepage", + Target: "https://example.com", + Status: "FAIL", + Success: false, + DurationUS: 60000000, + FailReason: "timeout", + FailureType: "timeout", + Steps: []browserDialStep{ + { + Seq: 1, + Name: "open", + Action: "goto", + Status: "OK", + DurationUS: 1000, + URL: "https://example.com", + Title: "Example Domain", + Performance: &browserPerformanceMetrics{ + TTFBMS: 12, + LoadingTimeMS: 123, + }, + }, + { + Seq: 2, + Name: "wait button", + Action: "wait_for_selector", + Status: "FAIL", + DurationUS: 60000000, + Title: "Example Domain", + Error: &browserDialError{ + Name: "errorsx.TimeoutError", + Message: "dial script timed out after 60000ms", + Stack: "goroutine 1 [running]", + }, + }, + { + Seq: 3, + Name: "click button", + Action: "click", + Status: "SKIP", + DurationUS: 0, + SkipReason: "previous_step_failed", + }, + }, + } + + _, fields := task.getResults() + assert.Equal(t, int64(2), fields["last_step"]) + assert.Equal(t, "https://example.com", fields["page_url"]) + assert.Equal(t, "Example Domain", fields["page_title"]) + + rawSteps, ok := fields["steps"].(string) + require.True(t, ok) + assert.Contains(t, rawSteps, "dial script timed out after 60000ms") + assert.NotContains(t, rawSteps, "goroutine 1") + assert.NotContains(t, rawSteps, `"stack"`) + + var steps []browserDialStep + require.NoError(t, json.Unmarshal([]byte(rawSteps), &steps)) + require.Len(t, steps, 3) + require.NotNil(t, steps[0].Performance) + assert.Equal(t, int64(12), steps[0].Performance.TTFBMS) + require.NotNil(t, steps[1].Error) + assert.Empty(t, steps[1].Error.Stack) + assert.Equal(t, "SKIP", steps[2].Status) +} + func TestBrowserTaskHostNameErrors(t *testing.T) { task := &BrowserTask{BrowserConfig: "name: homepage\nsteps:\n - action: click\n"} _, err := task.getHostName() @@ -919,6 +990,14 @@ func TestBrowserDialHelperProcess(t *testing.T) { { "seq": 1, "name": "open", "action": "goto", "status": "OK", "duration_us": 1000, "url": "https://example.com", "title": "Example", "screenshot": "/tmp/browser-dial/run-1-step-1.png", + "performance": map[string]interface{}{ + "ttfb_ms": 12, + "loading_time_ms": 123, + "lcp_ms": 98, + "cls": 0.03, + "dom_content_loaded_ms": 88, + "load_event_end_ms": 123, + }, }, { "seq": 2, "name": "assert body", "action": "assert_text", "selector": "main", "expected": "Example Domain", From 56ffcf757b4af440c31721b092f1ef7eb091884f Mon Sep 17 00:00:00 2001 From: zhengb Date: Mon, 1 Jun 2026 13:44:57 +0800 Subject: [PATCH 14/21] feat(dialtesting): report browser retry records --- dialtesting/browser.go | 127 ++++++++++++++++++++++++++++++------ dialtesting/browser_test.go | 13 ++++ 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 0e94eb6d..1651925c 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -105,13 +105,16 @@ type browserTaskCancel struct { } type browserViewportResult struct { - viewport BrowserViewport - duration time.Duration - result browserDialRun - exitCode int - reqError string - stderr string - attempts int + viewport BrowserViewport + duration time.Duration + startedAt string + endedAt string + result browserDialRun + exitCode int + reqError string + stderr string + attempts int + retryRecords []browserRetryRecord } type browserConfig struct { @@ -156,18 +159,21 @@ type browserDialDaemonRequest struct { } type browserDialRun struct { - RunID string `json:"run_id"` - Name string `json:"name"` - Target string `json:"target,omitempty"` - Status string `json:"status"` - Success bool `json:"success"` - DurationUS int64 `json:"duration_us"` - Steps []browserDialStep `json:"steps"` - TraceIDs []string `json:"trace_ids,omitempty"` - Performance *browserPerformanceMetrics `json:"performance,omitempty"` - Error *browserDialError `json:"error,omitempty"` - FailReason string `json:"fail_reason,omitempty"` - FailureType string `json:"failure_type,omitempty"` + RunID string `json:"run_id"` + Name string `json:"name"` + Target string `json:"target,omitempty"` + Status string `json:"status"` + Success bool `json:"success"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us"` + Steps []browserDialStep `json:"steps"` + TraceIDs []string `json:"trace_ids,omitempty"` + Performance *browserPerformanceMetrics `json:"performance,omitempty"` + Error *browserDialError `json:"error,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + FailureType string `json:"failure_type,omitempty"` + RetryRecords []browserRetryRecord `json:"retry_records,omitempty"` } type browserDialStep struct { @@ -214,6 +220,19 @@ type browserDialError struct { Stack string `json:"stack,omitempty"` } +type browserRetryRecord struct { + Attempt int `json:"attempt"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us,omitempty"` + Status string `json:"status"` + Success bool `json:"success"` + FailedStep int `json:"failed_step,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + FailureType string `json:"failure_type,omitempty"` + Message string `json:"message,omitempty"` +} + func (t *BrowserTask) clear() { t.duration = 0 t.result = browserDialRun{} @@ -267,9 +286,18 @@ func (t *BrowserTask) runViewport(path string, viewport BrowserViewport) browser } var result browserViewportResult + retryRecords := make([]browserRetryRecord, 0, maxAttempts) for attempt := 1; attempt <= maxAttempts; attempt++ { result = t.runBrowserDial(path, viewport) result.attempts = attempt + if len(result.result.RetryRecords) > 0 { + retryRecords = append(retryRecords, result.result.RetryRecords...) + } else { + retryRecords = append(retryRecords, browserRetryRecordFromResult(result, attempt)) + } + if maxAttempts > 1 { + result.retryRecords = append([]browserRetryRecord(nil), retryRecords...) + } if result.reqError == "" && result.result.Success { return result } @@ -295,7 +323,7 @@ func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) brow cancelState := t.setCancel(cancel) defer t.clearCancel(cancelState) - result := browserViewportResult{viewport: viewport} + result := browserViewportResult{viewport: viewport, startedAt: start.UTC().Format(time.RFC3339Nano)} args := []string{ "run", path, "--dry-run", @@ -322,6 +350,7 @@ func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) brow err := cmd.Run() result.duration = time.Since(start) + result.endedAt = time.Now().UTC().Format(time.RFC3339Nano) result.stderr = strings.TrimSpace(stderr.String()) if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { result.exitCode = exitErr.ExitCode() @@ -351,7 +380,7 @@ func (t *BrowserTask) runBrowserDialDaemon(path string, viewport BrowserViewport cancelState := t.setCancel(cancel) defer t.clearCancel(cancelState) - result := browserViewportResult{viewport: viewport} + result := browserViewportResult{viewport: viewport, startedAt: start.UTC().Format(time.RFC3339Nano)} if t.browserDialURL() == "" { result.reqError = "browser_dial_url should not be empty when browser_dial_mode is daemon" return result @@ -394,6 +423,7 @@ func (t *BrowserTask) runBrowserDialDaemon(path string, viewport BrowserViewport resp, err := http.DefaultClient.Do(req) result.duration = time.Since(start) + result.endedAt = time.Now().UTC().Format(time.RFC3339Nano) if err != nil { result.reqError = err.Error() return result @@ -429,6 +459,52 @@ func (t *BrowserTask) setLastResult(result browserViewportResult) { t.stderr = result.stderr } +func browserRetryRecordFromResult(result browserViewportResult, attempt int) browserRetryRecord { + status := result.result.Status + success := result.reqError == "" && result.result.Success + if status == "" { + if success { + status = "OK" + } else { + status = "FAIL" + } + } + record := browserRetryRecord{ + Attempt: attempt, + StartedAt: firstNonEmpty(result.result.StartedAt, result.startedAt), + EndedAt: firstNonEmpty(result.result.EndedAt, result.endedAt), + DurationUS: result.result.DurationUS, + Status: status, + Success: success, + FailReason: result.result.FailReason, + FailureType: result.result.FailureType, + } + if record.DurationUS == 0 && result.duration > 0 { + record.DurationUS = int64(result.duration) / 1000 + } + if result.reqError != "" { + record.Message = result.reqError + if record.FailureType == "" { + record.FailureType = "runner_error" + } + return record + } + if result.result.Error != nil { + record.Message = result.result.Error.Message + } + for _, step := range result.result.Steps { + if !strings.EqualFold(step.Status, "FAIL") { + continue + } + record.FailedStep = step.Seq + if record.Message == "" && step.Error != nil { + record.Message = step.Error.Message + } + break + } + return record +} + func (t *BrowserTask) browserDialOptions(viewport BrowserViewport) []string { args := []string{ "--viewport-width", fmt.Sprintf("%d", viewport.Width), @@ -675,6 +751,15 @@ func (t *BrowserTask) getResults() (tags map[string]string, fields map[string]in if steps, err := json.Marshal(compactBrowserSteps(t.result.Steps)); err == nil { fields["steps"] = string(steps) } + if len(result.retryRecords) > 0 { + if data, err := json.Marshal(result.retryRecords); err == nil { + fields["retry_records"] = string(data) + } + } else if len(t.result.RetryRecords) > 0 { + if data, err := json.Marshal(t.result.RetryRecords); err == nil { + fields["retry_records"] = string(data) + } + } if vars := browserConfigResultVars(cfg.ConfigVars); len(vars) > 0 { if data, err := json.Marshal(vars); err == nil { fields["browser_config_vars"] = string(data) diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 25d0b7f1..693e8e38 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -645,6 +645,19 @@ func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { assert.Equal(t, "RETRY_OK", tags["status"]) assert.Equal(t, int64(1), fields["retry_count"]) assert.Equal(t, int64(1), fields["success"]) + rawRecords, ok := fields["retry_records"].(string) + require.True(t, ok) + var records []browserRetryRecord + require.NoError(t, json.Unmarshal([]byte(rawRecords), &records)) + require.Len(t, records, 2) + assert.Equal(t, 1, records[0].Attempt) + assert.Equal(t, "FAIL", records[0].Status) + assert.False(t, records[0].Success) + assert.Equal(t, 2, records[0].FailedStep) + assert.Equal(t, "assertion_failed", records[0].FailureType) + assert.Equal(t, 2, records[1].Attempt) + assert.Equal(t, "OK", records[1].Status) + assert.True(t, records[1].Success) assert.Len(t, readHelperArgs(t, dir+"/args.log"), 2) } From 549b1cfe125c74b15e5cb5df7c93ee6bdc71d0d2 Mon Sep 17 00:00:00 2001 From: zhengb Date: Mon, 1 Jun 2026 13:57:45 +0800 Subject: [PATCH 15/21] fix(dialtesting): omit retry records without retry --- dialtesting/browser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 1651925c..49ccecb5 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -295,7 +295,7 @@ func (t *BrowserTask) runViewport(path string, viewport BrowserViewport) browser } else { retryRecords = append(retryRecords, browserRetryRecordFromResult(result, attempt)) } - if maxAttempts > 1 { + if maxAttempts > 1 && (len(retryRecords) > 1 || result.reqError != "" || !result.result.Success) { result.retryRecords = append([]browserRetryRecord(nil), retryRecords...) } if result.reqError == "" && result.result.Success { From 9f071476dac32e4a1a4d9636f846b48ce8574506 Mon Sep 17 00:00:00 2001 From: zhengb Date: Mon, 1 Jun 2026 19:12:56 +0800 Subject: [PATCH 16/21] feat(dialtesting): embed browser runner --- .github/workflows/go.yml | 2 +- aggregate/tail-sampling.go | 2 +- dialtesting/README.md | 34 +- dialtesting/browser.go | 319 +- dialtesting/browser_test.go | 541 +- dialtesting/http.go | 4 +- go.mod | 33 +- go.sum | 88 +- internal/browserdial/chrome/chrome.go | 538 + internal/browserdial/chrome/pool.go | 170 + internal/browserdial/errors/errors.go | 69 + internal/browserdial/evidence/types.go | 88 + internal/browserdial/lightpanda/lightpanda.go | 587 + internal/browserdial/lightpanda/trace.go | 123 + internal/browserdial/runner/runner.go | 850 + internal/browserdial/script/script.go | 248 + internal/browserdial/util/sanitize.go | 78 + internal/browserdial/util/time.go | 11 + internal/browserdial/util/uuid.go | 17 + point/kvs_test.go | 2 +- point/point_test.go | 2 +- vendor/github.com/chromedp/cdproto/.gitignore | 2 + .../LICENSE.txt => chromedp/cdproto/LICENSE} | 12 +- vendor/github.com/chromedp/cdproto/README.md | 29 + .../cdproto/accessibility/accessibility.go | 413 + .../chromedp/cdproto/accessibility/events.go | 20 + .../chromedp/cdproto/accessibility/types.go | 414 + .../chromedp/cdproto/animation/animation.go | 298 + .../chromedp/cdproto/animation/events.go | 31 + .../chromedp/cdproto/animation/types.go | 106 + .../chromedp/cdproto/audits/audits.go | 193 + .../chromedp/cdproto/audits/events.go | 10 + .../chromedp/cdproto/audits/types.go | 1783 ++ .../chromedp/cdproto/autofill/autofill.go | 113 + .../chromedp/cdproto/autofill/events.go | 11 + .../chromedp/cdproto/autofill/types.go | 103 + .../backgroundservice/backgroundservice.go | 118 + .../cdproto/backgroundservice/events.go | 21 + .../cdproto/backgroundservice/types.go | 80 + .../bluetoothemulation/bluetoothemulation.go | 436 + .../cdproto/bluetoothemulation/events.go | 12 + .../cdproto/bluetoothemulation/types.go | 121 + .../chromedp/cdproto/browser/browser.go | 677 + .../chromedp/cdproto/browser/events.go | 28 + .../chromedp/cdproto/browser/types.go | 411 + .../cdproto/cachestorage/cachestorage.go | 237 + .../chromedp/cdproto/cachestorage/types.go | 104 + .../github.com/chromedp/cdproto/cast/cast.go | 169 + .../chromedp/cdproto/cast/events.go | 19 + .../github.com/chromedp/cdproto/cast/types.go | 12 + .../github.com/chromedp/cdproto/cdp/types.go | 1279 ++ vendor/github.com/chromedp/cdproto/cdproto.go | 2458 +++ vendor/github.com/chromedp/cdproto/css/css.go | 1522 ++ .../github.com/chromedp/cdproto/css/events.go | 53 + .../github.com/chromedp/cdproto/css/types.go | 584 + .../chromedp/cdproto/debugger/debugger.go | 1297 ++ .../chromedp/cdproto/debugger/events.go | 79 + .../chromedp/cdproto/debugger/types.go | 521 + .../cdproto/deviceaccess/deviceaccess.go | 104 + .../chromedp/cdproto/deviceaccess/events.go | 12 + .../chromedp/cdproto/deviceaccess/types.go | 32 + .../deviceorientation/deviceorientation.go | 64 + vendor/github.com/chromedp/cdproto/dom/dom.go | 2051 ++ .../github.com/chromedp/cdproto/dom/events.go | 138 + .../github.com/chromedp/cdproto/dom/types.go | 233 + .../cdproto/domdebugger/domdebugger.go | 273 + .../chromedp/cdproto/domdebugger/types.go | 94 + .../cdproto/domsnapshot/domsnapshot.go | 142 + .../chromedp/cdproto/domsnapshot/types.go | 199 + .../chromedp/cdproto/domstorage/domstorage.go | 176 + .../chromedp/cdproto/domstorage/events.go | 37 + .../chromedp/cdproto/domstorage/types.go | 27 + .../chromedp/cdproto/emulation/emulation.go | 1147 + .../chromedp/cdproto/emulation/events.go | 9 + .../chromedp/cdproto/emulation/types.go | 505 + .../eventbreakpoints/eventbreakpoints.go | 89 + .../chromedp/cdproto/extensions/extensions.go | 240 + .../chromedp/cdproto/extensions/types.go | 46 + .../chromedp/cdproto/fedcm/events.go | 22 + .../chromedp/cdproto/fedcm/fedcm.go | 199 + .../chromedp/cdproto/fedcm/types.go | 162 + .../chromedp/cdproto/fetch/events.go | 47 + .../chromedp/cdproto/fetch/fetch.go | 447 + .../chromedp/cdproto/fetch/types.go | 161 + .../chromedp/cdproto/filesystem/filesystem.go | 57 + .../chromedp/cdproto/filesystem/types.go | 36 + .../headlessexperimental.go | 111 + .../cdproto/headlessexperimental/types.go | 52 + .../chromedp/cdproto/heapprofiler/events.go | 44 + .../cdproto/heapprofiler/heapprofiler.go | 420 + .../chromedp/cdproto/heapprofiler/types.go | 45 + .../chromedp/cdproto/indexeddb/indexeddb.go | 475 + .../chromedp/cdproto/indexeddb/types.go | 151 + .../chromedp/cdproto/input/events.go | 12 + .../chromedp/cdproto/input/input.go | 793 + .../chromedp/cdproto/input/types.go | 400 + .../chromedp/cdproto/inspector/events.go | 22 + .../chromedp/cdproto/inspector/inspector.go | 49 + .../chromedp/cdproto/inspector/types.go | 46 + vendor/github.com/chromedp/cdproto/io/io.go | 145 + .../github.com/chromedp/cdproto/io/types.go | 14 + .../chromedp/cdproto/layertree/events.go | 22 + .../chromedp/cdproto/layertree/layertree.go | 367 + .../chromedp/cdproto/layertree/types.go | 121 + .../github.com/chromedp/cdproto/log/events.go | 10 + vendor/github.com/chromedp/cdproto/log/log.go | 111 + .../github.com/chromedp/cdproto/log/types.go | 215 + .../chromedp/cdproto/media/events.go | 50 + .../chromedp/cdproto/media/media.go | 51 + .../chromedp/cdproto/media/types.go | 121 + .../chromedp/cdproto/memory/memory.go | 330 + .../chromedp/cdproto/memory/types.go | 75 + .../chromedp/cdproto/network/events.go | 373 + .../chromedp/cdproto/network/network.go | 1135 + .../chromedp/cdproto/network/types.go | 2181 ++ .../chromedp/cdproto/overlay/events.go | 38 + .../chromedp/cdproto/overlay/overlay.go | 830 + .../chromedp/cdproto/overlay/types.go | 342 + .../chromedp/cdproto/page/events.go | 223 + .../github.com/chromedp/cdproto/page/page.go | 1843 ++ .../github.com/chromedp/cdproto/page/types.go | 1836 ++ .../chromedp/cdproto/performance/events.go | 11 + .../cdproto/performance/performance.go | 92 + .../chromedp/cdproto/performance/types.go | 49 + .../cdproto/performancetimeline/events.go | 11 + .../performancetimeline.go | 46 + .../cdproto/performancetimeline/types.go | 55 + .../chromedp/cdproto/preload/events.go | 69 + .../chromedp/cdproto/preload/preload.go | 49 + .../chromedp/cdproto/preload/types.go | 607 + .../chromedp/cdproto/profiler/events.go | 40 + .../chromedp/cdproto/profiler/profiler.go | 278 + .../chromedp/cdproto/profiler/types.go | 67 + vendor/github.com/chromedp/cdproto/pwa/pwa.go | 335 + .../github.com/chromedp/cdproto/pwa/types.go | 58 + .../chromedp/cdproto/runtime/events.go | 76 + .../chromedp/cdproto/runtime/runtime.go | 1127 + .../chromedp/cdproto/runtime/types.go | 642 + .../chromedp/cdproto/security/events.go | 10 + .../chromedp/cdproto/security/security.go | 77 + .../chromedp/cdproto/security/types.go | 223 + .../chromedp/cdproto/serviceworker/events.go | 24 + .../cdproto/serviceworker/serviceworker.go | 326 + .../chromedp/cdproto/serviceworker/types.go | 139 + .../chromedp/cdproto/storage/events.go | 134 + .../chromedp/cdproto/storage/storage.go | 1170 + .../chromedp/cdproto/storage/types.go | 983 + .../chromedp/cdproto/systeminfo/systeminfo.go | 130 + .../chromedp/cdproto/systeminfo/types.go | 156 + .../chromedp/cdproto/target/events.go | 62 + .../chromedp/cdproto/target/target.go | 738 + .../chromedp/cdproto/target/types.go | 113 + .../chromedp/cdproto/tethering/events.go | 12 + .../chromedp/cdproto/tethering/tethering.go | 67 + .../chromedp/cdproto/tracing/events.go | 37 + .../chromedp/cdproto/tracing/tracing.go | 218 + .../chromedp/cdproto/tracing/types.go | 242 + .../chromedp/cdproto/webaudio/events.go | 122 + .../chromedp/cdproto/webaudio/types.go | 270 + .../chromedp/cdproto/webaudio/webaudio.go | 95 + .../chromedp/cdproto/webauthn/events.go | 39 + .../chromedp/cdproto/webauthn/types.go | 159 + .../chromedp/cdproto/webauthn/webauthn.go | 457 + .../github.com/chromedp/chromedp/.gitignore | 13 + .../chromedp/LICENSE} | 22 +- vendor/github.com/chromedp/chromedp/README.md | 94 + .../github.com/chromedp/chromedp/allocate.go | 611 + .../chromedp/chromedp/allocate_linux.go | 22 + .../chromedp/chromedp/allocate_other.go | 9 + .../github.com/chromedp/chromedp/browser.go | 376 + vendor/github.com/chromedp/chromedp/call.go | 95 + .../github.com/chromedp/chromedp/chromedp.go | 870 + vendor/github.com/chromedp/chromedp/conn.go | 152 + .../chromedp/chromedp/device/device.go | 595 + .../github.com/chromedp/chromedp/emulate.go | 121 + vendor/github.com/chromedp/chromedp/errors.go | 57 + vendor/github.com/chromedp/chromedp/eval.go | 145 + vendor/github.com/chromedp/chromedp/input.go | 207 + vendor/github.com/chromedp/chromedp/js.go | 59 + .../chromedp/chromedp/js/attribute.js | 3 + .../github.com/chromedp/chromedp/js/blur.js | 4 + .../chromedp/chromedp/js/getClientRect.js | 10 + .../github.com/chromedp/chromedp/js/reset.js | 10 + .../chromedp/chromedp/js/setAttribute.js | 8 + .../github.com/chromedp/chromedp/js/submit.js | 10 + .../github.com/chromedp/chromedp/js/text.js | 6 + .../chromedp/chromedp/js/textContent.js | 3 + .../chromedp/chromedp/js/visible.js | 3 + .../js/waitForPredicatePageFunction.js | 83 + vendor/github.com/chromedp/chromedp/kb/kb.go | 488 + vendor/github.com/chromedp/chromedp/nav.go | 112 + vendor/github.com/chromedp/chromedp/poll.go | 182 + vendor/github.com/chromedp/chromedp/query.go | 1257 ++ .../chromedp/chromedp/screenshot.go | 182 + vendor/github.com/chromedp/chromedp/target.go | 435 + vendor/github.com/chromedp/chromedp/util.go | 414 + .../github.com/chromedp/sysutil/.travis.yml | 6 + vendor/github.com/chromedp/sysutil/LICENSE | 21 + vendor/github.com/chromedp/sysutil/README.md | 6 + vendor/github.com/chromedp/sysutil/sysutil.go | 17 + .../chromedp/sysutil/sysutil_bsd.go | 26 + .../chromedp/sysutil/sysutil_linux.go | 34 + .../chromedp/sysutil/sysutil_other.go | 9 + .../chromedp/sysutil/sysutil_windows.go | 22 + vendor/github.com/chromedp/sysutil/timeval.go | 9 + .../github.com/chromedp/sysutil/timeval32.go | 8 + .../go-json-experiment/json/AUTHORS | 3 + .../go-json-experiment/json/CONTRIBUTORS | 3 + .../json}/LICENSE | 2 +- .../go-json-experiment/json/README.md | 175 + .../go-json-experiment/json/api.png | Bin 0 -> 154576 bytes .../go-json-experiment/json/arshal.go | 581 + .../go-json-experiment/json/arshal_any.go | 281 + .../go-json-experiment/json/arshal_default.go | 1908 ++ .../go-json-experiment/json/arshal_funcs.go | 430 + .../go-json-experiment/json/arshal_inlined.go | 228 + .../go-json-experiment/json/arshal_methods.go | 362 + .../go-json-experiment/json/arshal_time.go | 644 + .../github.com/go-json-experiment/json/doc.go | 168 + .../go-json-experiment/json/errors.go | 418 + .../go-json-experiment/json/fields.go | 644 + .../go-json-experiment/json/fold.go | 56 + .../go-json-experiment/json/intern.go | 86 + .../json/internal/internal.go | 39 + .../json/internal/jsonflags/flags.go | 203 + .../json/internal/jsonopts/options.go | 188 + .../json/internal/jsonwire/decode.go | 627 + .../json/internal/jsonwire/encode.go | 292 + .../json/internal/jsonwire/wire.go | 215 + .../json/jsontext/decode.go | 1153 + .../go-json-experiment/json/jsontext/doc.go | 105 + .../json/jsontext/encode.go | 957 + .../json/jsontext/errors.go | 180 + .../json/jsontext/export.go | 75 + .../json/jsontext/options.go | 301 + .../go-json-experiment/json/jsontext/pools.go | 150 + .../go-json-experiment/json/jsontext/quote.go | 39 + .../go-json-experiment/json/jsontext/state.go | 826 + .../go-json-experiment/json/jsontext/token.go | 525 + .../go-json-experiment/json/jsontext/value.go | 393 + .../go-json-experiment/json/migrate.sh | 231 + .../go-json-experiment/json/options.go | 288 + .../go-task/slim-sprig/.editorconfig | 14 - .../go-task/slim-sprig/.gitattributes | 1 - .../github.com/go-task/slim-sprig/.gitignore | 2 - .../go-task/slim-sprig/CHANGELOG.md | 364 - .../github.com/go-task/slim-sprig/README.md | 73 - .../go-task/slim-sprig/Taskfile.yml | 12 - .../github.com/go-task/slim-sprig/crypto.go | 24 - vendor/github.com/go-task/slim-sprig/date.go | 152 - .../github.com/go-task/slim-sprig/defaults.go | 163 - vendor/github.com/go-task/slim-sprig/dict.go | 118 - vendor/github.com/go-task/slim-sprig/doc.go | 19 - .../go-task/slim-sprig/functions.go | 317 - vendor/github.com/go-task/slim-sprig/list.go | 464 - .../github.com/go-task/slim-sprig/network.go | 12 - .../github.com/go-task/slim-sprig/numeric.go | 228 - .../github.com/go-task/slim-sprig/reflect.go | 28 - vendor/github.com/go-task/slim-sprig/regex.go | 83 - .../github.com/go-task/slim-sprig/strings.go | 189 - vendor/github.com/go-task/slim-sprig/url.go | 66 - vendor/github.com/gobwas/ws/Makefile | 2 +- vendor/github.com/gobwas/ws/cipher.go | 18 +- vendor/github.com/gobwas/ws/dialer.go | 60 +- .../github.com/gobwas/ws/dialer_tls_go18.go | 1 + vendor/github.com/gobwas/ws/doc.go | 80 +- vendor/github.com/gobwas/ws/frame.go | 6 +- vendor/github.com/gobwas/ws/hijack_go119.go | 18 + vendor/github.com/gobwas/ws/hijack_go120.go | 19 + vendor/github.com/gobwas/ws/http.go | 47 +- vendor/github.com/gobwas/ws/nonce.go | 2 - vendor/github.com/gobwas/ws/read.go | 24 +- vendor/github.com/gobwas/ws/server.go | 29 +- vendor/github.com/gobwas/ws/server_test.s | 0 vendor/github.com/gobwas/ws/util.go | 19 +- vendor/github.com/gobwas/ws/util_purego.go | 12 + vendor/github.com/gobwas/ws/util_unsafe.go | 22 + vendor/github.com/gobwas/ws/wsutil/cipher.go | 4 +- vendor/github.com/gobwas/ws/wsutil/dialer.go | 1 + vendor/github.com/gobwas/ws/wsutil/handler.go | 2 +- vendor/github.com/gobwas/ws/wsutil/helper.go | 4 +- vendor/github.com/gobwas/ws/wsutil/reader.go | 104 +- vendor/github.com/gobwas/ws/wsutil/utf8.go | 2 +- vendor/github.com/gobwas/ws/wsutil/wsutil.go | 76 +- vendor/github.com/golang/mock/AUTHORS | 12 - vendor/github.com/golang/mock/CONTRIBUTORS | 37 - vendor/github.com/golang/mock/LICENSE | 202 - .../github.com/golang/mock/mockgen/mockgen.go | 701 - .../golang/mock/mockgen/model/model.go | 495 - .../github.com/golang/mock/mockgen/parse.go | 644 - .../github.com/golang/mock/mockgen/reflect.go | 256 - .../golang/mock/mockgen/version.1.11.go | 26 - .../golang/mock/mockgen/version.1.12.go | 35 - vendor/github.com/onsi/ginkgo/v2/LICENSE | 20 - .../onsi/ginkgo/v2/config/deprecated.go | 69 - .../ginkgo/v2/formatter/colorable_windows.go | 809 - .../onsi/ginkgo/v2/formatter/formatter.go | 230 - .../ginkgo/v2/ginkgo/build/build_command.go | 63 - .../onsi/ginkgo/v2/ginkgo/command/abort.go | 61 - .../onsi/ginkgo/v2/ginkgo/command/command.go | 50 - .../onsi/ginkgo/v2/ginkgo/command/program.go | 182 - .../ginkgo/generators/boostrap_templates.go | 48 - .../v2/ginkgo/generators/bootstrap_command.go | 133 - .../v2/ginkgo/generators/generate_command.go | 259 - .../ginkgo/generators/generate_templates.go | 41 - .../v2/ginkgo/generators/generators_common.go | 64 - .../onsi/ginkgo/v2/ginkgo/internal/compile.go | 161 - .../ginkgo/internal/profiles_and_reports.go | 237 - .../onsi/ginkgo/v2/ginkgo/internal/run.go | 355 - .../ginkgo/v2/ginkgo/internal/test_suite.go | 283 - .../onsi/ginkgo/v2/ginkgo/internal/utils.go | 86 - .../v2/ginkgo/internal/verify_version.go | 54 - .../ginkgo/v2/ginkgo/labels/labels_command.go | 123 - .../github.com/onsi/ginkgo/v2/ginkgo/main.go | 58 - .../onsi/ginkgo/v2/ginkgo/outline/ginkgo.go | 302 - .../onsi/ginkgo/v2/ginkgo/outline/import.go | 65 - .../onsi/ginkgo/v2/ginkgo/outline/outline.go | 110 - .../v2/ginkgo/outline/outline_command.go | 98 - .../onsi/ginkgo/v2/ginkgo/run/run_command.go | 232 - .../v2/ginkgo/unfocus/unfocus_command.go | 186 - .../onsi/ginkgo/v2/ginkgo/watch/delta.go | 22 - .../ginkgo/v2/ginkgo/watch/delta_tracker.go | 75 - .../ginkgo/v2/ginkgo/watch/dependencies.go | 92 - .../ginkgo/v2/ginkgo/watch/package_hash.go | 108 - .../ginkgo/v2/ginkgo/watch/package_hashes.go | 85 - .../onsi/ginkgo/v2/ginkgo/watch/suite.go | 87 - .../ginkgo/v2/ginkgo/watch/watch_command.go | 192 - .../interrupt_handler/interrupt_handler.go | 177 - .../sigquit_swallower_unix.go | 15 - .../sigquit_swallower_windows.go | 8 - .../parallel_support/client_server.go | 72 - .../internal/parallel_support/http_client.go | 169 - .../internal/parallel_support/http_server.go | 242 - .../internal/parallel_support/rpc_client.go | 136 - .../internal/parallel_support/rpc_server.go | 75 - .../parallel_support/server_handler.go | 234 - .../ginkgo/v2/reporters/default_reporter.go | 759 - .../v2/reporters/deprecated_reporter.go | 149 - .../onsi/ginkgo/v2/reporters/json_report.go | 60 - .../onsi/ginkgo/v2/reporters/junit_report.go | 369 - .../onsi/ginkgo/v2/reporters/reporter.go | 29 - .../ginkgo/v2/reporters/teamcity_report.go | 101 - .../onsi/ginkgo/v2/types/code_location.go | 159 - .../github.com/onsi/ginkgo/v2/types/config.go | 757 - .../onsi/ginkgo/v2/types/deprecated_types.go | 141 - .../ginkgo/v2/types/deprecation_support.go | 177 - .../onsi/ginkgo/v2/types/enum_support.go | 43 - .../github.com/onsi/ginkgo/v2/types/errors.go | 630 - .../onsi/ginkgo/v2/types/file_filter.go | 106 - .../github.com/onsi/ginkgo/v2/types/flags.go | 489 - .../onsi/ginkgo/v2/types/label_filter.go | 358 - .../onsi/ginkgo/v2/types/report_entry.go | 190 - .../github.com/onsi/ginkgo/v2/types/types.go | 916 - .../onsi/ginkgo/v2/types/version.go | 3 - vendor/github.com/quic-go/qpack/.gitmodules | 4 +- vendor/github.com/quic-go/qpack/.golangci.yml | 27 +- vendor/github.com/quic-go/qpack/README.md | 13 +- vendor/github.com/quic-go/qpack/decoder.go | 266 +- .../github.com/quic-go/qpack/static_table.go | 2 +- vendor/github.com/quic-go/qpack/tools.go | 5 - vendor/github.com/quic-go/qpack/varint.go | 11 +- .../github.com/quic-go/qtls-go1-19/README.md | 6 - .../github.com/quic-go/qtls-go1-19/alert.go | 102 - vendor/github.com/quic-go/qtls-go1-19/auth.go | 293 - .../quic-go/qtls-go1-19/cipher_suites.go | 693 - .../github.com/quic-go/qtls-go1-19/common.go | 1513 -- vendor/github.com/quic-go/qtls-go1-19/conn.go | 1649 -- vendor/github.com/quic-go/qtls-go1-19/cpu.go | 22 - .../quic-go/qtls-go1-19/cpu_other.go | 12 - .../quic-go/qtls-go1-19/handshake_client.go | 1123 - .../qtls-go1-19/handshake_client_tls13.go | 755 - .../quic-go/qtls-go1-19/handshake_messages.go | 1875 -- .../quic-go/qtls-go1-19/handshake_server.go | 922 - .../qtls-go1-19/handshake_server_tls13.go | 903 - .../quic-go/qtls-go1-19/key_agreement.go | 357 - .../quic-go/qtls-go1-19/key_schedule.go | 216 - .../quic-go/qtls-go1-19/notboring.go | 18 - vendor/github.com/quic-go/qtls-go1-19/prf.go | 283 - .../github.com/quic-go/qtls-go1-19/ticket.go | 277 - vendor/github.com/quic-go/qtls-go1-19/tls.go | 362 - .../github.com/quic-go/qtls-go1-19/unsafe.go | 96 - vendor/github.com/quic-go/qtls-go1-20/LICENSE | 27 - .../github.com/quic-go/qtls-go1-20/README.md | 6 - .../github.com/quic-go/qtls-go1-20/alert.go | 109 - vendor/github.com/quic-go/qtls-go1-20/auth.go | 293 - .../github.com/quic-go/qtls-go1-20/cache.go | 95 - .../quic-go/qtls-go1-20/cipher_suites.go | 691 - .../github.com/quic-go/qtls-go1-20/common.go | 1454 -- vendor/github.com/quic-go/qtls-go1-20/conn.go | 1643 -- .../quic-go/qtls-go1-20/handshake_client.go | 1130 - .../qtls-go1-20/handshake_client_tls13.go | 782 - .../quic-go/qtls-go1-20/handshake_messages.go | 1886 -- .../quic-go/qtls-go1-20/handshake_server.go | 899 - .../qtls-go1-20/handshake_server_tls13.go | 979 - .../quic-go/qtls-go1-20/key_agreement.go | 366 - .../quic-go/qtls-go1-20/key_schedule.go | 159 - .../quic-go/qtls-go1-20/notboring.go | 18 - vendor/github.com/quic-go/qtls-go1-20/prf.go | 283 - vendor/github.com/quic-go/qtls-go1-20/quic.go | 412 - .../github.com/quic-go/qtls-go1-20/ticket.go | 203 - vendor/github.com/quic-go/qtls-go1-20/tls.go | 356 - .../github.com/quic-go/qtls-go1-20/unsafe.go | 101 - vendor/github.com/quic-go/quic-go/.gitignore | 1 + .../github.com/quic-go/quic-go/.golangci.yml | 127 +- .../github.com/quic-go/quic-go/Changelog.md | 109 - vendor/github.com/quic-go/quic-go/README.md | 220 +- vendor/github.com/quic-go/quic-go/SECURITY.md | 17 +- vendor/github.com/quic-go/quic-go/client.go | 188 +- .../github.com/quic-go/quic-go/closed_conn.go | 40 +- vendor/github.com/quic-go/quic-go/codecov.yml | 15 +- vendor/github.com/quic-go/quic-go/config.go | 42 +- .../quic-go/quic-go/conn_id_generator.go | 141 +- .../quic-go/quic-go/conn_id_manager.go | 169 +- .../github.com/quic-go/quic-go/connection.go | 3211 +-- .../quic-go/quic-go/connection_logging.go | 315 + .../quic-go/quic-go/connection_timer.go | 51 - .../quic-go/quic-go/crypto_stream.go | 224 +- .../quic-go/quic-go/crypto_stream_manager.go | 72 +- .../quic-go/quic-go/datagram_queue.go | 92 +- vendor/github.com/quic-go/quic-go/errors.go | 96 +- .../quic-go/quic-go/frame_sorter.go | 37 + vendor/github.com/quic-go/quic-go/framer.go | 296 +- .../quic-go/quic-go/http3/README.md | 9 + .../github.com/quic-go/quic-go/http3/body.go | 156 +- .../quic-go/quic-go/http3/capsule.go | 20 +- .../quic-go/quic-go/http3/client.go | 735 +- .../github.com/quic-go/quic-go/http3/conn.go | 321 + .../github.com/quic-go/quic-go/http3/error.go | 63 + .../quic-go/quic-go/http3/error_codes.go | 49 +- .../quic-go/quic-go/http3/frames.go | 257 +- .../quic-go/quic-go/http3/headers.go | 418 + .../quic-go/quic-go/http3/http_stream.go | 76 - .../quic-go/quic-go/http3/ip_addr.go | 48 + .../quic-go/quic-go/http3/mockgen.go | 9 +- .../github.com/quic-go/quic-go/http3/qlog.go | 56 + .../quic-go/quic-go/http3/qlog/event.go | 138 + .../quic-go/quic-go/http3/qlog/frame.go | 220 + .../quic-go/quic-go/http3/qlog/qlog_dir.go | 15 + .../quic-go/quic-go/http3/request.go | 111 - .../quic-go/quic-go/http3/request_writer.go | 98 +- .../quic-go/quic-go/http3/response_writer.go | 346 +- .../quic-go/quic-go/http3/roundtrip.go | 301 - .../quic-go/quic-go/http3/server.go | 749 +- .../quic-go/quic-go/http3/server_conn.go | 261 + .../quic-go/http3/state_tracking_stream.go | 173 + .../quic-go/quic-go/http3/stream.go | 406 + .../github.com/quic-go/quic-go/http3/trace.go | 105 + .../quic-go/quic-go/http3/transport.go | 538 + .../github.com/quic-go/quic-go/interface.go | 277 +- .../internal/ackhandler/ack_eliciting.go | 13 + .../quic-go/internal/ackhandler/ackhandler.go | 23 - .../quic-go/internal/ackhandler/ecn.go | 340 + .../quic-go/internal/ackhandler/interfaces.go | 39 +- .../ackhandler/lost_packet_tracker.go | 73 + .../quic-go/internal/ackhandler/mockgen.go | 4 +- .../quic-go/internal/ackhandler/packet.go | 27 +- .../ackhandler/packet_number_generator.go | 2 +- .../ackhandler/received_packet_handler.go | 88 +- .../ackhandler/received_packet_history.go | 130 +- .../ackhandler/received_packet_tracker.go | 248 +- .../ackhandler/sent_packet_handler.go | 768 +- .../ackhandler/sent_packet_history.go | 191 +- .../quic-go/internal/congestion/bandwidth.go | 3 - .../quic-go/internal/congestion/clock.go | 10 +- .../quic-go/internal/congestion/cubic.go | 22 +- .../internal/congestion/cubic_sender.go | 74 +- .../internal/congestion/hybrid_slow_start.go | 5 +- .../quic-go/internal/congestion/interface.go | 13 +- .../quic-go/internal/congestion/pacer.go | 66 +- .../flowcontrol/base_flow_controller.go | 27 +- .../flowcontrol/connection_flow_controller.go | 63 +- .../quic-go/internal/flowcontrol/interface.go | 26 +- .../flowcontrol/stream_flow_controller.go | 55 +- .../quic-go/internal/handshake/aead.go | 109 +- .../internal/handshake/cipher_suite.go | 104 + .../internal/handshake/crypto_setup.go | 865 +- .../quic-go/internal/handshake/fake_conn.go | 21 + .../internal/handshake/header_protector.go | 14 +- .../quic-go/internal/handshake/hkdf.go | 4 +- .../internal/handshake/initial_aead.go | 30 +- .../quic-go/internal/handshake/interface.go | 82 +- .../quic-go/internal/handshake/mockgen.go | 6 - .../quic-go/internal/handshake/retry.go | 40 +- .../internal/handshake/session_ticket.go | 33 +- .../quic-go/internal/handshake/tls_config.go | 39 + .../handshake/tls_extension_handler.go | 68 - .../internal/handshake/token_generator.go | 21 +- .../internal/handshake/token_protector.go | 47 +- .../internal/handshake/updatable_aead.go | 109 +- .../quic-go/internal/logutils/frame.go | 50 - .../quic-go/quic-go/internal/monotime/time.go | 90 + .../internal/protocol/connection_id.go | 6 +- .../internal/protocol/encryption_level.go | 35 + .../internal/protocol/packet_number.go | 68 +- .../quic-go/internal/protocol/params.go | 43 +- .../quic-go/internal/protocol/perspective.go | 4 +- .../quic-go/internal/protocol/protocol.go | 64 +- .../quic-go/internal/protocol/stream.go | 26 + .../quic-go/internal/protocol/version.go | 82 +- .../quic-go/internal/qerr/error_codes.go | 5 +- .../quic-go/quic-go/internal/qerr/errors.go | 61 +- .../quic-go/quic-go/internal/qtls/go119.go | 145 - .../quic-go/quic-go/internal/qtls/go120.go | 145 - .../quic-go/quic-go/internal/qtls/go121.go | 5 - .../quic-go/internal/qtls/go_oldversion.go | 5 - .../internal/utils/buffered_write_closer.go | 2 +- .../quic-go/internal/utils/byteorder.go | 21 - .../internal/utils/byteorder_big_endian.go | 103 - .../quic-go/internal/utils/connstats.go | 14 + .../quic-go/quic-go/internal/utils/ip.go | 10 - .../quic-go/quic-go/internal/utils/log.go | 14 +- .../quic-go/quic-go/internal/utils/minmax.go | 72 - .../internal/utils/ringbuffer/ringbuffer.go | 12 +- .../quic-go/internal/utils/rtt_stats.go | 114 +- .../quic-go/quic-go/internal/utils/timer.go | 57 - .../quic-go/internal/wire/ack_frame.go | 148 +- .../internal/wire/ack_frequency_frame.go | 65 + .../internal/wire/connection_close_frame.go | 47 +- .../quic-go/internal/wire/crypto_frame.go | 37 +- .../internal/wire/data_blocked_frame.go | 18 +- .../quic-go/internal/wire/datagram_frame.go | 39 +- .../quic-go/internal/wire/extended_header.go | 76 +- .../quic-go/quic-go/internal/wire/frame.go | 33 + .../quic-go/internal/wire/frame_parser.go | 283 +- .../quic-go/internal/wire/frame_type.go | 81 + .../internal/wire/handshake_done_frame.go | 6 +- .../quic-go/quic-go/internal/wire/header.go | 182 +- .../internal/wire/immediate_ack_frame.go | 18 + .../quic-go/internal/wire/interface.go | 17 - .../quic-go/quic-go/internal/wire/log.go | 4 +- .../quic-go/internal/wire/max_data_frame.go | 18 +- .../internal/wire/max_stream_data_frame.go | 25 +- .../internal/wire/max_streams_frame.go | 26 +- .../internal/wire/new_connection_id_frame.go | 59 +- .../quic-go/internal/wire/new_token_frame.go | 30 +- .../internal/wire/path_challenge_frame.go | 21 +- .../internal/wire/path_response_frame.go | 21 +- .../quic-go/internal/wire/ping_frame.go | 6 +- .../quic-go/quic-go/internal/wire/pool.go | 2 +- .../internal/wire/reset_stream_frame.go | 73 +- .../wire/retire_connection_id_frame.go | 18 +- .../quic-go/internal/wire/short_header.go | 17 +- .../internal/wire/stop_sending_frame.go | 25 +- .../wire/stream_data_blocked_frame.go | 22 +- .../quic-go/internal/wire/stream_frame.go | 61 +- .../internal/wire/streams_blocked_frame.go | 26 +- .../internal/wire/transport_parameters.go | 255 +- .../internal/wire/version_negotiation.go | 38 +- .../quic-go/quic-go/logging/frame.go | 66 - .../quic-go/quic-go/logging/interface.go | 139 - .../quic-go/quic-go/logging/mockgen.go | 4 - .../quic-go/quic-go/logging/multiplex.go | 226 - .../quic-go/quic-go/logging/null_tracer.go | 58 - .../quic-go/quic-go/logging/packet_header.go | 24 - .../quic-go/quic-go/logging/types.go | 94 - vendor/github.com/quic-go/quic-go/mockgen.go | 67 +- .../quic-go/quic-go/mtu_discoverer.go | 224 +- .../github.com/quic-go/quic-go/multiplexer.go | 75 - vendor/github.com/quic-go/quic-go/oss-fuzz.sh | 42 + .../quic-go/quic-go/packet_handler_map.go | 283 - .../quic-go/quic-go/packet_packer.go | 404 +- .../quic-go/quic-go/packet_unpacker.go | 32 +- .../quic-go/quic-go/path_manager.go | 206 + .../quic-go/quic-go/path_manager_outgoing.go | 314 + .../github.com/quic-go/quic-go/qlog/event.go | 849 + .../github.com/quic-go/quic-go/qlog/frame.go | 481 + .../quic-go/quic-go/qlog/packet_header.go | 96 + .../quic-go/quic-go/qlog/qlog_dir.go | 61 + .../github.com/quic-go/quic-go/qlog/types.go | 304 + .../quic-go/qlogwriter/jsontext/encoder.go | 324 + .../quic-go/quic-go/qlogwriter/trace.go | 124 + .../quic-go/quic-go/qlogwriter/writer.go | 229 + .../quic-go/quic-go/quicvarint/io.go | 36 +- .../quic-go/quic-go/quicvarint/varint.go | 75 +- .../quic-go/quic-go/receive_stream.go | 425 +- .../quic-go/quic-go/retransmission_queue.go | 158 +- .../github.com/quic-go/quic-go/send_conn.go | 127 +- .../github.com/quic-go/quic-go/send_queue.go | 26 +- .../github.com/quic-go/quic-go/send_stream.go | 571 +- vendor/github.com/quic-go/quic-go/server.go | 822 +- vendor/github.com/quic-go/quic-go/sni.go | 136 + .../quic-go/quic-go/stateless_reset.go | 42 + vendor/github.com/quic-go/quic-go/stream.go | 198 +- .../github.com/quic-go/quic-go/streams_map.go | 288 +- .../quic-go/quic-go/streams_map_incoming.go | 106 +- .../quic-go/quic-go/streams_map_outgoing.go | 138 +- vendor/github.com/quic-go/quic-go/sys_conn.go | 91 +- .../quic-go/quic-go/sys_conn_buffers.go | 6 +- .../quic-go/quic-go/sys_conn_buffers_write.go | 6 +- .../github.com/quic-go/quic-go/sys_conn_df.go | 9 +- .../quic-go/quic-go/sys_conn_df_darwin.go | 92 + .../quic-go/quic-go/sys_conn_df_linux.go | 49 +- .../quic-go/quic-go/sys_conn_df_windows.go | 19 +- .../quic-go/quic-go/sys_conn_helper_darwin.go | 33 +- .../quic-go/sys_conn_helper_freebsd.go | 30 +- .../quic-go/quic-go/sys_conn_helper_linux.go | 129 +- .../quic-go/sys_conn_helper_nonlinux.go | 4 + .../quic-go/quic-go/sys_conn_no_gso.go | 8 - .../quic-go/quic-go/sys_conn_oob.go | 149 +- .../github.com/quic-go/quic-go/token_store.go | 5 +- vendor/github.com/quic-go/quic-go/tools.go | 8 - .../github.com/quic-go/quic-go/transport.go | 697 +- .../quic-go/quic-go/window_update_queue.go | 71 - .../testify/assert/assertion_compare.go | 45 +- .../testify/assert/assertion_format.go | 85 +- .../testify/assert/assertion_forward.go | 170 +- .../testify/assert/assertion_order.go | 12 +- .../stretchr/testify/assert/assertions.go | 512 +- .../github.com/stretchr/testify/assert/doc.go | 4 + .../testify/assert/http_assertions.go | 4 +- .../testify/assert/yaml/yaml_custom.go | 24 + .../testify/assert/yaml/yaml_default.go | 36 + .../stretchr/testify/assert/yaml/yaml_fail.go | 17 + .../stretchr/testify/require/doc.go | 2 + .../stretchr/testify/require/require.go | 532 +- .../stretchr/testify/require/require.go.tmpl | 2 +- .../testify/require/require_forward.go | 170 +- .../stretchr/testify/require/requirements.go | 2 +- vendor/golang.org/x/crypto/LICENSE | 4 +- .../x/crypto/chacha20/chacha_arm64.go | 1 - .../x/crypto/chacha20/chacha_arm64.s | 1 - .../x/crypto/chacha20/chacha_noasm.go | 3 +- .../{chacha_ppc64le.go => chacha_ppc64x.go} | 3 +- .../{chacha_ppc64le.s => chacha_ppc64x.s} | 217 +- .../x/crypto/chacha20/chacha_s390x.go | 1 - .../x/crypto/chacha20/chacha_s390x.s | 1 - .../chacha20poly1305/chacha20poly1305.go | 2 +- .../chacha20poly1305_amd64.go | 1 - .../chacha20poly1305/chacha20poly1305_amd64.s | 11484 ++++++++-- .../chacha20poly1305_noasm.go | 1 - vendor/golang.org/x/crypto/cryptobyte/asn1.go | 824 - .../x/crypto/cryptobyte/asn1/asn1.go | 46 - .../golang.org/x/crypto/cryptobyte/builder.go | 350 - .../golang.org/x/crypto/cryptobyte/string.go | 183 - .../x/crypto/curve25519/curve25519.go | 59 - .../x/crypto/curve25519/curve25519_compat.go | 105 - .../x/crypto/curve25519/curve25519_go120.go | 46 - .../x/crypto/curve25519/internal/field/README | 7 - .../x/crypto/curve25519/internal/field/fe.go | 416 - .../curve25519/internal/field/fe_amd64.go | 16 - .../curve25519/internal/field/fe_amd64.s | 379 - .../internal/field/fe_amd64_noasm.go | 12 - .../curve25519/internal/field/fe_arm64.go | 16 - .../curve25519/internal/field/fe_arm64.s | 43 - .../internal/field/fe_arm64_noasm.go | 12 - .../curve25519/internal/field/fe_generic.go | 264 - .../curve25519/internal/field/sync.checkpoint | 1 - .../crypto/curve25519/internal/field/sync.sh | 19 - vendor/golang.org/x/crypto/hkdf/hkdf.go | 6 +- .../x/crypto/internal/alias/alias.go | 1 - .../x/crypto/internal/alias/alias_purego.go | 1 - .../x/crypto/internal/poly1305/bits_compat.go | 40 - .../x/crypto/internal/poly1305/bits_go1.13.go | 22 - .../x/crypto/internal/poly1305/mac_noasm.go | 3 +- .../x/crypto/internal/poly1305/sum_amd64.s | 134 +- .../poly1305/{sum_amd64.go => sum_asm.go} | 3 +- .../x/crypto/internal/poly1305/sum_generic.go | 43 +- .../x/crypto/internal/poly1305/sum_loong64.s | 123 + .../x/crypto/internal/poly1305/sum_ppc64le.go | 48 - .../poly1305/{sum_ppc64le.s => sum_ppc64x.s} | 45 +- .../x/crypto/internal/poly1305/sum_s390x.go | 1 - .../x/crypto/internal/poly1305/sum_s390x.s | 1 - vendor/golang.org/x/crypto/sha3/doc.go | 6 +- vendor/golang.org/x/crypto/sha3/hashes.go | 67 +- .../x/crypto/sha3/hashes_generic.go | 28 - .../golang.org/x/crypto/sha3/hashes_noasm.go | 23 + vendor/golang.org/x/crypto/sha3/keccakf.go | 1 - .../golang.org/x/crypto/sha3/keccakf_amd64.go | 1 - .../golang.org/x/crypto/sha3/keccakf_amd64.s | 5792 ++++- vendor/golang.org/x/crypto/sha3/register.go | 19 - vendor/golang.org/x/crypto/sha3/sha3.go | 205 +- vendor/golang.org/x/crypto/sha3/sha3_s390x.go | 68 +- vendor/golang.org/x/crypto/sha3/sha3_s390x.s | 1 - vendor/golang.org/x/crypto/sha3/shake.go | 103 +- .../golang.org/x/crypto/sha3/shake_generic.go | 20 - .../golang.org/x/crypto/sha3/shake_noasm.go | 15 + vendor/golang.org/x/crypto/sha3/xor.go | 24 - .../golang.org/x/crypto/sha3/xor_generic.go | 28 - .../golang.org/x/crypto/sha3/xor_unaligned.go | 68 - vendor/golang.org/x/mod/LICENSE | 4 +- .../x/mod/internal/lazyregexp/lazyre.go | 78 - vendor/golang.org/x/mod/modfile/print.go | 184 - vendor/golang.org/x/mod/modfile/read.go | 958 - vendor/golang.org/x/mod/modfile/rule.go | 1663 -- vendor/golang.org/x/mod/modfile/work.go | 285 - vendor/golang.org/x/mod/module/module.go | 841 - vendor/golang.org/x/mod/module/pseudo.go | 250 - vendor/golang.org/x/mod/semver/semver.go | 30 +- vendor/golang.org/x/net/LICENSE | 4 +- vendor/golang.org/x/net/html/atom/table.go | 1256 +- vendor/golang.org/x/net/html/doc.go | 9 +- vendor/golang.org/x/net/html/doctype.go | 2 +- vendor/golang.org/x/net/html/foreign.go | 3 +- vendor/golang.org/x/net/html/iter.go | 56 + vendor/golang.org/x/net/html/node.go | 4 + vendor/golang.org/x/net/html/parse.go | 12 +- vendor/golang.org/x/net/html/token.go | 30 +- .../golang.org/x/net/http/httpguts/httplex.go | 13 +- .../x/net/http2/client_conn_pool.go | 8 +- vendor/golang.org/x/net/http2/config.go | 122 + vendor/golang.org/x/net/http2/config_go124.go | 61 + .../x/net/http2/config_pre_go124.go | 16 + vendor/golang.org/x/net/http2/databuffer.go | 59 +- vendor/golang.org/x/net/http2/frame.go | 80 +- vendor/golang.org/x/net/http2/go111.go | 30 - vendor/golang.org/x/net/http2/go115.go | 27 - vendor/golang.org/x/net/http2/go118.go | 17 - vendor/golang.org/x/net/http2/h2c/h2c.go | 14 +- vendor/golang.org/x/net/http2/http2.go | 133 +- vendor/golang.org/x/net/http2/not_go111.go | 21 - vendor/golang.org/x/net/http2/not_go115.go | 31 - vendor/golang.org/x/net/http2/not_go118.go | 17 - vendor/golang.org/x/net/http2/pipe.go | 11 +- vendor/golang.org/x/net/http2/server.go | 538 +- vendor/golang.org/x/net/http2/timer.go | 20 + vendor/golang.org/x/net/http2/transport.go | 992 +- vendor/golang.org/x/net/http2/unencrypted.go | 32 + vendor/golang.org/x/net/http2/write.go | 13 +- .../x/net/http2/writesched_priority.go | 4 +- vendor/golang.org/x/net/icmp/helper_posix.go | 1 - vendor/golang.org/x/net/icmp/listen_posix.go | 1 - vendor/golang.org/x/net/icmp/listen_stub.go | 1 - vendor/golang.org/x/net/idna/go118.go | 1 - vendor/golang.org/x/net/idna/idna10.0.0.go | 1 - vendor/golang.org/x/net/idna/idna9.0.0.go | 1 - vendor/golang.org/x/net/idna/pre_go118.go | 1 - vendor/golang.org/x/net/idna/tables10.0.0.go | 1 - vendor/golang.org/x/net/idna/tables11.0.0.go | 1 - vendor/golang.org/x/net/idna/tables12.0.0.go | 1 - vendor/golang.org/x/net/idna/tables13.0.0.go | 1 - vendor/golang.org/x/net/idna/tables15.0.0.go | 1 - vendor/golang.org/x/net/idna/tables9.0.0.go | 1 - vendor/golang.org/x/net/idna/trie12.0.0.go | 1 - vendor/golang.org/x/net/idna/trie13.0.0.go | 1 - .../x/net/internal/httpcommon/ascii.go | 53 + .../httpcommon}/headermap.go | 24 +- .../x/net/internal/httpcommon/request.go | 467 + .../x/net/internal/socket/cmsghdr.go | 1 - .../x/net/internal/socket/cmsghdr_bsd.go | 1 - .../internal/socket/cmsghdr_linux_32bit.go | 2 - .../internal/socket/cmsghdr_linux_64bit.go | 2 - .../internal/socket/cmsghdr_solaris_64bit.go | 1 - .../x/net/internal/socket/cmsghdr_stub.go | 1 - .../x/net/internal/socket/cmsghdr_unix.go | 1 - .../net/internal/socket/complete_dontwait.go | 1 - .../internal/socket/complete_nodontwait.go | 1 - .../golang.org/x/net/internal/socket/empty.s | 1 - .../x/net/internal/socket/error_unix.go | 1 - .../x/net/internal/socket/iovec_32bit.go | 2 - .../x/net/internal/socket/iovec_64bit.go | 2 - .../internal/socket/iovec_solaris_64bit.go | 1 - .../x/net/internal/socket/iovec_stub.go | 1 - .../x/net/internal/socket/mmsghdr_stub.go | 1 - .../x/net/internal/socket/mmsghdr_unix.go | 1 - .../x/net/internal/socket/msghdr_bsd.go | 1 - .../x/net/internal/socket/msghdr_bsdvar.go | 1 - .../net/internal/socket/msghdr_linux_32bit.go | 2 - .../net/internal/socket/msghdr_linux_64bit.go | 2 - .../internal/socket/msghdr_solaris_64bit.go | 1 - .../x/net/internal/socket/msghdr_stub.go | 1 - .../x/net/internal/socket/msghdr_zos_s390x.go | 1 - .../x/net/internal/socket/norace.go | 1 - .../golang.org/x/net/internal/socket/race.go | 1 - .../x/net/internal/socket/rawconn_mmsg.go | 1 - .../x/net/internal/socket/rawconn_msg.go | 1 - .../x/net/internal/socket/rawconn_nommsg.go | 1 - .../x/net/internal/socket/rawconn_nomsg.go | 1 - .../x/net/internal/socket/sys_bsd.go | 1 - .../x/net/internal/socket/sys_const_unix.go | 1 - .../x/net/internal/socket/sys_linux.go | 1 - .../net/internal/socket/sys_linux_loong64.go | 1 - .../net/internal/socket/sys_linux_riscv64.go | 1 - .../x/net/internal/socket/sys_posix.go | 1 - .../x/net/internal/socket/sys_stub.go | 1 - .../x/net/internal/socket/sys_unix.go | 1 - .../x/net/internal/socket/zsys_aix_ppc64.go | 1 - .../net/internal/socket/zsys_linux_loong64.go | 1 - .../net/internal/socket/zsys_linux_riscv64.go | 1 - .../net/internal/socket/zsys_openbsd_ppc64.go | 28 +- .../internal/socket/zsys_openbsd_riscv64.go | 28 +- vendor/golang.org/x/net/ipv4/control_bsd.go | 1 - .../golang.org/x/net/ipv4/control_pktinfo.go | 1 - vendor/golang.org/x/net/ipv4/control_stub.go | 1 - vendor/golang.org/x/net/ipv4/control_unix.go | 1 - vendor/golang.org/x/net/ipv4/icmp_stub.go | 1 - vendor/golang.org/x/net/ipv4/payload_cmsg.go | 1 - .../golang.org/x/net/ipv4/payload_nocmsg.go | 1 - vendor/golang.org/x/net/ipv4/sockopt_posix.go | 1 - vendor/golang.org/x/net/ipv4/sockopt_stub.go | 1 - vendor/golang.org/x/net/ipv4/sys_aix.go | 1 - vendor/golang.org/x/net/ipv4/sys_asmreq.go | 1 - .../golang.org/x/net/ipv4/sys_asmreq_stub.go | 1 - vendor/golang.org/x/net/ipv4/sys_asmreqn.go | 1 - .../golang.org/x/net/ipv4/sys_asmreqn_stub.go | 1 - vendor/golang.org/x/net/ipv4/sys_bpf.go | 1 - vendor/golang.org/x/net/ipv4/sys_bpf_stub.go | 1 - vendor/golang.org/x/net/ipv4/sys_bsd.go | 1 - vendor/golang.org/x/net/ipv4/sys_ssmreq.go | 1 - .../golang.org/x/net/ipv4/sys_ssmreq_stub.go | 1 - vendor/golang.org/x/net/ipv4/sys_stub.go | 1 - .../golang.org/x/net/ipv4/zsys_aix_ppc64.go | 1 - .../x/net/ipv4/zsys_linux_loong64.go | 1 - .../x/net/ipv4/zsys_linux_riscv64.go | 1 - .../x/net/ipv6/control_rfc2292_unix.go | 1 - .../x/net/ipv6/control_rfc3542_unix.go | 1 - vendor/golang.org/x/net/ipv6/control_stub.go | 1 - vendor/golang.org/x/net/ipv6/control_unix.go | 1 - vendor/golang.org/x/net/ipv6/icmp_bsd.go | 1 - vendor/golang.org/x/net/ipv6/icmp_stub.go | 1 - vendor/golang.org/x/net/ipv6/payload_cmsg.go | 1 - .../golang.org/x/net/ipv6/payload_nocmsg.go | 1 - vendor/golang.org/x/net/ipv6/sockopt_posix.go | 1 - vendor/golang.org/x/net/ipv6/sockopt_stub.go | 1 - vendor/golang.org/x/net/ipv6/sys_aix.go | 1 - vendor/golang.org/x/net/ipv6/sys_asmreq.go | 1 - .../golang.org/x/net/ipv6/sys_asmreq_stub.go | 1 - vendor/golang.org/x/net/ipv6/sys_bpf.go | 1 - vendor/golang.org/x/net/ipv6/sys_bpf_stub.go | 1 - vendor/golang.org/x/net/ipv6/sys_bsd.go | 1 - vendor/golang.org/x/net/ipv6/sys_ssmreq.go | 1 - .../golang.org/x/net/ipv6/sys_ssmreq_stub.go | 1 - vendor/golang.org/x/net/ipv6/sys_stub.go | 1 - .../golang.org/x/net/ipv6/zsys_aix_ppc64.go | 1 - .../x/net/ipv6/zsys_linux_loong64.go | 1 - .../x/net/ipv6/zsys_linux_riscv64.go | 1 - vendor/golang.org/x/net/trace/events.go | 2 +- vendor/golang.org/x/sync/LICENSE | 4 +- vendor/golang.org/x/sync/errgroup/errgroup.go | 151 + .../golang.org/x/sync/semaphore/semaphore.go | 42 +- vendor/golang.org/x/sys/LICENSE | 4 +- vendor/golang.org/x/sys/cpu/asm_aix_ppc64.s | 1 - .../x/sys/cpu/asm_darwin_arm64_gc.s | 12 + .../golang.org/x/sys/cpu/asm_darwin_x86_gc.s | 17 + vendor/golang.org/x/sys/cpu/cpu.go | 48 + vendor/golang.org/x/sys/cpu/cpu_aix.go | 1 - vendor/golang.org/x/sys/cpu/cpu_arm64.go | 31 +- vendor/golang.org/x/sys/cpu/cpu_arm64.s | 17 +- .../golang.org/x/sys/cpu/cpu_darwin_arm64.go | 67 + .../x/sys/cpu/cpu_darwin_arm64_other.go | 29 + vendor/golang.org/x/sys/cpu/cpu_darwin_x86.go | 61 + vendor/golang.org/x/sys/cpu/cpu_gc_arm64.go | 2 +- vendor/golang.org/x/sys/cpu/cpu_gc_s390x.go | 1 - vendor/golang.org/x/sys/cpu/cpu_gc_x86.go | 6 +- .../x/sys/cpu/{cpu_x86.s => cpu_gc_x86.s} | 4 +- .../golang.org/x/sys/cpu/cpu_gccgo_arm64.go | 2 +- .../golang.org/x/sys/cpu/cpu_gccgo_s390x.go | 1 - vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.c | 2 - vendor/golang.org/x/sys/cpu/cpu_gccgo_x86.go | 8 - vendor/golang.org/x/sys/cpu/cpu_linux.go | 1 - .../golang.org/x/sys/cpu/cpu_linux_arm64.go | 9 + .../golang.org/x/sys/cpu/cpu_linux_loong64.go | 22 + .../golang.org/x/sys/cpu/cpu_linux_mips64x.go | 2 - .../golang.org/x/sys/cpu/cpu_linux_noinit.go | 3 +- .../golang.org/x/sys/cpu/cpu_linux_ppc64x.go | 2 - .../golang.org/x/sys/cpu/cpu_linux_riscv64.go | 160 + vendor/golang.org/x/sys/cpu/cpu_loong64.go | 39 +- vendor/golang.org/x/sys/cpu/cpu_loong64.s | 13 + vendor/golang.org/x/sys/cpu/cpu_mips64x.go | 1 - vendor/golang.org/x/sys/cpu/cpu_mipsx.go | 1 - vendor/golang.org/x/sys/cpu/cpu_other_arm.go | 1 - .../golang.org/x/sys/cpu/cpu_other_arm64.go | 7 +- .../golang.org/x/sys/cpu/cpu_other_mips64x.go | 2 - .../golang.org/x/sys/cpu/cpu_other_ppc64x.go | 3 - .../golang.org/x/sys/cpu/cpu_other_riscv64.go | 1 - vendor/golang.org/x/sys/cpu/cpu_other_x86.go | 11 + vendor/golang.org/x/sys/cpu/cpu_ppc64x.go | 1 - vendor/golang.org/x/sys/cpu/cpu_riscv64.go | 24 +- vendor/golang.org/x/sys/cpu/cpu_s390x.s | 1 - vendor/golang.org/x/sys/cpu/cpu_wasm.go | 1 - .../golang.org/x/sys/cpu/cpu_windows_arm64.go | 42 + vendor/golang.org/x/sys/cpu/cpu_x86.go | 192 +- vendor/golang.org/x/sys/cpu/endian_big.go | 1 - vendor/golang.org/x/sys/cpu/endian_little.go | 1 - vendor/golang.org/x/sys/cpu/parse.go | 4 +- .../x/sys/cpu/proc_cpuinfo_linux.go | 1 - .../x/sys/cpu/runtime_auxv_go121.go | 1 - .../golang.org/x/sys/cpu/syscall_aix_gccgo.go | 1 - .../x/sys/cpu/syscall_aix_ppc64_gc.go | 1 - .../x/sys/cpu/syscall_darwin_arm64_gc.go | 54 + .../x/sys/cpu/syscall_darwin_x86_gc.go | 98 + vendor/golang.org/x/sys/execabs/execabs.go | 102 - .../golang.org/x/sys/execabs/execabs_go118.go | 18 - .../golang.org/x/sys/execabs/execabs_go119.go | 21 - vendor/golang.org/x/sys/unix/README.md | 2 +- .../golang.org/x/sys/unix/affinity_linux.go | 9 +- vendor/golang.org/x/sys/unix/aliases.go | 4 +- vendor/golang.org/x/sys/unix/asm_aix_ppc64.s | 1 - vendor/golang.org/x/sys/unix/asm_bsd_386.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_amd64.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_arm.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_arm64.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_ppc64.s | 2 - .../golang.org/x/sys/unix/asm_bsd_riscv64.s | 2 - vendor/golang.org/x/sys/unix/asm_linux_386.s | 1 - .../golang.org/x/sys/unix/asm_linux_amd64.s | 1 - vendor/golang.org/x/sys/unix/asm_linux_arm.s | 1 - .../golang.org/x/sys/unix/asm_linux_arm64.s | 3 - .../golang.org/x/sys/unix/asm_linux_loong64.s | 3 - .../golang.org/x/sys/unix/asm_linux_mips64x.s | 3 - .../golang.org/x/sys/unix/asm_linux_mipsx.s | 3 - .../golang.org/x/sys/unix/asm_linux_ppc64x.s | 3 - .../golang.org/x/sys/unix/asm_linux_riscv64.s | 2 - .../golang.org/x/sys/unix/asm_linux_s390x.s | 3 - .../x/sys/unix/asm_openbsd_mips64.s | 1 - .../golang.org/x/sys/unix/asm_solaris_amd64.s | 1 - vendor/golang.org/x/sys/unix/asm_zos_s390x.s | 668 +- vendor/golang.org/x/sys/unix/auxv.go | 36 + .../golang.org/x/sys/unix/auxv_unsupported.go | 13 + vendor/golang.org/x/sys/unix/bpxsvc_zos.go | 657 + vendor/golang.org/x/sys/unix/bpxsvc_zos.s | 192 + vendor/golang.org/x/sys/unix/cap_freebsd.go | 1 - vendor/golang.org/x/sys/unix/constants.go | 1 - vendor/golang.org/x/sys/unix/dev_aix_ppc.go | 1 - vendor/golang.org/x/sys/unix/dev_aix_ppc64.go | 1 - vendor/golang.org/x/sys/unix/dev_zos.go | 1 - vendor/golang.org/x/sys/unix/dirent.go | 1 - vendor/golang.org/x/sys/unix/endian_big.go | 1 - vendor/golang.org/x/sys/unix/endian_little.go | 1 - vendor/golang.org/x/sys/unix/env_unix.go | 1 - vendor/golang.org/x/sys/unix/epoll_zos.go | 221 - vendor/golang.org/x/sys/unix/fcntl.go | 3 +- .../x/sys/unix/fcntl_linux_32bit.go | 1 - vendor/golang.org/x/sys/unix/fdset.go | 5 +- vendor/golang.org/x/sys/unix/fstatfs_zos.go | 164 - vendor/golang.org/x/sys/unix/gccgo.go | 1 - vendor/golang.org/x/sys/unix/gccgo_c.c | 1 - .../x/sys/unix/gccgo_linux_amd64.go | 1 - vendor/golang.org/x/sys/unix/ifreq_linux.go | 5 +- vendor/golang.org/x/sys/unix/ioctl_linux.go | 101 + vendor/golang.org/x/sys/unix/ioctl_signed.go | 12 +- .../golang.org/x/sys/unix/ioctl_unsigned.go | 12 +- vendor/golang.org/x/sys/unix/ioctl_zos.go | 1 - vendor/golang.org/x/sys/unix/mkall.sh | 1 + vendor/golang.org/x/sys/unix/mkerrors.sh | 71 +- vendor/golang.org/x/sys/unix/mmap_nomremap.go | 3 +- vendor/golang.org/x/sys/unix/mremap.go | 6 +- vendor/golang.org/x/sys/unix/pagesize_unix.go | 3 +- .../golang.org/x/sys/unix/pledge_openbsd.go | 92 +- vendor/golang.org/x/sys/unix/ptrace_darwin.go | 1 - vendor/golang.org/x/sys/unix/ptrace_ios.go | 1 - vendor/golang.org/x/sys/unix/race.go | 1 - vendor/golang.org/x/sys/unix/race0.go | 1 - .../x/sys/unix/readdirent_getdents.go | 1 - .../x/sys/unix/readdirent_getdirentries.go | 3 +- vendor/golang.org/x/sys/unix/sockcmsg_unix.go | 1 - .../x/sys/unix/sockcmsg_unix_other.go | 1 - vendor/golang.org/x/sys/unix/sockcmsg_zos.go | 58 + .../golang.org/x/sys/unix/symaddr_zos_s390x.s | 75 + vendor/golang.org/x/sys/unix/syscall.go | 1 - vendor/golang.org/x/sys/unix/syscall_aix.go | 6 +- .../golang.org/x/sys/unix/syscall_aix_ppc.go | 1 - .../x/sys/unix/syscall_aix_ppc64.go | 1 - vendor/golang.org/x/sys/unix/syscall_bsd.go | 3 +- .../golang.org/x/sys/unix/syscall_darwin.go | 154 + .../x/sys/unix/syscall_darwin_amd64.go | 1 - .../x/sys/unix/syscall_darwin_arm64.go | 1 - .../x/sys/unix/syscall_darwin_libSystem.go | 3 +- .../x/sys/unix/syscall_dragonfly.go | 12 + .../x/sys/unix/syscall_dragonfly_amd64.go | 1 - .../golang.org/x/sys/unix/syscall_freebsd.go | 12 +- .../x/sys/unix/syscall_freebsd_386.go | 1 - .../x/sys/unix/syscall_freebsd_amd64.go | 1 - .../x/sys/unix/syscall_freebsd_arm.go | 1 - .../x/sys/unix/syscall_freebsd_arm64.go | 1 - .../x/sys/unix/syscall_freebsd_riscv64.go | 1 - vendor/golang.org/x/sys/unix/syscall_hurd.go | 2 +- .../golang.org/x/sys/unix/syscall_hurd_386.go | 1 - .../golang.org/x/sys/unix/syscall_illumos.go | 1 - vendor/golang.org/x/sys/unix/syscall_linux.go | 249 +- .../x/sys/unix/syscall_linux_386.go | 1 - .../x/sys/unix/syscall_linux_alarm.go | 2 - .../x/sys/unix/syscall_linux_amd64.go | 1 - .../x/sys/unix/syscall_linux_amd64_gc.go | 1 - .../x/sys/unix/syscall_linux_arm.go | 1 - .../x/sys/unix/syscall_linux_arm64.go | 3 +- .../golang.org/x/sys/unix/syscall_linux_gc.go | 1 - .../x/sys/unix/syscall_linux_gc_386.go | 1 - .../x/sys/unix/syscall_linux_gc_arm.go | 1 - .../x/sys/unix/syscall_linux_gccgo_386.go | 1 - .../x/sys/unix/syscall_linux_gccgo_arm.go | 1 - .../x/sys/unix/syscall_linux_loong64.go | 3 +- .../x/sys/unix/syscall_linux_mips64x.go | 2 - .../x/sys/unix/syscall_linux_mipsx.go | 2 - .../x/sys/unix/syscall_linux_ppc.go | 1 - .../x/sys/unix/syscall_linux_ppc64x.go | 2 - .../x/sys/unix/syscall_linux_riscv64.go | 3 +- .../x/sys/unix/syscall_linux_s390x.go | 1 - .../x/sys/unix/syscall_linux_sparc64.go | 1 - .../golang.org/x/sys/unix/syscall_netbsd.go | 17 + .../x/sys/unix/syscall_netbsd_386.go | 1 - .../x/sys/unix/syscall_netbsd_amd64.go | 1 - .../x/sys/unix/syscall_netbsd_arm.go | 1 - .../x/sys/unix/syscall_netbsd_arm64.go | 1 - .../golang.org/x/sys/unix/syscall_openbsd.go | 29 +- .../x/sys/unix/syscall_openbsd_386.go | 1 - .../x/sys/unix/syscall_openbsd_amd64.go | 1 - .../x/sys/unix/syscall_openbsd_arm.go | 1 - .../x/sys/unix/syscall_openbsd_arm64.go | 1 - .../x/sys/unix/syscall_openbsd_libc.go | 1 - .../x/sys/unix/syscall_openbsd_ppc64.go | 1 - .../x/sys/unix/syscall_openbsd_riscv64.go | 1 - .../golang.org/x/sys/unix/syscall_solaris.go | 102 +- .../x/sys/unix/syscall_solaris_amd64.go | 1 - vendor/golang.org/x/sys/unix/syscall_unix.go | 20 +- .../golang.org/x/sys/unix/syscall_unix_gc.go | 2 - .../x/sys/unix/syscall_unix_gc_ppc64x.go | 3 - .../x/sys/unix/syscall_zos_s390x.go | 1590 +- vendor/golang.org/x/sys/unix/sysvshm_linux.go | 1 - vendor/golang.org/x/sys/unix/sysvshm_unix.go | 3 +- .../x/sys/unix/sysvshm_unix_other.go | 3 +- vendor/golang.org/x/sys/unix/timestruct.go | 1 - .../golang.org/x/sys/unix/unveil_openbsd.go | 41 +- .../golang.org/x/sys/unix/vgetrandom_linux.go | 13 + .../x/sys/unix/vgetrandom_unsupported.go | 11 + vendor/golang.org/x/sys/unix/xattr_bsd.go | 1 - .../golang.org/x/sys/unix/zerrors_aix_ppc.go | 1 - .../x/sys/unix/zerrors_aix_ppc64.go | 1 - .../x/sys/unix/zerrors_darwin_amd64.go | 13 +- .../x/sys/unix/zerrors_darwin_arm64.go | 13 +- .../x/sys/unix/zerrors_dragonfly_amd64.go | 1 - .../x/sys/unix/zerrors_freebsd_386.go | 1 - .../x/sys/unix/zerrors_freebsd_amd64.go | 1 - .../x/sys/unix/zerrors_freebsd_arm.go | 1 - .../x/sys/unix/zerrors_freebsd_arm64.go | 1 - .../x/sys/unix/zerrors_freebsd_riscv64.go | 1 - vendor/golang.org/x/sys/unix/zerrors_linux.go | 651 +- .../x/sys/unix/zerrors_linux_386.go | 40 +- .../x/sys/unix/zerrors_linux_amd64.go | 40 +- .../x/sys/unix/zerrors_linux_arm.go | 39 +- .../x/sys/unix/zerrors_linux_arm64.go | 42 +- .../x/sys/unix/zerrors_linux_loong64.go | 40 +- .../x/sys/unix/zerrors_linux_mips.go | 39 +- .../x/sys/unix/zerrors_linux_mips64.go | 39 +- .../x/sys/unix/zerrors_linux_mips64le.go | 39 +- .../x/sys/unix/zerrors_linux_mipsle.go | 39 +- .../x/sys/unix/zerrors_linux_ppc.go | 39 +- .../x/sys/unix/zerrors_linux_ppc64.go | 39 +- .../x/sys/unix/zerrors_linux_ppc64le.go | 39 +- .../x/sys/unix/zerrors_linux_riscv64.go | 42 +- .../x/sys/unix/zerrors_linux_s390x.go | 39 +- .../x/sys/unix/zerrors_linux_sparc64.go | 39 +- .../x/sys/unix/zerrors_netbsd_386.go | 1 - .../x/sys/unix/zerrors_netbsd_amd64.go | 1 - .../x/sys/unix/zerrors_netbsd_arm.go | 1 - .../x/sys/unix/zerrors_netbsd_arm64.go | 1 - .../x/sys/unix/zerrors_openbsd_386.go | 1 - .../x/sys/unix/zerrors_openbsd_amd64.go | 1 - .../x/sys/unix/zerrors_openbsd_arm.go | 1 - .../x/sys/unix/zerrors_openbsd_arm64.go | 1 - .../x/sys/unix/zerrors_openbsd_mips64.go | 1 - .../x/sys/unix/zerrors_openbsd_ppc64.go | 1 - .../x/sys/unix/zerrors_openbsd_riscv64.go | 1 - .../x/sys/unix/zerrors_solaris_amd64.go | 1 - .../x/sys/unix/zerrors_zos_s390x.go | 236 +- .../x/sys/unix/zptrace_armnn_linux.go | 2 - .../x/sys/unix/zptrace_mipsnn_linux.go | 2 - .../x/sys/unix/zptrace_mipsnnle_linux.go | 2 - .../x/sys/unix/zptrace_x86_linux.go | 2 - .../x/sys/unix/zsymaddr_zos_s390x.s | 364 + .../golang.org/x/sys/unix/zsyscall_aix_ppc.go | 1 - .../x/sys/unix/zsyscall_aix_ppc64.go | 1 - .../x/sys/unix/zsyscall_aix_ppc64_gc.go | 1 - .../x/sys/unix/zsyscall_aix_ppc64_gccgo.go | 1 - .../x/sys/unix/zsyscall_darwin_amd64.go | 186 +- .../x/sys/unix/zsyscall_darwin_amd64.s | 45 + .../x/sys/unix/zsyscall_darwin_arm64.go | 186 +- .../x/sys/unix/zsyscall_darwin_arm64.s | 45 + .../x/sys/unix/zsyscall_dragonfly_amd64.go | 1 - .../x/sys/unix/zsyscall_freebsd_386.go | 1 - .../x/sys/unix/zsyscall_freebsd_amd64.go | 1 - .../x/sys/unix/zsyscall_freebsd_arm.go | 1 - .../x/sys/unix/zsyscall_freebsd_arm64.go | 1 - .../x/sys/unix/zsyscall_freebsd_riscv64.go | 1 - .../x/sys/unix/zsyscall_illumos_amd64.go | 1 - .../golang.org/x/sys/unix/zsyscall_linux.go | 89 +- .../x/sys/unix/zsyscall_linux_386.go | 1 - .../x/sys/unix/zsyscall_linux_amd64.go | 1 - .../x/sys/unix/zsyscall_linux_arm.go | 1 - .../x/sys/unix/zsyscall_linux_arm64.go | 1 - .../x/sys/unix/zsyscall_linux_loong64.go | 1 - .../x/sys/unix/zsyscall_linux_mips.go | 1 - .../x/sys/unix/zsyscall_linux_mips64.go | 1 - .../x/sys/unix/zsyscall_linux_mips64le.go | 1 - .../x/sys/unix/zsyscall_linux_mipsle.go | 1 - .../x/sys/unix/zsyscall_linux_ppc.go | 1 - .../x/sys/unix/zsyscall_linux_ppc64.go | 1 - .../x/sys/unix/zsyscall_linux_ppc64le.go | 1 - .../x/sys/unix/zsyscall_linux_riscv64.go | 1 - .../x/sys/unix/zsyscall_linux_s390x.go | 1 - .../x/sys/unix/zsyscall_linux_sparc64.go | 1 - .../x/sys/unix/zsyscall_netbsd_386.go | 1 - .../x/sys/unix/zsyscall_netbsd_amd64.go | 1 - .../x/sys/unix/zsyscall_netbsd_arm.go | 1 - .../x/sys/unix/zsyscall_netbsd_arm64.go | 1 - .../x/sys/unix/zsyscall_openbsd_386.go | 94 +- .../x/sys/unix/zsyscall_openbsd_386.s | 25 + .../x/sys/unix/zsyscall_openbsd_amd64.go | 94 +- .../x/sys/unix/zsyscall_openbsd_amd64.s | 25 + .../x/sys/unix/zsyscall_openbsd_arm.go | 94 +- .../x/sys/unix/zsyscall_openbsd_arm.s | 25 + .../x/sys/unix/zsyscall_openbsd_arm64.go | 94 +- .../x/sys/unix/zsyscall_openbsd_arm64.s | 25 + .../x/sys/unix/zsyscall_openbsd_mips64.go | 94 +- .../x/sys/unix/zsyscall_openbsd_mips64.s | 25 + .../x/sys/unix/zsyscall_openbsd_ppc64.go | 94 +- .../x/sys/unix/zsyscall_openbsd_ppc64.s | 30 + .../x/sys/unix/zsyscall_openbsd_riscv64.go | 94 +- .../x/sys/unix/zsyscall_openbsd_riscv64.s | 25 + .../x/sys/unix/zsyscall_solaris_amd64.go | 123 +- .../x/sys/unix/zsyscall_zos_s390x.go | 3114 ++- .../x/sys/unix/zsysctl_openbsd_386.go | 1 - .../x/sys/unix/zsysctl_openbsd_amd64.go | 1 - .../x/sys/unix/zsysctl_openbsd_arm.go | 1 - .../x/sys/unix/zsysctl_openbsd_arm64.go | 1 - .../x/sys/unix/zsysctl_openbsd_mips64.go | 1 - .../x/sys/unix/zsysctl_openbsd_ppc64.go | 1 - .../x/sys/unix/zsysctl_openbsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_darwin_amd64.go | 1 - .../x/sys/unix/zsysnum_darwin_arm64.go | 1 - .../x/sys/unix/zsysnum_dragonfly_amd64.go | 1 - .../x/sys/unix/zsysnum_freebsd_386.go | 1 - .../x/sys/unix/zsysnum_freebsd_amd64.go | 1 - .../x/sys/unix/zsysnum_freebsd_arm.go | 1 - .../x/sys/unix/zsysnum_freebsd_arm64.go | 1 - .../x/sys/unix/zsysnum_freebsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_linux_386.go | 17 +- .../x/sys/unix/zsysnum_linux_amd64.go | 18 +- .../x/sys/unix/zsysnum_linux_arm.go | 17 +- .../x/sys/unix/zsysnum_linux_arm64.go | 19 +- .../x/sys/unix/zsysnum_linux_loong64.go | 19 +- .../x/sys/unix/zsysnum_linux_mips.go | 17 +- .../x/sys/unix/zsysnum_linux_mips64.go | 17 +- .../x/sys/unix/zsysnum_linux_mips64le.go | 17 +- .../x/sys/unix/zsysnum_linux_mipsle.go | 17 +- .../x/sys/unix/zsysnum_linux_ppc.go | 17 +- .../x/sys/unix/zsysnum_linux_ppc64.go | 17 +- .../x/sys/unix/zsysnum_linux_ppc64le.go | 17 +- .../x/sys/unix/zsysnum_linux_riscv64.go | 19 +- .../x/sys/unix/zsysnum_linux_s390x.go | 17 +- .../x/sys/unix/zsysnum_linux_sparc64.go | 17 +- .../x/sys/unix/zsysnum_netbsd_386.go | 1 - .../x/sys/unix/zsysnum_netbsd_amd64.go | 1 - .../x/sys/unix/zsysnum_netbsd_arm.go | 1 - .../x/sys/unix/zsysnum_netbsd_arm64.go | 1 - .../x/sys/unix/zsysnum_openbsd_386.go | 1 - .../x/sys/unix/zsysnum_openbsd_amd64.go | 1 - .../x/sys/unix/zsysnum_openbsd_arm.go | 1 - .../x/sys/unix/zsysnum_openbsd_arm64.go | 1 - .../x/sys/unix/zsysnum_openbsd_mips64.go | 1 - .../x/sys/unix/zsysnum_openbsd_ppc64.go | 1 - .../x/sys/unix/zsysnum_openbsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_zos_s390x.go | 5508 ++--- .../golang.org/x/sys/unix/ztypes_aix_ppc.go | 1 - .../golang.org/x/sys/unix/ztypes_aix_ppc64.go | 1 - .../x/sys/unix/ztypes_darwin_amd64.go | 74 +- .../x/sys/unix/ztypes_darwin_arm64.go | 74 +- .../x/sys/unix/ztypes_dragonfly_amd64.go | 1 - .../x/sys/unix/ztypes_freebsd_386.go | 2 +- .../x/sys/unix/ztypes_freebsd_amd64.go | 2 +- .../x/sys/unix/ztypes_freebsd_arm.go | 2 +- .../x/sys/unix/ztypes_freebsd_arm64.go | 2 +- .../x/sys/unix/ztypes_freebsd_riscv64.go | 2 +- vendor/golang.org/x/sys/unix/ztypes_linux.go | 917 +- .../golang.org/x/sys/unix/ztypes_linux_386.go | 27 +- .../x/sys/unix/ztypes_linux_amd64.go | 26 +- .../golang.org/x/sys/unix/ztypes_linux_arm.go | 30 +- .../x/sys/unix/ztypes_linux_arm64.go | 26 +- .../x/sys/unix/ztypes_linux_loong64.go | 26 +- .../x/sys/unix/ztypes_linux_mips.go | 28 +- .../x/sys/unix/ztypes_linux_mips64.go | 26 +- .../x/sys/unix/ztypes_linux_mips64le.go | 26 +- .../x/sys/unix/ztypes_linux_mipsle.go | 28 +- .../golang.org/x/sys/unix/ztypes_linux_ppc.go | 30 +- .../x/sys/unix/ztypes_linux_ppc64.go | 26 +- .../x/sys/unix/ztypes_linux_ppc64le.go | 26 +- .../x/sys/unix/ztypes_linux_riscv64.go | 59 +- .../x/sys/unix/ztypes_linux_s390x.go | 26 +- .../x/sys/unix/ztypes_linux_sparc64.go | 26 +- .../x/sys/unix/ztypes_netbsd_386.go | 1 - .../x/sys/unix/ztypes_netbsd_amd64.go | 1 - .../x/sys/unix/ztypes_netbsd_arm.go | 3 +- .../x/sys/unix/ztypes_netbsd_arm64.go | 1 - .../x/sys/unix/ztypes_openbsd_386.go | 1 - .../x/sys/unix/ztypes_openbsd_amd64.go | 1 - .../x/sys/unix/ztypes_openbsd_arm.go | 1 - .../x/sys/unix/ztypes_openbsd_arm64.go | 1 - .../x/sys/unix/ztypes_openbsd_mips64.go | 1 - .../x/sys/unix/ztypes_openbsd_ppc64.go | 1 - .../x/sys/unix/ztypes_openbsd_riscv64.go | 1 - .../x/sys/unix/ztypes_solaris_amd64.go | 1 - .../golang.org/x/sys/unix/ztypes_zos_s390x.go | 153 +- vendor/golang.org/x/sys/windows/aliases.go | 4 +- .../golang.org/x/sys/windows/dll_windows.go | 13 +- vendor/golang.org/x/sys/windows/empty.s | 9 - .../golang.org/x/sys/windows/env_windows.go | 17 +- vendor/golang.org/x/sys/windows/eventlog.go | 1 - vendor/golang.org/x/sys/windows/mksyscall.go | 1 - vendor/golang.org/x/sys/windows/race.go | 1 - vendor/golang.org/x/sys/windows/race0.go | 1 - .../golang.org/x/sys/windows/registry/key.go | 29 +- .../x/sys/windows/registry/mksyscall.go | 1 - .../x/sys/windows/registry/syscall.go | 1 - .../x/sys/windows/registry/value.go | 7 +- .../sys/windows/registry/zsyscall_windows.go | 16 +- .../x/sys/windows/security_windows.go | 74 +- vendor/golang.org/x/sys/windows/service.go | 1 - vendor/golang.org/x/sys/windows/str.go | 1 - vendor/golang.org/x/sys/windows/syscall.go | 1 - .../x/sys/windows/syscall_windows.go | 182 +- .../golang.org/x/sys/windows/types_windows.go | 673 +- .../x/sys/windows/zsyscall_windows.go | 1274 +- vendor/golang.org/x/text/LICENSE | 4 +- vendor/golang.org/x/text/language/parse.go | 2 +- .../x/text/secure/bidirule/bidirule10.0.0.go | 1 - .../x/text/secure/bidirule/bidirule9.0.0.go | 1 - .../x/text/unicode/bidi/tables10.0.0.go | 1 - .../x/text/unicode/bidi/tables11.0.0.go | 1 - .../x/text/unicode/bidi/tables12.0.0.go | 1 - .../x/text/unicode/bidi/tables13.0.0.go | 1 - .../x/text/unicode/bidi/tables15.0.0.go | 1 - .../x/text/unicode/bidi/tables9.0.0.go | 1 - .../x/text/unicode/norm/tables10.0.0.go | 1 - .../x/text/unicode/norm/tables11.0.0.go | 1 - .../x/text/unicode/norm/tables12.0.0.go | 1 - .../x/text/unicode/norm/tables13.0.0.go | 1 - .../x/text/unicode/norm/tables15.0.0.go | 1 - .../x/text/unicode/norm/tables9.0.0.go | 1 - vendor/golang.org/x/tools/LICENSE | 4 +- .../x/tools/cmd/stringer/stringer.go | 204 +- .../x/tools/go/ast/astutil/enclosing.go | 636 - .../x/tools/go/ast/astutil/imports.go | 485 - .../x/tools/go/ast/astutil/rewrite.go | 488 - .../golang.org/x/tools/go/ast/astutil/util.go | 18 - .../x/tools/go/ast/inspector/inspector.go | 220 - .../x/tools/go/ast/inspector/typeof.go | 229 - .../x/tools/go/gcexportdata/gcexportdata.go | 126 +- .../tools/go/internal/packagesdriver/sizes.go | 48 - vendor/golang.org/x/tools/go/packages/doc.go | 65 +- .../x/tools/go/packages/external.go | 104 +- .../golang.org/x/tools/go/packages/golist.go | 293 +- .../x/tools/go/packages/golist_overlay.go | 494 +- .../x/tools/go/packages/loadmode_string.go | 71 +- .../x/tools/go/packages/packages.go | 778 +- .../golang.org/x/tools/go/packages/visit.go | 9 + .../x/tools/go/types/objectpath/objectpath.go | 320 +- .../x/tools/go/types/typeutil/callee.go | 85 + .../x/tools/go/types/typeutil/imports.go | 30 + .../x/tools/go/types/typeutil/map.go | 475 + .../tools/go/types/typeutil/methodsetcache.go | 71 + .../x/tools/go/types/typeutil/ui.go | 53 + vendor/golang.org/x/tools/imports/forward.go | 77 - .../x/tools/internal/aliases/aliases.go | 38 + .../x/tools/internal/aliases/aliases_go122.go | 80 + .../x/tools/internal/event/keys/keys.go | 6 +- .../x/tools/internal/event/keys/util.go | 21 + .../x/tools/internal/event/label/label.go | 13 +- .../x/tools/internal/event/tag/tag.go | 59 - .../x/tools/internal/fastwalk/fastwalk.go | 196 - .../internal/fastwalk/fastwalk_darwin.go | 119 - .../fastwalk/fastwalk_dirent_fileno.go | 14 - .../internal/fastwalk/fastwalk_dirent_ino.go | 15 - .../fastwalk/fastwalk_dirent_namlen_bsd.go | 14 - .../fastwalk/fastwalk_dirent_namlen_linux.go | 29 - .../internal/fastwalk/fastwalk_portable.go | 41 - .../tools/internal/fastwalk/fastwalk_unix.go | 153 - .../x/tools/internal/gcimporter/bimport.go | 63 +- .../x/tools/internal/gcimporter/exportdata.go | 426 +- .../x/tools/internal/gcimporter/gcimporter.go | 189 +- .../x/tools/internal/gcimporter/iexport.go | 374 +- .../x/tools/internal/gcimporter/iimport.go | 145 +- .../internal/gcimporter/iimport_go122.go | 53 + .../internal/gcimporter/newInterface10.go | 22 - .../internal/gcimporter/newInterface11.go | 14 - .../tools/internal/gcimporter/predeclared.go | 91 + .../x/tools/internal/gcimporter/support.go | 30 + .../internal/gcimporter/support_go117.go | 16 - .../internal/gcimporter/support_go118.go | 37 - .../x/tools/internal/gcimporter/unified_no.go | 10 - .../tools/internal/gcimporter/unified_yes.go | 10 - .../x/tools/internal/gcimporter/ureader_no.go | 19 - .../tools/internal/gcimporter/ureader_yes.go | 67 +- .../x/tools/internal/gocommand/invoke.go | 211 +- .../internal/gocommand/invoke_notunix.go | 13 + .../x/tools/internal/gocommand/invoke_unix.go | 13 + .../x/tools/internal/gocommand/vendor.go | 54 + .../x/tools/internal/gopathwalk/walk.go | 260 - .../x/tools/internal/imports/fix.go | 1767 -- .../x/tools/internal/imports/imports.go | 356 - .../x/tools/internal/imports/mod.go | 723 - .../x/tools/internal/imports/mod_cache.go | 236 - .../x/tools/internal/imports/sortimports.go | 297 - .../x/tools/internal/imports/zstdlib.go | 11345 ---------- .../internal/packagesinternal/packages.go | 17 +- .../x/tools/internal/pkgbits/decoder.go | 36 +- .../x/tools/internal/pkgbits/encoder.go | 43 +- .../x/tools/internal/pkgbits/frames_go1.go | 21 - .../x/tools/internal/pkgbits/frames_go17.go | 28 - .../x/tools/internal/pkgbits/support.go | 2 +- .../x/tools/internal/pkgbits/sync.go | 23 + .../internal/pkgbits/syncmarker_string.go | 7 +- .../x/tools/internal/pkgbits/version.go | 85 + .../x/tools/internal/stdlib/deps.go | 359 + .../x/tools/internal/stdlib/import.go | 89 + .../x/tools/internal/stdlib/manifest.go | 17676 ++++++++++++++++ .../x/tools/internal/stdlib/stdlib.go | 105 + .../internal/tokeninternal/tokeninternal.go | 151 - .../x/tools/internal/typeparams/common.go | 152 +- .../x/tools/internal/typeparams/coretype.go | 53 +- .../internal/typeparams/enabled_go117.go | 12 - .../internal/typeparams/enabled_go118.go | 15 - .../x/tools/internal/typeparams/free.go | 131 + .../x/tools/internal/typeparams/normalize.go | 22 +- .../x/tools/internal/typeparams/termlist.go | 12 +- .../internal/typeparams/typeparams_go117.go | 197 - .../internal/typeparams/typeparams_go118.go | 151 - .../x/tools/internal/typeparams/typeterm.go | 3 + .../internal/typesinternal/classify_call.go | 137 + .../x/tools/internal/typesinternal/element.go | 133 + .../tools/internal/typesinternal/errorcode.go | 16 +- .../internal/typesinternal/objectpath.go | 24 - .../tools/internal/typesinternal/qualifier.go | 46 + .../x/tools/internal/typesinternal/recv.go | 44 + .../x/tools/internal/typesinternal/toonew.go | 89 + .../x/tools/internal/typesinternal/types.go | 109 +- .../tools/internal/typesinternal/types_118.go | 19 - .../x/tools/internal/typesinternal/varkind.go | 40 + .../tools/internal/typesinternal/zerovalue.go | 392 + .../x/tools/internal/versions/features.go | 43 + .../x/tools/internal/versions/gover.go | 172 + .../x/tools/internal/versions/types.go | 33 + .../x/tools/internal/versions/versions.go | 57 + vendor/modules.txt | 176 +- 1332 files changed, 143625 insertions(+), 89378 deletions(-) create mode 100644 internal/browserdial/chrome/chrome.go create mode 100644 internal/browserdial/chrome/pool.go create mode 100644 internal/browserdial/errors/errors.go create mode 100644 internal/browserdial/evidence/types.go create mode 100644 internal/browserdial/lightpanda/lightpanda.go create mode 100644 internal/browserdial/lightpanda/trace.go create mode 100644 internal/browserdial/runner/runner.go create mode 100644 internal/browserdial/script/script.go create mode 100644 internal/browserdial/util/sanitize.go create mode 100644 internal/browserdial/util/time.go create mode 100644 internal/browserdial/util/uuid.go create mode 100644 vendor/github.com/chromedp/cdproto/.gitignore rename vendor/github.com/{go-task/slim-sprig/LICENSE.txt => chromedp/cdproto/LICENSE} (86%) create mode 100644 vendor/github.com/chromedp/cdproto/README.md create mode 100644 vendor/github.com/chromedp/cdproto/accessibility/accessibility.go create mode 100644 vendor/github.com/chromedp/cdproto/accessibility/events.go create mode 100644 vendor/github.com/chromedp/cdproto/accessibility/types.go create mode 100644 vendor/github.com/chromedp/cdproto/animation/animation.go create mode 100644 vendor/github.com/chromedp/cdproto/animation/events.go create mode 100644 vendor/github.com/chromedp/cdproto/animation/types.go create mode 100644 vendor/github.com/chromedp/cdproto/audits/audits.go create mode 100644 vendor/github.com/chromedp/cdproto/audits/events.go create mode 100644 vendor/github.com/chromedp/cdproto/audits/types.go create mode 100644 vendor/github.com/chromedp/cdproto/autofill/autofill.go create mode 100644 vendor/github.com/chromedp/cdproto/autofill/events.go create mode 100644 vendor/github.com/chromedp/cdproto/autofill/types.go create mode 100644 vendor/github.com/chromedp/cdproto/backgroundservice/backgroundservice.go create mode 100644 vendor/github.com/chromedp/cdproto/backgroundservice/events.go create mode 100644 vendor/github.com/chromedp/cdproto/backgroundservice/types.go create mode 100644 vendor/github.com/chromedp/cdproto/bluetoothemulation/bluetoothemulation.go create mode 100644 vendor/github.com/chromedp/cdproto/bluetoothemulation/events.go create mode 100644 vendor/github.com/chromedp/cdproto/bluetoothemulation/types.go create mode 100644 vendor/github.com/chromedp/cdproto/browser/browser.go create mode 100644 vendor/github.com/chromedp/cdproto/browser/events.go create mode 100644 vendor/github.com/chromedp/cdproto/browser/types.go create mode 100644 vendor/github.com/chromedp/cdproto/cachestorage/cachestorage.go create mode 100644 vendor/github.com/chromedp/cdproto/cachestorage/types.go create mode 100644 vendor/github.com/chromedp/cdproto/cast/cast.go create mode 100644 vendor/github.com/chromedp/cdproto/cast/events.go create mode 100644 vendor/github.com/chromedp/cdproto/cast/types.go create mode 100644 vendor/github.com/chromedp/cdproto/cdp/types.go create mode 100644 vendor/github.com/chromedp/cdproto/cdproto.go create mode 100644 vendor/github.com/chromedp/cdproto/css/css.go create mode 100644 vendor/github.com/chromedp/cdproto/css/events.go create mode 100644 vendor/github.com/chromedp/cdproto/css/types.go create mode 100644 vendor/github.com/chromedp/cdproto/debugger/debugger.go create mode 100644 vendor/github.com/chromedp/cdproto/debugger/events.go create mode 100644 vendor/github.com/chromedp/cdproto/debugger/types.go create mode 100644 vendor/github.com/chromedp/cdproto/deviceaccess/deviceaccess.go create mode 100644 vendor/github.com/chromedp/cdproto/deviceaccess/events.go create mode 100644 vendor/github.com/chromedp/cdproto/deviceaccess/types.go create mode 100644 vendor/github.com/chromedp/cdproto/deviceorientation/deviceorientation.go create mode 100644 vendor/github.com/chromedp/cdproto/dom/dom.go create mode 100644 vendor/github.com/chromedp/cdproto/dom/events.go create mode 100644 vendor/github.com/chromedp/cdproto/dom/types.go create mode 100644 vendor/github.com/chromedp/cdproto/domdebugger/domdebugger.go create mode 100644 vendor/github.com/chromedp/cdproto/domdebugger/types.go create mode 100644 vendor/github.com/chromedp/cdproto/domsnapshot/domsnapshot.go create mode 100644 vendor/github.com/chromedp/cdproto/domsnapshot/types.go create mode 100644 vendor/github.com/chromedp/cdproto/domstorage/domstorage.go create mode 100644 vendor/github.com/chromedp/cdproto/domstorage/events.go create mode 100644 vendor/github.com/chromedp/cdproto/domstorage/types.go create mode 100644 vendor/github.com/chromedp/cdproto/emulation/emulation.go create mode 100644 vendor/github.com/chromedp/cdproto/emulation/events.go create mode 100644 vendor/github.com/chromedp/cdproto/emulation/types.go create mode 100644 vendor/github.com/chromedp/cdproto/eventbreakpoints/eventbreakpoints.go create mode 100644 vendor/github.com/chromedp/cdproto/extensions/extensions.go create mode 100644 vendor/github.com/chromedp/cdproto/extensions/types.go create mode 100644 vendor/github.com/chromedp/cdproto/fedcm/events.go create mode 100644 vendor/github.com/chromedp/cdproto/fedcm/fedcm.go create mode 100644 vendor/github.com/chromedp/cdproto/fedcm/types.go create mode 100644 vendor/github.com/chromedp/cdproto/fetch/events.go create mode 100644 vendor/github.com/chromedp/cdproto/fetch/fetch.go create mode 100644 vendor/github.com/chromedp/cdproto/fetch/types.go create mode 100644 vendor/github.com/chromedp/cdproto/filesystem/filesystem.go create mode 100644 vendor/github.com/chromedp/cdproto/filesystem/types.go create mode 100644 vendor/github.com/chromedp/cdproto/headlessexperimental/headlessexperimental.go create mode 100644 vendor/github.com/chromedp/cdproto/headlessexperimental/types.go create mode 100644 vendor/github.com/chromedp/cdproto/heapprofiler/events.go create mode 100644 vendor/github.com/chromedp/cdproto/heapprofiler/heapprofiler.go create mode 100644 vendor/github.com/chromedp/cdproto/heapprofiler/types.go create mode 100644 vendor/github.com/chromedp/cdproto/indexeddb/indexeddb.go create mode 100644 vendor/github.com/chromedp/cdproto/indexeddb/types.go create mode 100644 vendor/github.com/chromedp/cdproto/input/events.go create mode 100644 vendor/github.com/chromedp/cdproto/input/input.go create mode 100644 vendor/github.com/chromedp/cdproto/input/types.go create mode 100644 vendor/github.com/chromedp/cdproto/inspector/events.go create mode 100644 vendor/github.com/chromedp/cdproto/inspector/inspector.go create mode 100644 vendor/github.com/chromedp/cdproto/inspector/types.go create mode 100644 vendor/github.com/chromedp/cdproto/io/io.go create mode 100644 vendor/github.com/chromedp/cdproto/io/types.go create mode 100644 vendor/github.com/chromedp/cdproto/layertree/events.go create mode 100644 vendor/github.com/chromedp/cdproto/layertree/layertree.go create mode 100644 vendor/github.com/chromedp/cdproto/layertree/types.go create mode 100644 vendor/github.com/chromedp/cdproto/log/events.go create mode 100644 vendor/github.com/chromedp/cdproto/log/log.go create mode 100644 vendor/github.com/chromedp/cdproto/log/types.go create mode 100644 vendor/github.com/chromedp/cdproto/media/events.go create mode 100644 vendor/github.com/chromedp/cdproto/media/media.go create mode 100644 vendor/github.com/chromedp/cdproto/media/types.go create mode 100644 vendor/github.com/chromedp/cdproto/memory/memory.go create mode 100644 vendor/github.com/chromedp/cdproto/memory/types.go create mode 100644 vendor/github.com/chromedp/cdproto/network/events.go create mode 100644 vendor/github.com/chromedp/cdproto/network/network.go create mode 100644 vendor/github.com/chromedp/cdproto/network/types.go create mode 100644 vendor/github.com/chromedp/cdproto/overlay/events.go create mode 100644 vendor/github.com/chromedp/cdproto/overlay/overlay.go create mode 100644 vendor/github.com/chromedp/cdproto/overlay/types.go create mode 100644 vendor/github.com/chromedp/cdproto/page/events.go create mode 100644 vendor/github.com/chromedp/cdproto/page/page.go create mode 100644 vendor/github.com/chromedp/cdproto/page/types.go create mode 100644 vendor/github.com/chromedp/cdproto/performance/events.go create mode 100644 vendor/github.com/chromedp/cdproto/performance/performance.go create mode 100644 vendor/github.com/chromedp/cdproto/performance/types.go create mode 100644 vendor/github.com/chromedp/cdproto/performancetimeline/events.go create mode 100644 vendor/github.com/chromedp/cdproto/performancetimeline/performancetimeline.go create mode 100644 vendor/github.com/chromedp/cdproto/performancetimeline/types.go create mode 100644 vendor/github.com/chromedp/cdproto/preload/events.go create mode 100644 vendor/github.com/chromedp/cdproto/preload/preload.go create mode 100644 vendor/github.com/chromedp/cdproto/preload/types.go create mode 100644 vendor/github.com/chromedp/cdproto/profiler/events.go create mode 100644 vendor/github.com/chromedp/cdproto/profiler/profiler.go create mode 100644 vendor/github.com/chromedp/cdproto/profiler/types.go create mode 100644 vendor/github.com/chromedp/cdproto/pwa/pwa.go create mode 100644 vendor/github.com/chromedp/cdproto/pwa/types.go create mode 100644 vendor/github.com/chromedp/cdproto/runtime/events.go create mode 100644 vendor/github.com/chromedp/cdproto/runtime/runtime.go create mode 100644 vendor/github.com/chromedp/cdproto/runtime/types.go create mode 100644 vendor/github.com/chromedp/cdproto/security/events.go create mode 100644 vendor/github.com/chromedp/cdproto/security/security.go create mode 100644 vendor/github.com/chromedp/cdproto/security/types.go create mode 100644 vendor/github.com/chromedp/cdproto/serviceworker/events.go create mode 100644 vendor/github.com/chromedp/cdproto/serviceworker/serviceworker.go create mode 100644 vendor/github.com/chromedp/cdproto/serviceworker/types.go create mode 100644 vendor/github.com/chromedp/cdproto/storage/events.go create mode 100644 vendor/github.com/chromedp/cdproto/storage/storage.go create mode 100644 vendor/github.com/chromedp/cdproto/storage/types.go create mode 100644 vendor/github.com/chromedp/cdproto/systeminfo/systeminfo.go create mode 100644 vendor/github.com/chromedp/cdproto/systeminfo/types.go create mode 100644 vendor/github.com/chromedp/cdproto/target/events.go create mode 100644 vendor/github.com/chromedp/cdproto/target/target.go create mode 100644 vendor/github.com/chromedp/cdproto/target/types.go create mode 100644 vendor/github.com/chromedp/cdproto/tethering/events.go create mode 100644 vendor/github.com/chromedp/cdproto/tethering/tethering.go create mode 100644 vendor/github.com/chromedp/cdproto/tracing/events.go create mode 100644 vendor/github.com/chromedp/cdproto/tracing/tracing.go create mode 100644 vendor/github.com/chromedp/cdproto/tracing/types.go create mode 100644 vendor/github.com/chromedp/cdproto/webaudio/events.go create mode 100644 vendor/github.com/chromedp/cdproto/webaudio/types.go create mode 100644 vendor/github.com/chromedp/cdproto/webaudio/webaudio.go create mode 100644 vendor/github.com/chromedp/cdproto/webauthn/events.go create mode 100644 vendor/github.com/chromedp/cdproto/webauthn/types.go create mode 100644 vendor/github.com/chromedp/cdproto/webauthn/webauthn.go create mode 100644 vendor/github.com/chromedp/chromedp/.gitignore rename vendor/github.com/{onsi/ginkgo/v2/formatter/colorable_others.go => chromedp/chromedp/LICENSE} (74%) create mode 100644 vendor/github.com/chromedp/chromedp/README.md create mode 100644 vendor/github.com/chromedp/chromedp/allocate.go create mode 100644 vendor/github.com/chromedp/chromedp/allocate_linux.go create mode 100644 vendor/github.com/chromedp/chromedp/allocate_other.go create mode 100644 vendor/github.com/chromedp/chromedp/browser.go create mode 100644 vendor/github.com/chromedp/chromedp/call.go create mode 100644 vendor/github.com/chromedp/chromedp/chromedp.go create mode 100644 vendor/github.com/chromedp/chromedp/conn.go create mode 100644 vendor/github.com/chromedp/chromedp/device/device.go create mode 100644 vendor/github.com/chromedp/chromedp/emulate.go create mode 100644 vendor/github.com/chromedp/chromedp/errors.go create mode 100644 vendor/github.com/chromedp/chromedp/eval.go create mode 100644 vendor/github.com/chromedp/chromedp/input.go create mode 100644 vendor/github.com/chromedp/chromedp/js.go create mode 100644 vendor/github.com/chromedp/chromedp/js/attribute.js create mode 100644 vendor/github.com/chromedp/chromedp/js/blur.js create mode 100644 vendor/github.com/chromedp/chromedp/js/getClientRect.js create mode 100644 vendor/github.com/chromedp/chromedp/js/reset.js create mode 100644 vendor/github.com/chromedp/chromedp/js/setAttribute.js create mode 100644 vendor/github.com/chromedp/chromedp/js/submit.js create mode 100644 vendor/github.com/chromedp/chromedp/js/text.js create mode 100644 vendor/github.com/chromedp/chromedp/js/textContent.js create mode 100644 vendor/github.com/chromedp/chromedp/js/visible.js create mode 100644 vendor/github.com/chromedp/chromedp/js/waitForPredicatePageFunction.js create mode 100644 vendor/github.com/chromedp/chromedp/kb/kb.go create mode 100644 vendor/github.com/chromedp/chromedp/nav.go create mode 100644 vendor/github.com/chromedp/chromedp/poll.go create mode 100644 vendor/github.com/chromedp/chromedp/query.go create mode 100644 vendor/github.com/chromedp/chromedp/screenshot.go create mode 100644 vendor/github.com/chromedp/chromedp/target.go create mode 100644 vendor/github.com/chromedp/chromedp/util.go create mode 100644 vendor/github.com/chromedp/sysutil/.travis.yml create mode 100644 vendor/github.com/chromedp/sysutil/LICENSE create mode 100644 vendor/github.com/chromedp/sysutil/README.md create mode 100644 vendor/github.com/chromedp/sysutil/sysutil.go create mode 100644 vendor/github.com/chromedp/sysutil/sysutil_bsd.go create mode 100644 vendor/github.com/chromedp/sysutil/sysutil_linux.go create mode 100644 vendor/github.com/chromedp/sysutil/sysutil_other.go create mode 100644 vendor/github.com/chromedp/sysutil/sysutil_windows.go create mode 100644 vendor/github.com/chromedp/sysutil/timeval.go create mode 100644 vendor/github.com/chromedp/sysutil/timeval32.go create mode 100644 vendor/github.com/go-json-experiment/json/AUTHORS create mode 100644 vendor/github.com/go-json-experiment/json/CONTRIBUTORS rename vendor/github.com/{quic-go/qtls-go1-19 => go-json-experiment/json}/LICENSE (96%) create mode 100644 vendor/github.com/go-json-experiment/json/README.md create mode 100644 vendor/github.com/go-json-experiment/json/api.png create mode 100644 vendor/github.com/go-json-experiment/json/arshal.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_any.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_default.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_funcs.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_inlined.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_methods.go create mode 100644 vendor/github.com/go-json-experiment/json/arshal_time.go create mode 100644 vendor/github.com/go-json-experiment/json/doc.go create mode 100644 vendor/github.com/go-json-experiment/json/errors.go create mode 100644 vendor/github.com/go-json-experiment/json/fields.go create mode 100644 vendor/github.com/go-json-experiment/json/fold.go create mode 100644 vendor/github.com/go-json-experiment/json/intern.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/internal.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/jsonflags/flags.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/jsonwire/decode.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/jsonwire/encode.go create mode 100644 vendor/github.com/go-json-experiment/json/internal/jsonwire/wire.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/decode.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/doc.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/encode.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/errors.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/export.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/options.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/pools.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/quote.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/state.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/token.go create mode 100644 vendor/github.com/go-json-experiment/json/jsontext/value.go create mode 100644 vendor/github.com/go-json-experiment/json/migrate.sh create mode 100644 vendor/github.com/go-json-experiment/json/options.go delete mode 100644 vendor/github.com/go-task/slim-sprig/.editorconfig delete mode 100644 vendor/github.com/go-task/slim-sprig/.gitattributes delete mode 100644 vendor/github.com/go-task/slim-sprig/.gitignore delete mode 100644 vendor/github.com/go-task/slim-sprig/CHANGELOG.md delete mode 100644 vendor/github.com/go-task/slim-sprig/README.md delete mode 100644 vendor/github.com/go-task/slim-sprig/Taskfile.yml delete mode 100644 vendor/github.com/go-task/slim-sprig/crypto.go delete mode 100644 vendor/github.com/go-task/slim-sprig/date.go delete mode 100644 vendor/github.com/go-task/slim-sprig/defaults.go delete mode 100644 vendor/github.com/go-task/slim-sprig/dict.go delete mode 100644 vendor/github.com/go-task/slim-sprig/doc.go delete mode 100644 vendor/github.com/go-task/slim-sprig/functions.go delete mode 100644 vendor/github.com/go-task/slim-sprig/list.go delete mode 100644 vendor/github.com/go-task/slim-sprig/network.go delete mode 100644 vendor/github.com/go-task/slim-sprig/numeric.go delete mode 100644 vendor/github.com/go-task/slim-sprig/reflect.go delete mode 100644 vendor/github.com/go-task/slim-sprig/regex.go delete mode 100644 vendor/github.com/go-task/slim-sprig/strings.go delete mode 100644 vendor/github.com/go-task/slim-sprig/url.go create mode 100644 vendor/github.com/gobwas/ws/hijack_go119.go create mode 100644 vendor/github.com/gobwas/ws/hijack_go120.go delete mode 100644 vendor/github.com/gobwas/ws/server_test.s create mode 100644 vendor/github.com/gobwas/ws/util_purego.go create mode 100644 vendor/github.com/gobwas/ws/util_unsafe.go delete mode 100644 vendor/github.com/golang/mock/AUTHORS delete mode 100644 vendor/github.com/golang/mock/CONTRIBUTORS delete mode 100644 vendor/github.com/golang/mock/LICENSE delete mode 100644 vendor/github.com/golang/mock/mockgen/mockgen.go delete mode 100644 vendor/github.com/golang/mock/mockgen/model/model.go delete mode 100644 vendor/github.com/golang/mock/mockgen/parse.go delete mode 100644 vendor/github.com/golang/mock/mockgen/reflect.go delete mode 100644 vendor/github.com/golang/mock/mockgen/version.1.11.go delete mode 100644 vendor/github.com/golang/mock/mockgen/version.1.12.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/LICENSE delete mode 100644 vendor/github.com/onsi/ginkgo/v2/config/deprecated.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/formatter/colorable_windows.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/formatter/formatter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/build/build_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/command/abort.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/command/command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/command/program.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/boostrap_templates.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/bootstrap_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generate_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generate_templates.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generators_common.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/compile.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/profiles_and_reports.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/run.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/test_suite.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/utils.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/verify_version.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/labels/labels_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/main.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/ginkgo.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/import.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/outline.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/outline_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/run/run_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/unfocus/unfocus_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/delta.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/delta_tracker.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/dependencies.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/package_hash.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/package_hashes.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/suite.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/watch_command.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/interrupt_handler.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/sigquit_swallower_unix.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/sigquit_swallower_windows.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/client_server.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/http_client.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/http_server.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/rpc_client.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/rpc_server.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/server_handler.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/default_reporter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/deprecated_reporter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/json_report.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/junit_report.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/reporter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/reporters/teamcity_report.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/code_location.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/config.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/deprecated_types.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/deprecation_support.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/enum_support.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/errors.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/file_filter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/flags.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/label_filter.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/report_entry.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/types.go delete mode 100644 vendor/github.com/onsi/ginkgo/v2/types/version.go delete mode 100644 vendor/github.com/quic-go/qpack/tools.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/README.md delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/alert.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/auth.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/cipher_suites.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/common.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/conn.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/cpu.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/cpu_other.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/handshake_client.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/handshake_client_tls13.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/handshake_messages.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/handshake_server.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/handshake_server_tls13.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/key_agreement.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/key_schedule.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/notboring.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/prf.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/ticket.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/tls.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-19/unsafe.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/LICENSE delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/README.md delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/alert.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/auth.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/cache.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/cipher_suites.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/common.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/conn.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/handshake_client.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/handshake_client_tls13.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/handshake_messages.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/handshake_server.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/handshake_server_tls13.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/key_agreement.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/key_schedule.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/notboring.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/prf.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/quic.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/ticket.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/tls.go delete mode 100644 vendor/github.com/quic-go/qtls-go1-20/unsafe.go delete mode 100644 vendor/github.com/quic-go/quic-go/Changelog.md create mode 100644 vendor/github.com/quic-go/quic-go/connection_logging.go delete mode 100644 vendor/github.com/quic-go/quic-go/connection_timer.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/README.md create mode 100644 vendor/github.com/quic-go/quic-go/http3/conn.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/error.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/headers.go delete mode 100644 vendor/github.com/quic-go/quic-go/http3/http_stream.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/ip_addr.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/qlog.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/qlog/event.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/qlog/frame.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/qlog/qlog_dir.go delete mode 100644 vendor/github.com/quic-go/quic-go/http3/request.go delete mode 100644 vendor/github.com/quic-go/quic-go/http3/roundtrip.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/server_conn.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/state_tracking_stream.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/stream.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/trace.go create mode 100644 vendor/github.com/quic-go/quic-go/http3/transport.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/ackhandler/ackhandler.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/ackhandler/ecn.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/ackhandler/lost_packet_tracker.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/handshake/cipher_suite.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/handshake/fake_conn.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/handshake/mockgen.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/handshake/tls_config.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/handshake/tls_extension_handler.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/logutils/frame.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/monotime/time.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/qtls/go119.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/qtls/go120.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/qtls/go121.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/qtls/go_oldversion.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/byteorder.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/byteorder_big_endian.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/connstats.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/ip.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/minmax.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/utils/timer.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/wire/ack_frequency_frame.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/wire/frame.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/wire/frame_type.go create mode 100644 vendor/github.com/quic-go/quic-go/internal/wire/immediate_ack_frame.go delete mode 100644 vendor/github.com/quic-go/quic-go/internal/wire/interface.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/frame.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/interface.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/mockgen.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/multiplex.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/null_tracer.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/packet_header.go delete mode 100644 vendor/github.com/quic-go/quic-go/logging/types.go delete mode 100644 vendor/github.com/quic-go/quic-go/multiplexer.go create mode 100644 vendor/github.com/quic-go/quic-go/oss-fuzz.sh delete mode 100644 vendor/github.com/quic-go/quic-go/packet_handler_map.go create mode 100644 vendor/github.com/quic-go/quic-go/path_manager.go create mode 100644 vendor/github.com/quic-go/quic-go/path_manager_outgoing.go create mode 100644 vendor/github.com/quic-go/quic-go/qlog/event.go create mode 100644 vendor/github.com/quic-go/quic-go/qlog/frame.go create mode 100644 vendor/github.com/quic-go/quic-go/qlog/packet_header.go create mode 100644 vendor/github.com/quic-go/quic-go/qlog/qlog_dir.go create mode 100644 vendor/github.com/quic-go/quic-go/qlog/types.go create mode 100644 vendor/github.com/quic-go/quic-go/qlogwriter/jsontext/encoder.go create mode 100644 vendor/github.com/quic-go/quic-go/qlogwriter/trace.go create mode 100644 vendor/github.com/quic-go/quic-go/qlogwriter/writer.go create mode 100644 vendor/github.com/quic-go/quic-go/sni.go create mode 100644 vendor/github.com/quic-go/quic-go/stateless_reset.go create mode 100644 vendor/github.com/quic-go/quic-go/sys_conn_df_darwin.go delete mode 100644 vendor/github.com/quic-go/quic-go/sys_conn_no_gso.go delete mode 100644 vendor/github.com/quic-go/quic-go/tools.go delete mode 100644 vendor/github.com/quic-go/quic-go/window_update_queue.go create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go create mode 100644 vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go rename vendor/golang.org/x/crypto/chacha20/{chacha_ppc64le.go => chacha_ppc64x.go} (89%) rename vendor/golang.org/x/crypto/chacha20/{chacha_ppc64le.s => chacha_ppc64x.s} (66%) delete mode 100644 vendor/golang.org/x/crypto/cryptobyte/asn1.go delete mode 100644 vendor/golang.org/x/crypto/cryptobyte/asn1/asn1.go delete mode 100644 vendor/golang.org/x/crypto/cryptobyte/builder.go delete mode 100644 vendor/golang.org/x/crypto/cryptobyte/string.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/curve25519.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/curve25519_compat.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/curve25519_go120.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/README delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_amd64.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_amd64.s delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_amd64_noasm.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_arm64.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_arm64.s delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_arm64_noasm.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/fe_generic.go delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/sync.checkpoint delete mode 100644 vendor/golang.org/x/crypto/curve25519/internal/field/sync.sh delete mode 100644 vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go delete mode 100644 vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go rename vendor/golang.org/x/crypto/internal/poly1305/{sum_amd64.go => sum_asm.go} (94%) create mode 100644 vendor/golang.org/x/crypto/internal/poly1305/sum_loong64.s delete mode 100644 vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.go rename vendor/golang.org/x/crypto/internal/poly1305/{sum_ppc64le.s => sum_ppc64x.s} (85%) delete mode 100644 vendor/golang.org/x/crypto/sha3/hashes_generic.go create mode 100644 vendor/golang.org/x/crypto/sha3/hashes_noasm.go delete mode 100644 vendor/golang.org/x/crypto/sha3/register.go delete mode 100644 vendor/golang.org/x/crypto/sha3/shake_generic.go create mode 100644 vendor/golang.org/x/crypto/sha3/shake_noasm.go delete mode 100644 vendor/golang.org/x/crypto/sha3/xor.go delete mode 100644 vendor/golang.org/x/crypto/sha3/xor_generic.go delete mode 100644 vendor/golang.org/x/crypto/sha3/xor_unaligned.go delete mode 100644 vendor/golang.org/x/mod/internal/lazyregexp/lazyre.go delete mode 100644 vendor/golang.org/x/mod/modfile/print.go delete mode 100644 vendor/golang.org/x/mod/modfile/read.go delete mode 100644 vendor/golang.org/x/mod/modfile/rule.go delete mode 100644 vendor/golang.org/x/mod/modfile/work.go delete mode 100644 vendor/golang.org/x/mod/module/module.go delete mode 100644 vendor/golang.org/x/mod/module/pseudo.go create mode 100644 vendor/golang.org/x/net/html/iter.go create mode 100644 vendor/golang.org/x/net/http2/config.go create mode 100644 vendor/golang.org/x/net/http2/config_go124.go create mode 100644 vendor/golang.org/x/net/http2/config_pre_go124.go delete mode 100644 vendor/golang.org/x/net/http2/go111.go delete mode 100644 vendor/golang.org/x/net/http2/go115.go delete mode 100644 vendor/golang.org/x/net/http2/go118.go delete mode 100644 vendor/golang.org/x/net/http2/not_go111.go delete mode 100644 vendor/golang.org/x/net/http2/not_go115.go delete mode 100644 vendor/golang.org/x/net/http2/not_go118.go create mode 100644 vendor/golang.org/x/net/http2/timer.go create mode 100644 vendor/golang.org/x/net/http2/unencrypted.go create mode 100644 vendor/golang.org/x/net/internal/httpcommon/ascii.go rename vendor/golang.org/x/net/{http2 => internal/httpcommon}/headermap.go (74%) create mode 100644 vendor/golang.org/x/net/internal/httpcommon/request.go create mode 100644 vendor/golang.org/x/sync/errgroup/errgroup.go create mode 100644 vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s create mode 100644 vendor/golang.org/x/sys/cpu/asm_darwin_x86_gc.s create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_x86.go rename vendor/golang.org/x/sys/cpu/{cpu_x86.s => cpu_gc_x86.s} (88%) create mode 100644 vendor/golang.org/x/sys/cpu/cpu_linux_loong64.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_loong64.s create mode 100644 vendor/golang.org/x/sys/cpu/cpu_other_x86.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go create mode 100644 vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go create mode 100644 vendor/golang.org/x/sys/cpu/syscall_darwin_x86_gc.go delete mode 100644 vendor/golang.org/x/sys/execabs/execabs.go delete mode 100644 vendor/golang.org/x/sys/execabs/execabs_go118.go delete mode 100644 vendor/golang.org/x/sys/execabs/execabs_go119.go create mode 100644 vendor/golang.org/x/sys/unix/auxv.go create mode 100644 vendor/golang.org/x/sys/unix/auxv_unsupported.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.s delete mode 100644 vendor/golang.org/x/sys/unix/epoll_zos.go delete mode 100644 vendor/golang.org/x/sys/unix/fstatfs_zos.go create mode 100644 vendor/golang.org/x/sys/unix/sockcmsg_zos.go create mode 100644 vendor/golang.org/x/sys/unix/symaddr_zos_s390x.s create mode 100644 vendor/golang.org/x/sys/unix/vgetrandom_linux.go create mode 100644 vendor/golang.org/x/sys/unix/vgetrandom_unsupported.go create mode 100644 vendor/golang.org/x/sys/unix/zsymaddr_zos_s390x.s delete mode 100644 vendor/golang.org/x/sys/windows/empty.s delete mode 100644 vendor/golang.org/x/tools/go/ast/astutil/enclosing.go delete mode 100644 vendor/golang.org/x/tools/go/ast/astutil/imports.go delete mode 100644 vendor/golang.org/x/tools/go/ast/astutil/rewrite.go delete mode 100644 vendor/golang.org/x/tools/go/ast/astutil/util.go delete mode 100644 vendor/golang.org/x/tools/go/ast/inspector/inspector.go delete mode 100644 vendor/golang.org/x/tools/go/ast/inspector/typeof.go delete mode 100644 vendor/golang.org/x/tools/go/internal/packagesdriver/sizes.go create mode 100644 vendor/golang.org/x/tools/go/types/typeutil/callee.go create mode 100644 vendor/golang.org/x/tools/go/types/typeutil/imports.go create mode 100644 vendor/golang.org/x/tools/go/types/typeutil/map.go create mode 100644 vendor/golang.org/x/tools/go/types/typeutil/methodsetcache.go create mode 100644 vendor/golang.org/x/tools/go/types/typeutil/ui.go delete mode 100644 vendor/golang.org/x/tools/imports/forward.go create mode 100644 vendor/golang.org/x/tools/internal/aliases/aliases.go create mode 100644 vendor/golang.org/x/tools/internal/aliases/aliases_go122.go create mode 100644 vendor/golang.org/x/tools/internal/event/keys/util.go delete mode 100644 vendor/golang.org/x/tools/internal/event/tag/tag.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_darwin.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_dirent_fileno.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_dirent_ino.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_dirent_namlen_bsd.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_dirent_namlen_linux.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_portable.go delete mode 100644 vendor/golang.org/x/tools/internal/fastwalk/fastwalk_unix.go create mode 100644 vendor/golang.org/x/tools/internal/gcimporter/iimport_go122.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/newInterface10.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/newInterface11.go create mode 100644 vendor/golang.org/x/tools/internal/gcimporter/predeclared.go create mode 100644 vendor/golang.org/x/tools/internal/gcimporter/support.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/support_go117.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/support_go118.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/unified_no.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/unified_yes.go delete mode 100644 vendor/golang.org/x/tools/internal/gcimporter/ureader_no.go create mode 100644 vendor/golang.org/x/tools/internal/gocommand/invoke_notunix.go create mode 100644 vendor/golang.org/x/tools/internal/gocommand/invoke_unix.go delete mode 100644 vendor/golang.org/x/tools/internal/gopathwalk/walk.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/fix.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/imports.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/mod.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/mod_cache.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/sortimports.go delete mode 100644 vendor/golang.org/x/tools/internal/imports/zstdlib.go delete mode 100644 vendor/golang.org/x/tools/internal/pkgbits/frames_go1.go delete mode 100644 vendor/golang.org/x/tools/internal/pkgbits/frames_go17.go create mode 100644 vendor/golang.org/x/tools/internal/pkgbits/version.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/deps.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/import.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/manifest.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/stdlib.go delete mode 100644 vendor/golang.org/x/tools/internal/tokeninternal/tokeninternal.go delete mode 100644 vendor/golang.org/x/tools/internal/typeparams/enabled_go117.go delete mode 100644 vendor/golang.org/x/tools/internal/typeparams/enabled_go118.go create mode 100644 vendor/golang.org/x/tools/internal/typeparams/free.go delete mode 100644 vendor/golang.org/x/tools/internal/typeparams/typeparams_go117.go delete mode 100644 vendor/golang.org/x/tools/internal/typeparams/typeparams_go118.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/classify_call.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/element.go delete mode 100644 vendor/golang.org/x/tools/internal/typesinternal/objectpath.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/qualifier.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/recv.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/toonew.go delete mode 100644 vendor/golang.org/x/tools/internal/typesinternal/types_118.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/varkind.go create mode 100644 vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go create mode 100644 vendor/golang.org/x/tools/internal/versions/features.go create mode 100644 vendor/golang.org/x/tools/internal/versions/gover.go create mode 100644 vendor/golang.org/x/tools/internal/versions/types.go create mode 100644 vendor/golang.org/x/tools/internal/versions/versions.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0479c0bb..74951840 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.19' + go-version: '1.25' - name: Set up lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2 diff --git a/aggregate/tail-sampling.go b/aggregate/tail-sampling.go index 24ebeca4..8dbeb919 100644 --- a/aggregate/tail-sampling.go +++ b/aggregate/tail-sampling.go @@ -367,7 +367,7 @@ func (t *TailSamplingConfigs) Init() error { } if len(errs) > 0 { - return fmt.Errorf(strings.Join(errs, "; ")) + return fmt.Errorf("%s", strings.Join(errs, "; ")) } return nil diff --git a/dialtesting/README.md b/dialtesting/README.md index ac9ede7a..9266a13f 100644 --- a/dialtesting/README.md +++ b/dialtesting/README.md @@ -7,37 +7,26 @@ Copyright 2021-present Guance, Inc. # Dialtesting Browser Tasks -`BROWSER` tasks run browser synthetic checks through an external `browser-dial` -process. The `dialtesting` package keeps the same task lifecycle as other dial +`BROWSER` tasks run browser synthetic checks through the embedded browser runner. +The `dialtesting` package keeps the same task lifecycle as other dial types: ```text task JSON -> NewTask -> Run -> GetResults -> report ``` -`browser-dial` executes the browser script and returns JSON. `dialtesting` -parses that JSON and reports the final `browser_dial_testing` point through the -existing reporting path. +The embedded runner executes the browser script and returns the final +`browser_dial_testing` point through the existing reporting path. ## Runtime Dependencies Browser tasks require these binaries on the dial node: -- `browser-dial` -- Chrome/Chromium, resolved by `browser-dial` when `engine=chrome` -- Lightpanda, resolved by `browser-dial` when `engine=lightpanda` +- Chrome/Chromium when `engine=chrome` +- Lightpanda when `engine=lightpanda` -`dialtesting` resolves `browser-dial` in this order: - -1. `Task.SetOption()["browser_dial_path"]` -2. `Task.SetOption()["browserDialPath"]` -3. `BROWSER_DIAL_PATH` -4. `browser-dial` from `PATH` - -Lightpanda is not configured in task JSON. Configure it in the node environment, -for example with `LIGHTPANDA_EXECUTABLE_PATH`, or make it available from `PATH`. -Chrome can be configured by `Task.SetOption()["chrome_path"]`; `dialtesting` -passes it to `browser-dial` as `CHROME_EXECUTABLE_PATH`. +Chrome can be configured by `Task.SetOption()["chrome_path"]`. Lightpanda can be +configured by `Task.SetOption()["lightpanda_path"]`. ## Task Fields @@ -73,7 +62,6 @@ Do not put these browser script fields at the outer task level: - `auth` - `success_when` - `success_when_logic` -- `browser_dial_path` - `chrome_path` - `lightpanda_path` @@ -96,12 +84,6 @@ Success rules belong in `browser_config.steps` as browser assertions such as } ``` -The generated command is equivalent to: - -```bash -browser-dial run --dry-run --skip-token-check --json -``` - ## Host Validation `GetHostName()` returns every explicitly configured browser destination so the diff --git a/dialtesting/browser.go b/dialtesting/browser.go index 49ccecb5..c9e890cb 100644 --- a/dialtesting/browser.go +++ b/dialtesting/browser.go @@ -6,46 +6,42 @@ package dialtesting import ( - "bytes" "context" "encoding/json" "errors" "fmt" - "io" - "net/http" "os" - "os/exec" "strings" "sync" "text/template" "time" + browserchrome "github.com/GuanceCloud/cliutils/internal/browserdial/chrome" + browserlightpanda "github.com/GuanceCloud/cliutils/internal/browserdial/lightpanda" + browserrunner "github.com/GuanceCloud/cliutils/internal/browserdial/runner" + browserscript "github.com/GuanceCloud/cliutils/internal/browserdial/script" "gopkg.in/yaml.v3" ) var ( _ TaskChild = (*BrowserTask)(nil) _ ITask = (*BrowserTask)(nil) + + browserEmbeddedChromeEngineFactory = browserchrome.NewEngine + browserEmbeddedLightpandaEngineFactory = browserlightpanda.NewEngine ) const ( - defaultBrowserDialPath = "browser-dial" defaultBrowserDialTimeout = 300_000 defaultBrowserWidth = 1920 defaultBrowserHeight = 1080 maxBrowserTotalTimeoutMS = 300_000 maxBrowserStepTimeoutMS = 60_000 - optionBrowserDialPath = "browser_dial_path" - optionBrowserDialPathCamel = "browserDialPath" - optionBrowserDialMode = "browser_dial_mode" - optionBrowserDialModeCamel = "browserDialMode" - optionBrowserDialURL = "browser_dial_url" - optionBrowserDialURLCamel = "browserDialURL" - optionLightpandaPath = "lightpanda_path" - optionLightpandaPathCamel = "lightpandaPath" - optionChromePath = "chrome_path" - optionChromePathCamel = "chromePath" + optionLightpandaPath = "lightpanda_path" + optionLightpandaPathCamel = "lightpandaPath" + optionChromePath = "chrome_path" + optionChromePathCamel = "chromePath" ) type BrowserTask struct { @@ -137,27 +133,6 @@ type browserConfigStep struct { TimeoutMS int `yaml:"timeout_ms"` } -type browserDialOutput struct { - ExitCode int `json:"exit_code"` - Run browserDialRun `json:"run"` -} - -type browserDialDaemonRequest struct { - Script string `json:"script"` - Name string `json:"name,omitempty"` - Engine string `json:"engine,omitempty"` - TimeoutMS int `json:"timeout_ms,omitempty"` - ChromePath string `json:"chrome_path,omitempty"` - LightpandaPath string `json:"lightpanda_path,omitempty"` - ScreenshotOnFailure bool `json:"screenshot_on_failure,omitempty"` - ViewportWidth int `json:"viewport_width,omitempty"` - ViewportHeight int `json:"viewport_height,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Cookies []BrowserCookie `json:"cookies,omitempty"` - IgnoreHTTPSErrors bool `json:"ignore_https_errors,omitempty"` - ProxyURL string `json:"proxy_url,omitempty"` -} - type browserDialRun struct { RunID string `json:"run_id"` Name string `json:"name"` @@ -233,6 +208,27 @@ type browserRetryRecord struct { Message string `json:"message,omitempty"` } +func browserDialRunFromEmbedded(result browserrunner.Result) browserDialRun { + var run browserDialRun + data, err := json.Marshal(result) + if err != nil { + return run + } + if err := json.Unmarshal(data, &run); err != nil { + return browserDialRun{ + RunID: result.RunID, + Name: result.Name, + Target: result.Target, + Status: string(result.Status), + Success: result.Success, + DurationUS: result.DurationUS, + FailReason: result.FailReason, + FailureType: result.FailureType, + } + } + return run +} + func (t *BrowserTask) clear() { t.duration = 0 t.result = browserDialRun{} @@ -312,67 +308,10 @@ func (t *BrowserTask) runViewport(path string, viewport BrowserViewport) browser } func (t *BrowserTask) runBrowserDial(path string, viewport BrowserViewport) browserViewportResult { - if t.useBrowserDialDaemon() { - return t.runBrowserDialDaemon(path, viewport) - } - - start := time.Now() - timeoutMS := t.effectiveTimeoutMS() - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) - defer cancel() - cancelState := t.setCancel(cancel) - defer t.clearCancel(cancelState) - - result := browserViewportResult{viewport: viewport, startedAt: start.UTC().Format(time.RFC3339Nano)} - args := []string{ - "run", path, - "--dry-run", - "--skip-token-check", - "--json", - "--timeout", fmt.Sprintf("%d", timeoutMS), - } - args = append(args, t.browserDialOptions(viewport)...) - - var stdout bytes.Buffer - var stderr bytes.Buffer - cmdArgs := executableArgs(args) - cmd := exec.CommandContext(ctx, t.executablePath(), cmdArgs...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - env := os.Environ() - if lightpandaPath := t.lightpandaPath(); lightpandaPath != "" { - env = setEnv(env, "LIGHTPANDA_EXECUTABLE_PATH", lightpandaPath) - } - if chromePath := t.chromePath(); chromePath != "" { - env = setEnv(env, "CHROME_EXECUTABLE_PATH", chromePath) - } - cmd.Env = env - - err := cmd.Run() - result.duration = time.Since(start) - result.endedAt = time.Now().UTC().Format(time.RFC3339Nano) - result.stderr = strings.TrimSpace(stderr.String()) - if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { - result.exitCode = exitErr.ExitCode() - } else if err != nil { - result.reqError = err.Error() - return result - } - - var output browserDialOutput - if decodeErr := json.Unmarshal(stdout.Bytes(), &output); decodeErr != nil { - result.reqError = fmt.Sprintf("parse browser-dial output failed: %s", decodeErr) - if result.stderr != "" { - result.reqError += ": " + result.stderr - } - return result - } - result.exitCode = output.ExitCode - result.result = output.Run - return result + return t.runBrowserDialEmbedded(path, viewport) } -func (t *BrowserTask) runBrowserDialDaemon(path string, viewport BrowserViewport) browserViewportResult { +func (t *BrowserTask) runBrowserDialEmbedded(path string, viewport BrowserViewport) browserViewportResult { start := time.Now() timeoutMS := t.effectiveTimeoutMS() ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMS)*time.Millisecond+15*time.Second) @@ -381,73 +320,39 @@ func (t *BrowserTask) runBrowserDialDaemon(path string, viewport BrowserViewport defer t.clearCancel(cancelState) result := browserViewportResult{viewport: viewport, startedAt: start.UTC().Format(time.RFC3339Nano)} - if t.browserDialURL() == "" { - result.reqError = "browser_dial_url should not be empty when browser_dial_mode is daemon" - return result - } - script, err := os.ReadFile(path) + engineName, engineFactory, err := t.embeddedEngineFactory() if err != nil { result.reqError = err.Error() + result.duration = time.Since(start) + result.endedAt = time.Now().UTC().Format(time.RFC3339Nano) return result } - payload := browserDialDaemonRequest{ - Script: string(script), - Name: t.Name, - Engine: t.effectiveEngine(), - TimeoutMS: timeoutMS, - ChromePath: t.chromePath(), - LightpandaPath: t.lightpandaPath(), - ViewportWidth: viewport.Width, - ViewportHeight: viewport.Height, - } - if t.AdvanceOptions != nil { - payload.ScreenshotOnFailure = t.AdvanceOptions.ScreenshotOnFailure - payload.Headers = t.AdvanceOptions.Headers - payload.Cookies = t.AdvanceOptions.Cookies - payload.IgnoreHTTPSErrors = t.AdvanceOptions.IgnoreHTTPSErrors - payload.ProxyURL = t.AdvanceOptions.ProxyURL - } - - body, err := json.Marshal(payload) - if err != nil { - result.reqError = err.Error() - return result - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(t.browserDialURL(), "/")+"/v1/run", bytes.NewReader(body)) - if err != nil { - result.reqError = err.Error() - return result - } - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) + runResult := browserrunner.Run(ctx, browserrunner.Options{ + ScriptPath: path, + Name: t.Name, + TimeoutMS: timeoutMS, + Tags: t.Tags, + EngineName: engineName, + LightpandaPath: t.lightpandaPath(), + ChromePath: t.chromePath(), + StartupTimeout: 5 * time.Second, + ScreenshotOnFailure: t.AdvanceOptions != nil && t.AdvanceOptions.ScreenshotOnFailure, + ViewportWidth: viewport.Width, + ViewportHeight: viewport.Height, + Headers: t.embeddedHeaders(), + Cookies: t.embeddedCookies(), + IgnoreHTTPSErrors: t.AdvanceOptions != nil && t.AdvanceOptions.IgnoreHTTPSErrors, + ProxyURL: t.embeddedProxyURL(), + EngineFactory: engineFactory, + }) result.duration = time.Since(start) result.endedAt = time.Now().UTC().Format(time.RFC3339Nano) - if err != nil { - result.reqError = err.Error() - return result - } - defer resp.Body.Close() //nolint:errcheck - - respBody, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) - if err != nil { - result.reqError = err.Error() - return result - } - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + result.exitCode = 0 + if !runResult.Success { result.exitCode = 1 - result.reqError = fmt.Sprintf("browser-dial daemon returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) - return result - } - - var output browserDialOutput - if decodeErr := json.Unmarshal(respBody, &output); decodeErr != nil { - result.reqError = fmt.Sprintf("parse browser-dial daemon output failed: %s", decodeErr) - return result } - result.exitCode = output.ExitCode - result.result = output.Run + result.result = browserDialRunFromEmbedded(runResult) return result } @@ -505,35 +410,6 @@ func browserRetryRecordFromResult(result browserViewportResult, attempt int) bro return record } -func (t *BrowserTask) browserDialOptions(viewport BrowserViewport) []string { - args := []string{ - "--viewport-width", fmt.Sprintf("%d", viewport.Width), - "--viewport-height", fmt.Sprintf("%d", viewport.Height), - } - if t.AdvanceOptions == nil { - return args - } - if t.AdvanceOptions.Engine != "" { - args = append(args, "--engine", t.AdvanceOptions.Engine) - } - if t.AdvanceOptions.ScreenshotOnFailure { - args = append(args, "--screenshot-on-failure") - } - for key, value := range t.AdvanceOptions.Headers { - args = append(args, "--header", key+"="+value) - } - for _, cookie := range t.AdvanceOptions.Cookies { - args = append(args, "--cookie", cookie.Name+"="+cookie.Value) - } - if t.AdvanceOptions.IgnoreHTTPSErrors { - args = append(args, "--ignore-https-errors") - } - if t.AdvanceOptions.ProxyURL != "" { - args = append(args, "--proxy-url", t.AdvanceOptions.ProxyURL) - } - return args -} - func (t *BrowserTask) setCancel(cancel context.CancelFunc) *browserTaskCancel { cancelState := &browserTaskCancel{cancel: cancel} t.cancelMu.Lock() @@ -550,24 +426,6 @@ func (t *BrowserTask) clearCancel(cancelState *browserTaskCancel) { t.cancelMu.Unlock() } -func executableArgs(args []string) []string { - if os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") == "" { - return args - } - return append([]string{"-test.run=TestBrowserDialHelperProcess", "--"}, args...) -} - -func setEnv(env []string, key string, value string) []string { - prefix := key + "=" - for index, item := range env { - if strings.HasPrefix(item, prefix) { - env[index] = prefix + value - return env - } - } - return append(env, prefix+value) -} - func (t *BrowserTask) writeScriptFile() (string, error) { file, err := os.CreateTemp("", "dialtesting-browser-*.yaml") if err != nil { @@ -587,41 +445,43 @@ func (t *BrowserTask) writeScriptFile() (string, error) { return file.Name(), nil } -func (t *BrowserTask) executablePath() string { - if value := t.GetOption()[optionBrowserDialPath]; value != "" { - return value - } - if value := t.GetOption()[optionBrowserDialPathCamel]; value != "" { - return value - } - if value := os.Getenv("BROWSER_DIAL_PATH"); value != "" { - return value +func (t *BrowserTask) embeddedEngineFactory() (string, browserrunner.EngineFactory, error) { + switch strings.TrimSpace(strings.ToLower(t.effectiveEngine())) { + case "", "chrome", "chromium": + return "chrome", browserEmbeddedChromeEngineFactory, nil + case "lightpanda": + return "lightpanda", browserEmbeddedLightpandaEngineFactory, nil + default: + return "", nil, fmt.Errorf("browser engine must be lightpanda or chrome") } - return defaultBrowserDialPath } -func (t *BrowserTask) browserDialMode() string { - if value := t.GetOption()[optionBrowserDialMode]; value != "" { - return strings.ToLower(strings.TrimSpace(value)) - } - if value := t.GetOption()[optionBrowserDialModeCamel]; value != "" { - return strings.ToLower(strings.TrimSpace(value)) +func (t *BrowserTask) embeddedHeaders() map[string]string { + if t.AdvanceOptions == nil { + return nil } - return "exec" + return t.AdvanceOptions.Headers } -func (t *BrowserTask) browserDialURL() string { - if value := t.GetOption()[optionBrowserDialURL]; value != "" { - return strings.TrimSpace(value) +func (t *BrowserTask) embeddedCookies() []browserscript.Cookie { + if t.AdvanceOptions == nil || len(t.AdvanceOptions.Cookies) == 0 { + return nil } - if value := t.GetOption()[optionBrowserDialURLCamel]; value != "" { - return strings.TrimSpace(value) + cookies := make([]browserscript.Cookie, 0, len(t.AdvanceOptions.Cookies)) + for _, cookie := range t.AdvanceOptions.Cookies { + cookies = append(cookies, browserscript.Cookie{ + Name: cookie.Name, + Value: cookie.Value, + }) } - return "" + return cookies } -func (t *BrowserTask) useBrowserDialDaemon() bool { - return t.browserDialMode() == "daemon" || t.browserDialURL() != "" +func (t *BrowserTask) embeddedProxyURL() string { + if t.AdvanceOptions == nil { + return "" + } + return t.AdvanceOptions.ProxyURL } func (t *BrowserTask) lightpandaPath() string { @@ -875,15 +735,6 @@ func (t *BrowserTask) check() error { if err := t.checkBrowserRetryOptions(); err != nil { return err } - switch t.browserDialMode() { - case "exec": - case "daemon": - if t.browserDialURL() == "" { - return errors.New("browser_dial_url should not be empty when browser_dial_mode is daemon") - } - default: - return errors.New("browser_dial_mode should be exec or daemon") - } return nil } diff --git a/dialtesting/browser_test.go b/dialtesting/browser_test.go index 693e8e38..c2770809 100644 --- a/dialtesting/browser_test.go +++ b/dialtesting/browser_test.go @@ -6,15 +6,16 @@ package dialtesting import ( + "context" "encoding/json" - "net/http" - "net/http/httptest" + "errors" "os" - "strconv" "strings" "testing" "time" + browserevidence "github.com/GuanceCloud/cliutils/internal/browserdial/evidence" + browserrunner "github.com/GuanceCloud/cliutils/internal/browserdial/runner" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,13 +26,12 @@ func TestCreateBrowserTaskChild(t *testing.T) { require.IsType(t, &BrowserTask{}, ct) } -func TestBrowserTaskRunExternalProcess(t *testing.T) { +func TestBrowserTaskRunEmbeddedByDefault(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) + stubBrowserEngine(t, nil, nil) - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") err = task.Run() require.NoError(t, err) @@ -40,113 +40,17 @@ func TestBrowserTaskRunExternalProcess(t *testing.T) { assert.Equal(t, ClassHeadless, task.Class()) assert.Equal(t, "browser_dial_testing", task.MetricName()) assert.Equal(t, int64(1), fields["success"]) - assert.Equal(t, int64(12345), fields["response_time"]) + assert.Greater(t, fields["response_time"], int64(0)) assert.Equal(t, int64(2), fields["last_step"]) - assert.Equal(t, "trace-1", fields["trace_id"]) - assert.Contains(t, fields["steps"], "open") - assert.Contains(t, fields["steps"], "assert_text") - assert.Contains(t, fields["steps"], "main") + assert.Contains(t, fields["steps"], "goto") + assert.Contains(t, fields["steps"], "assert_title") assert.Contains(t, fields["steps"], "Example Domain") - assert.Contains(t, fields["steps"], "/tmp/browser-dial/run-1-step-1.png") assert.Equal(t, "https://example.com", tags["url"]) assert.Equal(t, "platform", tags["owner"]) assert.Equal(t, "1920x1080", tags["viewport"]) assert.Equal(t, int64(1920), fields["viewport_width"]) assert.Equal(t, int64(1080), fields["viewport_height"]) assert.Equal(t, int64(0), fields["retry_count"]) - assert.Equal(t, int64(12), fields["ttfb"]) - assert.Equal(t, int64(123), fields["loading_time"]) - assert.Equal(t, int64(98), fields["lcp"]) - assert.Equal(t, 0.03, fields["cls"]) -} - -func TestBrowserTaskRunDaemon(t *testing.T) { - browserTask := newBrowserTaskForTest() - browserTask.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 1366, Height: 768}}} - browserTask.AdvanceOptions = &BrowserAdvanceOption{ - Engine: "chrome", - ScreenshotOnFailure: true, - Headers: map[string]string{"X-Test": "ok"}, - Cookies: []BrowserCookie{{Name: "sid", Value: "abc"}}, - IgnoreHTTPSErrors: true, - ProxyURL: "http://127.0.0.1:7897", - } - - var got browserDialDaemonRequest - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodPost, r.Method) - assert.Equal(t, "/v1/run", r.URL.Path) - require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) - assert.Contains(t, got.Script, "name: homepage") - assert.Equal(t, "homepage", got.Name) - assert.Equal(t, "chrome", got.Engine) - assert.Equal(t, 1000, got.TimeoutMS) - assert.Equal(t, "/opt/datakit-browser/chrome/chrome", got.ChromePath) - assert.Equal(t, 1366, got.ViewportWidth) - assert.Equal(t, 768, got.ViewportHeight) - assert.True(t, got.ScreenshotOnFailure) - assert.Equal(t, "ok", got.Headers["X-Test"]) - assert.Equal(t, []BrowserCookie{{Name: "sid", Value: "abc"}}, got.Cookies) - assert.True(t, got.IgnoreHTTPSErrors) - assert.Equal(t, "http://127.0.0.1:7897", got.ProxyURL) - - _ = json.NewEncoder(w).Encode(browserDialOutput{ - ExitCode: 0, - Run: browserDialRun{ - RunID: "run-daemon", - Name: "homepage", - Target: "https://example.com", - Status: "OK", - Success: true, - DurationUS: 23456, - Steps: []browserDialStep{{ - Seq: 1, - Name: "open", - Action: "goto", - Status: "OK", - DurationUS: 1000, - }}, - }, - }) - })) - defer server.Close() - - task, err := NewTask("", browserTask) - require.NoError(t, err) - task.SetOption(map[string]string{ - optionBrowserDialMode: "daemon", - optionBrowserDialURL: server.URL, - optionChromePath: "/opt/datakit-browser/chrome/chrome", - }) - - require.NoError(t, task.Run()) - - tags, fields := task.GetResults() - assert.Equal(t, "OK", tags["status"]) - assert.Equal(t, "1366x768", tags["viewport"]) - assert.Equal(t, int64(1), fields["success"]) - assert.Equal(t, int64(23456), fields["response_time"]) - assert.Equal(t, "run-daemon", fields["browser_run_id"]) -} - -func TestBrowserTaskRunDaemonFailure(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "busy", http.StatusServiceUnavailable) - })) - defer server.Close() - - browserTask := newBrowserTaskForTest() - task, err := NewTask("", browserTask) - require.NoError(t, err) - task.SetOption(map[string]string{ - optionBrowserDialMode: "daemon", - optionBrowserDialURL: server.URL, - }) - - require.NoError(t, task.Run()) - _, fields := task.GetResults() - assert.Equal(t, int64(-1), fields["success"]) - assert.Contains(t, fields["message"], "browser-dial daemon returned HTTP 503") } func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { @@ -154,17 +58,18 @@ func TestBrowserTaskRunSetsLightpandaPath(t *testing.T) { task, err := NewTask("", browserTask) require.NoError(t, err) task.SetOption(map[string]string{ - optionBrowserDialPath: os.Args[0], - optionLightpandaPath: "/opt/datakit/lightpanda", + optionLightpandaPath: "/opt/datakit/lightpanda", }) - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "check-lightpanda") - err = task.Run() - require.NoError(t, err) + var gotOptions browserrunner.EngineOptions + browserTask.AdvanceOptions = &BrowserAdvanceOption{Engine: "lightpanda"} + stubBrowserEngine(t, &gotOptions, nil) + require.NoError(t, task.Run()) tags, fields := task.GetResults() assert.Equal(t, "OK", tags["status"]) assert.Equal(t, int64(1), fields["success"]) + assert.Equal(t, "/opt/datakit/lightpanda", gotOptions.LightpandaPath) } func TestBrowserTaskRunSetsChromePath(t *testing.T) { @@ -172,26 +77,25 @@ func TestBrowserTaskRunSetsChromePath(t *testing.T) { task, err := NewTask("", browserTask) require.NoError(t, err) task.SetOption(map[string]string{ - optionBrowserDialPath: os.Args[0], - optionChromePath: "/opt/datakit-browser/chrome/chrome", + optionChromePath: "/opt/datakit-browser/chrome/chrome", }) - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "check-chrome") - err = task.Run() - require.NoError(t, err) + var gotOptions browserrunner.EngineOptions + stubBrowserEngine(t, &gotOptions, nil) + require.NoError(t, task.Run()) tags, fields := task.GetResults() assert.Equal(t, "OK", tags["status"]) assert.Equal(t, int64(1), fields["success"]) + assert.Equal(t, "/opt/datakit-browser/chrome/chrome", gotOptions.ChromePath) } -func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { +func TestBrowserTaskRunEmbeddedFailure(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "failure") + stubBrowserEngine(t, nil, nil, "Wrong Title") err = task.Run() require.NoError(t, err) @@ -200,44 +104,28 @@ func TestBrowserTaskRunExternalProcessFailure(t *testing.T) { assert.Equal(t, int64(-1), fields["success"]) assert.Contains(t, fields["fail_reason"], "step_error") assert.Equal(t, "assertion_failed", fields["failure_type"]) - assert.Contains(t, fields["message"], "title mismatch") + assert.Contains(t, fields["message"], "title assertion failed") assert.Equal(t, int64(2), fields["last_step"]) assert.Contains(t, fields["steps"], "title") } -func TestBrowserTaskStopCancelsExternalProcess(t *testing.T) { +func TestBrowserTaskRunEmbeddedEngineError(t *testing.T) { browserTask := newBrowserTaskForTest() task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - - startedPath := t.TempDir() + "/started" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "sleep") - t.Setenv("BROWSER_DIAL_HELPER_STARTED", startedPath) - - done := make(chan error, 1) - go func() { - done <- task.Run() - }() - - require.Eventually(t, func() bool { - _, err := os.Stat(startedPath) - return err == nil - }, 2*time.Second, 10*time.Millisecond) - - task.Stop() - task.Stop() - - select { - case err := <-done: - require.NoError(t, err) - case <-time.After(3 * time.Second): - t.Fatal("browser task did not stop external process") + + oldChromeFactory := browserEmbeddedChromeEngineFactory + browserEmbeddedChromeEngineFactory = func(context.Context, browserrunner.EngineOptions) (browserrunner.Engine, error) { + return nil, errors.New("start chrome failed") } + t.Cleanup(func() { + browserEmbeddedChromeEngineFactory = oldChromeFactory + }) + require.NoError(t, task.Run()) _, fields := task.GetResults() assert.Equal(t, int64(-1), fields["success"]) - assert.NotEmpty(t, fields["message"]) + assert.Contains(t, fields["message"], "start chrome failed") } func TestBrowserTaskRenderTemplate(t *testing.T) { @@ -482,17 +370,13 @@ func TestBrowserTaskRunSingleViewport(t *testing.T) { browserTask.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 1366, Height: 768}}} task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - argsPath := t.TempDir() + "/args.log" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + var gotOptions browserrunner.EngineOptions + stubBrowserEngine(t, &gotOptions, nil) require.NoError(t, task.Run()) - lines := readHelperArgs(t, argsPath) - require.Len(t, lines, 1) - assert.Contains(t, lines[0], "--viewport-width 1366 --viewport-height 768") - assert.Contains(t, lines[0], "--timeout 1000") + assert.Equal(t, 1366, gotOptions.ViewportWidth) + assert.Equal(t, 768, gotOptions.ViewportHeight) tags, fields := task.GetResults() assert.Equal(t, "1366x768", tags["viewport"]) @@ -523,15 +407,15 @@ func TestBrowserTaskRunRejectsInvalidRetryOptionsAtRuntime(t *testing.T) { browserTask.RetryOptions = tc.options task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - argsPath := t.TempDir() + "/args.log" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + var calls int + stubBrowserEngineWithFactory(t, func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + calls++ + return &fakeBrowserEngine{}, nil + }) require.NoError(t, task.Run()) - _, err = os.Stat(argsPath) - assert.True(t, os.IsNotExist(err)) + assert.Equal(t, 0, calls) tags, fields := task.GetResults() assert.Equal(t, "FAIL", tags["status"]) @@ -573,15 +457,15 @@ func TestBrowserTaskConfigErrorsReportConsistentFields(t *testing.T) { browserTask.BrowserConfig = tc.config task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - argsPath := t.TempDir() + "/args.log" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + var calls int + stubBrowserEngineWithFactory(t, func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + calls++ + return &fakeBrowserEngine{}, nil + }) require.NoError(t, task.Run()) - _, err = os.Stat(argsPath) - assert.True(t, os.IsNotExist(err)) + assert.Equal(t, 0, calls) tags, fields := task.GetResults() assert.Equal(t, "FAIL", tags["status"]) @@ -608,20 +492,18 @@ func TestBrowserTaskRunAdvanceOptions(t *testing.T) { } task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - argsPath := t.TempDir() + "/args.log" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + var gotOptions browserrunner.EngineOptions + var gotConfig browserrunner.BrowserConfig + stubBrowserEngine(t, &gotOptions, &gotConfig) require.NoError(t, task.Run()) - line := readHelperArgs(t, argsPath)[0] - assert.Contains(t, line, "--engine chrome") - assert.Contains(t, line, "--screenshot-on-failure") - assert.Contains(t, line, "--header X-Test=ok") - assert.Contains(t, line, "--cookie sid=abc") - assert.Contains(t, line, "--ignore-https-errors") - assert.Contains(t, line, "--proxy-url http://127.0.0.1:7897") + assert.Equal(t, "http://127.0.0.1:7897", gotOptions.ProxyURL) + assert.True(t, gotOptions.IgnoreHTTPSErrors) + assert.Equal(t, "ok", gotConfig.Headers["X-Test"]) + require.Len(t, gotConfig.Cookies, 1) + assert.Equal(t, "sid", gotConfig.Cookies[0].Name) + assert.Equal(t, "abc", gotConfig.Cookies[0].Value) } func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { @@ -633,12 +515,15 @@ func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { browserTask.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 2, IntervalSec: 5} task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - dir := t.TempDir() - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "fail-once") - t.Setenv("BROWSER_DIAL_HELPER_COUNT", dir+"/count") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", dir+"/args.log") + var calls int + stubBrowserEngineWithFactory(t, func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + calls++ + if calls == 1 { + return &fakeBrowserEngine{title: "Wrong Title"}, nil + } + return &fakeBrowserEngine{}, nil + }) require.NoError(t, task.Run()) tags, fields := task.GetResults() @@ -658,7 +543,7 @@ func TestBrowserTaskRetryStopsAfterSuccess(t *testing.T) { assert.Equal(t, 2, records[1].Attempt) assert.Equal(t, "OK", records[1].Status) assert.True(t, records[1].Success) - assert.Len(t, readHelperArgs(t, dir+"/args.log"), 2) + assert.Equal(t, 2, calls) } func TestBrowserTaskParseFailureDoesNotRetry(t *testing.T) { @@ -667,15 +552,15 @@ func TestBrowserTaskParseFailureDoesNotRetry(t *testing.T) { browserTask.RetryOptions = &BrowserRetryOption{Enabled: true, Count: 2, IntervalSec: 5} task, err := NewTask("", browserTask) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - argsPath := t.TempDir() + "/args.log" - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") - t.Setenv("BROWSER_DIAL_HELPER_ARGS", argsPath) + var calls int + stubBrowserEngineWithFactory(t, func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + calls++ + return &fakeBrowserEngine{}, nil + }) require.NoError(t, task.Run()) - _, err = os.Stat(argsPath) - assert.True(t, os.IsNotExist(err)) + assert.Equal(t, 0, calls) _, fields := task.GetResults() assert.Contains(t, fields["message"], "parse browser_config failed") } @@ -710,9 +595,8 @@ func TestBrowserTaskIgnoresOuterSuccessWhen(t *testing.T) { require.NoError(t, err) task, err := NewTask(taskJSON, child) require.NoError(t, err) - task.SetOption(map[string]string{optionBrowserDialPath: os.Args[0]}) - t.Setenv("GO_WANT_BROWSER_DIAL_HELPER", "success") + stubBrowserEngine(t, nil, nil) require.NoError(t, task.Check()) require.NoError(t, task.Run()) tags, fields := task.GetResults() @@ -767,54 +651,6 @@ func TestBrowserTaskChromePathOption(t *testing.T) { assert.Empty(t, task.chromePath()) } -func TestSetEnvOverridesExistingValue(t *testing.T) { - env := setEnv([]string{"A=1", "CHROME_EXECUTABLE_PATH=/old/chrome"}, "CHROME_EXECUTABLE_PATH", "/new/chrome") - assert.Equal(t, []string{"A=1", "CHROME_EXECUTABLE_PATH=/new/chrome"}, env) - - env = setEnv([]string{"A=1"}, "CHROME_EXECUTABLE_PATH", "/new/chrome") - assert.Equal(t, []string{"A=1", "CHROME_EXECUTABLE_PATH=/new/chrome"}, env) -} - -func TestBrowserTaskExecutablePathOption(t *testing.T) { - task := &BrowserTask{Task: &Task{}} - task.SetOption(map[string]string{optionBrowserDialPath: "/opt/browser-dial"}) - assert.Equal(t, "/opt/browser-dial", task.executablePath()) - - task.SetOption(map[string]string{optionBrowserDialPathCamel: "/opt/browser-dial-camel"}) - assert.Equal(t, "/opt/browser-dial-camel", task.executablePath()) - - task.SetOption(map[string]string{}) - t.Setenv("BROWSER_DIAL_PATH", "/env/browser-dial") - assert.Equal(t, "/env/browser-dial", task.executablePath()) - - t.Setenv("BROWSER_DIAL_PATH", "") - assert.Equal(t, defaultBrowserDialPath, task.executablePath()) -} - -func TestBrowserTaskDaemonOptions(t *testing.T) { - task := &BrowserTask{Task: &Task{}} - task.SetOption(map[string]string{ - optionBrowserDialMode: "daemon", - optionBrowserDialURL: "http://127.0.0.1:18080", - }) - assert.Equal(t, "daemon", task.browserDialMode()) - assert.Equal(t, "http://127.0.0.1:18080", task.browserDialURL()) - assert.True(t, task.useBrowserDialDaemon()) - - task.SetOption(map[string]string{ - optionBrowserDialModeCamel: "daemon", - optionBrowserDialURLCamel: "http://127.0.0.1:18081", - }) - assert.Equal(t, "daemon", task.browserDialMode()) - assert.Equal(t, "http://127.0.0.1:18081", task.browserDialURL()) - assert.True(t, task.useBrowserDialDaemon()) - - task.SetOption(map[string]string{}) - assert.Equal(t, "exec", task.browserDialMode()) - assert.Empty(t, task.browserDialURL()) - assert.False(t, task.useBrowserDialDaemon()) -} - func TestBrowserTaskResultFallbacks(t *testing.T) { task := newBrowserTaskForTest() task.stderr = "stderr failure" @@ -960,103 +796,132 @@ func TestBrowserTaskSmallHelpers(t *testing.T) { assert.Equal(t, []string{}, dedupBrowserHostNames([]string{"", ""})) } -func TestBrowserDialHelperProcess(t *testing.T) { - mode := os.Getenv("GO_WANT_BROWSER_DIAL_HELPER") - if mode == "" { - return - } - if len(os.Args) == 0 || !strings.Contains(strings.Join(os.Args, " "), "run") { - os.Exit(2) - } - if argsPath := os.Getenv("BROWSER_DIAL_HELPER_ARGS"); argsPath != "" { - file, err := os.OpenFile(argsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) - if err != nil { - os.Exit(4) - } - _, _ = file.WriteString(strings.Join(os.Args, " ") + "\n") - _ = file.Close() - } - if mode == "check-lightpanda" && os.Getenv("LIGHTPANDA_EXECUTABLE_PATH") != "/opt/datakit/lightpanda" { - os.Exit(3) - } - if mode == "check-chrome" && os.Getenv("CHROME_EXECUTABLE_PATH") != "/opt/datakit-browser/chrome/chrome" { - os.Exit(3) +func TestBrowserTaskRunEmbedded(t *testing.T) { + browserTask := newBrowserTaskForTest() + browserTask.BrowserWindow = &BrowserWindowOption{Viewports: []BrowserViewport{{Width: 1366, Height: 768}}} + browserTask.AdvanceOptions = &BrowserAdvanceOption{ + Engine: "chrome", + ScreenshotOnFailure: true, + Headers: map[string]string{"X-Test": "ok"}, + Cookies: []BrowserCookie{{Name: "sid", Value: "abc"}}, + IgnoreHTTPSErrors: true, + ProxyURL: "http://127.0.0.1:7897", } - if mode == "sleep" { - if startedPath := os.Getenv("BROWSER_DIAL_HELPER_STARTED"); startedPath != "" { - _ = os.WriteFile(startedPath, []byte("started"), 0o600) - } - time.Sleep(10 * time.Second) - os.Exit(0) + + var gotOptions browserrunner.EngineOptions + var gotConfig browserrunner.BrowserConfig + oldChromeFactory := browserEmbeddedChromeEngineFactory + browserEmbeddedChromeEngineFactory = func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + gotOptions = options + return &fakeBrowserEngine{config: &gotConfig}, nil } + t.Cleanup(func() { + browserEmbeddedChromeEngineFactory = oldChromeFactory + }) - output := map[string]interface{}{ - "exit_code": 0, - "run": map[string]interface{}{ - "run_id": "run-1", - "name": "homepage", - "target": "https://example.com", - "status": "OK", - "success": true, - "duration_us": 12345, - "steps": []map[string]interface{}{ - { - "seq": 1, "name": "open", "action": "goto", "status": "OK", "duration_us": 1000, - "url": "https://example.com", "title": "Example", "screenshot": "/tmp/browser-dial/run-1-step-1.png", - "performance": map[string]interface{}{ - "ttfb_ms": 12, - "loading_time_ms": 123, - "lcp_ms": 98, - "cls": 0.03, - "dom_content_loaded_ms": 88, - "load_event_end_ms": 123, - }, - }, - { - "seq": 2, "name": "assert body", "action": "assert_text", "selector": "main", "expected": "Example Domain", - "status": "OK", "duration_us": 2000, "url": "https://example.com", "title": "Example", - }, - }, - "performance": map[string]interface{}{ - "ttfb_ms": 12, - "loading_time_ms": 123, - "lcp_ms": 98, - "cls": 0.03, - "dom_content_loaded_ms": 88, - "load_event_end_ms": 123, - }, - "trace_ids": []string{"trace-1"}, - }, + task, err := NewTask("", browserTask) + require.NoError(t, err) + task.SetOption(map[string]string{ + optionChromePath: "/opt/datakit-browser/chrome/chrome", + }) + require.NoError(t, task.Run()) + + tags, fields := task.GetResults() + assert.Equal(t, "OK", tags["status"]) + assert.Equal(t, "browser-dial", tags["runner"]) + assert.Equal(t, "chrome", tags["browser_engine"]) + assert.Equal(t, "1366x768", tags["viewport"]) + assert.Equal(t, int64(1), fields["success"]) + assert.Equal(t, "success", fields["message"]) + assert.Equal(t, int64(1366), fields["viewport_width"]) + assert.Equal(t, int64(768), fields["viewport_height"]) + assert.Equal(t, "/opt/datakit-browser/chrome/chrome", gotOptions.ChromePath) + assert.Equal(t, 1366, gotOptions.ViewportWidth) + assert.Equal(t, 768, gotOptions.ViewportHeight) + assert.Equal(t, "http://127.0.0.1:7897", gotOptions.ProxyURL) + assert.True(t, gotOptions.IgnoreHTTPSErrors) + assert.Equal(t, "ok", gotConfig.Headers["X-Test"]) + require.Len(t, gotConfig.Cookies, 1) + assert.Equal(t, "sid", gotConfig.Cookies[0].Name) + assert.Equal(t, "abc", gotConfig.Cookies[0].Value) +} + +type fakeBrowserEngine struct { + config *browserrunner.BrowserConfig + title string +} + +func (e *fakeBrowserEngine) Close(context.Context) error { return nil } + +func (e *fakeBrowserEngine) Navigate(context.Context, string) error { return nil } + +func (e *fakeBrowserEngine) WaitForSelector(context.Context, string) error { return nil } + +func (e *fakeBrowserEngine) Click(context.Context, string) error { return nil } + +func (e *fakeBrowserEngine) Fill(context.Context, string, string) error { return nil } + +func (e *fakeBrowserEngine) Title(context.Context) (string, error) { + if e.title != "" { + return e.title, nil } - exitCode := 0 - if mode == "fail-once" && incrementHelperCount(os.Getenv("BROWSER_DIAL_HELPER_COUNT")) == 1 { - mode = "failure" + return "Example Domain", nil +} + +func (e *fakeBrowserEngine) URL(context.Context) (string, error) { return "https://example.com", nil } + +func (e *fakeBrowserEngine) Text(context.Context, string) (string, error) { + return "Example Domain", nil +} + +func (e *fakeBrowserEngine) Eval(context.Context, string) (string, error) { return "", nil } + +func (e *fakeBrowserEngine) CaptureDOM(context.Context) (browserevidence.DomSnapshot, error) { + return browserevidence.DomSnapshot{}, nil +} + +func (e *fakeBrowserEngine) ConsoleEvents() []browserevidence.ConsoleEvent { return nil } + +func (e *fakeBrowserEngine) NetworkEvents() []browserevidence.NetworkEvent { return nil } + +func (e *fakeBrowserEngine) ConfigureBrowser(_ context.Context, config browserrunner.BrowserConfig) error { + if e.config != nil { + *e.config = config } - if mode == "failure" { - output["exit_code"] = 1 - output["run"] = map[string]interface{}{ - "run_id": "run-2", - "name": "homepage", - "target": "https://example.com", - "status": "FAIL", - "success": false, - "duration_us": 54321, - "fail_reason": "step_error", - "failure_type": "assertion_failed", - "error": map[string]interface{}{ - "name": "assertion", - "message": "title mismatch", - }, - "steps": []map[string]interface{}{ - {"seq": 1, "name": "open", "status": "OK", "duration_us": 1000}, - {"seq": 2, "name": "title", "status": "FAIL", "duration_us": 1000}, - }, + return nil +} + +func stubBrowserEngine( + t *testing.T, + gotOptions *browserrunner.EngineOptions, + gotConfig *browserrunner.BrowserConfig, + titles ...string, +) { + t.Helper() + var calls int + stubBrowserEngineWithFactory(t, func(_ context.Context, options browserrunner.EngineOptions) (browserrunner.Engine, error) { + if gotOptions != nil { + *gotOptions = options } - exitCode = 1 - } + title := "" + if calls < len(titles) { + title = titles[calls] + } + calls++ + return &fakeBrowserEngine{config: gotConfig, title: title}, nil + }) +} - _ = json.NewEncoder(os.Stdout).Encode(output) - os.Exit(exitCode) +func stubBrowserEngineWithFactory(t *testing.T, factory browserrunner.EngineFactory) { + t.Helper() + oldChromeFactory := browserEmbeddedChromeEngineFactory + oldLightpandaFactory := browserEmbeddedLightpandaEngineFactory + browserEmbeddedChromeEngineFactory = factory + browserEmbeddedLightpandaEngineFactory = factory + t.Cleanup(func() { + browserEmbeddedChromeEngineFactory = oldChromeFactory + browserEmbeddedLightpandaEngineFactory = oldLightpandaFactory + }) } func newBrowserTaskForTest() *BrowserTask { @@ -1078,23 +943,3 @@ func mustJSON(t *testing.T, value interface{}) string { require.NoError(t, err) return string(data) } - -func readHelperArgs(t *testing.T, path string) []string { - t.Helper() - data, err := os.ReadFile(path) - require.NoError(t, err) - return strings.Split(strings.TrimSpace(string(data)), "\n") -} - -func incrementHelperCount(path string) int { - if path == "" { - return 1 - } - count := 0 - if data, err := os.ReadFile(path); err == nil { - count, _ = strconv.Atoi(strings.TrimSpace(string(data))) - } - count++ - _ = os.WriteFile(path, []byte(strconv.Itoa(count)), 0o600) - return count -} diff --git a/dialtesting/http.go b/dialtesting/http.go index b0419b78..217e7044 100644 --- a/dialtesting/http.go +++ b/dialtesting/http.go @@ -815,9 +815,9 @@ func (t *HTTPTask) closeHTTP3Transport() { } func (t *HTTPTask) newHTTP3RoundTripper(tlsConfig *tls.Config, httpTimeout time.Duration) http.RoundTripper { - return &http3.RoundTripper{ + return &http3.Transport{ TLSClientConfig: tlsConfig, - Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err diff --git a/go.mod b/go.mod index a0adc2ea..57396385 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/GuanceCloud/cliutils -go 1.19 +go 1.25 require ( github.com/BurntSushi/toml v1.2.1 @@ -10,9 +10,11 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v2.1.2+incompatible github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/cespare/xxhash/v2 v2.2.0 + github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b + github.com/chromedp/chromedp v0.13.7 github.com/didip/tollbooth/v6 v6.1.2 github.com/gin-gonic/gin v1.9.0 - github.com/gobwas/ws v1.1.0 + github.com/gobwas/ws v1.4.0 github.com/gogo/protobuf v1.3.2 github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 github.com/gorilla/websocket v1.5.0 @@ -26,14 +28,14 @@ require ( github.com/prometheus/client_model v0.4.0 github.com/prometheus/common v0.44.0 github.com/prometheus/prometheus v0.39.1 - github.com/quic-go/quic-go v0.36.0 + github.com/quic-go/quic-go v0.59.1 github.com/robfig/cron/v3 v3.0.1 github.com/rs/xid v1.2.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/net v0.16.0 - golang.org/x/sys v0.13.0 + golang.org/x/net v0.43.0 + golang.org/x/sys v0.42.0 golang.org/x/time v0.5.0 google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.31.0 @@ -59,19 +61,19 @@ require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/bytedance/sonic v1.8.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect github.com/go-pkgz/expirable-cache v0.0.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.11.2 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect @@ -87,15 +89,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mssola/user_agent v0.6.0 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/outcaste-io/ristretto v0.2.1 // indirect github.com/philhofer/fwd v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/procfs v0.11.0 // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-19 v0.3.2 // indirect - github.com/quic-go/qtls-go1-20 v0.3.2 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/cast v1.5.1 // indirect github.com/tidwall/gjson v1.17.0 // indirect @@ -107,11 +106,11 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.14.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 // indirect lukechampine.com/uint128 v1.2.0 // indirect diff --git a/go.sum b/go.sum index e732c40d..c4608d38 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,7 @@ github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -158,6 +159,12 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b h1:jJmiCljLNTaq/O1ju9Bzz2MPpFlmiTn0F7LwCoeDZVw= +github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.13.7 h1:vt+mslxscyvUr58eC+6DLSeeo74jpV/HI2nWetjv/W4= +github.com/chromedp/chromedp v0.13.7/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -222,6 +229,7 @@ github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -232,6 +240,7 @@ github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -248,6 +257,8 @@ github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= +github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -258,7 +269,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -268,6 +278,7 @@ github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6d github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -284,8 +295,6 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= @@ -316,8 +325,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= -github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gocql/gocql v0.0.0-20210817081954-bc256bbb90de/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= @@ -345,7 +354,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -385,7 +393,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -446,6 +455,7 @@ github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= @@ -581,6 +591,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -589,6 +600,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo/v4 v4.6.0/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= @@ -609,6 +622,7 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -624,6 +638,7 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -670,8 +685,6 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -679,7 +692,6 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -690,6 +702,8 @@ github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.m github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/outcaste-io/ristretto v0.2.1 h1:KCItuNIGJZcursqHr3ghO7fc5ddZLEHspL9UR0cQM64= github.com/outcaste-io/ristretto v0.2.1/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -741,14 +755,10 @@ github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuR github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/prometheus v0.39.1 h1:abZM6A+sKAv2eKTbRIaHq4amM/nT07MuxRm0+QTaTj0= github.com/prometheus/prometheus v0.39.1/go.mod h1:GjQjgLhHMc0oo4Ko7qt/yBSJMY4hUoiAZwsYQgjaePA= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= -github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= -github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.36.0 h1:JIrO7p7Ug6hssFcARjWDiqS2RAKJHCiwPxBAA989rbI= -github.com/quic-go/quic-go v0.36.0/go.mod h1:zPetvwDlILVxt15n3hr3Gf/I3mDf7LpLKPhR4Ez0AZQ= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -760,6 +770,7 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= @@ -799,6 +810,7 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -808,8 +820,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tidwall/btree v0.6.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= github.com/tidwall/buntdb v1.2.6/go.mod h1:zpXqlA5D2772I4cTqV3ifr2AZihDgi8FV7xAQu6edfc= @@ -883,6 +895,9 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -923,8 +938,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -963,8 +978,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1020,8 +1035,8 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1050,8 +1065,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1107,7 +1122,6 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1136,8 +1150,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1153,8 +1167,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1232,8 +1246,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1445,7 +1459,9 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= @@ -1459,9 +1475,11 @@ modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/internal/browserdial/chrome/chrome.go b/internal/browserdial/chrome/chrome.go new file mode 100644 index 00000000..47adefe2 --- /dev/null +++ b/internal/browserdial/chrome/chrome.go @@ -0,0 +1,538 @@ +package chrome + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + goruntime "runtime" + "strings" + "sync" + + "github.com/GuanceCloud/cliutils/internal/browserdial/evidence" + "github.com/GuanceCloud/cliutils/internal/browserdial/runner" + "github.com/GuanceCloud/cliutils/internal/browserdial/util" + "github.com/chromedp/cdproto/network" + "github.com/chromedp/cdproto/page" + cdpruntime "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/cdproto/security" + "github.com/chromedp/chromedp" +) + +type Engine struct { + ctx context.Context + cancel context.CancelFunc + run func(context.Context, ...chromedp.Action) error + mu sync.Mutex + console []evidence.ConsoleEvent + network []evidence.NetworkEvent + requests map[network.RequestID]requestInfo + responses map[network.RequestID]struct{} +} + +type requestInfo struct { + URL string + Method string + ResourceType string + TraceID string +} + +func NewEngine(ctx context.Context, options runner.EngineOptions) (runner.Engine, error) { + executable, err := resolveExecutable(options.ChromePath) + if err != nil { + return nil, err + } + width, height := viewportSize(options.ViewportWidth, options.ViewportHeight) + allocatorOptions := chromeAllocatorOptions(executable, width, height, options) + allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, allocatorOptions...) + tabCtx, tabCancel := chromedp.NewContext(allocCtx) + engine, err := newEngineFromContext(tabCtx, chromedp.Run, func() { + tabCancel() + allocCancel() + }) + if err != nil { + tabCancel() + allocCancel() + return nil, err + } + return engine, nil +} + +func chromeAllocatorOptions(executable string, width int, height int, options runner.EngineOptions) []chromedp.ExecAllocatorOption { + allocatorOptions := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.ExecPath(executable), + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-first-run", true), + chromedp.Flag("no-default-browser-check", true), + chromedp.Flag("ignore-certificate-errors", options.IgnoreHTTPSErrors), + chromedp.WindowSize(width, height), + ) + if strings.TrimSpace(options.ProxyURL) != "" { + allocatorOptions = append(allocatorOptions, chromedp.ProxyServer(options.ProxyURL)) + } + return allocatorOptions +} + +func newEngineFromContext(tabCtx context.Context, run func(context.Context, ...chromedp.Action) error, cancel context.CancelFunc) (*Engine, error) { + engine := &Engine{ + ctx: tabCtx, + run: run, + requests: map[network.RequestID]requestInfo{}, + responses: map[network.RequestID]struct{}{}, + } + engine.cancel = cancel + chromedp.ListenTarget(tabCtx, engine.listen) + if err := engine.run(tabCtx, network.Enable(), cdpruntime.Enable(), chromedp.ActionFunc(func(ctx context.Context) error { + _, err := page.AddScriptToEvaluateOnNewDocument(performanceObserverScript).Do(ctx) + return err + })); err != nil { + engine.cancel() + return nil, err + } + return engine, nil +} + +func (e *Engine) ConfigureBrowser(ctx context.Context, config runner.BrowserConfig) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + actions := []chromedp.Action{} + if config.IgnoreHTTPSErrors { + actions = append(actions, security.SetIgnoreCertificateErrors(true)) + } + if len(config.Headers) > 0 { + headers := network.Headers{} + for key, value := range config.Headers { + if strings.TrimSpace(key) != "" { + headers[key] = value + } + } + actions = append(actions, network.SetExtraHTTPHeaders(headers)) + } + for _, cookie := range config.Cookies { + params := network.SetCookie(cookie.Name, cookie.Value). + WithPath(firstNonEmpty(cookie.Path, "/")). + WithSecure(cookie.Secure). + WithHTTPOnly(cookie.HTTPOnly) + if cookie.Domain != "" { + params = params.WithDomain(cookie.Domain) + } else if config.Target != "" { + params = params.WithURL(config.Target) + } + if sameSite, ok := sameSite(cookie.SameSite); ok { + params = params.WithSameSite(sameSite) + } + actions = append(actions, params) + } + if len(actions) == 0 { + return nil + } + return e.run(actionCtx, actions...) +} + +func viewportSize(width int, height int) (int, int) { + if width <= 0 { + width = 1920 + } + if height <= 0 { + height = 1080 + } + return width, height +} + +func (e *Engine) Close(context.Context) error { + if e.cancel != nil { + e.cancel() + } + return nil +} + +func (e *Engine) Navigate(ctx context.Context, target string) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + return e.run(actionCtx, chromedp.Navigate(target)) +} + +func (e *Engine) WaitForSelector(ctx context.Context, selector string) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + return e.run(actionCtx, chromedp.WaitReady(selector, chromedp.ByQuery)) +} + +func (e *Engine) Click(ctx context.Context, selector string) error { + var ok bool + expression := fmt.Sprintf(`(() => { +const el = document.querySelector(%s); +if (!el) throw new Error("selector not found: %s"); +el.click(); +return true; +})()`, jsString(selector), escapeJSMessage(selector)) + return e.evaluate(ctx, expression, &ok) +} + +func (e *Engine) Fill(ctx context.Context, selector string, value string) error { + var ok bool + expression := fmt.Sprintf(`(() => { +const el = document.querySelector(%s); +if (!el) throw new Error("selector not found: %s"); +if (typeof el.focus === "function") el.focus(); +el.value = %s; +el.dispatchEvent(new Event("input", { bubbles: true })); +el.dispatchEvent(new Event("change", { bubbles: true })); +return true; +})()`, jsString(selector), escapeJSMessage(selector), jsString(value)) + return e.evaluate(ctx, expression, &ok) +} + +func (e *Engine) Title(ctx context.Context) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + var title string + err := e.run(actionCtx, chromedp.Title(&title)) + return title, err +} + +func (e *Engine) URL(ctx context.Context) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + var location string + err := e.run(actionCtx, chromedp.Location(&location)) + return location, err +} + +func (e *Engine) Text(ctx context.Context, selector string) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + var text string + err := e.run(actionCtx, chromedp.Text(selector, &text, chromedp.ByQuery)) + return text, err +} + +func (e *Engine) Eval(ctx context.Context, expression string) (string, error) { + var result any + err := e.evaluate(ctx, expression, &result) + return util.JSONString(result, 8_000), err +} + +func (e *Engine) CaptureDOM(ctx context.Context) (evidence.DomSnapshot, error) { + snapshot := evidence.DomSnapshot{CapturedAt: util.NowISO()} + if currentURL, err := e.URL(ctx); err == nil { + snapshot.URL = currentURL + } + if title, err := e.Title(ctx); err == nil { + snapshot.Title = title + } + + var dom struct { + Text string `json:"text"` + HTML string `json:"html"` + } + expression := `(() => { +const root = document.documentElement; +return { + text: (document.body && (document.body.innerText || document.body.textContent)) || (root && root.textContent) || "", + html: (root && root.outerHTML) || "" +}; +})()` + if err := e.evaluate(ctx, expression, &dom); err != nil { + snapshot.Error = err.Error() + return snapshot, err + } + snapshot.Text = util.Truncate(dom.Text, 16_000) + snapshot.HTML = util.Truncate(dom.HTML, 32_000) + return snapshot, nil +} + +func (e *Engine) CapturePerformance(ctx context.Context) (evidence.PerformanceMetrics, error) { + var metrics evidence.PerformanceMetrics + expression := `(() => { +const nav = performance.getEntriesByType("navigation")[0] || {}; +const lcpEntries = performance.getEntriesByType("largest-contentful-paint") || []; +const cached = window.__browserDialPerf || {}; +const lcp = cached.lcp || (lcpEntries.length ? lcpEntries[lcpEntries.length - 1].startTime : 0); +const cls = cached.cls || (performance.getEntriesByType("layout-shift") || []) + .filter(entry => !entry.hadRecentInput) + .reduce((sum, entry) => sum + (entry.value || 0), 0); +const round = value => Number.isFinite(value) && value > 0 ? Math.round(value) : 0; +return { + ttfb_ms: round((nav.responseStart || 0) - (nav.requestStart || nav.startTime || 0)), + loading_time_ms: round((nav.loadEventEnd || 0) - (nav.startTime || 0)), + lcp_ms: round(lcp), + cls: Number.isFinite(cls) ? cls : 0, + dom_content_loaded_ms: round((nav.domContentLoadedEventEnd || 0) - (nav.startTime || 0)), + load_event_end_ms: round((nav.loadEventEnd || 0) - (nav.startTime || 0)) +}; +})()` + if err := e.evaluate(ctx, expression, &metrics); err != nil { + return evidence.PerformanceMetrics{}, err + } + return metrics, nil +} + +const performanceObserverScript = `(() => { +window.__browserDialPerf = window.__browserDialPerf || { lcp: 0, cls: 0 }; +try { + new PerformanceObserver(list => { + const entries = list.getEntries(); + const last = entries[entries.length - 1]; + if (last) window.__browserDialPerf.lcp = last.startTime || 0; + }).observe({ type: "largest-contentful-paint", buffered: true }); +} catch (_) {} +try { + new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + if (!entry.hadRecentInput) window.__browserDialPerf.cls += entry.value || 0; + } + }).observe({ type: "layout-shift", buffered: true }); +} catch (_) {} +})()` + +func (e *Engine) CaptureScreenshot(ctx context.Context, path string, fullPage bool) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return "", err + } + var image []byte + if fullPage { + if err := e.run(actionCtx, chromedp.FullScreenshot(&image, 80)); err != nil { + return "", err + } + } else { + if err := e.run(actionCtx, chromedp.CaptureScreenshot(&image)); err != nil { + return "", err + } + } + if err := os.WriteFile(path, image, 0o644); err != nil { + return "", err + } + return path, nil +} + +func (e *Engine) ConsoleEvents() []evidence.ConsoleEvent { + e.mu.Lock() + defer e.mu.Unlock() + if e.console == nil { + return []evidence.ConsoleEvent{} + } + return append([]evidence.ConsoleEvent(nil), e.console...) +} + +func (e *Engine) NetworkEvents() []evidence.NetworkEvent { + e.mu.Lock() + defer e.mu.Unlock() + if e.network == nil { + return []evidence.NetworkEvent{} + } + return append([]evidence.NetworkEvent(nil), e.network...) +} + +func (e *Engine) evaluate(ctx context.Context, expression string, out any) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + return e.run(actionCtx, chromedp.Evaluate(expression, out)) +} + +func (e *Engine) actionContext(ctx context.Context) (context.Context, context.CancelFunc) { + actionCtx, cancel := context.WithCancel(e.ctx) + if deadline, ok := ctx.Deadline(); ok { + actionCtx, cancel = context.WithDeadline(e.ctx, deadline) + } + go func() { + select { + case <-ctx.Done(): + cancel() + case <-actionCtx.Done(): + } + }() + return actionCtx, cancel +} + +func (e *Engine) listen(event any) { + e.mu.Lock() + defer e.mu.Unlock() + switch ev := event.(type) { + case *cdpruntime.EventConsoleAPICalled: + parts := make([]string, 0, len(ev.Args)) + for _, arg := range ev.Args { + if len(arg.Value) > 0 { + parts = append(parts, string(arg.Value)) + } else if arg.Description != "" { + parts = append(parts, arg.Description) + } + } + e.console = append(e.console, evidence.ConsoleEvent{ + Seq: len(e.console) + 1, + Timestamp: util.NowISO(), + Type: string(ev.Type), + Text: util.Truncate(strings.Join(parts, " "), 8_000), + }) + case *network.EventRequestWillBeSent: + if _, seen := e.requests[ev.RequestID]; seen { + return + } + info := requestInfo{ + URL: ev.Request.URL, + Method: ev.Request.Method, + ResourceType: string(ev.Type), + TraceID: extractTraceID(ev.Request.URL, ev.Request.Headers), + } + e.requests[ev.RequestID] = info + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "request", + URL: info.URL, + Method: info.Method, + ResourceType: info.ResourceType, + TraceID: info.TraceID, + }) + case *network.EventResponseReceived: + if _, seen := e.responses[ev.RequestID]; seen { + return + } + e.responses[ev.RequestID] = struct{}{} + info := e.requests[ev.RequestID] + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "response", + URL: ev.Response.URL, + Method: info.Method, + ResourceType: string(ev.Type), + TraceID: firstNonEmpty(info.TraceID, extractTraceID(ev.Response.URL, ev.Response.RequestHeaders, ev.Response.Headers)), + Status: int64(ev.Response.Status), + }) + case *network.EventLoadingFailed: + info := e.requests[ev.RequestID] + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "request_failed", + URL: info.URL, + Method: info.Method, + ResourceType: info.ResourceType, + TraceID: info.TraceID, + Failure: ev.ErrorText, + }) + } +} + +func resolveExecutable(override string) (string, error) { + candidates := []string{} + if override != "" { + candidates = append(candidates, override) + } + if env := os.Getenv("CHROME_EXECUTABLE_PATH"); env != "" { + candidates = append(candidates, env) + } + if goruntime.GOOS == "darwin" { + candidates = append(candidates, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + filepath.Join(os.Getenv("HOME"), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), + ) + } + for _, name := range []string{"google-chrome", "chromium", "chromium-browser", "chrome"} { + if path, err := exec.LookPath(name); err == nil { + candidates = append(candidates, path) + } + } + + seen := map[string]struct{}{} + var problems []string + for _, candidate := range candidates { + if candidate == "" { + continue + } + if _, ok := seen[candidate]; ok { + continue + } + seen[candidate] = struct{}{} + if err := checkExecutable(candidate); err != nil { + problems = append(problems, fmt.Sprintf("%s: %v", candidate, err)) + continue + } + return candidate, nil + } + if len(problems) > 0 { + return "", fmt.Errorf("no usable chrome executable found (%s)", strings.Join(problems, "; ")) + } + return "", fmt.Errorf("no chrome executable found; set --chrome-path or CHROME_EXECUTABLE_PATH") +} + +func checkExecutable(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("is a directory") + } + if info.Size() == 0 { + return fmt.Errorf("file is empty") + } + if goruntime.GOOS != "windows" && info.Mode()&0o111 == 0 { + return fmt.Errorf("file is not executable") + } + return nil +} + +func extractTraceID(rawURL string, headers ...network.Headers) string { + if traceID := traceFromURL(rawURL); traceID != "" { + return traceID + } + for _, headerSet := range headers { + for key, value := range headerSet { + if strings.EqualFold(key, "traceparent") || strings.EqualFold(key, "x-datadog-trace-id") || strings.EqualFold(key, "x-trace-id") { + return fmt.Sprint(value) + } + } + } + return "" +} + +func traceFromURL(rawURL string) string { + for _, marker := range []string{"trace_id=", "traceid=", "traceId="} { + if index := strings.Index(rawURL, marker); index >= 0 { + value := rawURL[index+len(marker):] + if cut := strings.IndexAny(value, "&#"); cut >= 0 { + value = value[:cut] + } + return value + } + } + return "" +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +func sameSite(value string) (network.CookieSameSite, bool) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "strict": + return network.CookieSameSiteStrict, true + case "lax": + return network.CookieSameSiteLax, true + case "none": + return network.CookieSameSiteNone, true + default: + return "", false + } +} + +func jsString(value string) string { + encoded, _ := json.Marshal(value) + return string(encoded) +} + +func escapeJSMessage(value string) string { + return strings.ReplaceAll(value, `"`, `\"`) +} diff --git a/internal/browserdial/chrome/pool.go b/internal/browserdial/chrome/pool.go new file mode 100644 index 00000000..23042e94 --- /dev/null +++ b/internal/browserdial/chrome/pool.go @@ -0,0 +1,170 @@ +package chrome + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/GuanceCloud/cliutils/internal/browserdial/runner" + "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/target" + "github.com/chromedp/chromedp" +) + +// Pool keeps Chrome browser processes warm and gives each run an isolated +// browser context. A worker runs one task at a time. +type Pool struct { + ctx context.Context + workers chan *poolWorker + done chan struct{} + + mu sync.Mutex + all []*poolWorker + closed bool +} + +type poolWorker struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewPool(ctx context.Context, size int, options runner.EngineOptions) (*Pool, error) { + if size <= 0 { + size = 1 + } + pool := &Pool{ + ctx: ctx, + workers: make(chan *poolWorker, size), + done: make(chan struct{}), + } + for i := 0; i < size; i++ { + worker, err := newPoolWorker(ctx, options) + if err != nil { + pool.Close() + return nil, err + } + pool.all = append(pool.all, worker) + pool.workers <- worker + } + return pool, nil +} + +func newPoolWorker(ctx context.Context, options runner.EngineOptions) (*poolWorker, error) { + executable, err := resolveExecutable(options.ChromePath) + if err != nil { + return nil, err + } + width, height := viewportSize(options.ViewportWidth, options.ViewportHeight) + allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, chromeAllocatorOptions(executable, width, height, options)...) + browserCtx, browserCancel := chromedp.NewContext(allocCtx) + if err := chromedp.Run(browserCtx, chromedp.Navigate("about:blank")); err != nil { + browserCancel() + allocCancel() + return nil, fmt.Errorf("start pooled chrome: %w", err) + } + return &poolWorker{ + ctx: browserCtx, + cancel: func() { + browserCancel() + allocCancel() + }, + }, nil +} + +func (p *Pool) Factory(ctx context.Context, options runner.EngineOptions) (runner.Engine, error) { + select { + case <-p.ctx.Done(): + return nil, p.ctx.Err() + case <-ctx.Done(): + return nil, ctx.Err() + case <-p.done: + return nil, fmt.Errorf("chrome pool is closed") + case worker := <-p.workers: + select { + case <-p.done: + return nil, fmt.Errorf("chrome pool is closed") + default: + } + browserContextID, targetID, err := worker.newIsolatedTarget(ctx) + if err != nil { + p.releaseWorker(worker) + return nil, err + } + taskCtx, taskCancel := chromedp.NewContext(worker.ctx, chromedp.WithTargetID(targetID)) + var releaseOnce sync.Once + release := func() { + releaseOnce.Do(func() { + taskCancel() + worker.disposeBrowserContext(browserContextID) + p.releaseWorker(worker) + }) + } + engine, err := newEngineFromContext(taskCtx, chromedp.Run, release) + if err != nil { + release() + return nil, err + } + return engine, nil + } +} + +func (w *poolWorker) newIsolatedTarget(ctx context.Context) (cdp.BrowserContextID, target.ID, error) { + opCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + browser := chromedp.FromContext(w.ctx).Browser + if browser == nil { + return "", "", fmt.Errorf("pooled chrome browser is not initialized") + } + executor := cdp.WithExecutor(opCtx, browser) + browserContextID, err := target.CreateBrowserContext().WithDisposeOnDetach(true).Do(executor) + if err != nil { + return "", "", err + } + targetID, err := target.CreateTarget("about:blank"). + WithBrowserContextID(browserContextID). + WithNewWindow(true). + Do(executor) + if err != nil { + _ = target.DisposeBrowserContext(browserContextID).Do(executor) + return "", "", err + } + return browserContextID, targetID, nil +} + +func (w *poolWorker) disposeBrowserContext(browserContextID cdp.BrowserContextID) { + if browserContextID == "" { + return + } + browser := chromedp.FromContext(w.ctx).Browser + if browser == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = target.DisposeBrowserContext(browserContextID).Do(cdp.WithExecutor(ctx, browser)) +} + +func (p *Pool) releaseWorker(worker *poolWorker) { + select { + case p.workers <- worker: + case <-p.ctx.Done(): + case <-p.done: + } +} + +func (p *Pool) Close() { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return + } + p.closed = true + close(p.done) + workers := append([]*poolWorker(nil), p.all...) + p.mu.Unlock() + + for _, worker := range workers { + worker.cancel() + } +} diff --git a/internal/browserdial/errors/errors.go b/internal/browserdial/errors/errors.go new file mode 100644 index 00000000..84493e05 --- /dev/null +++ b/internal/browserdial/errors/errors.go @@ -0,0 +1,69 @@ +package errorsx + +import ( + "fmt" + "runtime/debug" + + "github.com/GuanceCloud/cliutils/internal/browserdial/evidence" +) + +type TimeoutError struct { + TimeoutMS int +} + +func (e TimeoutError) Error() string { + return fmt.Sprintf("dial script timed out after %dms", e.TimeoutMS) +} + +type ScriptLoadError struct { + Message string +} + +func (e ScriptLoadError) Error() string { + return e.Message +} + +type AuthError struct { + Err error +} + +func (e AuthError) Error() string { + if e.Err == nil { + return "authentication failed" + } + return e.Err.Error() +} + +func (e AuthError) Unwrap() error { + return e.Err +} + +func ErrorInfo(err error) *evidence.ErrorInfo { + if err == nil { + return nil + } + + return &evidence.ErrorInfo{ + Name: fmt.Sprintf("%T", err), + Message: err.Error(), + Stack: string(debug.Stack()), + } +} + +func FailureReason(err error, hasFailedStep bool) string { + if err == nil { + return "" + } + switch err.(type) { + case AuthError: + return "auth_error" + case TimeoutError: + return "timeout" + case ScriptLoadError: + return "script_load_error" + } + if hasFailedStep { + return "step_error" + } + return "script_error" +} diff --git a/internal/browserdial/evidence/types.go b/internal/browserdial/evidence/types.go new file mode 100644 index 00000000..f79f2c90 --- /dev/null +++ b/internal/browserdial/evidence/types.go @@ -0,0 +1,88 @@ +package evidence + +type RunStatus string + +const ( + StatusOK RunStatus = "OK" + StatusFail RunStatus = "FAIL" + StatusSkip RunStatus = "SKIP" +) + +type ErrorInfo struct { + Name string `json:"name"` + Message string `json:"message"` + Stack string `json:"stack,omitempty"` +} + +type StepResult struct { + Seq int `json:"seq"` + Name string `json:"name"` + Action string `json:"action,omitempty"` + Selector string `json:"selector,omitempty"` + InputDisplay string `json:"input_display,omitempty"` + ValueFrom string `json:"value_from,omitempty"` + Expected string `json:"expected,omitempty"` + TimeoutMS int `json:"timeout_ms,omitempty"` + Auth bool `json:"auth,omitempty"` + Status RunStatus `json:"status"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Performance *PerformanceMetrics `json:"performance,omitempty"` + Screenshot string `json:"screenshot,omitempty"` + SkipReason string `json:"skip_reason,omitempty"` + Error *ErrorInfo `json:"error,omitempty"` +} + +type RetryRecord struct { + Attempt int `json:"attempt"` + StartedAt string `json:"started_at,omitempty"` + EndedAt string `json:"ended_at,omitempty"` + DurationUS int64 `json:"duration_us,omitempty"` + Status RunStatus `json:"status"` + Success bool `json:"success"` + FailedStep int `json:"failed_step,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + FailureType string `json:"failure_type,omitempty"` + Message string `json:"message,omitempty"` +} + +type ConsoleEvent struct { + Seq int `json:"seq"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` + Text string `json:"text"` + Location any `json:"location,omitempty"` +} + +type NetworkEvent struct { + Seq int `json:"seq"` + Timestamp string `json:"timestamp"` + Event string `json:"event"` + URL string `json:"url"` + Method string `json:"method,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + TraceID string `json:"trace_id,omitempty"` + Status int64 `json:"status,omitempty"` + Failure string `json:"failure,omitempty"` +} + +type DomSnapshot struct { + CapturedAt string `json:"captured_at"` + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + HTML string `json:"html,omitempty"` + Error string `json:"error,omitempty"` +} + +type PerformanceMetrics struct { + TTFBMS int64 `json:"ttfb_ms,omitempty"` + LoadingTimeMS int64 `json:"loading_time_ms,omitempty"` + LCPMS int64 `json:"lcp_ms,omitempty"` + CLS float64 `json:"cls,omitempty"` + DOMContentLoadedMS int64 `json:"dom_content_loaded_ms,omitempty"` + LoadEventEndMS int64 `json:"load_event_end_ms,omitempty"` +} diff --git a/internal/browserdial/lightpanda/lightpanda.go b/internal/browserdial/lightpanda/lightpanda.go new file mode 100644 index 00000000..301cd65d --- /dev/null +++ b/internal/browserdial/lightpanda/lightpanda.go @@ -0,0 +1,587 @@ +package lightpanda + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + goruntime "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/GuanceCloud/cliutils/internal/browserdial/evidence" + "github.com/GuanceCloud/cliutils/internal/browserdial/runner" + "github.com/GuanceCloud/cliutils/internal/browserdial/util" + "github.com/chromedp/cdproto/network" + cdpruntime "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/cdproto/security" + "github.com/chromedp/chromedp" +) + +const defaultHost = "127.0.0.1" + +type Engine struct { + ctx context.Context + cancel context.CancelFunc + run func(context.Context, ...chromedp.Action) error + session *session + mu sync.Mutex + console []evidence.ConsoleEvent + network []evidence.NetworkEvent + requests map[network.RequestID]requestInfo + responses map[network.RequestID]struct{} +} + +type requestInfo struct { + URL string + Method string + ResourceType string + TraceID string +} + +func NewEngine(ctx context.Context, options runner.EngineOptions) (runner.Engine, error) { + executable, err := resolveExecutable(options.LightpandaPath) + if err != nil { + return nil, err + } + activeSession, err := start(ctx, executable, options.StartupTimeout) + if err != nil { + return nil, err + } + cdpURL := activeSession.endpoint + + websocketURL, err := resolveWebsocketURL(ctx, cdpURL) + if err != nil { + activeSession.Close() + return nil, err + } + + allocCtx, allocCancel := chromedp.NewRemoteAllocator(ctx, websocketURL) + tabCtx, tabCancel := chromedp.NewContext(allocCtx) + engine := &Engine{ + ctx: tabCtx, + run: chromedp.Run, + session: activeSession, + requests: map[network.RequestID]requestInfo{}, + responses: map[network.RequestID]struct{}{}, + } + engine.cancel = func() { + tabCancel() + allocCancel() + if engine.session != nil { + engine.session.Close() + } + } + + chromedp.ListenTarget(tabCtx, engine.listen) + if err := engine.run(tabCtx, network.Enable(), cdpruntime.Enable()); err != nil { + engine.cancel() + return nil, err + } + return engine, nil +} + +func (e *Engine) ConfigureBrowser(ctx context.Context, config runner.BrowserConfig) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + actions := []chromedp.Action{} + if config.IgnoreHTTPSErrors { + actions = append(actions, security.SetIgnoreCertificateErrors(true)) + } + if len(config.Headers) > 0 { + headers := network.Headers{} + for key, value := range config.Headers { + if strings.TrimSpace(key) != "" { + headers[key] = value + } + } + actions = append(actions, network.SetExtraHTTPHeaders(headers)) + } + for _, cookie := range config.Cookies { + params := network.SetCookie(cookie.Name, cookie.Value). + WithPath(firstNonEmpty(cookie.Path, "/")). + WithSecure(cookie.Secure). + WithHTTPOnly(cookie.HTTPOnly) + if cookie.Domain != "" { + params = params.WithDomain(cookie.Domain) + } else if config.Target != "" { + params = params.WithURL(config.Target) + } + if sameSite, ok := sameSite(cookie.SameSite); ok { + params = params.WithSameSite(sameSite) + } + actions = append(actions, params) + } + if len(actions) == 0 { + return nil + } + return e.run(actionCtx, actions...) +} + +func (e *Engine) Close(context.Context) error { + if e.cancel != nil { + e.cancel() + } + return nil +} + +func (e *Engine) Navigate(ctx context.Context, target string) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + return e.run(actionCtx, chromedp.Navigate(target)) +} + +func (e *Engine) WaitForSelector(ctx context.Context, selector string) error { + for { + var exists bool + expression := fmt.Sprintf(`document.querySelector(%s) !== null`, jsString(selector)) + if err := e.evaluate(ctx, expression, &exists); err != nil { + return err + } + if exists { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(100 * time.Millisecond): + } + } +} + +func (e *Engine) Click(ctx context.Context, selector string) error { + var ok bool + expression := fmt.Sprintf(`(() => { +const el = document.querySelector(%s); +if (!el) throw new Error("selector not found: %s"); +el.click(); +return true; +})()`, jsString(selector), escapeJSMessage(selector)) + return e.evaluate(ctx, expression, &ok) +} + +func (e *Engine) Fill(ctx context.Context, selector string, value string) error { + var ok bool + expression := fmt.Sprintf(`(() => { +const el = document.querySelector(%s); +if (!el) throw new Error("selector not found: %s"); +if (typeof el.focus === "function") el.focus(); +el.value = %s; +el.dispatchEvent(new Event("input", { bubbles: true })); +el.dispatchEvent(new Event("change", { bubbles: true })); +return true; +})()`, jsString(selector), escapeJSMessage(selector), jsString(value)) + return e.evaluate(ctx, expression, &ok) +} + +func (e *Engine) Title(ctx context.Context) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + var title string + err := e.run(actionCtx, chromedp.Title(&title)) + return title, err +} + +func (e *Engine) URL(ctx context.Context) (string, error) { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + var location string + err := e.run(actionCtx, chromedp.Location(&location)) + return location, err +} + +func (e *Engine) Text(ctx context.Context, selector string) (string, error) { + var text string + expression := fmt.Sprintf(`(() => { +const el = document.querySelector(%s); +if (!el) throw new Error("selector not found: %s"); +return el.innerText || el.textContent || ""; +})()`, jsString(selector), escapeJSMessage(selector)) + err := e.evaluate(ctx, expression, &text) + return text, err +} + +func (e *Engine) Eval(ctx context.Context, expression string) (string, error) { + var result any + err := e.evaluate(ctx, expression, &result) + return util.JSONString(result, 8_000), err +} + +func (e *Engine) CaptureDOM(ctx context.Context) (evidence.DomSnapshot, error) { + snapshot := evidence.DomSnapshot{CapturedAt: util.NowISO()} + if currentURL, err := e.URL(ctx); err == nil { + snapshot.URL = currentURL + } + if title, err := e.Title(ctx); err == nil { + snapshot.Title = title + } + + var dom struct { + Text string `json:"text"` + HTML string `json:"html"` + } + expression := `(() => { +const root = document.documentElement; +return { + text: (document.body && (document.body.innerText || document.body.textContent)) || (root && root.textContent) || "", + html: (root && root.outerHTML) || "" +}; +})()` + if err := e.evaluate(ctx, expression, &dom); err != nil { + snapshot.Error = err.Error() + return snapshot, err + } + snapshot.Text = util.Truncate(dom.Text, 16_000) + snapshot.HTML = util.Truncate(dom.HTML, 32_000) + return snapshot, nil +} + +func (e *Engine) ConsoleEvents() []evidence.ConsoleEvent { + e.mu.Lock() + defer e.mu.Unlock() + if e.console == nil { + return []evidence.ConsoleEvent{} + } + return append([]evidence.ConsoleEvent(nil), e.console...) +} + +func (e *Engine) NetworkEvents() []evidence.NetworkEvent { + e.mu.Lock() + defer e.mu.Unlock() + if e.network == nil { + return []evidence.NetworkEvent{} + } + return append([]evidence.NetworkEvent(nil), e.network...) +} + +func (e *Engine) evaluate(ctx context.Context, expression string, out any) error { + actionCtx, cancel := e.actionContext(ctx) + defer cancel() + return e.run(actionCtx, chromedp.Evaluate(expression, out)) +} + +func (e *Engine) actionContext(ctx context.Context) (context.Context, context.CancelFunc) { + actionCtx, cancel := context.WithCancel(e.ctx) + if deadline, ok := ctx.Deadline(); ok { + actionCtx, cancel = context.WithDeadline(e.ctx, deadline) + } + go func() { + select { + case <-ctx.Done(): + cancel() + case <-actionCtx.Done(): + } + }() + return actionCtx, cancel +} + +func (e *Engine) listen(event any) { + e.mu.Lock() + defer e.mu.Unlock() + switch ev := event.(type) { + case *cdpruntime.EventConsoleAPICalled: + parts := make([]string, 0, len(ev.Args)) + for _, arg := range ev.Args { + if len(arg.Value) > 0 { + parts = append(parts, string(arg.Value)) + } else if arg.Description != "" { + parts = append(parts, arg.Description) + } + } + e.console = append(e.console, evidence.ConsoleEvent{ + Seq: len(e.console) + 1, + Timestamp: util.NowISO(), + Type: string(ev.Type), + Text: util.Truncate(strings.Join(parts, " "), 8_000), + }) + case *network.EventRequestWillBeSent: + if _, seen := e.requests[ev.RequestID]; seen { + return + } + info := requestInfo{ + URL: ev.Request.URL, + Method: ev.Request.Method, + ResourceType: string(ev.Type), + TraceID: extractTraceID(ev.Request.URL, ev.Request.Headers), + } + e.requests[ev.RequestID] = info + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "request", + URL: info.URL, + Method: info.Method, + ResourceType: info.ResourceType, + TraceID: info.TraceID, + }) + case *network.EventResponseReceived: + if _, seen := e.responses[ev.RequestID]; seen { + return + } + e.responses[ev.RequestID] = struct{}{} + info := e.requests[ev.RequestID] + status := int64(ev.Response.Status) + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "response", + URL: ev.Response.URL, + Method: info.Method, + ResourceType: string(ev.Type), + TraceID: firstNonEmpty(info.TraceID, extractTraceID(ev.Response.URL, ev.Response.RequestHeaders, ev.Response.Headers)), + Status: status, + }) + case *network.EventLoadingFailed: + info := e.requests[ev.RequestID] + e.network = append(e.network, evidence.NetworkEvent{ + Seq: len(e.network) + 1, + Timestamp: util.NowISO(), + Event: "request_failed", + URL: info.URL, + Method: info.Method, + ResourceType: info.ResourceType, + TraceID: info.TraceID, + Failure: ev.ErrorText, + }) + } +} + +type session struct { + endpoint string + cmd *exec.Cmd + cancel context.CancelFunc + done chan error + logs *limitedBuffer +} + +func start(parent context.Context, executable string, startupTimeout time.Duration) (*session, error) { + if startupTimeout <= 0 { + startupTimeout = 5 * time.Second + } + port, err := freePort(defaultHost) + if err != nil { + return nil, err + } + endpoint := fmt.Sprintf("http://%s:%d", defaultHost, port) + procCtx, cancel := context.WithCancel(parent) + logs := &limitedBuffer{limit: 8_000} + cmd := exec.CommandContext(procCtx, executable, "serve", "--host", defaultHost, "--port", strconv.Itoa(port)) + cmd.Stdout = logs + cmd.Stderr = logs + if err := cmd.Start(); err != nil { + cancel() + return nil, err + } + s := &session{ + endpoint: endpoint, + cmd: cmd, + cancel: cancel, + done: make(chan error, 1), + logs: logs, + } + go func() { + s.done <- cmd.Wait() + }() + if err := waitReady(parent, endpoint, startupTimeout, s); err != nil { + s.Close() + return nil, err + } + return s, nil +} + +func (s *session) Close() { + if s == nil { + return + } + s.cancel() + select { + case <-s.done: + case <-time.After(2 * time.Second): + if s.cmd != nil && s.cmd.Process != nil { + _ = s.cmd.Process.Kill() + } + } +} + +func waitReady(parent context.Context, endpoint string, timeout time.Duration, s *session) error { + deadline := time.NewTimer(timeout) + defer deadline.Stop() + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + if _, err := resolveWebsocketURL(parent, endpoint); err == nil { + return nil + } + select { + case err := <-s.done: + return fmt.Errorf("lightpanda exited before CDP was ready: %v\n%s", err, s.logs.String()) + case <-deadline.C: + return fmt.Errorf("lightpanda CDP server did not become ready at %s\n%s", endpoint, s.logs.String()) + case <-ticker.C: + case <-parent.Done(): + return parent.Err() + } + } +} + +func resolveExecutable(override string) (string, error) { + candidates := []string{} + if override != "" { + candidates = append(candidates, override) + } + if env := os.Getenv("LIGHTPANDA_EXECUTABLE_PATH"); env != "" { + candidates = append(candidates, env) + } + if path, err := exec.LookPath("lightpanda"); err == nil { + candidates = append(candidates, path) + } + if home, err := os.UserHomeDir(); err == nil { + candidates = append(candidates, filepath.Join(home, ".cache", "lightpanda-node", "lightpanda")) + } + + seen := map[string]struct{}{} + var problems []string + for _, candidate := range candidates { + if candidate == "" { + continue + } + if _, ok := seen[candidate]; ok { + continue + } + seen[candidate] = struct{}{} + if err := checkExecutable(candidate); err != nil { + problems = append(problems, fmt.Sprintf("%s: %v", candidate, err)) + continue + } + return candidate, nil + } + + if len(problems) > 0 { + return "", fmt.Errorf("no usable lightpanda executable found (%s)", strings.Join(problems, "; ")) + } + return "", fmt.Errorf("no lightpanda executable found; set LIGHTPANDA_EXECUTABLE_PATH or install lightpanda in PATH") +} + +func checkExecutable(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("is a directory") + } + if info.Size() == 0 { + return fmt.Errorf("file is empty") + } + if goruntime.GOOS != "windows" && info.Mode()&0o111 == 0 { + return fmt.Errorf("file is not executable") + } + return nil +} + +func resolveWebsocketURL(ctx context.Context, raw string) (string, error) { + if strings.HasPrefix(raw, "ws://") || strings.HasPrefix(raw, "wss://") { + return raw, nil + } + parsed, err := url.Parse(raw) + if err != nil { + return "", err + } + parsed.Path = strings.TrimRight(parsed.Path, "/") + "/json/version" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("%s returned %s", parsed.String(), resp.Status) + } + var payload struct { + WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return "", err + } + if payload.WebSocketDebuggerURL == "" { + return "", fmt.Errorf("%s did not return webSocketDebuggerUrl", parsed.String()) + } + return payload.WebSocketDebuggerURL, nil +} + +func freePort(host string) (int, error) { + listener, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + if err != nil { + return 0, err + } + defer listener.Close() + return listener.Addr().(*net.TCPAddr).Port, nil +} + +func jsString(value string) string { + encoded, _ := json.Marshal(value) + return string(encoded) +} + +func escapeJSMessage(value string) string { + return strings.ReplaceAll(value, `"`, `\"`) +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +func sameSite(value string) (network.CookieSameSite, bool) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "strict": + return network.CookieSameSiteStrict, true + case "lax": + return network.CookieSameSiteLax, true + case "none": + return network.CookieSameSiteNone, true + default: + return "", false + } +} + +type limitedBuffer struct { + mu sync.Mutex + limit int + buf bytes.Buffer +} + +func (b *limitedBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + n, err := b.buf.Write(p) + if b.buf.Len() > b.limit { + content := b.buf.Bytes() + keep := append([]byte(nil), content[len(content)-b.limit:]...) + b.buf.Reset() + _, _ = b.buf.Write(keep) + } + return n, err +} + +func (b *limitedBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} diff --git a/internal/browserdial/lightpanda/trace.go b/internal/browserdial/lightpanda/trace.go new file mode 100644 index 00000000..017805c3 --- /dev/null +++ b/internal/browserdial/lightpanda/trace.go @@ -0,0 +1,123 @@ +package lightpanda + +import ( + "fmt" + "net/url" + "strings" + + "github.com/chromedp/cdproto/network" +) + +var traceIDKeys = map[string]struct{}{ + "traceid": {}, + "trace-id": {}, + "traceparent": {}, + "x-trace-id": {}, + "x-traceid": {}, + "x-b3-traceid": {}, + "x-datadog-trace-id": {}, + "dd-trace-id": {}, + "uber-trace-id": {}, +} + +func extractTraceID(rawURL string, headerSets ...network.Headers) string { + for _, headers := range headerSets { + if traceID := extractTraceIDFromHeaders(headers); traceID != "" { + return traceID + } + } + return extractTraceIDFromURL(rawURL) +} + +func extractTraceIDFromHeaders(headers network.Headers) string { + for key, value := range headers { + if _, ok := traceIDKeys[normalizeTraceKey(key)]; !ok { + continue + } + if traceID := normalizeTraceValue(key, headerString(value)); traceID != "" { + return traceID + } + } + return "" +} + +func extractTraceIDFromURL(rawURL string) string { + parsed, err := url.Parse(rawURL) + if err != nil { + return "" + } + for key, values := range parsed.Query() { + if _, ok := traceIDKeys[normalizeTraceKey(key)]; !ok { + continue + } + for _, value := range values { + if traceID := normalizeTraceValue(key, value); traceID != "" { + return traceID + } + } + } + return "" +} + +func normalizeTraceKey(key string) string { + key = strings.ToLower(strings.TrimSpace(key)) + key = strings.ReplaceAll(key, "_", "-") + return key +} + +func normalizeTraceValue(key string, value string) string { + value = strings.TrimSpace(value) + value = strings.Trim(value, `"`) + if value == "" { + return "" + } + if index := strings.Index(value, ","); index > 0 { + value = strings.TrimSpace(value[:index]) + } + switch normalizeTraceKey(key) { + case "traceparent": + return traceIDFromTraceparent(value) + case "uber-trace-id": + parts := strings.Split(value, ":") + if len(parts) > 0 { + return cleanTraceID(parts[0]) + } + return "" + default: + return cleanTraceID(value) + } +} + +func traceIDFromTraceparent(value string) string { + parts := strings.Split(value, "-") + if len(parts) >= 2 { + return cleanTraceID(parts[1]) + } + return cleanTraceID(value) +} + +func cleanTraceID(value string) string { + value = strings.TrimSpace(value) + value = strings.Trim(value, `"`) + value = strings.Trim(value, "'") + return value +} + +func headerString(value any) string { + switch typed := value.(type) { + case nil: + return "" + case string: + return typed + case []string: + return strings.Join(typed, ",") + case []any: + parts := make([]string, 0, len(typed)) + for _, item := range typed { + parts = append(parts, fmt.Sprint(item)) + } + return strings.Join(parts, ",") + default: + return fmt.Sprint(value) + } +} diff --git a/internal/browserdial/runner/runner.go b/internal/browserdial/runner/runner.go new file mode 100644 index 00000000..4259cd0c --- /dev/null +++ b/internal/browserdial/runner/runner.go @@ -0,0 +1,850 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + errorsx "github.com/GuanceCloud/cliutils/internal/browserdial/errors" + "github.com/GuanceCloud/cliutils/internal/browserdial/evidence" + "github.com/GuanceCloud/cliutils/internal/browserdial/script" + "github.com/GuanceCloud/cliutils/internal/browserdial/util" +) + +type Engine interface { + Close(context.Context) error + Navigate(context.Context, string) error + WaitForSelector(context.Context, string) error + Click(context.Context, string) error + Fill(context.Context, string, string) error + Title(context.Context) (string, error) + URL(context.Context) (string, error) + Text(context.Context, string) (string, error) + Eval(context.Context, string) (string, error) + CaptureDOM(context.Context) (evidence.DomSnapshot, error) + ConsoleEvents() []evidence.ConsoleEvent + NetworkEvents() []evidence.NetworkEvent +} + +type BrowserConfigurator interface { + ConfigureBrowser(context.Context, BrowserConfig) error +} + +type Screenshotter interface { + CaptureScreenshot(context.Context, string, bool) (string, error) +} + +type PerformanceCollector interface { + CapturePerformance(context.Context) (evidence.PerformanceMetrics, error) +} + +type BrowserConfig struct { + Target string + Headers map[string]string + Cookies []BrowserCookie + IgnoreHTTPSErrors bool +} + +type BrowserCookie struct { + Name string + Value string + Domain string + Path string + Secure bool + HTTPOnly bool + SameSite string +} + +type EngineFactory func(context.Context, EngineOptions) (Engine, error) + +type EngineOptions struct { + LightpandaPath string + ChromePath string + ScreenshotDir string + RunID string + ViewportWidth int + ViewportHeight int + ProxyURL string + IgnoreHTTPSErrors bool + StartupTimeout time.Duration +} + +type Options struct { + ScriptPath string + Name string + TimeoutMS int + Tags map[string]string + EngineName string + LightpandaPath string + ChromePath string + StartupTimeout time.Duration + ScreenshotOnFailure bool + ScreenshotPerStep bool + ScreenshotDir string + ViewportWidth int + ViewportHeight int + Headers map[string]string + Cookies []script.Cookie + IgnoreHTTPSErrors bool + ProxyURL string + RetryCount int + EngineFactory EngineFactory + IgnoredOptionLogger func(option string, reason string) +} + +type Result struct { + RunID string `json:"run_id"` + Name string `json:"name"` + Engine string `json:"engine,omitempty"` + Target string `json:"target,omitempty"` + PostURL string `json:"post_url,omitempty"` + ScriptPath string `json:"script_path"` + Status evidence.RunStatus `json:"status"` + Success bool `json:"success"` + StartedAt time.Time `json:"-"` + StartedAtText string `json:"started_at"` + EndedAtText string `json:"ended_at"` + DurationUS int64 `json:"duration_us"` + TimeoutMS int `json:"timeout_ms"` + ViewportWidth int `json:"viewport_width,omitempty"` + ViewportHeight int `json:"viewport_height,omitempty"` + Steps []evidence.StepResult `json:"steps"` + Tags map[string]string `json:"tags"` + Metadata map[string]any `json:"metadata"` + ConsoleEvents []evidence.ConsoleEvent `json:"console_events"` + NetworkEvents []evidence.NetworkEvent `json:"network_events"` + TraceIDs []string `json:"trace_ids,omitempty"` + DomSnapshot *evidence.DomSnapshot `json:"dom_snapshot,omitempty"` + Performance *evidence.PerformanceMetrics `json:"performance,omitempty"` + Error *evidence.ErrorInfo `json:"error,omitempty"` + FailReason string `json:"fail_reason,omitempty"` + FailureType string `json:"failure_type,omitempty"` + Attempt int `json:"attempt,omitempty"` + MaxAttempts int `json:"max_attempts,omitempty"` + RetryRecords []evidence.RetryRecord `json:"retry_records,omitempty"` +} + +func Run(ctx context.Context, options Options) Result { + start := time.Now().UTC() + runID := util.NewRunID() + resolvedPath := options.ScriptPath + if options.ScriptPath != "" { + resolvedPath, _ = filepath.Abs(options.ScriptPath) + } + name := firstNonEmpty(options.Name, filepath.Base(options.ScriptPath)) + timeoutMS := options.TimeoutMS + if timeoutMS <= 0 { + timeoutMS = 30_000 + } + baseTags := cloneStringMap(options.Tags) + + loaded, err := script.Load(resolvedPath) + if err != nil { + return withViewport(failureResult(runID, name, resolvedPath, timeoutMS, start, baseTags, nil, err, "script_load_error"), options) + } + + return runLoaded(ctx, loaded, options, runID, resolvedPath, start, baseTags) +} + +func RunScript(ctx context.Context, loaded script.Script, options Options) Result { + start := time.Now().UTC() + runID := util.NewRunID() + baseTags := cloneStringMap(options.Tags) + if err := loaded.Validate(); err != nil { + name := firstNonEmpty(options.Name, loaded.Name, options.ScriptPath, "browser-task") + timeoutMS := options.TimeoutMS + if timeoutMS <= 0 { + timeoutMS = 30_000 + } + return withViewport(failureResult(runID, name, options.ScriptPath, timeoutMS, start, baseTags, &loaded, err, "script_load_error"), options) + } + return runLoaded(ctx, loaded, options, runID, options.ScriptPath, start, baseTags) +} + +func runLoaded(ctx context.Context, loaded script.Script, options Options, runID string, resolvedPath string, start time.Time, baseTags map[string]string) Result { + nameFallback := "browser-task" + if options.ScriptPath != "" { + nameFallback = filepath.Base(options.ScriptPath) + } + name := firstNonEmpty(options.Name, firstNonEmpty(loaded.Name, nameFallback)) + timeoutMS := options.TimeoutMS + if timeoutMS <= 0 { + timeoutMS = 30_000 + } + if loaded.TimeoutMS > 0 && options.TimeoutMS <= 0 { + timeoutMS = loaded.TimeoutMS + } + tags := mergeTags(baseTags, loaded.Tags) + vars := configVarMap(loaded.ConfigVars) + browserConfig, err := browserConfig(loaded, options, vars) + if err != nil { + return withViewport(failureResult(runID, name, resolvedPath, timeoutMS, start, tags, &loaded, err, "script_load_error"), options) + } + factory := options.EngineFactory + if factory == nil { + return withViewport(failureResult(runID, name, resolvedPath, timeoutMS, start, tags, &loaded, fmt.Errorf("runner engine factory is not configured"), "runner_error"), options) + } + engineName := normalizedEngineName(options.EngineName) + if engineName == "lightpanda" && strings.TrimSpace(loaded.ProxyURL) != "" { + logIgnoredOption(options, "proxy_url", "lightpanda does not support proxy_url") + } + maxAttempts := options.RetryCount + 1 + if maxAttempts < 1 { + maxAttempts = 1 + } + + var last Result + retryRecords := make([]evidence.RetryRecord, 0, maxAttempts) + for attempt := 1; attempt <= maxAttempts; attempt++ { + attemptStart := start + if attempt > 1 { + attemptStart = time.Now().UTC() + } + last = runAttempt(ctx, loaded, options, runID, resolvedPath, attemptStart, tags, vars, browserConfig, name, timeoutMS, engineName, attempt, maxAttempts) + if maxAttempts > 1 { + retryRecords = append(retryRecords, retryRecordFromResult(last)) + if len(retryRecords) > 1 || !last.Success { + last.RetryRecords = retryRecords + } + } + if last.Success { + return last + } + } + return last +} + +func retryRecordFromResult(result Result) evidence.RetryRecord { + record := evidence.RetryRecord{ + Attempt: result.Attempt, + StartedAt: result.StartedAtText, + EndedAt: result.EndedAtText, + DurationUS: result.DurationUS, + Status: result.Status, + Success: result.Success, + FailReason: result.FailReason, + FailureType: result.FailureType, + } + if result.Error != nil { + record.Message = result.Error.Message + } + for _, step := range result.Steps { + if step.Status != evidence.StatusFail { + continue + } + record.FailedStep = step.Seq + if record.Message == "" && step.Error != nil { + record.Message = step.Error.Message + } + break + } + return record +} + +func runAttempt(ctx context.Context, loaded script.Script, options Options, runID string, resolvedPath string, start time.Time, tags map[string]string, vars map[string]string, browserConfig BrowserConfig, name string, timeoutMS int, engineName string, attempt int, maxAttempts int) Result { + proxyURL := engineProxyURL(engineName, options.ProxyURL, loaded.ProxyURL) + engineCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutMS)*time.Millisecond) + defer cancel() + + engine, err := options.EngineFactory(engineCtx, EngineOptions{ + LightpandaPath: options.LightpandaPath, + ChromePath: options.ChromePath, + ScreenshotDir: options.ScreenshotDir, + RunID: runID, + ViewportWidth: options.ViewportWidth, + ViewportHeight: options.ViewportHeight, + ProxyURL: proxyURL, + IgnoreHTTPSErrors: browserConfig.IgnoreHTTPSErrors, + StartupTimeout: options.StartupTimeout, + }) + if err != nil { + reason := "runner_error" + if errors.Is(err, context.DeadlineExceeded) || errors.Is(engineCtx.Err(), context.DeadlineExceeded) { + err = errorsx.TimeoutError{TimeoutMS: timeoutMS} + reason = "timeout" + } + result := failureResult(runID, name, resolvedPath, timeoutMS, start, tags, &loaded, err, reason) + result.Engine = engineName + result.Attempt = attempt + result.MaxAttempts = maxAttempts + result.ViewportWidth = options.ViewportWidth + result.ViewportHeight = options.ViewportHeight + return result + } + defer engine.Close(context.Background()) + if configurator, ok := engine.(BrowserConfigurator); ok { + if err := configurator.ConfigureBrowser(engineCtx, browserConfig); err != nil { + reason := "runner_error" + if errors.Is(err, context.DeadlineExceeded) || errors.Is(engineCtx.Err(), context.DeadlineExceeded) { + err = errorsx.TimeoutError{TimeoutMS: timeoutMS} + reason = "timeout" + } + result := failureResult(runID, name, resolvedPath, timeoutMS, start, tags, &loaded, err, reason) + result.Engine = engineName + result.Attempt = attempt + result.MaxAttempts = maxAttempts + result.ViewportWidth = options.ViewportWidth + result.ViewportHeight = options.ViewportHeight + return result + } + } + + steps, runErr := executeSteps(engineCtx, engine, loaded, timeoutMS, vars, engineScreenshotOptions(engineName, screenshotOptions{ + OnFailure: options.ScreenshotOnFailure, + PerStep: options.ScreenshotPerStep, + Dir: options.ScreenshotDir, + RunID: runID, + })) + var dom *evidence.DomSnapshot + if runErr != nil { + if snapshot, err := engine.CaptureDOM(context.Background()); err == nil { + dom = &snapshot + } else { + dom = &evidence.DomSnapshot{CapturedAt: util.NowISO(), Error: err.Error()} + } + } + consoleEvents := engine.ConsoleEvents() + if consoleEvents == nil { + consoleEvents = []evidence.ConsoleEvent{} + } + networkEvents := engine.NetworkEvents() + if networkEvents == nil { + networkEvents = []evidence.NetworkEvent{} + } + var performance *evidence.PerformanceMetrics + if collector, ok := engine.(PerformanceCollector); ok { + perfCtx, perfCancel := context.WithTimeout(context.Background(), 2*time.Second) + if metrics, err := collector.CapturePerformance(perfCtx); err == nil && !isZeroPerformance(metrics) { + performance = &metrics + } + perfCancel() + } + + result := Result{ + RunID: runID, + Name: name, + Engine: engineName, + Target: loaded.Target, + PostURL: loaded.PostURL, + ScriptPath: resolvedPath, + Status: evidence.StatusOK, + Success: true, + StartedAt: start, + StartedAtText: start.Format(time.RFC3339Nano), + EndedAtText: time.Now().UTC().Format(time.RFC3339Nano), + DurationUS: time.Since(start).Microseconds(), + TimeoutMS: timeoutMS, + ViewportWidth: options.ViewportWidth, + ViewportHeight: options.ViewportHeight, + Steps: steps, + Tags: withNodeName(tags), + Metadata: loaded.Metadata, + ConsoleEvents: consoleEvents, + NetworkEvents: networkEvents, + TraceIDs: collectTraceIDs(networkEvents), + DomSnapshot: dom, + Performance: performance, + Attempt: attempt, + MaxAttempts: maxAttempts, + } + + if runErr != nil { + hasFailedStep := false + for _, step := range steps { + if step.Status == evidence.StatusFail { + hasFailedStep = true + break + } + } + result.Status = evidence.StatusFail + result.Success = false + result.Error = errorsx.ErrorInfo(runErr) + result.FailReason = errorsx.FailureReason(runErr, hasFailedStep) + result.FailureType = classifyFailureType(runErr, steps, result.FailReason) + } + return result +} + +func withViewport(result Result, options Options) Result { + result.ViewportWidth = options.ViewportWidth + result.ViewportHeight = options.ViewportHeight + return result +} + +func isZeroPerformance(metrics evidence.PerformanceMetrics) bool { + return metrics.TTFBMS == 0 && + metrics.LoadingTimeMS == 0 && + metrics.LCPMS == 0 && + metrics.CLS == 0 && + metrics.DOMContentLoadedMS == 0 && + metrics.LoadEventEndMS == 0 +} + +func browserConfig(loaded script.Script, options Options, vars map[string]string) (BrowserConfig, error) { + headers := cloneStringMap(loaded.Headers) + for key, value := range options.Headers { + headers[key] = value + } + cookies := make([]BrowserCookie, 0, len(loaded.Cookies)+len(options.Cookies)) + for _, cookie := range append(append([]script.Cookie{}, loaded.Cookies...), options.Cookies...) { + value := cookie.Value + if strings.TrimSpace(cookie.ValueFrom) != "" { + var ok bool + value, ok = vars[strings.TrimSpace(cookie.ValueFrom)] + if !ok { + return BrowserConfig{}, fmt.Errorf("cookie variable %q is required", cookie.ValueFrom) + } + } + cookies = append(cookies, BrowserCookie{ + Name: strings.TrimSpace(cookie.Name), + Value: value, + Domain: strings.TrimSpace(cookie.Domain), + Path: firstNonEmpty(strings.TrimSpace(cookie.Path), "/"), + Secure: cookie.Secure, + HTTPOnly: cookie.HTTPOnly, + SameSite: strings.TrimSpace(cookie.SameSite), + }) + } + return BrowserConfig{ + Target: cookieTarget(loaded), + Headers: headers, + Cookies: cookies, + IgnoreHTTPSErrors: loaded.IgnoreHTTPSErrors || options.IgnoreHTTPSErrors, + }, nil +} + +func cookieTarget(loaded script.Script) string { + if strings.TrimSpace(loaded.Target) != "" { + return loaded.Target + } + for _, step := range stepPlans(loaded) { + if step.Step.Action == "goto" && strings.TrimSpace(step.Step.URL) != "" { + return step.Step.URL + } + } + return "" +} + +type screenshotOptions struct { + OnFailure bool + PerStep bool + Dir string + RunID string +} + +func engineProxyURL(engineName string, values ...string) string { + if engineName == "lightpanda" { + return "" + } + return firstNonEmpty(values...) +} + +func logIgnoredOption(options Options, option string, reason string) { + if options.IgnoredOptionLogger == nil { + return + } + options.IgnoredOptionLogger(option, reason) +} + +func engineScreenshotOptions(engineName string, options screenshotOptions) screenshotOptions { + if engineName == "lightpanda" { + return screenshotOptions{Dir: options.Dir, RunID: options.RunID} + } + return options +} + +func collectTraceIDs(events []evidence.NetworkEvent) []string { + traceIDs := []string{} + seen := map[string]struct{}{} + for _, event := range events { + if event.TraceID == "" { + continue + } + if _, ok := seen[event.TraceID]; ok { + continue + } + seen[event.TraceID] = struct{}{} + traceIDs = append(traceIDs, event.TraceID) + } + return traceIDs +} + +func executeSteps(ctx context.Context, engine Engine, s script.Script, timeoutMS int, vars map[string]string, screenshots screenshotOptions) ([]evidence.StepResult, error) { + plans := stepPlans(s) + steps := make([]evidence.StepResult, 0, len(plans)) + if strings.EqualFold(s.Auth.Mode, "form") { + // Auth steps are already included in the flattened plan. + } + for index, plan := range plans { + step := plan.Step + stepStart := time.Now().UTC() + stepCtx := ctx + cancel := func() {} + if step.TimeoutMS > 0 { + stepCtx, cancel = context.WithTimeout(ctx, time.Duration(step.TimeoutMS)*time.Millisecond) + } + err := executeStep(stepCtx, engine, s, step, vars) + runErr := ctx.Err() + stepErr := stepCtx.Err() + cancel() + + currentURL, _ := engine.URL(context.Background()) + title, _ := engine.Title(context.Background()) + seq := index + 1 + record := stepRecord(seq, step, plan.Auth, evidence.StatusOK) + record.StartedAt = stepStart.Format(time.RFC3339Nano) + record.EndedAt = time.Now().UTC().Format(time.RFC3339Nano) + record.DurationUS = time.Since(stepStart).Microseconds() + record.URL = currentURL + record.Title = title + if err == nil && step.Action == "goto" { + record.Performance = captureStepPerformance(engine) + } + if err == nil && screenshots.PerStep { + captureStepScreenshot(context.Background(), engine, &record, screenshots, false) + } + if err != nil { + if errors.Is(runErr, context.DeadlineExceeded) { + err = errorsx.TimeoutError{TimeoutMS: timeoutMS} + } else if errors.Is(stepErr, context.DeadlineExceeded) { + err = errorsx.TimeoutError{TimeoutMS: deadlineTimeoutMS(ctx, stepCtx, timeoutMS, step.TimeoutMS)} + } else if errors.Is(err, context.DeadlineExceeded) { + err = errorsx.TimeoutError{TimeoutMS: deadlineTimeoutMS(ctx, stepCtx, timeoutMS, step.TimeoutMS)} + } + if plan.Auth { + err = errorsx.AuthError{Err: err} + } + record.Status = evidence.StatusFail + record.Error = errorsx.ErrorInfo(err) + if screenshots.OnFailure || screenshots.PerStep { + captureStepScreenshot(context.Background(), engine, &record, screenshots, false) + } + if record.Screenshot == "" && (screenshots.OnFailure || screenshots.PerStep) && record.Error != nil { + record.Error.Message = record.Error.Message + "; screenshot capture unavailable" + } + steps = append(steps, record) + steps = appendSkippedSteps(steps, plans[index+1:], seq+1) + return steps, err + } + steps = append(steps, record) + } + return steps, nil +} + +func captureStepPerformance(engine Engine) *evidence.PerformanceMetrics { + collector, ok := engine.(PerformanceCollector) + if !ok { + return nil + } + perfCtx, perfCancel := context.WithTimeout(context.Background(), 2*time.Second) + defer perfCancel() + metrics, err := collector.CapturePerformance(perfCtx) + if err != nil || isZeroPerformance(metrics) { + return nil + } + return &metrics +} + +func deadlineTimeoutMS(runCtx context.Context, stepCtx context.Context, runTimeoutMS int, stepTimeoutMS int) int { + if stepTimeoutMS <= 0 { + return runTimeoutMS + } + runDeadline, hasRunDeadline := runCtx.Deadline() + stepDeadline, hasStepDeadline := stepCtx.Deadline() + if hasStepDeadline && (!hasRunDeadline || stepDeadline.Before(runDeadline)) { + return stepTimeoutMS + } + return runTimeoutMS +} + +type stepPlan struct { + Step script.Step + Auth bool +} + +func stepPlans(s script.Script) []stepPlan { + plans := make([]stepPlan, 0, len(s.Auth.Steps)+len(s.Steps)) + if strings.EqualFold(s.Auth.Mode, "form") { + for _, step := range s.Auth.Steps { + plans = append(plans, stepPlan{Step: step, Auth: true}) + } + } + for _, step := range s.Steps { + plans = append(plans, stepPlan{Step: step}) + } + return plans +} + +func appendSkippedSteps(results []evidence.StepResult, plans []stepPlan, startSeq int) []evidence.StepResult { + for index, plan := range plans { + record := stepRecord(startSeq+index, plan.Step, plan.Auth, evidence.StatusSkip) + record.SkipReason = "previous_step_failed" + results = append(results, record) + } + return results +} + +func stepRecord(seq int, step script.Step, auth bool, status evidence.RunStatus) evidence.StepResult { + mode, expected := script.Expected(step) + if expected != "" { + expected = mode + ":" + expected + } + record := evidence.StepResult{ + Seq: seq, + Name: step.StepName(seq), + Action: step.Action, + Selector: step.Selector, + ValueFrom: step.ValueFrom, + Expected: expected, + TimeoutMS: step.TimeoutMS, + Auth: auth, + Status: status, + InputDisplay: inputDisplay(step), + } + return record +} + +func inputDisplay(step script.Step) string { + if step.Action != "fill" { + return "" + } + if step.Sensitive { + return "***" + } + if strings.TrimSpace(step.ValueFrom) != "" { + return fmt.Sprintf("${%s}", strings.TrimSpace(step.ValueFrom)) + } + return step.Value +} + +func captureStepScreenshot(ctx context.Context, engine Engine, record *evidence.StepResult, options screenshotOptions, fullPage bool) { + screenshotter, ok := engine.(Screenshotter) + if !ok { + return + } + extension := ".png" + if fullPage { + extension = ".jpg" + } + path := filepath.Join(options.Dir, options.RunID, fmt.Sprintf("step-%d%s", record.Seq, extension)) + if options.Dir == "" { + path = filepath.Join(os.TempDir(), "browser-dial-evidence", options.RunID, fmt.Sprintf("step-%d%s", record.Seq, extension)) + } + saved, err := screenshotter.CaptureScreenshot(ctx, path, fullPage) + if err == nil { + record.Screenshot = saved + } +} + +func normalizedEngineName(name string) string { + switch strings.TrimSpace(strings.ToLower(name)) { + case "": + return "chrome" + case "lightpanda": + return "lightpanda" + default: + return strings.TrimSpace(strings.ToLower(name)) + } +} + +func executeStep(ctx context.Context, engine Engine, s script.Script, step script.Step, vars map[string]string) error { + switch step.Action { + case "goto": + target := step.URL + if target == "" { + target = s.Target + } + return engine.Navigate(ctx, target) + case "wait_for_selector": + return engine.WaitForSelector(ctx, step.Selector) + case "click": + return engine.Click(ctx, step.Selector) + case "fill": + value, err := stepValue(step, vars) + if err != nil { + return err + } + return engine.Fill(ctx, step.Selector, value) + case "assert_title": + title, err := engine.Title(ctx) + if err != nil { + return err + } + return compare("title", title, step) + case "assert_url": + currentURL, err := engine.URL(ctx) + if err != nil { + return err + } + return compare("url", currentURL, step) + case "assert_text": + text, err := engine.Text(ctx, step.Selector) + if err != nil { + return err + } + return compare("text", text, step) + case "eval": + expression := step.Value + if expression == "" { + expression = step.Text + } + _, err := engine.Eval(ctx, expression) + return err + default: + return fmt.Errorf("unsupported action %q", step.Action) + } +} + +func stepValue(step script.Step, vars map[string]string) (string, error) { + name := strings.TrimSpace(step.ValueFrom) + if name == "" { + return step.Value, nil + } + value, ok := vars[name] + if !ok { + return "", fmt.Errorf("config variable %q is required", name) + } + return value, nil +} + +func configVarMap(vars []script.ConfigVar) map[string]string { + out := map[string]string{} + for _, variable := range vars { + out[strings.TrimSpace(variable.Name)] = variable.Value + } + return out +} + +func compare(label string, actual string, step script.Step) error { + mode, expected := script.Expected(step) + switch mode { + case "equals": + if actual != expected { + return fmt.Errorf("%s assertion failed: expected %q, got %q", label, expected, actual) + } + default: + if !strings.Contains(actual, expected) { + return fmt.Errorf("%s assertion failed: expected %q to contain %q", label, actual, expected) + } + } + return nil +} + +func failureResult(runID string, name string, scriptPath string, timeoutMS int, start time.Time, tags map[string]string, loaded *script.Script, err error, reason string) Result { + target := "" + metadata := map[string]any{} + if loaded != nil { + target = loaded.Target + metadata = loaded.Metadata + } + postURL := "" + if loaded != nil { + postURL = loaded.PostURL + } + return Result{ + RunID: runID, + Name: name, + Target: target, + PostURL: postURL, + ScriptPath: scriptPath, + Status: evidence.StatusFail, + Success: false, + StartedAt: start, + StartedAtText: start.Format(time.RFC3339Nano), + EndedAtText: time.Now().UTC().Format(time.RFC3339Nano), + DurationUS: time.Since(start).Microseconds(), + TimeoutMS: timeoutMS, + Steps: []evidence.StepResult{}, + Tags: withNodeName(tags), + Metadata: metadata, + ConsoleEvents: []evidence.ConsoleEvent{}, + NetworkEvents: []evidence.NetworkEvent{}, + Error: errorsx.ErrorInfo(err), + FailReason: reason, + FailureType: classifyFailureType(err, nil, reason), + } +} + +func classifyFailureType(err error, steps []evidence.StepResult, failReason string) string { + if err == nil { + return "" + } + var authErr errorsx.AuthError + if errors.As(err, &authErr) { + return "auth_failed" + } + var timeoutErr errorsx.TimeoutError + if errors.As(err, &timeoutErr) { + return "timeout" + } + message := strings.ToLower(err.Error()) + if strings.Contains(message, "config variable") || strings.Contains(message, "cookie variable") { + return "config_error" + } + for _, step := range steps { + if step.Status != evidence.StatusFail { + continue + } + action := strings.TrimSpace(step.Action) + if strings.Contains(message, "selector not found") { + return "selector_not_found" + } + switch action { + case "assert_title", "assert_url", "assert_text": + return "assertion_failed" + case "goto": + return "navigation_failed" + case "wait_for_selector", "click", "fill": + return "selector_not_found" + case "eval": + return "script_error" + } + } + switch failReason { + case "script_load_error": + return "config_error" + case "runner_error": + return "browser_error" + case "script_error": + return "script_error" + } + return "browser_error" +} + +func mergeTags(first map[string]string, second map[string]string) map[string]string { + out := cloneStringMap(first) + for key, value := range second { + out[key] = value + } + return out +} + +func cloneStringMap(input map[string]string) map[string]string { + out := map[string]string{} + for key, value := range input { + out[key] = value + } + return out +} + +func withNodeName(tags map[string]string) map[string]string { + out := cloneStringMap(tags) + if out["node_name"] == "" { + host, _ := os.Hostname() + out["node_name"] = host + } + return util.SanitizeTags(out) +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} diff --git a/internal/browserdial/script/script.go b/internal/browserdial/script/script.go new file mode 100644 index 00000000..6162ec98 --- /dev/null +++ b/internal/browserdial/script/script.go @@ -0,0 +1,248 @@ +package script + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + errorsx "github.com/GuanceCloud/cliutils/internal/browserdial/errors" + "gopkg.in/yaml.v3" +) + +type Script struct { + Name string `json:"name" yaml:"name"` + Target string `json:"target" yaml:"target"` + PostURL string `json:"post_url" yaml:"post_url"` + TimeoutMS int `json:"timeout_ms" yaml:"timeout_ms"` + Headers map[string]string `json:"headers" yaml:"headers"` + Cookies []Cookie `json:"cookies" yaml:"cookies"` + IgnoreHTTPSErrors bool `json:"ignore_https_errors" yaml:"ignore_https_errors"` + ProxyURL string `json:"proxy_url" yaml:"proxy_url"` + Tags map[string]string `json:"tags" yaml:"tags"` + Metadata map[string]any `json:"metadata" yaml:"metadata"` + Auth Auth `json:"auth" yaml:"auth"` + ConfigVars []ConfigVar `json:"config_vars" yaml:"config_vars"` + Steps []Step `json:"steps" yaml:"steps"` +} + +type Auth struct { + Mode string `json:"mode" yaml:"mode"` + Steps []Step `json:"steps" yaml:"steps"` +} + +type ConfigVar struct { + Name string `json:"name" yaml:"name"` + Value string `json:"value" yaml:"value"` + Secure bool `json:"secure" yaml:"secure"` +} + +type Cookie struct { + Name string `json:"name" yaml:"name"` + Value string `json:"value" yaml:"value"` + ValueFrom string `json:"value_from" yaml:"value_from"` + Domain string `json:"domain" yaml:"domain"` + Path string `json:"path" yaml:"path"` + Secure bool `json:"secure" yaml:"secure"` + HTTPOnly bool `json:"http_only" yaml:"http_only"` + SameSite string `json:"same_site" yaml:"same_site"` +} + +type Step struct { + Name string `json:"name" yaml:"name"` + Action string `json:"action" yaml:"action"` + URL string `json:"url" yaml:"url"` + Selector string `json:"selector" yaml:"selector"` + Value string `json:"value" yaml:"value"` + ValueFrom string `json:"value_from" yaml:"value_from"` + Text string `json:"text" yaml:"text"` + Contains string `json:"contains" yaml:"contains"` + Equals string `json:"equals" yaml:"equals"` + TimeoutMS int `json:"timeout_ms" yaml:"timeout_ms"` + Sensitive bool `json:"sensitive" yaml:"sensitive"` +} + +var supportedActions = map[string]struct{}{ + "goto": {}, + "wait_for_selector": {}, + "click": {}, + "fill": {}, + "assert_title": {}, + "assert_url": {}, + "assert_text": {}, + "eval": {}, +} + +func Load(path string) (Script, error) { + content, err := os.ReadFile(path) + if err != nil { + return Script{}, errorsx.ScriptLoadError{Message: err.Error()} + } + + var out Script + switch strings.ToLower(filepath.Ext(path)) { + case ".json": + if err := json.Unmarshal(content, &out); err != nil { + return Script{}, errorsx.ScriptLoadError{Message: fmt.Sprintf("parse %s: %v", path, err)} + } + default: + if err := yaml.Unmarshal(content, &out); err != nil { + return Script{}, errorsx.ScriptLoadError{Message: fmt.Sprintf("parse %s: %v", path, err)} + } + } + + if err := out.Validate(); err != nil { + return Script{}, errorsx.ScriptLoadError{Message: err.Error()} + } + return out, nil +} + +func (s Script) Validate() error { + if err := s.validateConfigVars(); err != nil { + return err + } + if err := s.validateHeadersAndCookies(); err != nil { + return err + } + if err := s.validateAuth(); err != nil { + return err + } + if len(s.Steps) == 0 { + return fmt.Errorf("script must define at least one step") + } + for index, step := range s.Steps { + if err := s.validateStep("step", index, step); err != nil { + return err + } + } + return nil +} + +func (s Script) validateHeadersAndCookies() error { + for key := range s.Headers { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("headers must not contain an empty key") + } + } + for index, cookie := range s.Cookies { + if strings.TrimSpace(cookie.Name) == "" { + return fmt.Errorf("cookies %d name is required", index+1) + } + if strings.TrimSpace(cookie.ValueFrom) != "" && cookie.Value != "" { + return fmt.Errorf("cookies %q must not define both value and value_from", cookie.Name) + } + switch strings.ToLower(strings.TrimSpace(cookie.SameSite)) { + case "", "lax", "strict", "none": + default: + return fmt.Errorf("cookies %q same_site must be Lax, Strict, or None", cookie.Name) + } + } + return nil +} + +func (s Script) validateConfigVars() error { + seen := map[string]struct{}{} + for index, variable := range s.ConfigVars { + name := strings.TrimSpace(variable.Name) + if name == "" { + return fmt.Errorf("config_vars %d name is required", index+1) + } + if _, ok := seen[name]; ok { + return fmt.Errorf("config_vars %q is duplicated", name) + } + seen[name] = struct{}{} + } + return nil +} + +func (s Script) validateAuth() error { + mode := strings.TrimSpace(strings.ToLower(s.Auth.Mode)) + if mode == "" { + mode = "none" + } + switch mode { + case "none": + if len(s.Auth.Steps) > 0 { + return fmt.Errorf("auth.steps requires auth.mode form") + } + case "form": + if len(s.Auth.Steps) == 0 { + return fmt.Errorf("auth.steps must define at least one step when auth.mode is form") + } + default: + return fmt.Errorf("auth.mode %q is not supported", s.Auth.Mode) + } + for index, step := range s.Auth.Steps { + if err := s.validateStep("auth step", index, step); err != nil { + return err + } + } + return nil +} + +func (s Script) validateStep(label string, index int, step Step) error { + action := strings.TrimSpace(step.Action) + if action == "" { + return fmt.Errorf("%s %d must define action", label, index+1) + } + if _, ok := supportedActions[action]; !ok { + return fmt.Errorf("%s %d action %q is not supported", label, index+1, action) + } + switch action { + case "goto": + if step.URL == "" && s.Target == "" { + return fmt.Errorf("%s %d goto requires url or top-level target", label, index+1) + } + case "wait_for_selector", "click", "fill": + if step.Selector == "" { + return fmt.Errorf("%s %d %s requires selector", label, index+1, action) + } + if action == "fill" && step.Sensitive { + if strings.TrimSpace(step.ValueFrom) == "" { + return fmt.Errorf("%s %d sensitive fill requires value_from", label, index+1) + } + if step.Value != "" { + return fmt.Errorf("%s %d sensitive fill must not define value", label, index+1) + } + } + case "assert_text": + if step.Selector == "" { + return fmt.Errorf("%s %d assert_text requires selector", label, index+1) + } + if expectedText(step) == "" { + return fmt.Errorf("%s %d assert_text requires contains, equals, or text", label, index+1) + } + case "assert_title", "assert_url": + if expectedText(step) == "" { + return fmt.Errorf("%s %d %s requires contains, equals, or text", label, index+1, action) + } + case "eval": + if step.Value == "" && step.Text == "" { + return fmt.Errorf("%s %d eval requires value or text", label, index+1) + } + } + return nil +} + +func (s Step) StepName(index int) string { + if strings.TrimSpace(s.Name) != "" { + return s.Name + } + return fmt.Sprintf("%02d %s", index, s.Action) +} + +func Expected(step Step) (mode string, expected string) { + if step.Equals != "" { + return "equals", step.Equals + } + if step.Contains != "" { + return "contains", step.Contains + } + return "contains", step.Text +} + +func expectedText(step Step) string { + _, expected := Expected(step) + return expected +} diff --git a/internal/browserdial/util/sanitize.go b/internal/browserdial/util/sanitize.go new file mode 100644 index 00000000..eac03d7f --- /dev/null +++ b/internal/browserdial/util/sanitize.go @@ -0,0 +1,78 @@ +package util + +import ( + "encoding/json" + "regexp" + "strings" + "unicode" +) + +var nonKeyChar = regexp.MustCompile(`[^A-Za-z0-9_]+`) +var repeatedUnderscore = regexp.MustCompile(`_+`) + +func SanitizeKey(input string, fallback string) string { + cleaned := strings.TrimSpace(input) + cleaned = camelToSnake(cleaned) + cleaned = nonKeyChar.ReplaceAllString(cleaned, "_") + cleaned = repeatedUnderscore.ReplaceAllString(cleaned, "_") + cleaned = strings.Trim(cleaned, "_") + cleaned = strings.ToLower(cleaned) + if cleaned == "" { + cleaned = fallback + } + if cleaned != "" && cleaned[0] >= '0' && cleaned[0] <= '9' { + return "k_" + cleaned + } + return cleaned +} + +func SanitizeTags(tags map[string]string) map[string]string { + out := make(map[string]string, len(tags)) + for key, value := range tags { + out[SanitizeKey(key, "tag")] = value + } + return out +} + +func SanitizeFields(fields map[string]any) map[string]any { + out := make(map[string]any, len(fields)) + for key, value := range fields { + if value == nil { + continue + } + out[SanitizeKey(key, "field")] = value + } + return out +} + +func Truncate(value string, maxLen int) string { + if maxLen <= 0 || len(value) <= maxLen { + return value + } + suffix := "...[truncated]" + if maxLen <= len(suffix) { + return value[:maxLen] + } + return value[:maxLen-len(suffix)] + suffix +} + +func JSONString(value any, maxLen int) string { + bytes, err := json.Marshal(value) + if err != nil { + return Truncate(`"`+err.Error()+`"`, maxLen) + } + return Truncate(string(bytes), maxLen) +} + +func camelToSnake(input string) string { + var out strings.Builder + var prev rune + for i, current := range input { + if i > 0 && unicode.IsUpper(current) && (unicode.IsLower(prev) || unicode.IsDigit(prev)) { + out.WriteRune('_') + } + out.WriteRune(current) + prev = current + } + return out.String() +} diff --git a/internal/browserdial/util/time.go b/internal/browserdial/util/time.go new file mode 100644 index 00000000..8733eb0f --- /dev/null +++ b/internal/browserdial/util/time.go @@ -0,0 +1,11 @@ +package util + +import "time" + +func NowISO() string { + return time.Now().UTC().Format(time.RFC3339Nano) +} + +func DurationUS(start time.Time) int64 { + return time.Since(start).Microseconds() +} diff --git a/internal/browserdial/util/uuid.go b/internal/browserdial/util/uuid.go new file mode 100644 index 00000000..85297452 --- /dev/null +++ b/internal/browserdial/util/uuid.go @@ -0,0 +1,17 @@ +package util + +import ( + "crypto/rand" + "fmt" + "time" +) + +func NewRunID() string { + var bytes [16]byte + if _, err := rand.Read(bytes[:]); err != nil { + return fmt.Sprintf("run-%d", time.Now().UnixNano()) + } + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16]) +} diff --git a/point/kvs_test.go b/point/kvs_test.go index 96e909a2..604eaaa8 100644 --- a/point/kvs_test.go +++ b/point/kvs_test.go @@ -608,7 +608,7 @@ func Test_shuffle(t *T.T) { p := NewPoint(t.Name(), kvs) - t.Logf(p.Pretty()) + t.Logf("%s", p.Pretty()) } func TestShuffleGzip(t *T.T) { diff --git a/point/point_test.go b/point/point_test.go index 85bc428c..ce4396d5 100644 --- a/point/point_test.go +++ b/point/point_test.go @@ -927,7 +927,7 @@ func FuzzPBPointString(f *T.F) { assert.NoError(t, err) if pt != nil { - t.Logf(pt.Pretty()) + t.Logf("%s", pt.Pretty()) } }) } diff --git a/vendor/github.com/chromedp/cdproto/.gitignore b/vendor/github.com/chromedp/cdproto/.gitignore new file mode 100644 index 00000000..c456f532 --- /dev/null +++ b/vendor/github.com/chromedp/cdproto/.gitignore @@ -0,0 +1,2 @@ +*.json +*.pdl diff --git a/vendor/github.com/go-task/slim-sprig/LICENSE.txt b/vendor/github.com/chromedp/cdproto/LICENSE similarity index 86% rename from vendor/github.com/go-task/slim-sprig/LICENSE.txt rename to vendor/github.com/chromedp/cdproto/LICENSE index f311b1ea..b9ba4a54 100644 --- a/vendor/github.com/go-task/slim-sprig/LICENSE.txt +++ b/vendor/github.com/chromedp/cdproto/LICENSE @@ -1,4 +1,6 @@ -Copyright (C) 2013-2020 Masterminds +The MIT License (MIT) + +Copyright (c) 2016-2025 Kenneth Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -7,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/chromedp/cdproto/README.md b/vendor/github.com/chromedp/cdproto/README.md new file mode 100644 index 00000000..a21f77ca --- /dev/null +++ b/vendor/github.com/chromedp/cdproto/README.md @@ -0,0 +1,29 @@ +# About cdproto + +Package `cdproto` contains the generated commands, types, and events for the +[Chrome DevTools Protocol domains][devtools-protocol]. + +[![Unit Tests][cdproto-ci-status]][cdproto-ci] +[![Go Reference][goref-cdproto-status]][goref-cdproto] + +This package is generated by the [`cdproto-gen`][cdproto-gen] command. Please +refer to that project and to the main [`chromedp`][chromedp] project for +information on using the commands, types, and events available here. + +## API + +Please see the [Go Reference][goref-cdproto]. + +## Contributing + +If you would like to submit a change to the code in this package, please submit +your Pull Request to the [`cdproto-gen`][cdproto-gen] project. Any Issues and +Pull Requests submitted to this project will be closed without being reviewed. + +[cdproto-ci]: https://github.com/chromedp/cdproto/actions/workflows/build.yml (Build CI) +[cdproto-ci-status]: https://github.com/chromedp/cdproto/actions/workflows/build.yml/badge.svg (Build CI) +[cdproto-gen]: https://github.com/chromedp/cdproto-gen +[chromedp]: https://github.com/chromedp/chromedp +[devtools-protocol]: https://chromedevtools.github.io/devtools-protocol/ +[goref-cdproto]: https://pkg.go.dev/github.com/chromedp/cdproto +[goref-cdproto-status]: https://pkg.go.dev/badge/github.com/chromedp/chromedp.svg diff --git a/vendor/github.com/chromedp/cdproto/accessibility/accessibility.go b/vendor/github.com/chromedp/cdproto/accessibility/accessibility.go new file mode 100644 index 00000000..80d0ca82 --- /dev/null +++ b/vendor/github.com/chromedp/cdproto/accessibility/accessibility.go @@ -0,0 +1,413 @@ +// Package accessibility provides the Chrome DevTools Protocol +// commands, types, and events for the Accessibility domain. +// +// Generated by the cdproto-gen command. +package accessibility + +// Code generated by cdproto-gen. DO NOT EDIT. + +import ( + "context" + + "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/runtime" +) + +// DisableParams disables the accessibility domain. +type DisableParams struct{} + +// Disable disables the accessibility domain. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-disable +func Disable() *DisableParams { + return &DisableParams{} +} + +// Do executes Accessibility.disable against the provided context. +func (p *DisableParams) Do(ctx context.Context) (err error) { + return cdp.Execute(ctx, CommandDisable, nil, nil) +} + +// EnableParams enables the accessibility domain which causes AXNodeIds to +// remain consistent between method calls. This turns on accessibility for the +// page, which can impact performance until accessibility is disabled. +type EnableParams struct{} + +// Enable enables the accessibility domain which causes AXNodeIds to remain +// consistent between method calls. This turns on accessibility for the page, +// which can impact performance until accessibility is disabled. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-enable +func Enable() *EnableParams { + return &EnableParams{} +} + +// Do executes Accessibility.enable against the provided context. +func (p *EnableParams) Do(ctx context.Context) (err error) { + return cdp.Execute(ctx, CommandEnable, nil, nil) +} + +// GetPartialAXTreeParams fetches the accessibility node and partial +// accessibility tree for this DOM node, if it exists. +type GetPartialAXTreeParams struct { + NodeID cdp.NodeID `json:"nodeId,omitempty,omitzero"` // Identifier of the node to get the partial accessibility tree for. + BackendNodeID cdp.BackendNodeID `json:"backendNodeId,omitempty,omitzero"` // Identifier of the backend node to get the partial accessibility tree for. + ObjectID runtime.RemoteObjectID `json:"objectId,omitempty,omitzero"` // JavaScript object id of the node wrapper to get the partial accessibility tree for. + FetchRelatives bool `json:"fetchRelatives"` // Whether to fetch this node's ancestors, siblings and children. Defaults to true. +} + +// GetPartialAXTree fetches the accessibility node and partial accessibility +// tree for this DOM node, if it exists. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-getPartialAXTree +// +// parameters: +func GetPartialAXTree() *GetPartialAXTreeParams { + return &GetPartialAXTreeParams{ + FetchRelatives: true, + } +} + +// WithNodeID identifier of the node to get the partial accessibility tree +// for. +func (p GetPartialAXTreeParams) WithNodeID(nodeID cdp.NodeID) *GetPartialAXTreeParams { + p.NodeID = nodeID + return &p +} + +// WithBackendNodeID identifier of the backend node to get the partial +// accessibility tree for. +func (p GetPartialAXTreeParams) WithBackendNodeID(backendNodeID cdp.BackendNodeID) *GetPartialAXTreeParams { + p.BackendNodeID = backendNodeID + return &p +} + +// WithObjectID JavaScript object id of the node wrapper to get the partial +// accessibility tree for. +func (p GetPartialAXTreeParams) WithObjectID(objectID runtime.RemoteObjectID) *GetPartialAXTreeParams { + p.ObjectID = objectID + return &p +} + +// WithFetchRelatives whether to fetch this node's ancestors, siblings and +// children. Defaults to true. +func (p GetPartialAXTreeParams) WithFetchRelatives(fetchRelatives bool) *GetPartialAXTreeParams { + p.FetchRelatives = fetchRelatives + return &p +} + +// GetPartialAXTreeReturns return values. +type GetPartialAXTreeReturns struct { + Nodes []*Node `json:"nodes,omitempty,omitzero"` // The Accessibility.AXNode for this DOM node, if it exists, plus its ancestors, siblings and children, if requested. +} + +// Do executes Accessibility.getPartialAXTree against the provided context. +// +// returns: +// +// nodes - The Accessibility.AXNode for this DOM node, if it exists, plus its ancestors, siblings and children, if requested. +func (p *GetPartialAXTreeParams) Do(ctx context.Context) (nodes []*Node, err error) { + // execute + var res GetPartialAXTreeReturns + err = cdp.Execute(ctx, CommandGetPartialAXTree, p, &res) + if err != nil { + return nil, err + } + + return res.Nodes, nil +} + +// GetFullAXTreeParams fetches the entire accessibility tree for the root +// Document. +type GetFullAXTreeParams struct { + Depth int64 `json:"depth,omitempty,omitzero"` // The maximum depth at which descendants of the root node should be retrieved. If omitted, the full tree is returned. + FrameID cdp.FrameID `json:"frameId,omitempty,omitzero"` // The frame for whose document the AX tree should be retrieved. If omitted, the root frame is used. +} + +// GetFullAXTree fetches the entire accessibility tree for the root Document. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-getFullAXTree +// +// parameters: +func GetFullAXTree() *GetFullAXTreeParams { + return &GetFullAXTreeParams{} +} + +// WithDepth the maximum depth at which descendants of the root node should +// be retrieved. If omitted, the full tree is returned. +func (p GetFullAXTreeParams) WithDepth(depth int64) *GetFullAXTreeParams { + p.Depth = depth + return &p +} + +// WithFrameID the frame for whose document the AX tree should be retrieved. +// If omitted, the root frame is used. +func (p GetFullAXTreeParams) WithFrameID(frameID cdp.FrameID) *GetFullAXTreeParams { + p.FrameID = frameID + return &p +} + +// GetFullAXTreeReturns return values. +type GetFullAXTreeReturns struct { + Nodes []*Node `json:"nodes,omitempty,omitzero"` +} + +// Do executes Accessibility.getFullAXTree against the provided context. +// +// returns: +// +// nodes +func (p *GetFullAXTreeParams) Do(ctx context.Context) (nodes []*Node, err error) { + // execute + var res GetFullAXTreeReturns + err = cdp.Execute(ctx, CommandGetFullAXTree, p, &res) + if err != nil { + return nil, err + } + + return res.Nodes, nil +} + +// GetRootAXNodeParams fetches the root node. Requires enable() to have been +// called previously. +type GetRootAXNodeParams struct { + FrameID cdp.FrameID `json:"frameId,omitempty,omitzero"` // The frame in whose document the node resides. If omitted, the root frame is used. +} + +// GetRootAXNode fetches the root node. Requires enable() to have been called +// previously. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-getRootAXNode +// +// parameters: +func GetRootAXNode() *GetRootAXNodeParams { + return &GetRootAXNodeParams{} +} + +// WithFrameID the frame in whose document the node resides. If omitted, the +// root frame is used. +func (p GetRootAXNodeParams) WithFrameID(frameID cdp.FrameID) *GetRootAXNodeParams { + p.FrameID = frameID + return &p +} + +// GetRootAXNodeReturns return values. +type GetRootAXNodeReturns struct { + Node *Node `json:"node,omitempty,omitzero"` +} + +// Do executes Accessibility.getRootAXNode against the provided context. +// +// returns: +// +// node +func (p *GetRootAXNodeParams) Do(ctx context.Context) (node *Node, err error) { + // execute + var res GetRootAXNodeReturns + err = cdp.Execute(ctx, CommandGetRootAXNode, p, &res) + if err != nil { + return nil, err + } + + return res.Node, nil +} + +// GetAXNodeAndAncestorsParams fetches a node and all ancestors up to and +// including the root. Requires enable() to have been called previously. +type GetAXNodeAndAncestorsParams struct { + NodeID cdp.NodeID `json:"nodeId,omitempty,omitzero"` // Identifier of the node to get. + BackendNodeID cdp.BackendNodeID `json:"backendNodeId,omitempty,omitzero"` // Identifier of the backend node to get. + ObjectID runtime.RemoteObjectID `json:"objectId,omitempty,omitzero"` // JavaScript object id of the node wrapper to get. +} + +// GetAXNodeAndAncestors fetches a node and all ancestors up to and including +// the root. Requires enable() to have been called previously. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-getAXNodeAndAncestors +// +// parameters: +func GetAXNodeAndAncestors() *GetAXNodeAndAncestorsParams { + return &GetAXNodeAndAncestorsParams{} +} + +// WithNodeID identifier of the node to get. +func (p GetAXNodeAndAncestorsParams) WithNodeID(nodeID cdp.NodeID) *GetAXNodeAndAncestorsParams { + p.NodeID = nodeID + return &p +} + +// WithBackendNodeID identifier of the backend node to get. +func (p GetAXNodeAndAncestorsParams) WithBackendNodeID(backendNodeID cdp.BackendNodeID) *GetAXNodeAndAncestorsParams { + p.BackendNodeID = backendNodeID + return &p +} + +// WithObjectID JavaScript object id of the node wrapper to get. +func (p GetAXNodeAndAncestorsParams) WithObjectID(objectID runtime.RemoteObjectID) *GetAXNodeAndAncestorsParams { + p.ObjectID = objectID + return &p +} + +// GetAXNodeAndAncestorsReturns return values. +type GetAXNodeAndAncestorsReturns struct { + Nodes []*Node `json:"nodes,omitempty,omitzero"` +} + +// Do executes Accessibility.getAXNodeAndAncestors against the provided context. +// +// returns: +// +// nodes +func (p *GetAXNodeAndAncestorsParams) Do(ctx context.Context) (nodes []*Node, err error) { + // execute + var res GetAXNodeAndAncestorsReturns + err = cdp.Execute(ctx, CommandGetAXNodeAndAncestors, p, &res) + if err != nil { + return nil, err + } + + return res.Nodes, nil +} + +// GetChildAXNodesParams fetches a particular accessibility node by AXNodeId. +// Requires enable() to have been called previously. +type GetChildAXNodesParams struct { + ID NodeID `json:"id"` + FrameID cdp.FrameID `json:"frameId,omitempty,omitzero"` // The frame in whose document the node resides. If omitted, the root frame is used. +} + +// GetChildAXNodes fetches a particular accessibility node by AXNodeId. +// Requires enable() to have been called previously. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-getChildAXNodes +// +// parameters: +// +// id +func GetChildAXNodes(id NodeID) *GetChildAXNodesParams { + return &GetChildAXNodesParams{ + ID: id, + } +} + +// WithFrameID the frame in whose document the node resides. If omitted, the +// root frame is used. +func (p GetChildAXNodesParams) WithFrameID(frameID cdp.FrameID) *GetChildAXNodesParams { + p.FrameID = frameID + return &p +} + +// GetChildAXNodesReturns return values. +type GetChildAXNodesReturns struct { + Nodes []*Node `json:"nodes,omitempty,omitzero"` +} + +// Do executes Accessibility.getChildAXNodes against the provided context. +// +// returns: +// +// nodes +func (p *GetChildAXNodesParams) Do(ctx context.Context) (nodes []*Node, err error) { + // execute + var res GetChildAXNodesReturns + err = cdp.Execute(ctx, CommandGetChildAXNodes, p, &res) + if err != nil { + return nil, err + } + + return res.Nodes, nil +} + +// QueryAXTreeParams query a DOM node's accessibility subtree for accessible +// name and role. This command computes the name and role for all nodes in the +// subtree, including those that are ignored for accessibility, and returns +// those that match the specified name and role. If no DOM node is specified, or +// the DOM node does not exist, the command returns an error. If neither +// accessibleName or role is specified, it returns all the accessibility nodes +// in the subtree. +type QueryAXTreeParams struct { + NodeID cdp.NodeID `json:"nodeId,omitempty,omitzero"` // Identifier of the node for the root to query. + BackendNodeID cdp.BackendNodeID `json:"backendNodeId,omitempty,omitzero"` // Identifier of the backend node for the root to query. + ObjectID runtime.RemoteObjectID `json:"objectId,omitempty,omitzero"` // JavaScript object id of the node wrapper for the root to query. + AccessibleName string `json:"accessibleName,omitempty,omitzero"` // Find nodes with this computed name. + Role string `json:"role,omitempty,omitzero"` // Find nodes with this computed role. +} + +// QueryAXTree query a DOM node's accessibility subtree for accessible name +// and role. This command computes the name and role for all nodes in the +// subtree, including those that are ignored for accessibility, and returns +// those that match the specified name and role. If no DOM node is specified, or +// the DOM node does not exist, the command returns an error. If neither +// accessibleName or role is specified, it returns all the accessibility nodes +// in the subtree. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#method-queryAXTree +// +// parameters: +func QueryAXTree() *QueryAXTreeParams { + return &QueryAXTreeParams{} +} + +// WithNodeID identifier of the node for the root to query. +func (p QueryAXTreeParams) WithNodeID(nodeID cdp.NodeID) *QueryAXTreeParams { + p.NodeID = nodeID + return &p +} + +// WithBackendNodeID identifier of the backend node for the root to query. +func (p QueryAXTreeParams) WithBackendNodeID(backendNodeID cdp.BackendNodeID) *QueryAXTreeParams { + p.BackendNodeID = backendNodeID + return &p +} + +// WithObjectID JavaScript object id of the node wrapper for the root to +// query. +func (p QueryAXTreeParams) WithObjectID(objectID runtime.RemoteObjectID) *QueryAXTreeParams { + p.ObjectID = objectID + return &p +} + +// WithAccessibleName find nodes with this computed name. +func (p QueryAXTreeParams) WithAccessibleName(accessibleName string) *QueryAXTreeParams { + p.AccessibleName = accessibleName + return &p +} + +// WithRole find nodes with this computed role. +func (p QueryAXTreeParams) WithRole(role string) *QueryAXTreeParams { + p.Role = role + return &p +} + +// QueryAXTreeReturns return values. +type QueryAXTreeReturns struct { + Nodes []*Node `json:"nodes,omitempty,omitzero"` // A list of Accessibility.AXNode matching the specified attributes, including nodes that are ignored for accessibility. +} + +// Do executes Accessibility.queryAXTree against the provided context. +// +// returns: +// +// nodes - A list of Accessibility.AXNode matching the specified attributes, including nodes that are ignored for accessibility. +func (p *QueryAXTreeParams) Do(ctx context.Context) (nodes []*Node, err error) { + // execute + var res QueryAXTreeReturns + err = cdp.Execute(ctx, CommandQueryAXTree, p, &res) + if err != nil { + return nil, err + } + + return res.Nodes, nil +} + +// Command names. +const ( + CommandDisable = "Accessibility.disable" + CommandEnable = "Accessibility.enable" + CommandGetPartialAXTree = "Accessibility.getPartialAXTree" + CommandGetFullAXTree = "Accessibility.getFullAXTree" + CommandGetRootAXNode = "Accessibility.getRootAXNode" + CommandGetAXNodeAndAncestors = "Accessibility.getAXNodeAndAncestors" + CommandGetChildAXNodes = "Accessibility.getChildAXNodes" + CommandQueryAXTree = "Accessibility.queryAXTree" +) diff --git a/vendor/github.com/chromedp/cdproto/accessibility/events.go b/vendor/github.com/chromedp/cdproto/accessibility/events.go new file mode 100644 index 00000000..ae3b511f --- /dev/null +++ b/vendor/github.com/chromedp/cdproto/accessibility/events.go @@ -0,0 +1,20 @@ +package accessibility + +// Code generated by cdproto-gen. DO NOT EDIT. + +// EventLoadComplete the loadComplete event mirrors the load complete event +// sent by the browser to assistive technology when the web page has finished +// loading. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#event-loadComplete +type EventLoadComplete struct { + Root *Node `json:"root"` // New document root node. +} + +// EventNodesUpdated the nodesUpdated event is sent every time a previously +// requested node has changed the in tree. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#event-nodesUpdated +type EventNodesUpdated struct { + Nodes []*Node `json:"nodes"` // Updated node data. +} diff --git a/vendor/github.com/chromedp/cdproto/accessibility/types.go b/vendor/github.com/chromedp/cdproto/accessibility/types.go new file mode 100644 index 00000000..e4af1e1b --- /dev/null +++ b/vendor/github.com/chromedp/cdproto/accessibility/types.go @@ -0,0 +1,414 @@ +package accessibility + +// Code generated by cdproto-gen. DO NOT EDIT. + +import ( + "fmt" + "strings" + + "github.com/chromedp/cdproto/cdp" + "github.com/go-json-experiment/json/jsontext" +) + +// NodeID unique accessibility node identifier. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#type-AXNodeId +type NodeID string + +// String returns the NodeID as string value. +func (t NodeID) String() string { + return string(t) +} + +// ValueType enum of possible property types. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#type-AXValueType +type ValueType string + +// String returns the ValueType as string value. +func (t ValueType) String() string { + return string(t) +} + +// ValueType values. +const ( + ValueTypeBoolean ValueType = "boolean" + ValueTypeTristate ValueType = "tristate" + ValueTypeBooleanOrUndefined ValueType = "booleanOrUndefined" + ValueTypeIdref ValueType = "idref" + ValueTypeIdrefList ValueType = "idrefList" + ValueTypeInteger ValueType = "integer" + ValueTypeNode ValueType = "node" + ValueTypeNodeList ValueType = "nodeList" + ValueTypeNumber ValueType = "number" + ValueTypeString ValueType = "string" + ValueTypeComputedString ValueType = "computedString" + ValueTypeToken ValueType = "token" + ValueTypeTokenList ValueType = "tokenList" + ValueTypeDomRelation ValueType = "domRelation" + ValueTypeRole ValueType = "role" + ValueTypeInternalRole ValueType = "internalRole" + ValueTypeValueUndefined ValueType = "valueUndefined" +) + +// UnmarshalJSON satisfies [json.Unmarshaler]. +func (t *ValueType) UnmarshalJSON(buf []byte) error { + s := string(buf) + s = strings.TrimSuffix(strings.TrimPrefix(s, `"`), `"`) + + switch ValueType(s) { + case ValueTypeBoolean: + *t = ValueTypeBoolean + case ValueTypeTristate: + *t = ValueTypeTristate + case ValueTypeBooleanOrUndefined: + *t = ValueTypeBooleanOrUndefined + case ValueTypeIdref: + *t = ValueTypeIdref + case ValueTypeIdrefList: + *t = ValueTypeIdrefList + case ValueTypeInteger: + *t = ValueTypeInteger + case ValueTypeNode: + *t = ValueTypeNode + case ValueTypeNodeList: + *t = ValueTypeNodeList + case ValueTypeNumber: + *t = ValueTypeNumber + case ValueTypeString: + *t = ValueTypeString + case ValueTypeComputedString: + *t = ValueTypeComputedString + case ValueTypeToken: + *t = ValueTypeToken + case ValueTypeTokenList: + *t = ValueTypeTokenList + case ValueTypeDomRelation: + *t = ValueTypeDomRelation + case ValueTypeRole: + *t = ValueTypeRole + case ValueTypeInternalRole: + *t = ValueTypeInternalRole + case ValueTypeValueUndefined: + *t = ValueTypeValueUndefined + default: + return fmt.Errorf("unknown ValueType value: %v", s) + } + return nil +} + +// ValueSourceType enum of possible property sources. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#type-AXValueSourceType +type ValueSourceType string + +// String returns the ValueSourceType as string value. +func (t ValueSourceType) String() string { + return string(t) +} + +// ValueSourceType values. +const ( + ValueSourceTypeAttribute ValueSourceType = "attribute" + ValueSourceTypeImplicit ValueSourceType = "implicit" + ValueSourceTypeStyle ValueSourceType = "style" + ValueSourceTypeContents ValueSourceType = "contents" + ValueSourceTypePlaceholder ValueSourceType = "placeholder" + ValueSourceTypeRelatedElement ValueSourceType = "relatedElement" +) + +// UnmarshalJSON satisfies [json.Unmarshaler]. +func (t *ValueSourceType) UnmarshalJSON(buf []byte) error { + s := string(buf) + s = strings.TrimSuffix(strings.TrimPrefix(s, `"`), `"`) + + switch ValueSourceType(s) { + case ValueSourceTypeAttribute: + *t = ValueSourceTypeAttribute + case ValueSourceTypeImplicit: + *t = ValueSourceTypeImplicit + case ValueSourceTypeStyle: + *t = ValueSourceTypeStyle + case ValueSourceTypeContents: + *t = ValueSourceTypeContents + case ValueSourceTypePlaceholder: + *t = ValueSourceTypePlaceholder + case ValueSourceTypeRelatedElement: + *t = ValueSourceTypeRelatedElement + default: + return fmt.Errorf("unknown ValueSourceType value: %v", s) + } + return nil +} + +// ValueNativeSourceType enum of possible native property sources (as a +// subtype of a particular AXValueSourceType). +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#type-AXValueNativeSourceType +type ValueNativeSourceType string + +// String returns the ValueNativeSourceType as string value. +func (t ValueNativeSourceType) String() string { + return string(t) +} + +// ValueNativeSourceType values. +const ( + ValueNativeSourceTypeDescription ValueNativeSourceType = "description" + ValueNativeSourceTypeFigcaption ValueNativeSourceType = "figcaption" + ValueNativeSourceTypeLabel ValueNativeSourceType = "label" + ValueNativeSourceTypeLabelfor ValueNativeSourceType = "labelfor" + ValueNativeSourceTypeLabelwrapped ValueNativeSourceType = "labelwrapped" + ValueNativeSourceTypeLegend ValueNativeSourceType = "legend" + ValueNativeSourceTypeRubyannotation ValueNativeSourceType = "rubyannotation" + ValueNativeSourceTypeTablecaption ValueNativeSourceType = "tablecaption" + ValueNativeSourceTypeTitle ValueNativeSourceType = "title" + ValueNativeSourceTypeOther ValueNativeSourceType = "other" +) + +// UnmarshalJSON satisfies [json.Unmarshaler]. +func (t *ValueNativeSourceType) UnmarshalJSON(buf []byte) error { + s := string(buf) + s = strings.TrimSuffix(strings.TrimPrefix(s, `"`), `"`) + + switch ValueNativeSourceType(s) { + case ValueNativeSourceTypeDescription: + *t = ValueNativeSourceTypeDescription + case ValueNativeSourceTypeFigcaption: + *t = ValueNativeSourceTypeFigcaption + case ValueNativeSourceTypeLabel: + *t = ValueNativeSourceTypeLabel + case ValueNativeSourceTypeLabelfor: + *t = ValueNativeSourceTypeLabelfor + case ValueNativeSourceTypeLabelwrapped: + *t = ValueNativeSourceTypeLabelwrapped + case ValueNativeSourceTypeLegend: + *t = ValueNativeSourceTypeLegend + case ValueNativeSourceTypeRubyannotation: + *t = ValueNativeSourceTypeRubyannotation + case ValueNativeSourceTypeTablecaption: + *t = ValueNativeSourceTypeTablecaption + case ValueNativeSourceTypeTitle: + *t = ValueNativeSourceTypeTitle + case ValueNativeSourceTypeOther: + *t = ValueNativeSourceTypeOther + default: + return fmt.Errorf("unknown ValueNativeSourceType value: %v", s) + } + return nil +} + +// ValueSource a single source for a computed AX property. +// +// See: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility#type-AXValueSource +type ValueSource struct { + Type ValueSourceType `json:"type"` // What type of source this is. + Value *Value `json:"value,omitempty,omitzero"` // The value of this property source. + Attribute string `json:"attribute,omitempty,omitzero"` // The name of the relevant attribute, if any. + AttributeValue *Value `json:"attributeValue,omitempty,omitzero"` // The value of the relevant attribute, if any. + Superseded bool `json:"superseded"` // Whether this source is superseded by a higher priority source. + NativeSource ValueNativeSourceType `json:"nativeSource,omitempty,omitzero"` // The native markup source for this value, e.g. a