| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build go1.21 && !openbsd && !js && !wasip1 && !solaris && !android && !386 |
| // +build go1.21,!openbsd,!js,!wasip1,!solaris,!android,!386 |
| |
| package telemetry_test |
| |
| import ( |
| "context" |
| "errors" |
| "os" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/telemetry/counter" |
| "golang.org/x/telemetry/counter/countertest" // requires go1.21+ |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/protocol/command" |
| "golang.org/x/tools/gopls/internal/telemetry" |
| . "golang.org/x/tools/gopls/internal/test/integration" |
| "golang.org/x/tools/gopls/internal/util/bug" |
| ) |
| |
| func TestMain(m *testing.M) { |
| tmp, err := os.MkdirTemp("", "gopls-telemetry-test-counters") |
| if err != nil { |
| panic(err) |
| } |
| countertest.Open(tmp) |
| code := Main(m) |
| os.RemoveAll(tmp) // golang/go#68243: ignore error; cleanup fails on Windows |
| os.Exit(code) |
| } |
| |
| func TestTelemetry(t *testing.T) { |
| var ( |
| goversion = "" |
| editor = "vscode" // We set ClientName("Visual Studio Code") below. |
| ) |
| |
| // Run gopls once to determine the Go version. |
| WithOptions( |
| Modes(Default), |
| ).Run(t, "", func(_ *testing.T, env *Env) { |
| goversion = strconv.Itoa(env.GoVersion()) |
| }) |
| |
| // counters that should be incremented once per session |
| sessionCounters := []*counter.Counter{ |
| counter.New("gopls/client:" + editor), |
| counter.New("gopls/goversion:1." + goversion), |
| counter.New("fwd/vscode/linter:a"), |
| counter.New("gopls/gotoolchain:local"), |
| } |
| initialCounts := make([]uint64, len(sessionCounters)) |
| for i, c := range sessionCounters { |
| count, err := countertest.ReadCounter(c) |
| if err != nil { |
| continue // counter db not open, or counter not found |
| } |
| initialCounts[i] = count |
| } |
| |
| // Verify that a properly configured session gets notified of a bug on the |
| // server. |
| WithOptions( |
| Modes(Default), // must be in-process to receive the bug report below |
| Settings{"showBugReports": true}, |
| ClientName("Visual Studio Code"), |
| EnvVars{ |
| "GOTOOLCHAIN": "local", // so that the local counter is incremented |
| }, |
| ).Run(t, "", func(_ *testing.T, env *Env) { |
| goversion = strconv.Itoa(env.GoVersion()) |
| addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1}) |
| const desc = "got a bug" |
| |
| // This will increment a counter named something like: |
| // |
| // `gopls/bug |
| // golang.org/x/tools/gopls/internal/util/bug.report:+35 |
| // golang.org/x/tools/gopls/internal/util/bug.Report:=68 |
| // golang.org/x/tools/gopls/internal/telemetry_test.TestTelemetry.func2:+4 |
| // golang.org/x/tools/gopls/internal/test/integration.(*Runner).Run.func1:+87 |
| // testing.tRunner:+150 |
| // runtime.goexit:+0` |
| // |
| bug.Report(desc) // want a stack counter with the trace starting from here. |
| |
| env.Await(ShownMessage(desc)) |
| }) |
| |
| // gopls/editor:client |
| // gopls/goversion:1.x |
| // fwd/vscode/linter:a |
| // gopls/gotoolchain:local |
| for i, c := range sessionCounters { |
| want := initialCounts[i] + 1 |
| got, err := countertest.ReadCounter(c) |
| if err != nil || got != want { |
| t.Errorf("ReadCounter(%q) = (%v, %v), want (%v, nil)", c.Name(), got, err, want) |
| t.Logf("Current timestamp = %v", time.Now().UTC()) |
| } |
| } |
| |
| // gopls/bug |
| bugcount := bug.BugReportCount |
| counts, err := countertest.ReadStackCounter(bugcount) |
| if err != nil { |
| t.Fatalf("ReadStackCounter(bugreportcount) failed - %v", err) |
| } |
| if len(counts) != 1 || !hasEntry(counts, t.Name(), 1) { |
| t.Errorf("read stackcounter(%q) = (%#v, %v), want one entry", "gopls/bug", counts, err) |
| t.Logf("Current timestamp = %v", time.Now().UTC()) |
| } |
| } |
| |
| func addForwardedCounters(env *Env, names []string, values []int64) { |
| args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{ |
| Names: names, Values: values, |
| }) |
| if err != nil { |
| env.T.Fatal(err) |
| } |
| var res error |
| env.ExecuteCommand(&protocol.ExecuteCommandParams{ |
| Command: command.AddTelemetryCounters.String(), |
| Arguments: args, |
| }, &res) |
| if res != nil { |
| env.T.Errorf("%v failed - %v", command.AddTelemetryCounters, res) |
| } |
| } |
| |
| func hasEntry(counts map[string]uint64, pattern string, want uint64) bool { |
| for k, v := range counts { |
| if strings.Contains(k, pattern) && v == want { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func TestLatencyCounter(t *testing.T) { |
| const operation = "TestLatencyCounter" // a unique operation name |
| |
| stop := telemetry.StartLatencyTimer(operation) |
| stop(context.Background(), nil) |
| |
| for isError, want := range map[bool]uint64{false: 1, true: 0} { |
| if got := totalLatencySamples(t, operation, isError); got != want { |
| t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) |
| } |
| } |
| } |
| |
| func TestLatencyCounter_Error(t *testing.T) { |
| const operation = "TestLatencyCounter_Error" // a unique operation name |
| |
| stop := telemetry.StartLatencyTimer(operation) |
| stop(context.Background(), errors.New("bad")) |
| |
| for isError, want := range map[bool]uint64{false: 0, true: 1} { |
| if got := totalLatencySamples(t, operation, isError); got != want { |
| t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) |
| } |
| } |
| } |
| |
| func TestLatencyCounter_Cancellation(t *testing.T) { |
| const operation = "TestLatencyCounter_Cancellation" |
| |
| stop := telemetry.StartLatencyTimer(operation) |
| ctx, cancel := context.WithCancel(context.Background()) |
| cancel() |
| stop(ctx, nil) |
| |
| for isError, want := range map[bool]uint64{false: 0, true: 0} { |
| if got := totalLatencySamples(t, operation, isError); got != want { |
| t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) |
| } |
| } |
| } |
| |
| func totalLatencySamples(t *testing.T, operation string, isError bool) uint64 { |
| var total uint64 |
| telemetry.ForEachLatencyCounter(operation, isError, func(c *counter.Counter) { |
| count, err := countertest.ReadCounter(c) |
| if err != nil { |
| t.Errorf("ReadCounter(%s) failed: %v", c.Name(), err) |
| } else { |
| total += count |
| } |
| }) |
| return total |
| } |
| |
| func TestLatencyInstrumentation(t *testing.T) { |
| const files = ` |
| -- go.mod -- |
| module mod.test/a |
| go 1.18 |
| -- a.go -- |
| package a |
| |
| func _() { |
| x := 0 |
| _ = x |
| } |
| ` |
| |
| // Verify that a properly configured session gets notified of a bug on the |
| // server. |
| WithOptions( |
| Modes(Default), // must be in-process to receive the bug report below |
| ).Run(t, files, func(_ *testing.T, env *Env) { |
| env.OpenFile("a.go") |
| before := totalLatencySamples(t, "completion", false) |
| loc := env.RegexpSearch("a.go", "x") |
| for i := 0; i < 10; i++ { |
| env.Completion(loc) |
| } |
| after := totalLatencySamples(t, "completion", false) |
| if after-before < 10 { |
| t.Errorf("after 10 completions, completion counter went from %d to %d", before, after) |
| } |
| }) |
| } |