...

Source file src/go/doc/example_test.go

Documentation: go/doc

     1  // Copyright 2013 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_test
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/doc"
    12  	"go/format"
    13  	"go/parser"
    14  	"go/token"
    15  	"internal/diff"
    16  	"internal/txtar"
    17  	"path/filepath"
    18  	"reflect"
    19  	"strings"
    20  	"testing"
    21  )
    22  
    23  func TestExamples(t *testing.T) {
    24  	dir := filepath.Join("testdata", "examples")
    25  	filenames, err := filepath.Glob(filepath.Join(dir, "*.go"))
    26  	if err != nil {
    27  		t.Fatal(err)
    28  	}
    29  	for _, filename := range filenames {
    30  		t.Run(strings.TrimSuffix(filepath.Base(filename), ".go"), func(t *testing.T) {
    31  			fset := token.NewFileSet()
    32  			astFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
    33  			if err != nil {
    34  				t.Fatal(err)
    35  			}
    36  			goldenFilename := strings.TrimSuffix(filename, ".go") + ".golden"
    37  			archive, err := txtar.ParseFile(goldenFilename)
    38  			if err != nil {
    39  				t.Fatal(err)
    40  			}
    41  			golden := map[string]string{}
    42  			for _, f := range archive.Files {
    43  				golden[f.Name] = strings.TrimSpace(string(f.Data))
    44  			}
    45  
    46  			// Collect the results of doc.Examples in a map keyed by example name.
    47  			examples := map[string]*doc.Example{}
    48  			for _, e := range doc.Examples(astFile) {
    49  				examples[e.Name] = e
    50  				// Treat missing sections in the golden as empty.
    51  				for _, kind := range []string{"Play", "Output"} {
    52  					key := e.Name + "." + kind
    53  					if _, ok := golden[key]; !ok {
    54  						golden[key] = ""
    55  					}
    56  				}
    57  			}
    58  
    59  			// Each section in the golden file corresponds to an example we expect
    60  			// to see.
    61  			for sectionName, want := range golden {
    62  				name, kind, found := strings.Cut(sectionName, ".")
    63  				if !found {
    64  					t.Fatalf("bad section name %q, want EXAMPLE_NAME.KIND", sectionName)
    65  				}
    66  				ex := examples[name]
    67  				if ex == nil {
    68  					t.Fatalf("no example named %q", name)
    69  				}
    70  
    71  				var got string
    72  				switch kind {
    73  				case "Play":
    74  					got = strings.TrimSpace(formatFile(t, fset, ex.Play))
    75  
    76  				case "Output":
    77  					got = strings.TrimSpace(ex.Output)
    78  				default:
    79  					t.Fatalf("bad section kind %q", kind)
    80  				}
    81  
    82  				if got != want {
    83  					t.Errorf("%s mismatch:\n%s", sectionName,
    84  						diff.Diff("want", []byte(want), "got", []byte(got)))
    85  				}
    86  			}
    87  		})
    88  	}
    89  }
    90  
    91  func formatFile(t *testing.T, fset *token.FileSet, n *ast.File) string {
    92  	t.Helper()
    93  	if n == nil {
    94  		return "<nil>"
    95  	}
    96  	var buf bytes.Buffer
    97  	if err := format.Node(&buf, fset, n); err != nil {
    98  		t.Fatal(err)
    99  	}
   100  	return buf.String()
   101  }
   102  
   103  // This example illustrates how to use NewFromFiles
   104  // to compute package documentation with examples.
   105  func ExampleNewFromFiles() {
   106  	// src and test are two source files that make up
   107  	// a package whose documentation will be computed.
   108  	const src = `
   109  // This is the package comment.
   110  package p
   111  
   112  import "fmt"
   113  
   114  // This comment is associated with the Greet function.
   115  func Greet(who string) {
   116  	fmt.Printf("Hello, %s!\n", who)
   117  }
   118  `
   119  	const test = `
   120  package p_test
   121  
   122  // This comment is associated with the ExampleGreet_world example.
   123  func ExampleGreet_world() {
   124  	Greet("world")
   125  }
   126  `
   127  
   128  	// Create the AST by parsing src and test.
   129  	fset := token.NewFileSet()
   130  	files := []*ast.File{
   131  		mustParse(fset, "src.go", src),
   132  		mustParse(fset, "src_test.go", test),
   133  	}
   134  
   135  	// Compute package documentation with examples.
   136  	p, err := doc.NewFromFiles(fset, files, "example.com/p")
   137  	if err != nil {
   138  		panic(err)
   139  	}
   140  
   141  	fmt.Printf("package %s - %s", p.Name, p.Doc)
   142  	fmt.Printf("func %s - %s", p.Funcs[0].Name, p.Funcs[0].Doc)
   143  	fmt.Printf(" ⤷ example with suffix %q - %s", p.Funcs[0].Examples[0].Suffix, p.Funcs[0].Examples[0].Doc)
   144  
   145  	// Output:
   146  	// package p - This is the package comment.
   147  	// func Greet - This comment is associated with the Greet function.
   148  	//  ⤷ example with suffix "world" - This comment is associated with the ExampleGreet_world example.
   149  }
   150  
   151  func TestClassifyExamples(t *testing.T) {
   152  	const src = `
   153  package p
   154  
   155  const Const1 = 0
   156  var   Var1   = 0
   157  
   158  type (
   159  	Type1     int
   160  	Type1_Foo int
   161  	Type1_foo int
   162  	type2     int
   163  
   164  	Embed struct { Type1 }
   165  	Uembed struct { type2 }
   166  )
   167  
   168  func Func1()     {}
   169  func Func1_Foo() {}
   170  func Func1_foo() {}
   171  func func2()     {}
   172  
   173  func (Type1) Func1() {}
   174  func (Type1) Func1_Foo() {}
   175  func (Type1) Func1_foo() {}
   176  func (Type1) func2() {}
   177  
   178  func (type2) Func1() {}
   179  
   180  type (
   181  	Conflict          int
   182  	Conflict_Conflict int
   183  	Conflict_conflict int
   184  )
   185  
   186  func (Conflict) Conflict() {}
   187  
   188  func GFunc[T any]() {}
   189  
   190  type GType[T any] int
   191  
   192  func (GType[T]) M() {}
   193  `
   194  	const test = `
   195  package p_test
   196  
   197  func ExampleConst1() {} // invalid - no support for consts and vars
   198  func ExampleVar1()   {} // invalid - no support for consts and vars
   199  
   200  func Example()               {}
   201  func Example_()              {} // invalid - suffix must start with a lower-case letter
   202  func Example_suffix()        {}
   203  func Example_suffix_xX_X_x() {}
   204  func Example_世界()           {} // invalid - suffix must start with a lower-case letter
   205  func Example_123()           {} // invalid - suffix must start with a lower-case letter
   206  func Example_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
   207  
   208  func ExampleType1()               {}
   209  func ExampleType1_()              {} // invalid - suffix must start with a lower-case letter
   210  func ExampleType1_suffix()        {}
   211  func ExampleType1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
   212  func ExampleType1_Foo()           {}
   213  func ExampleType1_Foo_suffix()    {}
   214  func ExampleType1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
   215  func ExampleType1_foo()           {}
   216  func ExampleType1_foo_suffix()    {}
   217  func ExampleType1_foo_Suffix()    {} // matches Type1, instead of Type1_foo
   218  func Exampletype2()               {} // invalid - cannot match unexported
   219  
   220  func ExampleFunc1()               {}
   221  func ExampleFunc1_()              {} // invalid - suffix must start with a lower-case letter
   222  func ExampleFunc1_suffix()        {}
   223  func ExampleFunc1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
   224  func ExampleFunc1_Foo()           {}
   225  func ExampleFunc1_Foo_suffix()    {}
   226  func ExampleFunc1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
   227  func ExampleFunc1_foo()           {}
   228  func ExampleFunc1_foo_suffix()    {}
   229  func ExampleFunc1_foo_Suffix()    {} // matches Func1, instead of Func1_foo
   230  func Examplefunc1()               {} // invalid - cannot match unexported
   231  
   232  func ExampleType1_Func1()               {}
   233  func ExampleType1_Func1_()              {} // invalid - suffix must start with a lower-case letter
   234  func ExampleType1_Func1_suffix()        {}
   235  func ExampleType1_Func1_BadSuffix()     {} // invalid - suffix must start with a lower-case letter
   236  func ExampleType1_Func1_Foo()           {}
   237  func ExampleType1_Func1_Foo_suffix()    {}
   238  func ExampleType1_Func1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
   239  func ExampleType1_Func1_foo()           {}
   240  func ExampleType1_Func1_foo_suffix()    {}
   241  func ExampleType1_Func1_foo_Suffix()    {} // matches Type1.Func1, instead of Type1.Func1_foo
   242  func ExampleType1_func2()               {} // matches Type1, instead of Type1.func2
   243  
   244  func ExampleEmbed_Func1()         {} // invalid - no support for forwarded methods from embedding exported type
   245  func ExampleUembed_Func1()        {} // methods from embedding unexported types are OK
   246  func ExampleUembed_Func1_suffix() {}
   247  
   248  func ExampleConflict_Conflict()        {} // ambiguous with either Conflict or Conflict_Conflict type
   249  func ExampleConflict_conflict()        {} // ambiguous with either Conflict or Conflict_conflict type
   250  func ExampleConflict_Conflict_suffix() {} // ambiguous with either Conflict or Conflict_Conflict type
   251  func ExampleConflict_conflict_suffix() {} // ambiguous with either Conflict or Conflict_conflict type
   252  
   253  func ExampleGFunc() {}
   254  func ExampleGFunc_suffix() {}
   255  
   256  func ExampleGType_M() {}
   257  func ExampleGType_M_suffix() {}
   258  `
   259  
   260  	// Parse literal source code as a *doc.Package.
   261  	fset := token.NewFileSet()
   262  	files := []*ast.File{
   263  		mustParse(fset, "src.go", src),
   264  		mustParse(fset, "src_test.go", test),
   265  	}
   266  	p, err := doc.NewFromFiles(fset, files, "example.com/p")
   267  	if err != nil {
   268  		t.Fatalf("doc.NewFromFiles: %v", err)
   269  	}
   270  
   271  	// Collect the association of examples to top-level identifiers.
   272  	got := map[string][]string{}
   273  	got[""] = exampleNames(p.Examples)
   274  	for _, f := range p.Funcs {
   275  		got[f.Name] = exampleNames(f.Examples)
   276  	}
   277  	for _, t := range p.Types {
   278  		got[t.Name] = exampleNames(t.Examples)
   279  		for _, f := range t.Funcs {
   280  			got[f.Name] = exampleNames(f.Examples)
   281  		}
   282  		for _, m := range t.Methods {
   283  			got[t.Name+"."+m.Name] = exampleNames(m.Examples)
   284  		}
   285  	}
   286  
   287  	want := map[string][]string{
   288  		"": {"", "suffix", "suffix_xX_X_x"}, // Package-level examples.
   289  
   290  		"Type1":     {"", "foo_Suffix", "func2", "suffix"},
   291  		"Type1_Foo": {"", "suffix"},
   292  		"Type1_foo": {"", "suffix"},
   293  
   294  		"Func1":     {"", "foo_Suffix", "suffix"},
   295  		"Func1_Foo": {"", "suffix"},
   296  		"Func1_foo": {"", "suffix"},
   297  
   298  		"Type1.Func1":     {"", "foo_Suffix", "suffix"},
   299  		"Type1.Func1_Foo": {"", "suffix"},
   300  		"Type1.Func1_foo": {"", "suffix"},
   301  
   302  		"Uembed.Func1": {"", "suffix"},
   303  
   304  		// These are implementation dependent due to the ambiguous parsing.
   305  		"Conflict_Conflict": {"", "suffix"},
   306  		"Conflict_conflict": {"", "suffix"},
   307  
   308  		"GFunc":   {"", "suffix"},
   309  		"GType.M": {"", "suffix"},
   310  	}
   311  
   312  	for id := range got {
   313  		if !reflect.DeepEqual(got[id], want[id]) {
   314  			t.Errorf("classification mismatch for %q:\ngot  %q\nwant %q", id, got[id], want[id])
   315  		}
   316  		delete(want, id)
   317  	}
   318  	if len(want) > 0 {
   319  		t.Errorf("did not find:\n%q", want)
   320  	}
   321  }
   322  
   323  func exampleNames(exs []*doc.Example) (out []string) {
   324  	for _, ex := range exs {
   325  		out = append(out, ex.Suffix)
   326  	}
   327  	return out
   328  }
   329  
   330  func mustParse(fset *token.FileSet, filename, src string) *ast.File {
   331  	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
   332  	if err != nil {
   333  		panic(err)
   334  	}
   335  	return f
   336  }
   337  

View as plain text