blob: 5599564bb25d5b4b2fb7e4f8efbd9a21b4fce586 [file] [log] [blame] [edit]
// 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) {
// 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