blob: 6c1944b22303392b8d152e9d41ff63eadd065666 [file] [log] [blame]
// 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")
if err != nil {
panic(err)
}
countertest.Open(tmp)
code := Main(m)
os.RemoveAll(tmp)
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"),
}
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"),
).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
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)
}
})
}