Skip to content

Commit

Permalink
internal/lsp/regtest: eliminate arbitrary timeouts
Browse files Browse the repository at this point in the history
We care that gopls operations complete within a reasonable time.
However, what is “reasonable” depends strongly on the specifics of the
user and the hardware they are running on: a timeout that would be
perfectly reasonable on a high-powered user workstation with little
other load may be far too short on an overloaded and/or underpowered
CI builder.

This change adjusts the regtest runner to use the test deadline
instead of an arbitrary, flag-defined timeout; we expect the user or
system running the test to scale the test timeout appropriately to the
specific platform and system load.

When the testing package gains support for per-test timeouts
(golang/go#48157), this approach will automatically apply those
timeouts too.

If we decide that we also want to test specific performance and/or
latency targets, we can set up specific configurations for that (as
either aggressive per-test timeouts or benchmarks) in a followup
change.

For golang/go#50582

Change-Id: I1ab11b2049effb097aa620046fe11609269f91c4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/380497
Trust: Bryan Mills <[email protected]>
Run-TryBot: Bryan Mills <[email protected]>
gopls-CI: kokoro <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Reviewed-by: Robert Findley <[email protected]>
  • Loading branch information
Bryan C. Mills committed Jan 25, 2022
1 parent 97de9ec commit c20fd7c
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 28 deletions.
7 changes: 3 additions & 4 deletions gopls/internal/regtest/bench/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"runtime/pprof"
"testing"
"time"

"golang.org/x/tools/gopls/internal/hooks"
"golang.org/x/tools/internal/lsp/fake"
Expand All @@ -32,9 +31,9 @@ func benchmarkOptions(dir string) []RunOption {
SkipLogs(),
// The Debug server only makes sense if running in singleton mode.
Modes(Singleton),
// Set a generous timeout. Individual tests should control their own
// graceful termination.
Timeout(20 * time.Minute),
// Remove the default timeout. Individual tests should control their
// own graceful termination.
NoDefaultTimeout(),

// Use the actual proxy, since we want our builds to succeed.
GOPROXY("https://proxy.golang.org"),
Expand Down
9 changes: 0 additions & 9 deletions gopls/internal/regtest/codelens/codelens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"runtime"
"strings"
"testing"
"time"

"golang.org/x/tools/gopls/internal/hooks"
. "golang.org/x/tools/internal/lsp/regtest"
Expand Down Expand Up @@ -292,13 +291,6 @@ func TestGCDetails(t *testing.T) {
t.Skipf("the gc details code lens doesn't work on Android")
}

// TestGCDetails seems to suffer from poor performance on certain builders.
// Give it as long as it needs to complete.
timeout := 60 * time.Second
if d, ok := testenv.Deadline(t); ok {
timeout = time.Until(d) * 19 / 20 // Leave 5% headroom for cleanup.
}

const mod = `
-- go.mod --
module mod.com
Expand All @@ -318,7 +310,6 @@ func main() {
CodeLenses: map[string]bool{
"gc_details": true,
}},
Timeout(timeout),
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.ExecuteCodeLensCommand("main.go", command.GCDetails)
Expand Down
15 changes: 14 additions & 1 deletion internal/lsp/regtest/regtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,24 @@ import (
var (
runSubprocessTests = flag.Bool("enable_gopls_subprocess_tests", false, "run regtests against a gopls subprocess")
goplsBinaryPath = flag.String("gopls_test_binary", "", "path to the gopls binary for use as a remote, for use with the -enable_gopls_subprocess_tests flag")
regtestTimeout = flag.Duration("regtest_timeout", 20*time.Second, "default timeout for each regtest")
regtestTimeout = flag.Duration("regtest_timeout", defaultRegtestTimeout(), "if nonzero, default timeout for each regtest; defaults to GOPLS_REGTEST_TIMEOUT")
skipCleanup = flag.Bool("regtest_skip_cleanup", false, "whether to skip cleaning up temp directories")
printGoroutinesOnFailure = flag.Bool("regtest_print_goroutines", false, "whether to print goroutines info on failure")
)

func defaultRegtestTimeout() time.Duration {
s := os.Getenv("GOPLS_REGTEST_TIMEOUT")
if s == "" {
return 0
}
d, err := time.ParseDuration(s)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid GOPLS_REGTEST_TIMEOUT %q: %v\n", s, err)
os.Exit(2)
}
return d
}

var runner *Runner

type regtestRunner interface {
Expand Down
40 changes: 26 additions & 14 deletions internal/lsp/regtest/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"golang.org/x/tools/internal/lsp/lsprpc"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/xcontext"
)

Expand Down Expand Up @@ -71,20 +72,19 @@ type Runner struct {
}

type runConfig struct {
editor fake.EditorConfig
sandbox fake.SandboxConfig
modes Mode
timeout time.Duration
debugAddr string
skipLogs bool
skipHooks bool
optionsHook func(*source.Options)
editor fake.EditorConfig
sandbox fake.SandboxConfig
modes Mode
noDefaultTimeout bool
debugAddr string
skipLogs bool
skipHooks bool
optionsHook func(*source.Options)
}

func (r *Runner) defaultConfig() *runConfig {
return &runConfig{
modes: r.DefaultModes,
timeout: r.Timeout,
optionsHook: r.OptionsHook,
}
}
Expand All @@ -100,10 +100,12 @@ func (f optionSetter) set(opts *runConfig) {
f(opts)
}

// Timeout configures a custom timeout for this test run.
func Timeout(d time.Duration) RunOption {
// NoDefaultTimeout removes the timeout set by the -regtest_timeout flag, for
// individual tests that are expected to run longer than is reasonable for
// ordinary regression tests.
func NoDefaultTimeout() RunOption {
return optionSetter(func(opts *runConfig) {
opts.timeout = d
opts.noDefaultTimeout = true
})
}

Expand Down Expand Up @@ -257,8 +259,18 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
}

t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), config.timeout)
defer cancel()
ctx := context.Background()
if r.Timeout != 0 && !config.noDefaultTimeout {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.Timeout)
defer cancel()
} else if d, ok := testenv.Deadline(t); ok {
timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup.
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}

ctx = debug.WithInstance(ctx, "", "off")
if config.debugAddr != "" {
di := debug.GetInstance(ctx)
Expand Down

0 comments on commit c20fd7c

Please sign in to comment.