whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,243 @@
// 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 regtest
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/internal/config"
icounter "golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
)
func TestRunProg(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
prog1 := NewProgram(t, "prog1", func() int {
fmt.Println("FuncB")
return 0
})
prog2 := NewProgram(t, "prog2", func() int {
fmt.Println("FuncC")
return 1
})
telemetryDir := t.TempDir()
if out, err := RunProg(t, telemetryDir, prog1); err != nil || !bytes.Contains(out, []byte("FuncB")) || bytes.Contains(out, []byte("FuncC")) {
t.Errorf("first RunProg = (%s, %v), want FuncB' and succeed", out, err)
}
t.Run("in subtest", func(t *testing.T) {
if out, err := RunProg(t, telemetryDir, prog2); err == nil || bytes.Contains(out, []byte("FuncB")) || !bytes.Contains(out, []byte("FuncC")) {
t.Errorf("second RunProg = (%s, %v), want 'FuncC' and fail", out, err)
}
})
}
func programIncCounters() int {
counter.Inc("counter")
counter.Inc("counter:surprise")
counter.New("gopls/editor:expected").Inc()
counter.New("gopls/editor:surprise").Inc()
counter.NewStack("stack/expected", 1).Inc()
counter.NewStack("stack-surprise", 1).Inc()
return 0
}
func TestE2E_off(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
log.Printf("GOOS=%s GOARCH=%s", runtime.GOOS, runtime.GOARCH)
prog := NewProgram(t, "prog", programIncCounters)
tests := []struct {
mode string // if empty, don't set the mode
wantLocalDir bool
}{
{"", true},
{"local", true},
{"on", true},
{"off", false},
}
for _, test := range tests {
t.Run(fmt.Sprintf("mode=%s", test.mode), func(t *testing.T) {
dir := telemetry.NewDir(t.TempDir())
if test.mode != "" {
if err := dir.SetMode(test.mode); err != nil {
t.Fatalf("SetMode failed: %v", err)
}
}
out, err := RunProg(t, dir.Dir(), prog)
if err != nil {
t.Fatalf("program failed unexpectedly (%v)\n%s", err, out)
}
_, err = os.Stat(dir.LocalDir())
if err != nil && !os.IsNotExist(err) {
t.Fatalf("os.Stat(%q): unexpected error: %v", dir.LocalDir(), err)
}
if gotLocalDir := err == nil; gotLocalDir != test.wantLocalDir {
t.Errorf("got /local dir: %v, want %v; out:\n%s", gotLocalDir, test.wantLocalDir, string(out))
}
})
}
}
func TestE2E(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
testenv.MustHaveExec(t)
programIncCounters := NewProgram(t, "prog", programIncCounters)
telemetryDir := t.TempDir()
goVers, progPath, progVers := ProgramInfo(t)
out, err := RunProg(t, telemetryDir, programIncCounters)
if err != nil {
t.Fatalf("program failed unexpectedly (%v)\n%s", err, out)
}
// TODO: retrieve config through a module proxy so we test internal/configstore code path.
cfg := &telemetry.UploadConfig{
GOOS: []string{runtime.GOOS},
GOARCH: []string{runtime.GOARCH},
GoVersion: []string{goVers},
Programs: []*telemetry.ProgramConfig{
{
Name: progPath,
Versions: []string{progVers},
Counters: []telemetry.CounterConfig{
{Name: "counter", Rate: 1},
{Name: "gopls/editor:{expected, other}", Rate: 1},
},
Stacks: []telemetry.CounterConfig{
{Name: "stack/expected", Rate: 1, Depth: 1},
},
},
},
}
// TODO: check if weekday file exists.
// TODO: test upload path.
// - change the global clock (maybe internal/clock package?)
// - start an upload server
// - Run(t, telemetryDir, func() int { upload.Run(...) })
// - check if the upload server received expected data
// - check if the local and upload directories in the expected state
uploaded, notUploaded, err := parseCounters(cfg, telemetryDir)
if err != nil {
t.Fatalf("failed to simulate upload: %v", err)
}
wantUpload := map[string]uint64{
"counter": 1,
"gopls/editor:expected": 1,
"stack/expected\n": 1, // prefix of the stack counter name + "\n". see parseCounters.
}
testCounterUploadStatus(t, uploaded, wantUpload, false)
wantNotUpload := map[string]uint64{
"counter:surprise": 1,
"gopls/editor:surprise": 1,
"stack-surprise\n": 1, // prefix of the stack counter name + "\n". see parseCounters.
}
testCounterUploadStatus(t, notUploaded, wantNotUpload, true)
}
// testCounterUploadStatus checks if got and want counter maps match.
// If allowUnexpected is true, it checks if got is a superset of want.
func testCounterUploadStatus(t *testing.T, got, want map[string]uint64, allowUnexpected bool) {
t.Helper()
seen := got
if allowUnexpected {
// filter out unexpected counters from got, for comparison.
m := make(map[string]uint64, len(want))
for k, v := range got {
if _, ok := want[k]; ok {
m[k] = v
}
}
seen = m
}
if !reflect.DeepEqual(seen, want) {
// TODO(hyangah) implement diff or copy Go project's internal/diff for pretty printing of diff.
// Or, move internal/regtest to the godev module where we can depend on go-cmp.
t.Errorf("unmet expectation:\ngot %v\nwant %v", stringify(got), stringify(want))
}
}
func stringify(a any) string {
encoded, err := json.MarshalIndent(a, "\t", " ")
if err != nil {
return fmt.Sprintf("unmarshallable - %v", err)
}
return string(encoded)
}
// parseCounters reads all counter files in the local telemetry directory, and
// returns all observed counters grouped by whether the counter names are included
// in the specified configuration.
// For simplicity in the comparison code, the returned maps represent a stack counter
// with its counter name prefix and "\n". For example, if there are "stackcounter\npkg.F:..."
// and "stackcounter\npkg.G:..", "stackcounter\n" will hold the sum of those counters.
func parseCounters(uc *telemetry.UploadConfig, telemetryDir string) (uploadable, notUploadable map[string]uint64, _ error) {
cfg := config.NewConfig(uc)
localDir := filepath.Join(telemetryDir, "local")
entries, err := os.ReadDir(localDir)
if err != nil {
return nil, nil, err
}
uploadable, notUploadable = make(map[string]uint64), make(map[string]uint64)
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".count" {
continue
}
data, err := os.ReadFile(filepath.Join(localDir, entry.Name()))
if err != nil { // ignore unreadable file.
continue
}
// TODO(hyangah): how about exposing "Parse" to public for testing? (i.e. countertest.Parse)?
parsed, err := icounter.Parse(entry.Name(), data)
if err != nil { // ignore unparsable file
continue
}
// The following is temporary until the upload package implements the exact same logic.
// TODO(hyangah): replace with the shared logic between the uploader and the local viewer.
maybeUploadable := true &&
cfg.HasGOOS(parsed.Meta["GOOS"]) &&
cfg.HasGOARCH(parsed.Meta["GOARCH"]) &&
cfg.HasGoVersion(parsed.Meta["GoVersion"]) &&
cfg.HasProgram(parsed.Meta["Program"]) &&
cfg.HasVersion(parsed.Meta["Program"], parsed.Meta["Version"])
for k, v := range parsed.Count {
counterPrefix, _, isStackCounter := strings.Cut(k, "\n")
isUploadable := maybeUploadable
key := k
if isStackCounter {
isUploadable = isUploadable && cfg.HasStack(parsed.Meta["Program"], counterPrefix)
key = counterPrefix + "\n"
} else {
isUploadable = isUploadable && cfg.HasCounter(parsed.Meta["Program"], k)
}
if isUploadable {
uploadable[key] = uploadable[key] + v
} else {
notUploadable[key] = notUploadable[key] + v
}
}
}
return uploadable, notUploadable, nil
}
@@ -0,0 +1,156 @@
// 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 regtest provides helpers for end-to-end testing
// involving counter and upload packages.
package regtest
import (
"fmt"
"log"
"os"
"os/exec"
"runtime"
"runtime/debug"
"strings"
"testing"
"time"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/counter/countertest"
internalcounter "golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
const (
telemetryDirEnvVar = "_COUNTERTEST_RUN_TELEMETRY_DIR"
asofEnvVar = "_COUNTERTEST_ASOF"
entryPointEnvVar = "_COUNTERTEST_ENTRYPOINT"
)
var (
telemetryDirEnvVarValue = os.Getenv(telemetryDirEnvVar)
asofEnvVarValue = os.Getenv(asofEnvVar)
entryPointEnvVarValue = os.Getenv(entryPointEnvVar)
)
// Program is a value that can be used to identify a program in the test.
type Program string
// NewProgram returns a Program value that can be used to identify a program
// to run by RunProg. The program must be registered with NewProgram before
// the first call to RunProg in the test function.
//
// RunProg runs this binary in a separate process with special environment
// variables that specify the entry point. When this binary runs with the
// environment variables that match the specified name, NewProgram calls
// the given fn and exits with the return value. Note that all the code
// before NewProgram is executed in both the main process and the subprocess.
func NewProgram(t *testing.T, name string, fn func() int) Program {
if telemetryDirEnvVarValue != "" && entryPointEnvVarValue == name {
// We are running the separate process that was spawned by RunProg.
fmt.Fprintf(os.Stderr, "running program %q\n", name)
if asofEnvVarValue != "" {
asof, err := time.Parse(telemetry.DateOnly, asofEnvVarValue)
if err != nil {
log.Fatalf("error parsing asof time %q: %v", asof, err)
}
fmt.Fprintf(os.Stderr, "setting counter time to %s\n", name)
internalcounter.CounterTime = func() time.Time {
return asof
}
}
countertest.Open(telemetryDirEnvVarValue)
os.Exit(fn())
}
testName, _, _ := strings.Cut(t.Name(), "/")
registered, ok := registeredPrograms[testName]
if !ok {
registered = make(map[string]bool)
}
if registered[name] {
t.Fatalf("program %q was already registered", name)
}
registered[name] = true
return Program(name)
}
// NewIncProgram returns a basic program that increments the given counters and
// exits with status 0.
func NewIncProgram(t *testing.T, name string, counters ...string) Program {
return NewProgram(t, name, func() int {
for _, c := range counters {
counter.Inc(c)
}
return 0
})
}
// registeredPrograms stores all registered program names to detect duplicate registrations.
var registeredPrograms = make(map[string]map[string]bool) // test name -> program name -> exist
// RunProg runs the program prog in a separate process with the specified
// telemetry directory. RunProg can be called multiple times in the same test,
// but all the programs must be registered with NewProgram before the first
// call to RunProg.
func RunProg(t *testing.T, telemetryDir string, prog Program) ([]byte, error) {
return RunProgAsOf(t, telemetryDir, time.Time{}, prog)
}
// RunProgAsOf is like RunProg, but executes the program as of a specific
// counter time.
func RunProgAsOf(t *testing.T, telemetryDir string, asof time.Time, prog Program) ([]byte, error) {
if telemetryDirEnvVarValue != "" {
fmt.Fprintf(os.Stderr, "unknown program %q\n %s %s", prog, telemetryDirEnvVarValue, entryPointEnvVarValue)
os.Exit(2)
}
testName, _, _ := strings.Cut(t.Name(), "/")
testBin, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("cannot determine the current process's executable name: %v", err)
}
// Spawn a subprocess to run the 'prog' by setting telemetryDirEnvVar.
cmd := exec.Command(testBin, "-test.run", fmt.Sprintf("^%s$", testName))
cmd.Env = append(os.Environ(), telemetryDirEnvVar+"="+telemetryDir, entryPointEnvVar+"="+string(prog))
if !asof.IsZero() {
cmd.Env = append(cmd.Env, asofEnvVar+"="+asof.Format(telemetry.DateOnly))
}
return cmd.CombinedOutput()
}
// ProgramInfo returns the go version, program name and version info the
// process would record in its counter file.
func ProgramInfo(t *testing.T) (goVersion, progPath, progVersion string) {
info, ok := debug.ReadBuildInfo()
if !ok {
t.Fatal("cannot read build info - it's likely this setup is unsupported by the counter package")
}
return telemetry.ProgramInfo(info)
}
// CreateTestUploadConfig creates a new upload config for the current program,
// permitting the given counters.
func CreateTestUploadConfig(t *testing.T, counterNames, stackCounterNames []string) *telemetry.UploadConfig {
goVersion, progPath, progVersion := ProgramInfo(t)
GOOS, GOARCH := runtime.GOOS, runtime.GOARCH
programConfig := &telemetry.ProgramConfig{
Name: progPath,
Versions: []string{progVersion},
}
for _, c := range counterNames {
programConfig.Counters = append(programConfig.Counters, telemetry.CounterConfig{Name: c, Rate: 1})
}
for _, c := range stackCounterNames {
programConfig.Stacks = append(programConfig.Stacks, telemetry.CounterConfig{Name: c, Rate: 1, Depth: 16})
}
return &telemetry.UploadConfig{
GOOS: []string{GOOS},
GOARCH: []string{GOARCH},
SampleRate: 1.0,
GoVersion: []string{goVersion},
Programs: []*telemetry.ProgramConfig{programConfig},
}
}