diff options
Diffstat (limited to 'src/go/doc/doc_test.go')
-rw-r--r-- | src/go/doc/doc_test.go | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/src/go/doc/doc_test.go b/src/go/doc/doc_test.go new file mode 100644 index 0000000..b79087e --- /dev/null +++ b/src/go/doc/doc_test.go @@ -0,0 +1,292 @@ +// 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}, + }, + }, + }, +} |