Skip to content

Commit 69313e6

Browse files
findleyrgopherbot
authored andcommitted
internal/counter: don't write counters to disk if mode=off
It was decided in golang/go#63832 that when the telemetry mode is "off" no telemetry data should be written to the file system. However, the current implementation of "off" still creates the counter file -- it just doesn't produce any reports. Fix this to not even create the counter file when the mode is off. This was rather tricky to implement, as it required auditing a lot of code to see that we don't reach openMapped. However, per discussion with pjw@ it should be sufficient to implement this check in Open, as is done here. This broke some tests because 1. some tests were reading the wrong mode file (fixed by setting telemetry.ModeFile in counter_test.go) 2. the E2E tests were incorrectly escaping the RunProg test.run regexp (fixed by simplifying the RunProg logic) For golang/go#63832 Change-Id: I47066a97a8fd17c4c2be776077d718e9cdbaf65a Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/542055 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Peter Weinberger <[email protected]>
1 parent 7324770 commit 69313e6

File tree

6 files changed

+122
-72
lines changed

6 files changed

+122
-72
lines changed

counter/counter.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,13 @@ func NewStack(name string, depth int) *StackCounter {
6767
return counter.NewStack(name, depth)
6868
}
6969

70-
// Open opens the counter file on disk and starts to mmap telemetry
71-
// counters to the file. Open also persists counters created and
72-
// incremented before it is called.
73-
// Programs are supposed to call this once.
70+
// Open prepares telemetry counters for recording to the file system.
71+
//
72+
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
73+
// counter file on disk and starts to mmap telemetry counters to the file.
74+
// Open also persists any counters already created in the current process.
75+
//
76+
// Programs using telemetry should call Open exactly once.
7477
func Open() {
7578
counter.Open()
7679
}

counter/countertest/countertest.go

-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ package countertest
1212

1313
import (
1414
"fmt"
15-
"os"
1615
"path/filepath"
1716
"sync"
1817
"testing"
@@ -51,11 +50,6 @@ func Open(telemetryDir string) {
5150
telemetry.ModeFile = telemetry.ModeFilePath(filepath.Join(telemetryDir, "mode"))
5251
telemetry.LocalDir = filepath.Join(telemetryDir, "local")
5352
telemetry.UploadDir = filepath.Join(telemetryDir, "upload")
54-
err1 := os.MkdirAll(telemetry.LocalDir, 0755)
55-
err2 := os.MkdirAll(telemetry.UploadDir, 0755)
56-
if err1 != nil || err2 != nil {
57-
panic(fmt.Sprintf("failed to create telemetry dirs: %v, %v", err1, err2))
58-
}
5953

6054
counter.Open()
6155
opened = true

internal/counter/counter_test.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ func TestParallel(t *testing.T) {
101101
t.Fatal(f.err)
102102
}
103103
current := f.current.Load()
104+
if current == nil {
105+
t.Fatal("no mapped file")
106+
}
104107
name := current.f.Name()
105108
t.Logf("wrote %s:\n%s", name, hexDump(current.mapping.Data))
106109

@@ -499,7 +502,11 @@ func TestStack(t *testing.T) {
499502
}
500503
}
501504
// check that Parse expands compressed counter names
502-
data := f.current.Load().mapping.Data
505+
current := f.current.Load()
506+
if current == nil {
507+
t.Fatal("no mapped file")
508+
}
509+
data := current.mapping.Data
503510
fname := "2023-01-01.v1.count" // bogus file name required by Parse.
504511
theFile, err := Parse(fname, data)
505512
if err != nil {
@@ -547,8 +554,8 @@ func setup(t *testing.T) {
547554
telemetry.UploadDir = tmpDir + "/upload"
548555
os.MkdirAll(telemetry.LocalDir, 0777)
549556
os.MkdirAll(telemetry.UploadDir, 0777)
557+
telemetry.ModeFile = telemetry.ModeFilePath(filepath.Join(tmpDir, "mode"))
550558
// os.UserConfigDir() is "" in tests so no point in looking at it
551-
552559
}
553560

554561
func restore() {

internal/counter/file.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type file struct {
3838
namePrefix string
3939
err error
4040
meta string
41-
current atomic.Pointer[mappedFile] // can be read without holding mu
41+
current atomic.Pointer[mappedFile] // can be read without holding mu, but may be nil
4242
}
4343

4444
var defaultFile file
@@ -350,6 +350,11 @@ var mainCounter = New("counter/main")
350350
// any reports are generated.
351351
// (Otherwise expired count files will not be deleted on Windows.)
352352
func Open() func() {
353+
if mode, _ := telemetry.Mode(); mode == "off" {
354+
// Don't open the file when telemetry is off.
355+
defaultFile.err = ErrDisabled
356+
return func() {} // No need to clean up.
357+
}
353358
debugPrintf("Open")
354359
mainCounter.Add(1)
355360
defaultFile.rotate()

internal/regtest/e2e_test.go

+72-21
Original file line numberDiff line numberDiff line change
@@ -21,47 +21,98 @@ import (
2121
"golang.org/x/telemetry/counter"
2222
"golang.org/x/telemetry/internal/config"
2323
icounter "golang.org/x/telemetry/internal/counter"
24+
it "golang.org/x/telemetry/internal/telemetry"
2425
"golang.org/x/telemetry/internal/testenv"
2526
)
2627

28+
// programs to run
29+
const (
30+
progIncCounters = "inccounters"
31+
prog1 = "prog1"
32+
prog2 = "prog2"
33+
)
34+
35+
func TestMain(m *testing.M) {
36+
Main(m, map[string]func() int{
37+
progIncCounters: func() int {
38+
counter.Inc("counter")
39+
counter.Inc("counter:surprise")
40+
counter.New("gopls/editor:expected").Inc()
41+
counter.New("gopls/editor:surprise").Inc()
42+
counter.NewStack("stack/expected", 1).Inc()
43+
counter.NewStack("stack-surprise", 1).Inc()
44+
return 0
45+
},
46+
prog1: func() int {
47+
fmt.Println("FuncB")
48+
return 0
49+
},
50+
prog2: func() int {
51+
fmt.Println("FuncC")
52+
return 1
53+
},
54+
})
55+
}
56+
2757
func TestRunProg(t *testing.T) {
2858
testenv.MustHaveExec(t)
2959
telemetryDir := t.TempDir()
30-
fmt.Println("START")
3160
t.Run("prog1", func(t *testing.T) {
32-
if out, err := RunProg(t, telemetryDir, func() int {
33-
fmt.Println("FuncB")
34-
return 0
35-
}); err != nil || !bytes.Contains(out, []byte("START")) || !bytes.Contains(out, []byte("FuncB")) {
36-
t.Errorf("first RunProg = (%s, %v), want 'START', 'FuncB', and succeed", out, err)
61+
if out, err := RunProg(telemetryDir, prog1); err != nil || !bytes.Contains(out, []byte("FuncB")) || bytes.Contains(out, []byte("FuncC")) {
62+
t.Errorf("first RunProg = (%s, %v), want FuncB' and succeed", out, err)
3763
}
3864
})
39-
fmt.Println("MIDDLE")
4065
t.Run("prog2", func(t *testing.T) {
41-
if out, err := RunProg(t, telemetryDir, func() int {
42-
fmt.Println("FuncC")
43-
return 1
44-
}); err == nil || !bytes.Contains(out, []byte("START")) || bytes.Contains(out, []byte("FuncB")) || !bytes.Contains(out, []byte("MIDDLE")) || !bytes.Contains(out, []byte("FuncC")) {
45-
t.Errorf("second RunProg = (%s, %v), want 'START', 'MIDDLE', 'FuncC' (but no 'FuncB') and fail", out, err)
66+
if out, err := RunProg(telemetryDir, prog2); err == nil || bytes.Contains(out, []byte("FuncB")) || !bytes.Contains(out, []byte("FuncC")) {
67+
t.Errorf("second RunProg = (%s, %v), want 'FuncC' and fail", out, err)
4668
}
4769
})
4870
}
4971

72+
func TestE2E_off(t *testing.T) {
73+
testenv.MustHaveExec(t)
74+
75+
tests := []struct {
76+
mode string // if empty, don't set the mode
77+
wantLocalDir bool
78+
}{
79+
{"", true},
80+
{"local", true},
81+
{"on", true},
82+
{"off", false},
83+
}
84+
85+
for _, test := range tests {
86+
t.Run(fmt.Sprintf("mode=%s", test.mode), func(t *testing.T) {
87+
telemetryDir := t.TempDir()
88+
if test.mode != "" {
89+
if err := it.ModeFilePath(filepath.Join(telemetryDir, "mode")).SetMode(test.mode); err != nil {
90+
t.Fatalf("SetMode failed: %v", err)
91+
}
92+
}
93+
out, err := RunProg(telemetryDir, progIncCounters)
94+
if err != nil {
95+
t.Fatalf("program failed unexpectedly (%v)\n%s", err, out)
96+
}
97+
localDir := filepath.Join(telemetryDir, "local")
98+
_, err = os.Stat(localDir)
99+
if err != nil && !os.IsNotExist(err) {
100+
t.Fatalf("os.Stat(%q): unexpected error: %v", localDir, err)
101+
}
102+
if gotLocalDir := err == nil; gotLocalDir != test.wantLocalDir {
103+
t.Errorf("got /local dir: %v, want %v; out:\n%s", gotLocalDir, test.wantLocalDir, string(out))
104+
}
105+
})
106+
}
107+
}
108+
50109
func TestE2E(t *testing.T) {
51110
testenv.MustHaveExec(t)
52111
telemetryDir := t.TempDir()
53112

54113
goVers, progVers, progName := ProgInfo(t)
55114

56-
out, err := RunProg(t, telemetryDir, func() int {
57-
counter.Inc("counter")
58-
counter.Inc("counter:surprise")
59-
counter.New("gopls/editor:expected").Inc()
60-
counter.New("gopls/editor:surprise").Inc()
61-
counter.NewStack("stack/expected", 1).Inc()
62-
counter.NewStack("stack-surprise", 1).Inc()
63-
return 0
64-
})
115+
out, err := RunProg(telemetryDir, progIncCounters)
65116
if err != nil {
66117
t.Fatalf("program failed unexpectedly (%v)\n%s", err, out)
67118
}

internal/regtest/regtest.go

+28-38
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,57 @@
99
package regtest
1010

1111
import (
12+
"flag"
13+
"fmt"
1214
"os"
1315
"os/exec"
1416
"path/filepath"
1517
"runtime/debug"
1618
"strings"
17-
"sync"
1819
"testing"
1920

2021
"golang.org/x/telemetry/counter/countertest"
2122
)
2223

23-
const telemetryDirEnvVar = "_COUNTERTEST_RUN_TELEMETRY_DIR"
24-
25-
var (
26-
runProgMu sync.Mutex
27-
hasRunProg = map[string]bool{} // test name -> RunProg already called for this test.
24+
const (
25+
telemetryDirEnvVar = "_COUNTERTEST_RUN_TELEMETRY_DIR"
26+
entryPointEnvVar = "_COUNTERTEST_ENTRYPOINT"
2827
)
2928

30-
func canRunProg(name string) bool {
31-
runProgMu.Lock()
32-
defer runProgMu.Unlock()
33-
if hasRunProg[name] {
34-
return false
35-
}
36-
hasRunProg[name] = true
37-
return true
38-
}
39-
40-
// RunProg runs prog in a separate process with the specified telemetry directory.
41-
// The return value of prog is the exit code of the process.
42-
// RunProg can be called at most once per testing.T.
43-
// If an integration test needs to run multiple programs, use subtests.
44-
func RunProg(t *testing.T, telemetryDir string, prog func() int) ([]byte, error) {
45-
testName := t.Name()
46-
if !canRunProg(testName) {
47-
t.Fatalf("RunProg was called more than once in test %v. Use subtests if a test needs it more than once", testName)
29+
// Main is a test main function for use in TestMain, which runs one of the
30+
// given programs when invoked as a separate process via RunProg.
31+
//
32+
// The return value of each program is the exit code of the process.
33+
func Main(m *testing.M, programs map[string]func() int) {
34+
if d := os.Getenv(telemetryDirEnvVar); d != "" {
35+
countertest.Open(d)
4836
}
49-
if telemetryDir := os.Getenv(telemetryDirEnvVar); telemetryDir != "" {
50-
// run the prog.
51-
countertest.Open(telemetryDir)
52-
os.Exit(prog())
37+
if e, ok := os.LookupEnv(entryPointEnvVar); ok {
38+
if prog, ok := programs[e]; ok {
39+
os.Exit(prog())
40+
}
41+
fmt.Fprintf(os.Stderr, "unknown program %q", e)
42+
os.Exit(2)
5343
}
44+
flag.Parse()
45+
os.Exit(m.Run())
46+
}
5447

55-
t.Helper()
56-
48+
// RunProg runs the named program in a separate process with the specified
49+
// telemetry directory, where prog is one of the programs passed to Main (which
50+
// must be invoked by TestMain).
51+
func RunProg(telemetryDir string, prog string) ([]byte, error) {
5752
testBin, err := os.Executable()
5853
if err != nil {
59-
t.Fatalf("cannot determine the current process's executable name: %v", err)
54+
return nil, fmt.Errorf("cannot determine the current process's executable name: %v", err)
6055
}
6156

6257
// Spawn a subprocess to run the `prog`, by setting subprocessKeyEnvVar and telemetryDirEnvVar.
63-
cmd := exec.Command(testBin, "-test.run", testName)
64-
cmd.Env = append(cmd.Env, telemetryDirEnvVar+"="+telemetryDir)
58+
cmd := exec.Command(testBin)
59+
cmd.Env = append(cmd.Env, telemetryDirEnvVar+"="+telemetryDir, entryPointEnvVar+"="+prog)
6560
return cmd.CombinedOutput()
6661
}
6762

68-
// InSubprocess returns whether the current process is a subprocess forked by RunProg.
69-
func InSubprocess() bool {
70-
return os.Getenv(telemetryDirEnvVar) != ""
71-
}
72-
7363
// ProgInfo returns the go version, program name and version info the process would record in its counter file.
7464
func ProgInfo(t *testing.T) (goVersion, progVersion, progName string) {
7565
info, ok := debug.ReadBuildInfo()

0 commit comments

Comments
 (0)