blob: b79087e5387d3de376b43d96d30249524ac4a6d0 [file] [log] [blame] [edit]
// Copyright 2012 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 doc
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"text/template"
)
var update = flag.Bool("update", false, "update golden (.out) files")
var files = flag.String("files", "", "consider only Go test files matching this regular expression")
const dataDir = "testdata"
var templateTxt = readTemplate("template.txt")
func readTemplate(filename string) *template.Template {
t := template.New(filename)
t.Funcs(template.FuncMap{
"node": nodeFmt,
"synopsis": synopsisFmt,
"indent": indentFmt,
})
return template.Must(t.ParseFiles(filepath.Join(dataDir, filename)))
}
func nodeFmt(node any, fset *token.FileSet) string {
var buf bytes.Buffer
printer.Fprint(&buf, fset, node)
return strings.ReplaceAll(strings.TrimSpace(buf.String()), "\n", "\n\t")
}
func synopsisFmt(s string) string {
const n = 64
if len(s) > n {
// cut off excess text and go back to a word boundary
s = s[0:n]
if i := strings.LastIndexAny(s, "\t\n "); i >= 0 {
s = s[0:i]
}
s = strings.TrimSpace(s) + " ..."
}
return "// " + strings.ReplaceAll(s, "\n", " ")
}
func indentFmt(indent, s string) string {
end := ""
if strings.HasSuffix(s, "\n") {
end = "\n"
s = s[:len(s)-1]
}
return indent + strings.ReplaceAll(s, "\n", "\n"+indent) + end
}
func isGoFile(fi fs.FileInfo) bool {
name := fi.Name()
return !fi.IsDir() &&
len(name) > 0 && name[0] != '.' && // ignore .files
filepath.Ext(name) == ".go"
}
type bundle struct {
*Package
FSet *token.FileSet
}
func test(t *testing.T, mode Mode) {
// determine file filter
filter := isGoFile
if *files != "" {
rx, err := regexp.Compile(*files)
if err != nil {
t.Fatal(err)
}
filter = func(fi fs.FileInfo) bool {
return isGoFile(fi) && rx.MatchString(fi.Name())
}
}
// get packages
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dataDir, filter, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
// test packages
for _, pkg := range pkgs {
t.Run(pkg.Name, func(t *testing.T) {
importPath := dataDir + "/" + pkg.Name
var files []*ast.File
for _, f := range pkg.Files {
files = append(files, f)
}
doc, err := NewFromFiles(fset, files, importPath, mode)
if err != nil {
t.Fatal(err)
}
// golden files always use / in filenames - canonicalize them
for i, filename := range doc.Filenames {
doc.Filenames[i] = filepath.ToSlash(filename)
}
// print documentation
var buf bytes.Buffer
if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
t.Fatal(err)
}
got := buf.Bytes()
// update golden file if necessary
golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
if *update {
err := os.WriteFile(golden, got, 0644)
if err != nil {
t.Fatal(err)
}
}
// get golden file
want, err := os.ReadFile(golden)
if err != nil {
t.Fatal(err)
}
// compare
if !bytes.Equal(got, want) {
t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
}
})
}
}
func Test(t *testing.T) {
t.Run("default", func(t *testing.T) { test(t, 0) })
t.Run("AllDecls", func(t *testing.T) { test(t, AllDecls) })
t.Run("AllMethods", func(t *testing.T) { test(t, AllMethods) })
}
func TestFuncs(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
if err != nil {
t.Fatal(err)
}
doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
if err != nil {
t.Fatal(err)
}
for _, f := range doc.Funcs {
f.Decl = nil
}
for _, ty := range doc.Types {
for _, f := range ty.Funcs {
f.Decl = nil
}
for _, m := range ty.Methods {
m.Decl = nil
}
}
compareFuncs := func(t *testing.T, msg string, got, want *Func) {
// ignore Decl and Examples
got.Decl = nil
got.Examples = nil
if !(got.Doc == want.Doc &&
got.Name == want.Name &&
got.Recv == want.Recv &&
got.Orig == want.Orig &&
got.Level == want.Level) {
t.Errorf("%s:\ngot %+v\nwant %+v", msg, got, want)
}
}
compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
if got.Name != want.Name {
t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
} else {
compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
}
})
}
func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
if len(got) != len(want) {
t.Errorf("%s: got %d, want %d", name, len(got), len(want))
}
for i := 0; i < len(got) && i < len(want); i++ {
compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
}
}
const funcsTestFile = `
package funcs
func F() {}
type S1 struct {
S2 // embedded, exported
s3 // embedded, unexported
}
func NewS1() S1 {return S1{} }
func NewS1p() *S1 { return &S1{} }
func (S1) M1() {}
func (r S1) M2() {}
func(S1) m3() {} // unexported not shown
func (*S1) P1() {} // pointer receiver
type S2 int
func (S2) M3() {} // shown on S2
type s3 int
func (s3) M4() {} // shown on S1
type G1[T any] struct {
*s3
}
func NewG1[T any]() G1[T] { return G1[T]{} }
func (G1[T]) MG1() {}
func (*G1[U]) MG2() {}
type G2[T, U any] struct {}
func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }
func (G2[T, U]) MG3() {}
func (*G2[A, B]) MG4() {}
`
var funcsPackage = &Package{
Funcs: []*Func{{Name: "F"}},
Types: []*Type{
{
Name: "G1",
Funcs: []*Func{{Name: "NewG1"}},
Methods: []*Func{
{Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
Orig: "s3", Level: 1},
{Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
{Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
},
},
{
Name: "G2",
Funcs: []*Func{{Name: "NewG2"}},
Methods: []*Func{
{Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
{Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
},
},
{
Name: "S1",
Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
Methods: []*Func{
{Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
{Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
{Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
{Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
},
},
{
Name: "S2",
Methods: []*Func{
{Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
},
},
},
}