| // 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, | 
 | 		) | 
 | 	}) | 
 | } |