| // 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}, |
| }, |
| }, |
| }, |
| } |