whatcanGOwrong
This commit is contained in:
+243
@@ -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
|
||||
}
|
||||
+156
@@ -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},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user