blob: d2ff7d3417f2c1c81a2df7deaf695e8ac7a638f7 [file] [log] [blame]
// 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, progVers, progName := ProgInfo(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: progName,
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
}