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 TestAnchorID(t *testing.T) {
   156  	const in = "Important Things 2 Know & Stuff"
   157  	const want = "hdr-Important_Things_2_Know___Stuff"
   158  	got := anchorID(in)
   159  	if got != want {
   160  		t.Errorf("anchorID(%q) = %q; want %q", in, got, want)
   161  	}
   162  }
   163  
   164  func TestFuncs(t *testing.T) {
   165  	fset := token.NewFileSet()
   166  	file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
   167  	if err != nil {
   168  		t.Fatal(err)
   169  	}
   170  	doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  
   175  	for _, f := range doc.Funcs {
   176  		f.Decl = nil
   177  	}
   178  	for _, ty := range doc.Types {
   179  		for _, f := range ty.Funcs {
   180  			f.Decl = nil
   181  		}
   182  		for _, m := range ty.Methods {
   183  			m.Decl = nil
   184  		}
   185  	}
   186  
   187  	compareFuncs := func(t *testing.T, msg string, got, want *Func) {
   188  		// ignore Decl and Examples
   189  		got.Decl = nil
   190  		got.Examples = nil
   191  		if !(got.Doc == want.Doc &&
   192  			got.Name == want.Name &&
   193  			got.Recv == want.Recv &&
   194  			got.Orig == want.Orig &&
   195  			got.Level == want.Level) {
   196  			t.Errorf("%s:\ngot  %+v\nwant %+v", msg, got, want)
   197  		}
   198  	}
   199  
   200  	compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
   201  	compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
   202  		if got.Name != want.Name {
   203  			t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
   204  		} else {
   205  			compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
   206  			compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
   207  		}
   208  	})
   209  }
   210  
   211  func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
   212  	if len(got) != len(want) {
   213  		t.Errorf("%s: got %d, want %d", name, len(got), len(want))
   214  	}
   215  	for i := 0; i < len(got) && i < len(want); i++ {
   216  		compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
   217  	}
   218  }
   219  
   220  const funcsTestFile = `
   221  package funcs
   222  
   223  func F() {}
   224  
   225  type S1 struct {
   226  	S2  // embedded, exported
   227  	s3  // embedded, unexported
   228  }
   229  
   230  func NewS1()  S1 {return S1{} }
   231  func NewS1p() *S1 { return &S1{} }
   232  
   233  func (S1) M1() {}
   234  func (r S1) M2() {}
   235  func(S1) m3() {}		// unexported not shown
   236  func (*S1) P1() {}		// pointer receiver
   237  
   238  type S2 int
   239  func (S2) M3() {}		// shown on S2
   240  
   241  type s3 int
   242  func (s3) M4() {}		// shown on S1
   243  
   244  type G1[T any] struct {
   245  	*s3
   246  }
   247  
   248  func NewG1[T any]() G1[T] { return G1[T]{} }
   249  
   250  func (G1[T]) MG1() {}
   251  func (*G1[U]) MG2() {}
   252  
   253  type G2[T, U any] struct {}
   254  
   255  func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }
   256  
   257  func (G2[T, U]) MG3() {}
   258  func (*G2[A, B]) MG4() {}
   259  
   260  
   261  `
   262  
   263  var funcsPackage = &Package{
   264  	Funcs: []*Func{{Name: "F"}},
   265  	Types: []*Type{
   266  		{
   267  			Name:  "G1",
   268  			Funcs: []*Func{{Name: "NewG1"}},
   269  			Methods: []*Func{
   270  				{Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
   271  					Orig: "s3", Level: 1},
   272  				{Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
   273  				{Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
   274  			},
   275  		},
   276  		{
   277  			Name:  "G2",
   278  			Funcs: []*Func{{Name: "NewG2"}},
   279  			Methods: []*Func{
   280  				{Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
   281  				{Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
   282  			},
   283  		},
   284  		{
   285  			Name:  "S1",
   286  			Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
   287  			Methods: []*Func{
   288  				{Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
   289  				{Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
   290  				{Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
   291  				{Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
   292  			},
   293  		},
   294  		{
   295  			Name: "S2",
   296  			Methods: []*Func{
   297  				{Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
   298  			},
   299  		},
   300  	},
   301  }
   302  

View as plain text