blob: 2a32d98fd7fd40aca12beecc8af06fc75ba49ee3 [file] [log] [blame]
// Copyright 2024 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 telemetry_test
import (
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"sync"
"testing"
"time"
"golang.org/x/telemetry"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/counter/countertest"
"golang.org/x/telemetry/internal/configtest"
ic "golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/regtest"
it "golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
)
// These environment variables are used to coordinate the fork+exec subprocess
// started by TestStart.
const (
runStartEnv = "X_TELEMETRY_TEST_START"
telemetryDirEnv = "X_TELEMETRY_TEST_START_TELEMETRY_DIR"
uploadURLEnv = "X_TELEMETRY_TEST_START_UPLOAD_URL"
asofEnv = "X_TELEMETRY_TEST_START_ASOF"
)
func TestMain(m *testing.M) {
// TestStart can't use internal/regtest, because Start itself also uses
// fork+exec to start a subprocess, which does not interact well with the
// fork+exec trick used by regtest.RunProg.
if prog := os.Getenv(runStartEnv); prog != "" {
os.Exit(runProg(prog))
}
os.Exit(m.Run())
}
// runProg runs the given program.
// See the switch statement below.
func runProg(prog string) int {
mustGetEnv := func(envvar string) string {
v := os.Getenv(envvar)
if v == "" {
log.Fatalf("missing required environment var %q", envvar)
}
return v
}
// Get the fake time used by all programs.
asof, err := time.Parse("2006-01-02", mustGetEnv(asofEnv))
if err != nil {
log.Fatalf("parsing %s: %v", asofEnv, err)
}
// Set global state.
ic.CounterTime = func() time.Time { return asof } // must be done before Open
countertest.Open(mustGetEnv(telemetryDirEnv))
switch prog {
case "setmode":
// Use the modified time above for the asof time.
if err := it.Default.SetModeAsOf("on", asof); err != nil {
log.Fatalf("setting mode: %v", err)
}
case "inc":
// (CounterTime is already set above)
counter.Inc("teststart/counter")
case "start":
res := telemetry.Start(telemetry.Config{
// No need to set TelemetryDir since the Default dir is already set by countertest.Open.
Upload: true,
UploadURL: mustGetEnv(uploadURLEnv),
UploadStartTime: asof,
})
res.Wait()
default:
log.Fatalf("unknown program %q", prog)
}
return 0
}
func execProg(t *testing.T, telemetryDir, prog string, asof time.Time, env ...string) {
// Run the runStart function below, via a fork+exec trick.
exe, err := os.Executable()
if err != nil {
t.Error(err)
return
}
cmd := exec.Command(exe, "** TestStart **") // this unused arg is just for ps(1)
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, asofEnv+"="+asof.Format("2006-01-02"))
cmd.Env = append(cmd.Env, telemetryDirEnv+"="+telemetryDir)
cmd.Env = append(cmd.Env, runStartEnv+"="+prog) // see TestMain
cmd.Env = append(cmd.Env, env...)
out, err := cmd.Output()
if err != nil {
t.Errorf("program failed unexpectedly (%v)\n%s", err, out)
}
}
func TestStart(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
// This test sets up a telemetry environment, and then runs a test program
// that increments a counter, and uploads via telemetry.Start.
telemetryDir := t.TempDir()
uploaded := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uploaded = true
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("error reading body: %v", err)
} else {
if body := string(body); !strings.Contains(body, "teststart/counter") {
t.Errorf("upload does not contain \"teststart/counter\":\n%s", body)
}
}
}))
uploadEnv := []string{uploadURLEnv + "=" + server.URL}
uc := regtest.CreateTestUploadConfig(t, []string{"teststart/counter"}, nil)
uploadEnv = append(uploadEnv, configtest.LocalProxyEnv(t, uc, "v1.2.3")...)
// Script programs.
now := time.Now()
execProg(t, telemetryDir, "setmode", now.Add(-30*24*time.Hour)) // back-date telemetry acceptance
execProg(t, telemetryDir, "inc", now.Add(-8*24*time.Hour)) // increment the counter
execProg(t, telemetryDir, "start", now, uploadEnv...) // run start
if !uploaded {
t.Fatalf("no upload occurred on %v", os.Getpid())
}
}
func TestConcurrentStart(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
telemetryDir := t.TempDir()
var uploadMu sync.Mutex
uploads := map[string]int{} // date -> uploads
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.URL.Path
if idx := strings.LastIndex(r.URL.Path, "/"); idx >= 0 {
key = r.URL.Path[idx+len("/"):]
}
uploadMu.Lock()
uploads[key]++
uploadMu.Unlock()
}))
uploadEnv := []string{uploadURLEnv + "=" + server.URL}
uc := regtest.CreateTestUploadConfig(t, []string{"teststart/counter"}, nil)
uploadEnv = append(uploadEnv, configtest.LocalProxyEnv(t, uc, "v1.2.3")...)
now := time.Now()
execProg(t, telemetryDir, "setmode", now.Add(-365*24*time.Hour)) // back-date telemetry acceptance
execProg(t, telemetryDir, "inc", now.Add(-8*24*time.Hour)) // increment the counter
// Populate three weeks of counters to upload.
for i := -28; i < -7; i++ { // Populate three weeks of counters to upload.
execProg(t, telemetryDir, "inc", now.Add(time.Duration(i)*24*time.Hour))
}
// Run start concurrently.
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
execProg(t, telemetryDir, "start", now, uploadEnv...)
}()
}
wg.Wait()
// Expect exactly three weeks to be uploaded.
if got, want := len(uploads), 3; got != want {
t.Errorf("got %d report dates, want %d", got, want)
}
for asof, n := range uploads {
if n != 1 {
t.Errorf("got %d reports for %s, want 1", n, asof)
}
}
}