| // 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 debug_test |
| |
| // Provide 'static type checking' of the templates. This guards against changes is various |
| // gopls datastructures causing template execution to fail. The checking is done by |
| // the github.com/jba/templatecheck pacakge. Before that is run, the test checks that |
| // its list of templates and their arguments corresponds to the arguments in |
| // calls to render(). The test assumes that all uses of templates are done through render(). |
| |
| import ( |
| "go/ast" |
| "html/template" |
| "log" |
| "runtime" |
| "sort" |
| "strings" |
| "testing" |
| |
| "github.com/jba/templatecheck" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/lsp/cache" |
| "golang.org/x/tools/internal/lsp/debug" |
| "golang.org/x/tools/internal/lsp/source" |
| "golang.org/x/tools/internal/span" |
| ) |
| |
| type tdata struct { |
| tmpl *template.Template |
| data interface{} // a value of the needed type |
| } |
| |
| var templates = map[string]tdata{ |
| "MainTmpl": {debug.MainTmpl, &debug.Instance{}}, |
| "DebugTmpl": {debug.DebugTmpl, nil}, |
| "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}}, |
| "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}}, |
| "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}}, |
| "SessionTmpl": {debug.SessionTmpl, &cache.Session{}}, |
| "ViewTmpl": {debug.ViewTmpl, &cache.View{}}, |
| "ClientTmpl": {debug.ClientTmpl, &debug.Client{}}, |
| "ServerTmpl": {debug.ServerTmpl, &debug.Server{}}, |
| //"FileTmpl": {FileTmpl, source.Overlay{}}, // need to construct a source.Overlay in init |
| "InfoTmpl": {debug.InfoTmpl, "something"}, |
| "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, |
| } |
| |
| // construct a source.Overlay for fileTmpl |
| type fakeOverlay struct{} |
| |
| func (fakeOverlay) Version() int32 { |
| return 0 |
| } |
| func (fakeOverlay) Session() string { |
| return "" |
| } |
| func (fakeOverlay) VersionedFileIdentity() source.VersionedFileIdentity { |
| return source.VersionedFileIdentity{} |
| } |
| func (fakeOverlay) FileIdentity() source.FileIdentity { |
| return source.FileIdentity{} |
| } |
| func (fakeOverlay) Kind() source.FileKind { |
| return 0 |
| } |
| func (fakeOverlay) Read() ([]byte, error) { |
| return nil, nil |
| } |
| func (fakeOverlay) Saved() bool { |
| return true |
| } |
| func (fakeOverlay) URI() span.URI { |
| return "" |
| } |
| |
| var _ source.Overlay = fakeOverlay{} |
| |
| func init() { |
| log.SetFlags(log.Lshortfile) |
| var v fakeOverlay |
| templates["FileTmpl"] = tdata{debug.FileTmpl, v} |
| } |
| |
| func TestTemplates(t *testing.T) { |
| if runtime.GOOS == "android" { |
| t.Skip("this test is not supported for Android") |
| } |
| cfg := &packages.Config{ |
| Mode: packages.NeedTypesInfo | packages.LoadAllSyntax, // figure out what's necessary PJW |
| } |
| pkgs, err := packages.Load(cfg, "golang.org/x/tools/internal/lsp/debug") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(pkgs) != 1 { |
| t.Fatalf("expected a single package, but got %d", len(pkgs)) |
| } |
| p := pkgs[0] |
| if len(p.Errors) != 0 { |
| t.Fatalf("compiler error, e.g. %v", p.Errors[0]) |
| } |
| // find the calls to render in serve.go |
| tree := treeOf(p, "serve.go") |
| if tree == nil { |
| t.Fatalf("found no syntax tree for %s", "serve.go") |
| } |
| renders := callsOf(p, tree, "render") |
| if len(renders) == 0 { |
| t.Fatalf("found no calls to render") |
| } |
| var found = make(map[string]bool) |
| for _, r := range renders { |
| if len(r.Args) != 2 { |
| // template, func |
| t.Fatalf("got %d args, expected 2", len(r.Args)) |
| } |
| t0, ok := p.TypesInfo.Types[r.Args[0]] |
| if !ok || !t0.IsValue() || t0.Type.String() != "*html/template.Template" { |
| t.Fatalf("no type info for template") |
| } |
| if id, ok := r.Args[0].(*ast.Ident); !ok { |
| t.Errorf("expected *ast.Ident, got %T", r.Args[0]) |
| } else { |
| found[id.Name] = true |
| } |
| } |
| // make sure found and templates have the same templates |
| for k := range found { |
| if _, ok := templates[k]; !ok { |
| t.Errorf("code has template %s, but test does not", k) |
| } |
| } |
| for k := range templates { |
| if _, ok := found[k]; !ok { |
| t.Errorf("test has template %s, code does not", k) |
| } |
| } |
| // now check all the known templates, in alphabetic order, for determinacy |
| keys := []string{} |
| for k := range templates { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| for _, k := range keys { |
| v := templates[k] |
| // the FuncMap is an annoyance; should not be necessary |
| if err := templatecheck.CheckHTML(v.tmpl, v.data); err != nil { |
| t.Errorf("%s: %v", k, err) |
| } |
| } |
| } |
| |
| func callsOf(p *packages.Package, tree *ast.File, name string) []*ast.CallExpr { |
| var ans []*ast.CallExpr |
| f := func(n ast.Node) bool { |
| x, ok := n.(*ast.CallExpr) |
| if !ok { |
| return true |
| } |
| if y, ok := x.Fun.(*ast.Ident); ok { |
| if y.Name == name { |
| ans = append(ans, x) |
| } |
| } |
| return true |
| } |
| ast.Inspect(tree, f) |
| return ans |
| } |
| func treeOf(p *packages.Package, fname string) *ast.File { |
| for _, tree := range p.Syntax { |
| loc := tree.Package |
| pos := p.Fset.PositionFor(loc, false) |
| if strings.HasSuffix(pos.Filename, fname) { |
| return tree |
| } |
| } |
| return nil |
| } |