| // Copyright 2020 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 integration provides a framework for writing integration tests of gopls. |
| // |
| // The behaviors that matter to users, and the scenarios they |
| // typically describe in bug report, are usually expressed in terms of |
| // editor interactions. For example: "When I open my editor in this |
| // directory, navigate to this file, and change this line, I get a |
| // diagnostic that doesn't make sense". The integration package |
| // provides an API for gopls maintainers to express these types of |
| // user interactions in ordinary Go tests, validate them, and run them |
| // in a variety of execution modes. |
| // |
| // # Test package setup |
| // |
| // The integration test package uses a couple of uncommon patterns to reduce |
| // boilerplate in test bodies. First, it is intended to be imported as "." so |
| // that helpers do not need to be qualified. Second, it requires some setup |
| // that is currently implemented in the integration.Main function, which must be |
| // invoked by TestMain. Therefore, a minimal integration testing package looks |
| // like this: |
| // |
| // package feature |
| // |
| // import ( |
| // "fmt" |
| // "testing" |
| // |
| // "golang.org/x/tools/gopls/internal/hooks" |
| // . "golang.org/x/tools/gopls/internal/test/integration" |
| // ) |
| // |
| // func TestMain(m *testing.M) { |
| // os.Exit(Main(m, hooks.Options)) |
| // } |
| // |
| // # Writing a simple integration test |
| // |
| // To run an integration test use the integration.Run function, which accepts a |
| // txtar-encoded archive defining the initial workspace state. This function |
| // sets up the workspace in a temporary directory, creates a fake text editor, |
| // starts gopls, and initializes an LSP session. It then invokes the provided |
| // test function with an *Env encapsulating the newly created |
| // environment. Because gopls may be run in various modes (as a sidecar or |
| // daemon process, with different settings), the test runner may perform this |
| // process multiple times, re-running the test function each time with a new |
| // environment. |
| // |
| // func TestOpenFile(t *testing.T) { |
| // const files = ` |
| // -- go.mod -- |
| // module mod.com |
| // |
| // go 1.12 |
| // -- foo.go -- |
| // package foo |
| // ` |
| // Run(t, files, func(t *testing.T, env *Env) { |
| // env.OpenFile("foo.go") |
| // }) |
| // } |
| // |
| // # Configuring integration test execution |
| // |
| // The integration package exposes several options that affect the setup process |
| // described above. To use these options, use the WithOptions function: |
| // |
| // WithOptions(opts...).Run(...) |
| // |
| // See options.go for a full list of available options. |
| // |
| // # Operating on editor state |
| // |
| // To operate on editor state within the test body, the Env type provides |
| // access to the workspace directory (Env.SandBox), text editor (Env.Editor), |
| // LSP server (Env.Server), and 'awaiter' (Env.Awaiter). |
| // |
| // In most cases, operations on these primitive building blocks of the |
| // integration test environment expect a Context (which should be a child of |
| // env.Ctx), and return an error. To avoid boilerplate, the Env exposes a set |
| // of wrappers in wrappers.go for use in scripting: |
| // |
| // env.CreateBuffer("c/c.go", "") |
| // env.EditBuffer("c/c.go", editor.Edit{ |
| // Text: `package c`, |
| // }) |
| // |
| // These wrappers thread through Env.Ctx, and call t.Fatal on any errors. |
| // |
| // # Expressing expectations |
| // |
| // The general pattern for an integration test is to script interactions with the |
| // fake editor and sandbox, and assert that gopls behaves correctly after each |
| // state change. Unfortunately, this is complicated by the fact that state |
| // changes are communicated to gopls via unidirectional client->server |
| // notifications (didOpen, didChange, etc.), and resulting gopls behavior such |
| // as diagnostics, logs, or messages is communicated back via server->client |
| // notifications. Therefore, within integration tests we must be able to say "do |
| // this, and then eventually gopls should do that". To achieve this, the |
| // integration package provides a framework for expressing conditions that must |
| // eventually be met, in terms of the Expectation type. |
| // |
| // To express the assertion that "eventually gopls must meet these |
| // expectations", use env.Await(...): |
| // |
| // env.RegexpReplace("x/x.go", `package x`, `package main`) |
| // env.Await(env.DiagnosticAtRegexp("x/main.go", `fmt`)) |
| // |
| // Await evaluates the provided expectations atomically, whenever the client |
| // receives a state-changing notification from gopls. See expectation.go for a |
| // full list of available expectations. |
| // |
| // A problem with this model is that if gopls never meets the provided |
| // expectations, the test runner will hang until the test timeout |
| // (which defaults to 10m). There are two ways to work around this |
| // poor behavior: |
| // |
| // 1. Use a precondition to define precisely when we expect conditions to be |
| // met. Gopls provides the OnceMet(precondition, expectations...) pattern |
| // to express ("once this precondition is met, the following expectations |
| // must all hold"). To instrument preconditions, gopls uses verbose |
| // progress notifications to inform the client about ongoing work (see |
| // CompletedWork). The most common precondition is to wait for gopls to be |
| // done processing all change notifications, for which the integration package |
| // provides the AfterChange helper. For example: |
| // |
| // // We expect diagnostics to be cleared after gopls is done processing the |
| // // didSave notification. |
| // env.SaveBuffer("a/go.mod") |
| // env.AfterChange(EmptyDiagnostics("a/go.mod")) |
| // |
| // 2. Set a shorter timeout during development, if you expect to be breaking |
| // tests. By setting the environment variable GOPLS_INTEGRATION_TEST_TIMEOUT=5s, |
| // integration tests will time out after 5 seconds. |
| // |
| // # Tips & Tricks |
| // |
| // Here are some tips and tricks for working with integration tests: |
| // |
| // 1. Set the environment variable GOPLS_INTEGRRATION_TEST_TIMEOUT=5s during development. |
| // 2. Run tests with -short. This will only run integration tests in the |
| // default gopls execution mode. |
| // 3. Use capture groups to narrow regexp positions. All regular-expression |
| // based positions (such as DiagnosticAtRegexp) will match the position of |
| // the first capture group, if any are provided. This can be used to |
| // identify a specific position in the code for a pattern that may occur in |
| // multiple places. For example `var (mu) sync.Mutex` matches the position |
| // of "mu" within the variable declaration. |
| // 4. Read diagnostics into a variable to implement more complicated |
| // assertions about diagnostic state in the editor. To do this, use the |
| // pattern OnceMet(precondition, ReadDiagnostics("file.go", &d)) to capture |
| // the current diagnostics as soon as the precondition is met. This is |
| // preferable to accessing the diagnostics directly, as it avoids races. |
| package integration |