blob: f4fddf7aca06914174d07b8e64114f04be1ff794 [file] [log] [blame]
// Copyright 2024 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 misc
import (
"html"
"io"
"net/http"
"regexp"
"strings"
"testing"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
)
// TestWebServer exercises the web server created on demand
// for code actions such as "View package documentation".
func TestWebServer(t *testing.T) {
const files = `
-- go.mod --
module example.com
-- a/a.go --
package a
const A = 1
// EOF
`
Run(t, files, func(t *testing.T, env *Env) {
// Assert that the HTML page contains the expected const declaration.
// (We may need to make allowances for HTML markup.)
uri1 := viewPkgDoc(t, env, "a/a.go")
doc1 := get(t, uri1)
checkMatch(t, true, doc1, "const A =.*1")
// Check that edits to the buffer (even unsaved) are
// reflected in the HTML document.
env.RegexpReplace("a/a.go", "// EOF", "func NewFunc() {}")
env.Await(env.DoneDiagnosingChanges())
doc2 := get(t, uri1)
checkMatch(t, true, doc2, "func NewFunc")
// TODO(adonovan): assert some basic properties of the
// HTML document using something like
// golang.org/x/pkgsite/internal/testing/htmlcheck.
// Grab the URL in the HTML source link for NewFunc.
// (We don't have a DOM or JS interpreter so we have
// to know something of the document internals here.)
rx := regexp.MustCompile(`<h3 id='NewFunc'.*httpGET\("(.*)"\)`)
openURL := html.UnescapeString(string(rx.FindSubmatch(doc2)[1]))
// Fetch the document. Its result isn't important,
// but it must have the side effect of another showDocument
// downcall, this time for a "file:" URL, causing the
// client editor to navigate to the source file.
t.Log("extracted /open URL", openURL)
get(t, openURL)
// Check that that shown location is that of NewFunc.
shownSource := shownDocument(t, env, "file:")
gotLoc := protocol.Location{
URI: protocol.DocumentURI(shownSource.URI), // fishy conversion
Range: *shownSource.Selection,
}
t.Log("showDocument(source file) URL:", gotLoc)
wantLoc := env.RegexpSearch("a/a.go", `func ()NewFunc`)
if gotLoc != wantLoc {
t.Errorf("got location %v, want %v", gotLoc, wantLoc)
}
})
}
func TestRenderNoPanic66449(t *testing.T) {
// This particular input triggered a latent bug in doc.New
// that would corrupt the AST while filtering out unexported
// symbols such as b, causing nodeHTML to panic.
// Now it doesn't crash.
//
// We also check cross-reference anchors for all symbols.
const files = `
-- go.mod --
module example.com
-- a/a.go --
package a
// The 'π' suffix is to elimimate spurious matches with other HTML substrings,
// in particular the random base64 secret tokens that appear in gopls URLs.
var Vπ, vπ = 0, 0
const Cπ, cπ = 0, 0
func Fπ()
func fπ()
type Tπ int
type tπ int
func (Tπ) Mπ() {}
func (Tπ) mπ() {}
func (tπ) Mπ() {}
func (tπ) mπ() {}
`
Run(t, files, func(t *testing.T, env *Env) {
uri1 := viewPkgDoc(t, env, "a/a.go")
doc := get(t, uri1)
// (Ideally our code rendering would also
// eliminate unexported symbols...)
checkMatch(t, true, doc, "var Vπ, vπ = .*0.*0")
checkMatch(t, true, doc, "const Cπ, cπ = .*0.*0")
// Unexported funcs/types/... must still be discarded.
checkMatch(t, true, doc, "Fπ")
checkMatch(t, false, doc, "fπ")
checkMatch(t, true, doc, "Tπ")
checkMatch(t, false, doc, "tπ")
// Also, check that anchors exist (only) for exported symbols.
// exported:
checkMatch(t, true, doc, "<a id='Vπ'")
checkMatch(t, true, doc, "<a id='Cπ'")
checkMatch(t, true, doc, "<h3 id='Tπ'")
checkMatch(t, true, doc, "<h3 id='Fπ'")
checkMatch(t, true, doc, "<h4 id='Tπ.Mπ'")
// unexported:
checkMatch(t, false, doc, "<a id='vπ'")
checkMatch(t, false, doc, "<a id='cπ'")
checkMatch(t, false, doc, "<h3 id='tπ'")
checkMatch(t, false, doc, "<h3 id='fπ'")
checkMatch(t, false, doc, "<h4 id='Tπ.mπ'")
checkMatch(t, false, doc, "<h4 id='tπ.Mπ'")
checkMatch(t, false, doc, "<h4 id='tπ.mπ'")
})
}
// viewPkgDoc invokes the "View package documention" code action in
// the specified file. It returns the URI of the document, or fails
// the test.
func viewPkgDoc(t *testing.T, env *Env, filename string) protocol.URI {
env.OpenFile(filename)
// Invoke the "View package documentation" code
// action to start the server.
var docAction *protocol.CodeAction
actions := env.CodeAction(filename, nil)
for _, act := range actions {
if act.Title == "View package documentation" {
docAction = &act
break
}
}
if docAction == nil {
t.Fatalf("can't find action with Title 'View package documentation', only %#v",
actions)
}
// Execute the command.
// Its side effect should be a single showDocument request.
params := &protocol.ExecuteCommandParams{
Command: docAction.Command.Command,
Arguments: docAction.Command.Arguments,
}
var result command.DebuggingResult
env.ExecuteCommand(params, &result)
doc := shownDocument(t, env, "http:")
if doc == nil {
t.Fatalf("no showDocument call had 'http:' prefix")
}
t.Log("showDocument(package doc) URL:", doc.URI)
return doc.URI
}
// shownDocument returns the first shown document matching the URI prefix.
// It may be nil.
//
// TODO(adonovan): the integration test framework
// needs a way to reset ShownDocuments so they don't
// accumulate, necessitating the fragile prefix hack.
func shownDocument(t *testing.T, env *Env, prefix string) *protocol.ShowDocumentParams {
t.Helper()
var shown []*protocol.ShowDocumentParams
env.Await(ShownDocuments(&shown))
var first *protocol.ShowDocumentParams
for _, sd := range shown {
if strings.HasPrefix(sd.URI, prefix) {
if first != nil {
t.Errorf("got multiple showDocument requests: %#v", shown)
break
}
first = sd
}
}
return first
}
// get fetches the content of a document over HTTP.
func get(t *testing.T, url string) []byte {
t.Helper()
resp, err := http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return got
}
// checkMatch asserts that got matches (or doesn't match, if !want) the pattern.
func checkMatch(t *testing.T, want bool, got []byte, pattern string) {
t.Helper()
if regexp.MustCompile(pattern).Match(got) != want {
if want {
t.Errorf("input did not match wanted pattern %q; got:\n%s", pattern, got)
} else {
t.Errorf("input matched unwanted pattern %q; got:\n%s", pattern, got)
}
}
}