Source file src/go/doc/doc_test.go

     1  // Copyright 2012 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package doc
     6  
     7  import (
     8  	"bytes"
     9  	"flag"
    10  	"fmt"
    11  	"go/ast"
    12  	"go/parser"
    13  	"go/printer"
    14  	"go/token"
    15  	"io/fs"
    16  	"os"
    17  	"path/filepath"
    18  	"regexp"
    19  	"strings"
    20  	"testing"
    21  	"text/template"
    22  )
    23  
    24  var update = flag.Bool("update", false, "update golden (.out) files")
    25  var files = flag.String("files", "", "consider only Go test files matching this regular expression")
    26  
    27  const dataDir = "testdata"
    28  
    29  var templateTxt = readTemplate("template.txt")
    30  
    31  func readTemplate(filename string) *template.Template {
    32  	t := template.New(filename)
    33  	t.Funcs(template.FuncMap{
    34  		"node":     nodeFmt,
    35  		"synopsis": synopsisFmt,
    36  		"indent":   indentFmt,
    37  	})
    38  	return template.Must(t.ParseFiles(filepath.Join(dataDir, filename)))
    39  }
    40  
    41  func nodeFmt(node any, fset *token.FileSet) string {
    42  	var buf bytes.Buffer
    43  	printer.Fprint(&buf, fset, node)
    44  	return strings.ReplaceAll(strings.TrimSpace(buf.String()), "\n", "\n\t")
    45  }
    46  
    47  func synopsisFmt(s string) string {
    48  	const n = 64
    49  	if len(s) > n {
    50  		// cut off excess text and go back to a word boundary
    51  		s = s[0:n]
    52  		if i := strings.LastIndexAny(s, "\t\n "); i >= 0 {
    53  			s = s[0:i]
    54  		}
    55  		s = strings.TrimSpace(s) + " ..."
    56  	}
    57  	return "// " + strings.ReplaceAll(s, "\n", " ")
    58  }
    59  
    60  func indentFmt(indent, s string) string {
    61  	end := ""
    62  	if strings.HasSuffix(s, "\n") {
    63  		end = "\n"
    64  		s = s[:len(s)-1]
    65  	}
    66  	return indent + strings.ReplaceAll(s, "\n", "\n"+indent) + end
    67  }
    68  
    69  func isGoFile(fi fs.FileInfo) bool {
    70  	name := fi.Name()
    71  	return !fi.IsDir() &&
    72  		len(name) > 0 && name[0] != '.' && // ignore .files
    73  		filepath.Ext(name) == ".go"
    74  }
    75  
    76  type bundle struct {
    77  	*Package
    78  	FSet *token.FileSet
    79  }
    80  
    81  func test(t *testing.T, mode Mode) {
    82  	// determine file filter
    83  	filter := isGoFile
    84  	if *files != "" {
    85  		rx, err := regexp.Compile(*files)
    86  		if err != nil {
    87  			t.Fatal(err)
    88  		}
    89  		filter = func(fi fs.FileInfo) bool {
    90  			return isGoFile(fi) && rx.MatchString(fi.Name())
    91  		}
    92  	}
    93  
    94  	// get packages
    95  	fset := token.NewFileSet()
    96  	pkgs, err := parser.ParseDir(fset, dataDir, filter, parser.ParseComments)
    97  	if err != nil {
    98  		t.Fatal(err)
    99  	}
   100  
   101  	// test packages
   102  	for _, pkg := range pkgs {
   103  		t.Run(pkg.Name, func(t *testing.T) {
   104  			importPath := dataDir + "/" + pkg.Name
   105  			var files []*ast.File
   106  			for _, f := range pkg.Files {
   107  				files = append(files, f)
   108  			}
   109  			doc, err := NewFromFiles(fset, files, importPath, mode)
   110  			if err != nil {
   111  				t.Fatal(err)
   112  			}
   113  
   114  			// golden files always use / in filenames - canonicalize them
   115  			for i, filename := range doc.Filenames {
   116  				doc.Filenames[i] = filepath.ToSlash(filename)
   117  			}
   118  
   119  			// print documentation
   120  			var buf bytes.Buffer
   121  			if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
   122  				t.Fatal(err)
   123  			}
   124  			got := buf.Bytes()
   125  
   126  			// update golden file if necessary
   127  			golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
   128  			if *update {
   129  				err := os.WriteFile(golden, got, 0644)
   130  				if err != nil {
   131  					t.Fatal(err)
   132  				}
   133  			}
   134  
   135  			// get golden file
   136  			want, err := os.ReadFile(golden)
   137  			if err != nil {
   138  				t.Fatal(err)
   139  			}
   140  
   141  			// compare
   142  			if !bytes.Equal(got, want) {
   143  				t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
   144  			}
   145  		})
   146  	}
   147  }
   148  
   149  func Test(t *testing.T) {
   150  	t.Run("default", func(t *testing.T) { test(t, 0) })
   151  	t.Run("AllDecls", func(t *testing.T) { test(t, AllDecls) })
   152  	t.Run("AllMethods", func(t *testing.T) { test(t, AllMethods) })
   153  }
   154  
   155  func TestFuncs(t *testing.T) {
   156  	fset := token.NewFileSet()
   157  	file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
   158  	if err != nil {
   159  		t.Fatal(err)
   160  	}
   161  	doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
   162  	if err != nil {
   163  		t.Fatal(err)
   164  	}
   165  
   166  	for _, f := range doc.Funcs {
   167  		f.Decl = nil
   168  	}
   169  	for _, ty := range doc.Types {
   170  		for _, f := range ty.Funcs {
   171  			f.Decl = nil
   172  		}
   173  		for _, m := range ty.Methods {
   174  			m.Decl = nil
   175  		}
   176  	}
   177  
   178  	compareFuncs := func(t *testing.T, msg string, got, want *Func) {
   179  		// ignore Decl and Examples
   180  		got.Decl = nil
   181  		got.Examples = nil
   182  		if !(got.Doc == want.Doc &&
   183  			got.Name == want.Name &&
   184  			got.Recv == want.Recv &&
   185  			got.Orig == want.Orig &&
   186  			got.Level == want.Level) {
   187  			t.Errorf("%s:\ngot  %+v\nwant %+v", msg, got, want)
   188  		}
   189  	}
   190  
   191  	compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
   192  	compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
   193  		if got.Name != want.Name {
   194  			t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
   195  		} else {
   196  			compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
   197  			compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
   198  		}
   199  	})
   200  }
   201  
   202  func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
   203  	if len(got) != len(want) {
   204  		t.Errorf("%s: got %d, want %d", name, len(got), len(want))
   205  	}
   206  	for i := 0; i < len(got) && i < len(want); i++ {
   207  		compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
   208  	}
   209  }
   210  
   211  const funcsTestFile = `
   212  package funcs
   213  
   214  func F() {}
   215  
   216  type S1 struct {
   217  	S2  // embedded, exported
   218  	s3  // embedded, unexported
   219  }
   220  
   221  func NewS1()  S1 {return S1{} }
   222  func NewS1p() *S1 { return &S1{} }
   223  
   224  func (S1) M1() {}
   225  func (r S1) M2() {}
   226  func(S1) m3() {}		// unexported not shown
   227  func (*S1) P1() {}		// pointer receiver
   228  
   229  type S2 int
   230  func (S2) M3() {}		// shown on S2
   231  
   232  type s3 int
   233  func (s3) M4() {}		// shown on S1
   234  
   235  type G1[T any] struct {
   236  	*s3
   237  }
   238  
   239  func NewG1[T any]() G1[T] { return G1[T]{} }
   240  
   241  func (G1[T]) MG1() {}
   242  func (*G1[U]) MG2() {}
   243  
   244  type G2[T, U any] struct {}
   245  
   246  func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }
   247  
   248  func (G2[T, U]) MG3() {}
   249  func (*G2[A, B]) MG4() {}
   250  
   251  
   252  `
   253  
   254  var funcsPackage = &Package{
   255  	Funcs: []*Func{{Name: "F"}},
   256  	Types: []*Type{
   257  		{
   258  			Name:  "G1",
   259  			Funcs: []*Func{{Name: "NewG1"}},
   260  			Methods: []*Func{
   261  				{Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
   262  					Orig: "s3", Level: 1},
   263  				{Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
   264  				{Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
   265  			},
   266  		},
   267  		{
   268  			Name:  "G2",
   269  			Funcs: []*Func{{Name: "NewG2"}},
   270  			Methods: []*Func{
   271  				{Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
   272  				{Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
   273  			},
   274  		},
   275  		{
   276  			Name:  "S1",
   277  			Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
   278  			Methods: []*Func{
   279  				{Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
   280  				{Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
   281  				{Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
   282  				{Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
   283  			},
   284  		},
   285  		{
   286  			Name: "S2",
   287  			Methods: []*Func{
   288  				{Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
   289  			},
   290  		},
   291  	},
   292  }
   293  

View as plain text