1
0
mirror of https://github.com/golang/go synced 2024-11-18 08:44:43 -07:00

internal/lsp/regtest: add run options to support stress testing

A bunch of options are added to enable long-running performance-oriented
tests in existing directories. They will be used in a later CL to
implement a simple stress test, as an example of what is possible.

Change-Id: I531b201b415362ea135978238b3d64b903226359
Reviewed-on: https://go-review.googlesource.com/c/tools/+/244440
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rob Findley 2020-07-22 21:15:22 -04:00 committed by Robert Findley
parent a5e28d8dab
commit 84d0e3d1cc
14 changed files with 190 additions and 84 deletions

View File

@ -10,13 +10,9 @@ import (
"golang.org/x/tools/internal/proxydir"
)
// Proxy is a file-based module proxy.
type Proxy struct {
proxydir string
}
// NewProxy creates a new proxy file tree using the txtar-encoded content.
func NewProxy(tmpdir, txt string) (*Proxy, error) {
// WriteProxy creates a new proxy file tree using the txtar-encoded content,
// and returns its URL.
func WriteProxy(tmpdir, txt string) (string, error) {
files := unpackTxt(txt)
type moduleVersion struct {
modulePath, version string
@ -33,14 +29,8 @@ func NewProxy(tmpdir, txt string) (*Proxy, error) {
}
for mv, files := range filesByModule {
if err := proxydir.WriteModuleVersion(tmpdir, mv.modulePath, mv.version, files); err != nil {
return nil, fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err)
return "", fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err)
}
}
return &Proxy{proxydir: tmpdir}, nil
}
// GOPROXY returns the GOPROXY environment variable value for this proxy
// directory.
func (p *Proxy) GOPROXY() string {
return proxydir.ToURL(p.proxydir)
return proxydir.ToURL(tmpdir), nil
}

View File

@ -22,7 +22,7 @@ import (
type Sandbox struct {
gopath string
basedir string
Proxy *Proxy
goproxy string
Workdir *Workdir
}
@ -35,12 +35,23 @@ type SandboxConfig struct {
// Files holds a txtar-encoded archive of files to populate the initial state
// of the working directory.
Files string
// ProxyFiles holds a txtar-encoded archive of files to populate a file-based
// Go proxy.
ProxyFiles string
// InGoPath specifies that the working directory should be within the
// temporary GOPATH.
InGoPath bool
// Workdir configures the working directory of the Sandbox, for running in a
// pre-existing directory. If unset, a new working directory will be created
// under RootDir.
//
// This option is incompatible with InGoPath or Files.
Workdir string
// ProxyFiles holds a txtar-encoded archive of files to populate a file-based
// Go proxy.
ProxyFiles string
// GOPROXY is the explicit GOPROXY value that should be used for the sandbox.
//
// This option is incompatible with ProxyFiles.
GOPROXY string
}
// NewSandbox creates a collection of named temporary resources, with a
@ -55,6 +66,15 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
if config == nil {
config = new(SandboxConfig)
}
if config.Workdir != "" && (config.Files != "" || config.InGoPath) {
return nil, fmt.Errorf("invalid SandboxConfig: Workdir cannot be used in conjunction with Files or InGoPath. Got %+v", config)
}
if config.GOPROXY != "" && config.ProxyFiles != "" {
return nil, fmt.Errorf("invalid SandboxConfig: GOPROXY cannot be set in conjunction with ProxyFiles. Got %+v", config)
}
sb := &Sandbox{}
defer func() {
// Clean up if we fail at any point in this constructor.
@ -68,22 +88,42 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
return nil, fmt.Errorf("creating temporary workdir: %v", err)
}
sb.basedir = baseDir
proxydir := filepath.Join(sb.basedir, "proxy")
sb.gopath = filepath.Join(sb.basedir, "gopath")
// Set the working directory as $GOPATH/src if inGopath is true.
workdir := filepath.Join(sb.gopath, "src")
dirs := []string{sb.gopath, proxydir}
if !config.InGoPath {
workdir = filepath.Join(sb.basedir, "work")
dirs = append(dirs, workdir)
if err := os.Mkdir(sb.gopath, 0755); err != nil {
return nil, err
}
for _, subdir := range dirs {
if err := os.Mkdir(subdir, 0755); err != nil {
if config.GOPROXY != "" {
sb.goproxy = config.GOPROXY
} else {
proxydir := filepath.Join(sb.basedir, "proxy")
if err := os.Mkdir(proxydir, 0755); err != nil {
return nil, err
}
sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles)
if err != nil {
return nil, err
}
}
if config.Workdir != "" {
sb.Workdir = NewWorkdir(config.Workdir)
} else {
workdir := config.Workdir
// If we don't have a pre-existing work dir, we want to create either
// $GOPATH/src or <RootDir/work>.
if config.InGoPath {
// Set the working directory as $GOPATH/src.
workdir = filepath.Join(sb.gopath, "src")
} else if workdir == "" {
workdir = filepath.Join(sb.basedir, "work")
}
if err := os.Mkdir(workdir, 0755); err != nil {
return nil, err
}
sb.Workdir = NewWorkdir(workdir)
if err := sb.Workdir.WriteInitialFiles(config.Files); err != nil {
return nil, err
}
}
sb.Proxy, err = NewProxy(proxydir, config.ProxyFiles)
sb.Workdir, err = NewWorkdir(workdir, config.Files)
return sb, nil
}
@ -126,7 +166,7 @@ func (sb *Sandbox) GOPATH() string {
func (sb *Sandbox) GoEnv() map[string]string {
vars := map[string]string{
"GOPATH": sb.GOPATH(),
"GOPROXY": sb.Proxy.GOPROXY(),
"GOPROXY": sb.goproxy,
"GO111MODULE": "",
"GOSUMDB": "off",
"GOPACKAGESDRIVER": "off",
@ -144,10 +184,15 @@ func (sb *Sandbox) RunGoCommand(ctx context.Context, verb string, args ...string
vars = append(vars, fmt.Sprintf("%s=%s", k, v))
}
inv := gocommand.Invocation{
Verb: verb,
Args: args,
WorkingDir: sb.Workdir.workdir,
Env: vars,
Verb: verb,
Args: args,
Env: vars,
}
// sb.Workdir may be nil if we exited the constructor with errors (we call
// Close to clean up any partial state from the constructor, which calls
// RunGoCommand).
if sb.Workdir != nil {
inv.WorkingDir = sb.Workdir.workdir
}
gocmdRunner := &gocommand.Runner{}
_, _, _, err := gocmdRunner.RunRaw(ctx, inv)
@ -156,8 +201,10 @@ func (sb *Sandbox) RunGoCommand(ctx context.Context, verb string, args ...string
}
// Since running a go command may result in changes to workspace files,
// check if we need to send any any "watched" file events.
if err := sb.Workdir.CheckForFileChanges(ctx); err != nil {
return fmt.Errorf("checking for file changes: %w", err)
if sb.Workdir != nil {
if err := sb.Workdir.CheckForFileChanges(ctx); err != nil {
return fmt.Errorf("checking for file changes: %w", err)
}
}
return nil
}

View File

@ -40,19 +40,22 @@ type Workdir struct {
// NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a
// Workir for operating on these files using
func NewWorkdir(dir, txt string) (*Workdir, error) {
w := &Workdir{workdir: dir}
func NewWorkdir(dir string) *Workdir {
return &Workdir{workdir: dir}
}
func (w *Workdir) WriteInitialFiles(txt string) error {
files := unpackTxt(txt)
for name, data := range files {
if err := w.writeFileData(name, string(data)); err != nil {
return nil, fmt.Errorf("writing to workdir: %w", err)
return fmt.Errorf("writing to workdir: %w", err)
}
}
// Poll to capture the current file state.
if _, err := w.pollFiles(); err != nil {
return nil, fmt.Errorf("polling files: %w", err)
return fmt.Errorf("polling files: %w", err)
}
return w, nil
return nil
}
// RootURI returns the root URI for this working directory of this scratch

View File

@ -29,8 +29,8 @@ func newWorkdir(t *testing.T) (*Workdir, <-chan []FileEvent, func()) {
if err != nil {
t.Fatal(err)
}
wd, err := NewWorkdir(tmpdir, data)
if err != nil {
wd := NewWorkdir(tmpdir)
if err := wd.WriteInitialFiles(data); err != nil {
t.Fatal(err)
}
cleanup := func() {

View File

@ -122,7 +122,7 @@ func main() {
if before == after {
t.Fatalf("go.mod file was unchanged by upgrade command")
}
}, WithProxy(proxyWithLatest))
}, WithProxyFiles(proxyWithLatest))
}
func TestRegenerateCgo(t *testing.T) {

View File

@ -402,7 +402,7 @@ func TestResolveDiagnosticWithDownload(t *testing.T) {
// diagnostic for the wrong formatting type.
// TODO: we should be able to easily also match the diagnostic message.
env.Await(env.DiagnosticAtRegexp("print.go", "fmt.Printf"))
}, WithProxy(testPackageWithRequireProxy))
}, WithProxyFiles(testPackageWithRequireProxy))
}
func TestMissingDependency(t *testing.T) {
@ -664,7 +664,7 @@ func main() {
env.Await(
env.DiagnosticAtRegexp("main.go", `"github.com/ardanlabs/conf"`),
)
}, WithProxy(ardanLabsProxy))
}, WithProxyFiles(ardanLabsProxy))
}
// Test for golang/go#38207.
@ -700,7 +700,7 @@ func main() {
env.Await(
EmptyDiagnostics("main.go"),
)
}, WithProxy(ardanLabsProxy))
}, WithProxyFiles(ardanLabsProxy))
}
// Test for golang/go#36960.

View File

@ -105,13 +105,13 @@ type condition struct {
// NewEnv creates a new test environment using the given scratch environment
// and gopls server.
func NewEnv(ctx context.Context, t *testing.T, scratch *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig) *Env {
func NewEnv(ctx context.Context, t *testing.T, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) *Env {
t.Helper()
conn := ts.Connect(ctx)
env := &Env{
T: t,
Ctx: ctx,
Sandbox: scratch,
Sandbox: sandbox,
Server: ts,
state: State{
diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
@ -120,15 +120,18 @@ func NewEnv(ctx context.Context, t *testing.T, scratch *fake.Sandbox, ts servert
},
waiters: make(map[int]*condition),
}
hooks := fake.ClientHooks{
OnDiagnostics: env.onDiagnostics,
OnLogMessage: env.onLogMessage,
OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate,
OnProgress: env.onProgress,
OnShowMessage: env.onShowMessage,
OnShowMessageRequest: env.onShowMessageRequest,
var hooks fake.ClientHooks
if withHooks {
hooks = fake.ClientHooks{
OnDiagnostics: env.onDiagnostics,
OnLogMessage: env.onLogMessage,
OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate,
OnProgress: env.onProgress,
OnShowMessage: env.onShowMessage,
OnShowMessageRequest: env.onShowMessageRequest,
}
}
editor, err := fake.NewEditor(scratch, editorConfig).Connect(ctx, conn, hooks)
editor, err := fake.NewEditor(sandbox, editorConfig).Connect(ctx, conn, hooks)
if err != nil {
t.Fatal(err)
}

View File

@ -144,7 +144,7 @@ var _, _ = x.X, y.Y
editorConfig := fake.EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}}
withOptions(
WithEditorConfig(editorConfig),
WithProxy(proxy),
WithProxyFiles(proxy),
).run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.Await(env.DiagnosticAtRegexp("main.go", `y.Y`))

View File

@ -81,5 +81,5 @@ const Hello = "Hello"
if len(links) != 0 {
t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links)
}
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
}

View File

@ -49,7 +49,7 @@ func main() {
}
`
t.Run("basic", func(t *testing.T) {
withOptions(WithProxy(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
// Open the file and make sure that the initial workspace load does not
// modify the go.mod file.
goModContent := env.ReadWorkspaceFile("go.mod")
@ -76,7 +76,7 @@ func main() {
t.Run("delete main.go", func(t *testing.T) {
t.Skipf("This test will be flaky until golang/go#40269 is resolved.")
withOptions(WithProxy(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
goModContent := env.ReadWorkspaceFile("go.mod")
mainContent := env.ReadWorkspaceFile("main.go")
env.OpenFile("main.go")
@ -140,7 +140,7 @@ require example.com v1.2.3
if got := env.Editor.BufferText("go.mod"); got != want {
t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
}
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
}
// Test to reproduce golang/go#39041. It adds a new require to a go.mod file
@ -207,7 +207,7 @@ require (
if got := env.Editor.BufferText("go.mod"); got != want {
t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(want, got))
}
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
}
// TODO: For this test to be effective, the sandbox's file watcher must respect
@ -242,7 +242,7 @@ go 1.12
env.Await(
EmptyDiagnostics("go.mod"),
)
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
}
func TestBadlyVersionedModule(t *testing.T) {
@ -328,7 +328,7 @@ require (
if got := env.Editor.BufferText("go.mod"); got != want {
t.Fatalf("suggested fixes failed:\n%s", tests.Diff(want, got))
}
}, WithProxy(badModule))
}, WithProxyFiles(badModule))
}
// Reproduces golang/go#38232.
@ -367,7 +367,7 @@ func main() {
env.Await(
env.DiagnosticAtRegexp("main.go", "x = "),
)
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
})
const known = `
@ -404,7 +404,7 @@ func main() {
env.Await(
env.DiagnosticAtRegexp("main.go", "x = "),
)
}, WithProxy(proxy))
}, WithProxyFiles(proxy))
})
}
@ -427,7 +427,7 @@ func main() {
fmt.Println(blah.Name)
}
`
withOptions(WithProxy(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
withOptions(WithProxyFiles(proxy)).run(t, untidyModule, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.Await(
env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
@ -487,7 +487,7 @@ func main() {
println(blah.Name)
}
`
withOptions(WithProxy(badProxy)).run(t, module, func(t *testing.T, env *Env) {
withOptions(WithProxyFiles(badProxy)).run(t, module, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.Await(
env.DiagnosticAtRegexp("go.mod", "require example.com v1.2.3"),

View File

@ -68,10 +68,13 @@ type Runner struct {
}
type runConfig struct {
editor fake.EditorConfig
sandbox fake.SandboxConfig
modes Mode
timeout time.Duration
editor fake.EditorConfig
sandbox fake.SandboxConfig
modes Mode
timeout time.Duration
debugAddr string
skipLogs bool
skipHooks bool
}
func (r *Runner) defaultConfig() *runConfig {
@ -99,8 +102,8 @@ func WithTimeout(d time.Duration) RunOption {
})
}
// WithProxy configures a file proxy using the given txtar-encoded string.
func WithProxy(txt string) RunOption {
// WithProxyFiles configures a file proxy using the given txtar-encoded string.
func WithProxyFiles(txt string) RunOption {
return optionSetter(func(opts *runConfig) {
opts.sandbox.ProxyFiles = txt
})
@ -148,6 +151,50 @@ func InGOPATH() RunOption {
})
}
// WithDebugAddress configures a debug server bound to addr. This option is
// currently only supported when executing in Singleton mode. It is intended to
// be used for long-running stress tests.
func WithDebugAddress(addr string) RunOption {
return optionSetter(func(opts *runConfig) {
opts.debugAddr = addr
})
}
// SkipLogs skips the buffering of logs during test execution. It is intended
// for long-running stress tests.
func SkipLogs() RunOption {
return optionSetter(func(opts *runConfig) {
opts.skipLogs = true
})
}
// InExistingDir runs the test in a pre-existing directory. If set, no initial
// files may be passed to the runner. It is intended for long-running stress
// tests.
func InExistingDir(dir string) RunOption {
return optionSetter(func(opts *runConfig) {
opts.sandbox.Workdir = dir
})
}
// NoHooks disables the test runner's client hooks that are used for
// instrumenting expectations (tracking diagnostics, logs, work done, etc.). It
// is intended for performance-sensitive stress tests.
func NoHooks() RunOption {
return optionSetter(func(opts *runConfig) {
opts.skipHooks = true
})
}
// WithGOPROXY configures the test environment to have an explicit proxy value.
// This is intended for stress tests -- to ensure their isolation, regtests
// should instead use WithProxyFiles.
func WithGOPROXY(goproxy string) RunOption {
return optionSetter(func(opts *runConfig) {
opts.sandbox.GOPROXY = goproxy
})
}
type TestFunc func(t *testing.T, env *Env)
// Run executes the test function in the default configured gopls execution
@ -175,10 +222,23 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
if config.modes&tc.mode == 0 {
continue
}
if config.debugAddr != "" && tc.mode != Singleton {
// Debugging is useful for running stress tests, but since the daemon has
// likely already been started, it would be too late to debug.
t.Fatalf("debugging regtest servers only works in Singleton mode, "+
"got debug addr %q and mode %v", config.debugAddr, tc.mode)
}
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), config.timeout)
defer cancel()
ctx = debug.WithInstance(ctx, "", "")
ctx = debug.WithInstance(ctx, "", "off")
if config.debugAddr != "" {
di := debug.GetInstance(ctx)
di.DebugAddress = config.debugAddr
di.Serve(ctx)
di.MonitorMemory(ctx)
}
tempDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name()))
if err := os.MkdirAll(tempDir, 0755); err != nil {
@ -198,10 +258,13 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
// exited before we clean up.
r.AddCloser(sandbox)
ss := tc.getServer(ctx, t)
framer := jsonrpc2.NewRawStream
ls := &loggingFramer{}
framer := ls.framer(jsonrpc2.NewRawStream)
if !config.skipLogs {
framer = ls.framer(jsonrpc2.NewRawStream)
}
ts := servertest.NewPipeServer(ctx, ss, framer)
env := NewEnv(ctx, t, sandbox, ts, config.editor)
env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks)
defer func() {
if t.Failed() && r.PrintGoroutinesOnFailure {
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
@ -261,7 +324,7 @@ func (r *Runner) getTestServer() *servertest.TCPServer {
defer r.mu.Unlock()
if r.ts == nil {
ctx := context.Background()
ctx = debug.WithInstance(ctx, "", "")
ctx = debug.WithInstance(ctx, "", "off")
ss := lsprpc.NewStreamServer(cache.New(ctx, nil), false)
r.ts = servertest.NewTCPServer(ctx, ss, nil)
}

View File

@ -28,7 +28,7 @@ func runShared(t *testing.T, program string, testFunc func(env1 *Env, env2 *Env)
runner.Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
// Create a second test session connected to the same workspace and server
// as the first.
env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config)
env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true)
testFunc(env1, env2)
}, WithModes(modes))
}

View File

@ -58,5 +58,5 @@ func _() {
DiagnosticAt("a/a1.go", 6, 5),
),
)
}, WithProxy(basicProxy))
}, WithProxyFiles(basicProxy))
}

View File

@ -74,7 +74,7 @@ func TestReferences(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
opts := []RunOption{WithProxy(workspaceProxy)}
opts := []RunOption{WithProxyFiles(workspaceProxy)}
if tt.rootPath != "" {
opts = append(opts, WithRootPath(tt.rootPath))
}
@ -95,7 +95,7 @@ func TestReferences(t *testing.T) {
// VS Code, where clicking on a reference result triggers a
// textDocument/didOpen without a corresponding textDocument/didClose.
func TestClearAnalysisDiagnostics(t *testing.T) {
withOptions(WithProxy(workspaceProxy), WithRootPath("inner")).run(t, workspaceModule, func(t *testing.T, env *Env) {
withOptions(WithProxyFiles(workspaceProxy), WithRootPath("inner")).run(t, workspaceModule, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.Await(
env.DiagnosticAtRegexp("main2.go", "fmt.Print"),