golang.org/x/tools/gopls@v0.15.3/internal/telemetry/telemetry_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.21 && !openbsd && !js && !wasip1 && !solaris && !android && !386
     6  // +build go1.21,!openbsd,!js,!wasip1,!solaris,!android,!386
     7  
     8  package telemetry_test
     9  
    10  import (
    11  	"context"
    12  	"errors"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	"golang.org/x/telemetry/counter"
    20  	"golang.org/x/telemetry/counter/countertest" // requires go1.21+
    21  	"golang.org/x/tools/gopls/internal/hooks"
    22  	"golang.org/x/tools/gopls/internal/protocol"
    23  	"golang.org/x/tools/gopls/internal/protocol/command"
    24  	"golang.org/x/tools/gopls/internal/telemetry"
    25  	. "golang.org/x/tools/gopls/internal/test/integration"
    26  	"golang.org/x/tools/gopls/internal/util/bug"
    27  )
    28  
    29  func TestMain(m *testing.M) {
    30  	tmp, err := os.MkdirTemp("", "gopls-telemetry-test")
    31  	if err != nil {
    32  		panic(err)
    33  	}
    34  	countertest.Open(tmp)
    35  	defer os.RemoveAll(tmp)
    36  	Main(m, hooks.Options)
    37  }
    38  
    39  func TestTelemetry(t *testing.T) {
    40  	var (
    41  		goversion = ""
    42  		editor    = "vscode" // We set ClientName("Visual Studio Code") below.
    43  	)
    44  
    45  	// Run gopls once to determine the Go version.
    46  	WithOptions(
    47  		Modes(Default),
    48  	).Run(t, "", func(_ *testing.T, env *Env) {
    49  		goversion = strconv.Itoa(env.GoVersion())
    50  	})
    51  
    52  	// counters that should be incremented once per session
    53  	sessionCounters := []*counter.Counter{
    54  		counter.New("gopls/client:" + editor),
    55  		counter.New("gopls/goversion:1." + goversion),
    56  		counter.New("fwd/vscode/linter:a"),
    57  	}
    58  	initialCounts := make([]uint64, len(sessionCounters))
    59  	for i, c := range sessionCounters {
    60  		count, err := countertest.ReadCounter(c)
    61  		if err != nil {
    62  			t.Fatalf("ReadCounter(%s): %v", c.Name(), err)
    63  		}
    64  		initialCounts[i] = count
    65  	}
    66  
    67  	// Verify that a properly configured session gets notified of a bug on the
    68  	// server.
    69  	WithOptions(
    70  		Modes(Default), // must be in-process to receive the bug report below
    71  		Settings{"showBugReports": true},
    72  		ClientName("Visual Studio Code"),
    73  	).Run(t, "", func(_ *testing.T, env *Env) {
    74  		goversion = strconv.Itoa(env.GoVersion())
    75  		addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1})
    76  		const desc = "got a bug"
    77  		bug.Report(desc) // want a stack counter with the trace starting from here.
    78  		env.Await(ShownMessage(desc))
    79  	})
    80  
    81  	// gopls/editor:client
    82  	// gopls/goversion:1.x
    83  	// fwd/vscode/linter:a
    84  	for i, c := range sessionCounters {
    85  		want := initialCounts[i] + 1
    86  		got, err := countertest.ReadCounter(c)
    87  		if err != nil || got != want {
    88  			t.Errorf("ReadCounter(%q) = (%v, %v), want (%v, nil)", c.Name(), got, err, want)
    89  			t.Logf("Current timestamp = %v", time.Now().UTC())
    90  		}
    91  	}
    92  
    93  	// gopls/bug
    94  	bugcount := bug.BugReportCount
    95  	counts, err := countertest.ReadStackCounter(bugcount)
    96  	if err != nil {
    97  		t.Fatalf("ReadStackCounter(bugreportcount) failed - %v", err)
    98  	}
    99  	if len(counts) != 1 || !hasEntry(counts, t.Name(), 1) {
   100  		t.Errorf("read stackcounter(%q) = (%#v, %v), want one entry", "gopls/bug", counts, err)
   101  		t.Logf("Current timestamp = %v", time.Now().UTC())
   102  	}
   103  }
   104  
   105  func addForwardedCounters(env *Env, names []string, values []int64) {
   106  	args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{
   107  		Names: names, Values: values,
   108  	})
   109  	if err != nil {
   110  		env.T.Fatal(err)
   111  	}
   112  	var res error
   113  	env.ExecuteCommand(&protocol.ExecuteCommandParams{
   114  		Command:   command.AddTelemetryCounters.ID(),
   115  		Arguments: args,
   116  	}, res)
   117  	if res != nil {
   118  		env.T.Errorf("%v failed - %v", command.AddTelemetryCounters.ID(), res)
   119  	}
   120  }
   121  
   122  func hasEntry(counts map[string]uint64, pattern string, want uint64) bool {
   123  	for k, v := range counts {
   124  		if strings.Contains(k, pattern) && v == want {
   125  			return true
   126  		}
   127  	}
   128  	return false
   129  }
   130  
   131  func TestLatencyCounter(t *testing.T) {
   132  	const operation = "TestLatencyCounter" // a unique operation name
   133  
   134  	stop := telemetry.StartLatencyTimer(operation)
   135  	stop(context.Background(), nil)
   136  
   137  	for isError, want := range map[bool]uint64{false: 1, true: 0} {
   138  		if got := totalLatencySamples(t, operation, isError); got != want {
   139  			t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
   140  		}
   141  	}
   142  }
   143  
   144  func TestLatencyCounter_Error(t *testing.T) {
   145  	const operation = "TestLatencyCounter_Error" // a unique operation name
   146  
   147  	stop := telemetry.StartLatencyTimer(operation)
   148  	stop(context.Background(), errors.New("bad"))
   149  
   150  	for isError, want := range map[bool]uint64{false: 0, true: 1} {
   151  		if got := totalLatencySamples(t, operation, isError); got != want {
   152  			t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
   153  		}
   154  	}
   155  }
   156  
   157  func TestLatencyCounter_Cancellation(t *testing.T) {
   158  	const operation = "TestLatencyCounter_Cancellation"
   159  
   160  	stop := telemetry.StartLatencyTimer(operation)
   161  	ctx, cancel := context.WithCancel(context.Background())
   162  	cancel()
   163  	stop(ctx, nil)
   164  
   165  	for isError, want := range map[bool]uint64{false: 0, true: 0} {
   166  		if got := totalLatencySamples(t, operation, isError); got != want {
   167  			t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want)
   168  		}
   169  	}
   170  }
   171  
   172  func totalLatencySamples(t *testing.T, operation string, isError bool) uint64 {
   173  	var total uint64
   174  	telemetry.ForEachLatencyCounter(operation, isError, func(c *counter.Counter) {
   175  		count, err := countertest.ReadCounter(c)
   176  		if err != nil {
   177  			t.Errorf("ReadCounter(%s) failed: %v", c.Name(), err)
   178  		} else {
   179  			total += count
   180  		}
   181  	})
   182  	return total
   183  }
   184  
   185  func TestLatencyInstrumentation(t *testing.T) {
   186  	const files = `
   187  -- go.mod --
   188  module mod.test/a
   189  go 1.18
   190  -- a.go --
   191  package a
   192  
   193  func _() {
   194  	x := 0
   195  	_ = x
   196  }
   197  `
   198  
   199  	// Verify that a properly configured session gets notified of a bug on the
   200  	// server.
   201  	WithOptions(
   202  		Modes(Default), // must be in-process to receive the bug report below
   203  	).Run(t, files, func(_ *testing.T, env *Env) {
   204  		env.OpenFile("a.go")
   205  		before := totalLatencySamples(t, "completion", false)
   206  		loc := env.RegexpSearch("a.go", "x")
   207  		for i := 0; i < 10; i++ {
   208  			env.Completion(loc)
   209  		}
   210  		after := totalLatencySamples(t, "completion", false)
   211  		if after-before < 10 {
   212  			t.Errorf("after 10 completions, completion counter went from %d to %d", before, after)
   213  		}
   214  	})
   215  }