blob: 9e87bd9ba36599d1d746f55a25a9078bfedc9858 [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.
package misc
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/counter/countertest"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/server"
. "golang.org/x/tools/gopls/internal/test/integration"
)
// Test prompt file in old and new formats are handled as expected.
func TestTelemetryPrompt_PromptFile(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {}
`
defaultTelemetryStartTime := "1714521600" // 2024-05-01
defaultToken := "7"
samplesPerMille := "500"
testCases := []struct {
name, in, want string
wantPrompt bool
}{
{
name: "empty",
in: "",
want: "failed 1 1714521600 7",
wantPrompt: true,
},
{
name: "v0.15-format/invalid",
in: "pending",
want: "failed 1 1714521600 7",
wantPrompt: true,
},
{
name: "v0.15-format/pPending",
in: "pending 1",
want: "failed 2 1714521600 7",
wantPrompt: true,
},
{
name: "v0.15-format/pPending",
in: "failed 1",
want: "failed 2 1714521600 7",
wantPrompt: true,
},
{
name: "v0.15-format/pYes",
in: "yes 1",
want: "yes 1", // untouched since short-circuited
},
{
name: "v0.16-format/pNotReady",
in: "- 0 1714521600 1000",
want: "- 0 1714521600 1000",
},
{
name: "v0.16-format/pPending",
in: "pending 1 1714521600 1",
want: "failed 2 1714521600 1",
wantPrompt: true,
},
{
name: "v0.16-format/pFailed",
in: "failed 2 1714521600 1",
want: "failed 3 1714521600 1",
wantPrompt: true,
},
{
name: "v0.16-format/invalid",
in: "xxx 0 12345 678",
want: "failed 1 1714521600 7",
wantPrompt: true,
},
{
name: "v0.16-format/extra",
in: "- 0 1714521600 1000 7777 xxx",
want: "- 0 1714521600 1000", // drop extra
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
modeFile := filepath.Join(t.TempDir(), "mode")
goplsConfigDir := t.TempDir()
promptDir := filepath.Join(goplsConfigDir, "prompt")
promptFile := filepath.Join(promptDir, "telemetry")
if err := os.MkdirAll(promptDir, 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(promptFile, []byte(tc.in), 0666); err != nil {
t.Fatal(err)
}
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: goplsConfigDir,
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: defaultTelemetryStartTime,
server.GoTelemetryGoplsClientTokenEnvvar: defaultToken,
server.FakeSamplesPerMille: samplesPerMille,
},
Settings{
"telemetryPrompt": true,
},
).Run(t, src, func(t *testing.T, env *Env) {
expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
if !tc.wantPrompt {
expectation = Not(expectation)
}
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 1, true),
expectation,
)
if got, err := os.ReadFile(promptFile); err != nil || string(got) != tc.want {
t.Fatalf("(%q) -> (%q, %v), want %q", tc.in, got, err, tc.want)
}
})
})
}
}
// Test that gopls prompts for telemetry only when it is supposed to.
func TestTelemetryPrompt_Conditions_Mode(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {
}
`
for _, enabled := range []bool{true, false} {
t.Run(fmt.Sprintf("telemetryPrompt=%v", enabled), func(t *testing.T) {
for _, initialMode := range []string{"", "local", "off", "on"} {
t.Run(fmt.Sprintf("initial_mode=%s", initialMode), func(t *testing.T) {
modeFile := filepath.Join(t.TempDir(), "mode")
if initialMode != "" {
if err := os.WriteFile(modeFile, []byte(initialMode), 0666); err != nil {
t.Fatal(err)
}
}
telemetryStartTime := time.Now().Add(-8 * 24 * time.Hour) // telemetry started a while ago
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: t.TempDir(),
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: strconv.FormatInt(telemetryStartTime.Unix(), 10),
server.GoTelemetryGoplsClientTokenEnvvar: "1", // always sample because samplingPerMille >= 1.
},
Settings{
"telemetryPrompt": enabled,
},
).Run(t, src, func(t *testing.T, env *Env) {
wantPrompt := enabled && (initialMode == "" || initialMode == "local")
expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
if !wantPrompt {
expectation = Not(expectation)
}
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 1, true),
expectation,
)
})
})
}
})
}
}
// Test that gopls prompts for telemetry only after instrumenting for a while, and
// when the token is within the range for sample.
func TestTelemetryPrompt_Conditions_StartTimeAndSamplingToken(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {
}
`
day := 24 * time.Hour
samplesPerMille := 50
for _, token := range []int{1, samplesPerMille, samplesPerMille + 1} {
wantSampled := token <= samplesPerMille
t.Run(fmt.Sprintf("to_sample=%t/tokens=%d", wantSampled, token), func(t *testing.T) {
for _, elapsed := range []time.Duration{8 * day, 1 * day, 0} {
telemetryStartTimeOrEmpty := ""
if elapsed > 0 {
telemetryStartTimeOrEmpty = strconv.FormatInt(time.Now().Add(-elapsed).Unix(), 10)
}
t.Run(fmt.Sprintf("elapsed=%s", elapsed), func(t *testing.T) {
modeFile := filepath.Join(t.TempDir(), "mode")
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: t.TempDir(),
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: telemetryStartTimeOrEmpty,
server.GoTelemetryGoplsClientTokenEnvvar: strconv.Itoa(token),
server.FakeSamplesPerMille: strconv.Itoa(samplesPerMille), // want token ∈ [1, 50] is always sampled.
},
Settings{
"telemetryPrompt": true,
},
).Run(t, src, func(t *testing.T, env *Env) {
wantPrompt := wantSampled && elapsed > 7*day
expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
if !wantPrompt {
expectation = Not(expectation)
}
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 1, true),
expectation,
)
})
})
}
})
}
}
// Test that responding to the telemetry prompt results in the expected state.
func TestTelemetryPrompt_Response(t *testing.T) {
if !countertest.SupportedPlatform {
t.Skip("requires counter support")
}
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {
}
`
var (
acceptanceCounter = "gopls/telemetryprompt/accepted"
declinedCounter = "gopls/telemetryprompt/declined"
attempt1Counter = "gopls/telemetryprompt/attempts:1"
allCounters = []string{acceptanceCounter, declinedCounter, attempt1Counter}
)
// We must increment counters in order for the initial reads below to
// succeed.
//
// TODO(rfindley): ReadCounter should simply return 0 for uninitialized
// counters.
for _, name := range allCounters {
counter.New(name).Inc()
}
readCounts := func(t *testing.T) map[string]uint64 {
t.Helper()
counts := make(map[string]uint64)
for _, name := range allCounters {
count, err := countertest.ReadCounter(counter.New(name))
if err != nil {
t.Fatalf("ReadCounter(%q) failed: %v", name, err)
}
counts[name] = count
}
return counts
}
tests := []struct {
name string // subtest name
response string // response to choose for the telemetry dialog
wantMode string // resulting telemetry mode
wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
wantInc uint64 // expected 'prompt accepted' counter increment
wantCounts map[string]uint64
}{
{"yes", server.TelemetryYes, "on", "uploading is now enabled", 1, map[string]uint64{
acceptanceCounter: 1,
declinedCounter: 0,
attempt1Counter: 1,
}},
{"no", server.TelemetryNo, "", "", 0, map[string]uint64{
acceptanceCounter: 0,
declinedCounter: 1,
attempt1Counter: 1,
}},
{"empty", "", "", "", 0, map[string]uint64{
acceptanceCounter: 0,
declinedCounter: 0,
attempt1Counter: 1,
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
initialCounts := readCounts(t)
modeFile := filepath.Join(t.TempDir(), "mode")
telemetryStartTime := time.Now().Add(-8 * 24 * time.Hour)
msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?")
respond := func(m *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
if msgRE.MatchString(m.Message) {
for _, item := range m.Actions {
if item.Title == test.response {
return &item, nil
}
}
if test.response != "" {
t.Errorf("action item %q not found", test.response)
}
}
return nil, nil
}
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: t.TempDir(),
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: strconv.FormatInt(telemetryStartTime.Unix(), 10),
server.GoTelemetryGoplsClientTokenEnvvar: "1", // always sample because samplingPerMille >= 1.
},
Settings{
"telemetryPrompt": true,
},
MessageResponder(respond),
).Run(t, src, func(t *testing.T, env *Env) {
var postConditions []Expectation
if test.wantMsg != "" {
postConditions = append(postConditions, ShownMessage(test.wantMsg))
}
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 1, true),
postConditions...,
)
gotMode := ""
if contents, err := os.ReadFile(modeFile); err == nil {
gotMode = string(contents)
} else if !os.IsNotExist(err) {
t.Fatal(err)
}
if gotMode != test.wantMode {
t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode)
}
// We increment the acceptance counter when checking the prompt file
// before prompting, so start a second, transient gopls session and
// verify that the acceptance counter is incremented.
env2 := ConnectGoplsEnv(t, env.Ctx, env.Sandbox, env.Editor.Config(), env.Server)
env2.Await(CompletedWork(server.TelemetryPromptWorkTitle, 1, true))
if err := env2.Editor.Close(env2.Ctx); err != nil {
t.Errorf("closing second editor: %v", err)
}
gotCounts := readCounts(t)
for k := range gotCounts {
gotCounts[k] -= initialCounts[k]
}
if diff := cmp.Diff(test.wantCounts, gotCounts); diff != "" {
t.Errorf("counter mismatch (-want +got):\n%s", diff)
}
})
})
}
}
// Test that we stop asking about telemetry after the user ignores the question
// 5 times.
func TestTelemetryPrompt_GivingUp(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {
}
`
// For this test, we want to share state across gopls sessions.
modeFile := filepath.Join(t.TempDir(), "mode")
telemetryStartTime := time.Now().Add(-30 * 24 * time.Hour)
configDir := t.TempDir()
const maxPrompts = 5 // internal prompt limit defined by gopls
for i := 0; i < maxPrompts+1; i++ {
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: configDir,
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: strconv.FormatInt(telemetryStartTime.Unix(), 10),
server.GoTelemetryGoplsClientTokenEnvvar: "1", // always sample because samplingPerMille >= 1.
},
Settings{
"telemetryPrompt": true,
},
).Run(t, src, func(t *testing.T, env *Env) {
wantPrompt := i < maxPrompts
expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
if !wantPrompt {
expectation = Not(expectation)
}
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 1, true),
expectation,
)
})
}
}
// Test that gopls prompts for telemetry only when it is supposed to.
func TestTelemetryPrompt_Conditions_Command(t *testing.T) {
const src = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
func main() {
}
`
modeFile := filepath.Join(t.TempDir(), "mode")
telemetryStartTime := time.Now().Add(-8 * 24 * time.Hour)
WithOptions(
Modes(Default), // no need to run this in all modes
EnvVars{
server.GoplsConfigDirEnvvar: t.TempDir(),
server.FakeTelemetryModefileEnvvar: modeFile,
server.GoTelemetryGoplsClientStartTimeEnvvar: fmt.Sprintf("%d", telemetryStartTime.Unix()),
server.GoTelemetryGoplsClientTokenEnvvar: "1", // always sample because samplingPerMille >= 1.
},
Settings{
// off because we are testing
// if we can trigger the prompt with command.
"telemetryPrompt": false,
},
).Run(t, src, func(t *testing.T, env *Env) {
cmd := command.NewMaybePromptForTelemetryCommand("prompt")
var err error
env.ExecuteCommand(&protocol.ExecuteCommandParams{
Command: cmd.Command,
}, &err)
if err != nil {
t.Fatal(err)
}
expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?")
env.OnceMet(
CompletedWork(server.TelemetryPromptWorkTitle, 2, true),
expectation,
)
})
}