golang.org/x/tools@v0.21.0/cmd/stringer/endtoend_test.go (about)

     1  // Copyright 2014 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  // go command is not available on android
     6  
     7  //go:build !android
     8  // +build !android
     9  
    10  package main
    11  
    12  import (
    13  	"bytes"
    14  	"flag"
    15  	"fmt"
    16  	"io"
    17  	"os"
    18  	"path"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync"
    22  	"testing"
    23  
    24  	"golang.org/x/tools/internal/testenv"
    25  )
    26  
    27  // This file contains a test that compiles and runs each program in testdata
    28  // after generating the string method for its type. The rule is that for testdata/x.go
    29  // we run stringer -type X and then compile and run the program. The resulting
    30  // binary panics if the String method for X is not correct, including for error cases.
    31  
    32  func TestMain(m *testing.M) {
    33  	if os.Getenv("STRINGER_TEST_IS_STRINGER") != "" {
    34  		main()
    35  		os.Exit(0)
    36  	}
    37  
    38  	// Inform subprocesses that they should run the cmd/stringer main instead of
    39  	// running tests. It's a close approximation to building and running the real
    40  	// command, and much less complicated and expensive to build and clean up.
    41  	os.Setenv("STRINGER_TEST_IS_STRINGER", "1")
    42  
    43  	flag.Parse()
    44  	if testing.Verbose() {
    45  		os.Setenv("GOPACKAGESDEBUG", "true")
    46  	}
    47  
    48  	os.Exit(m.Run())
    49  }
    50  
    51  func TestEndToEnd(t *testing.T) {
    52  	testenv.NeedsTool(t, "go")
    53  
    54  	stringer := stringerPath(t)
    55  	// Read the testdata directory.
    56  	fd, err := os.Open("testdata")
    57  	if err != nil {
    58  		t.Fatal(err)
    59  	}
    60  	defer fd.Close()
    61  	names, err := fd.Readdirnames(-1)
    62  	if err != nil {
    63  		t.Fatalf("Readdirnames: %s", err)
    64  	}
    65  	// Generate, compile, and run the test programs.
    66  	for _, name := range names {
    67  		if name == "typeparams" {
    68  			// ignore the directory containing the tests with type params
    69  			continue
    70  		}
    71  		if !strings.HasSuffix(name, ".go") {
    72  			t.Errorf("%s is not a Go file", name)
    73  			continue
    74  		}
    75  		if strings.HasPrefix(name, "tag_") || strings.HasPrefix(name, "vary_") {
    76  			// This file is used for tag processing in TestTags or TestConstValueChange, below.
    77  			continue
    78  		}
    79  		t.Run(name, func(t *testing.T) {
    80  			if name == "cgo.go" {
    81  				testenv.NeedsTool(t, "cgo")
    82  			}
    83  			stringerCompileAndRun(t, t.TempDir(), stringer, typeName(name), name)
    84  		})
    85  	}
    86  }
    87  
    88  // a type name for stringer. use the last component of the file name with the .go
    89  func typeName(fname string) string {
    90  	// file names are known to be ascii and end .go
    91  	base := path.Base(fname)
    92  	return fmt.Sprintf("%c%s", base[0]+'A'-'a', base[1:len(base)-len(".go")])
    93  }
    94  
    95  func moreTests(t *testing.T, dirname, prefix string) []string {
    96  	x, err := os.ReadDir(dirname)
    97  	if err != nil {
    98  		// error, but try the rest of the tests
    99  		t.Errorf("can't read type param tess from %s: %v", dirname, err)
   100  		return nil
   101  	}
   102  	names := make([]string, len(x))
   103  	for i, f := range x {
   104  		names[i] = prefix + "/" + f.Name()
   105  	}
   106  	return names
   107  }
   108  
   109  // TestTags verifies that the -tags flag works as advertised.
   110  func TestTags(t *testing.T) {
   111  	stringer := stringerPath(t)
   112  	dir := t.TempDir()
   113  	var (
   114  		protectedConst = []byte("TagProtected")
   115  		output         = filepath.Join(dir, "const_string.go")
   116  	)
   117  	for _, file := range []string{"tag_main.go", "tag_tag.go"} {
   118  		err := copy(filepath.Join(dir, file), filepath.Join("testdata", file))
   119  		if err != nil {
   120  			t.Fatal(err)
   121  		}
   122  	}
   123  	// Run stringer in the directory that contains the package files.
   124  	// We cannot run stringer in the current directory for the following reasons:
   125  	// - Versions of Go earlier than Go 1.11, do not support absolute directories as a pattern.
   126  	// - When the current directory is inside a go module, the path will not be considered
   127  	//   a valid path to a package.
   128  	err := runInDir(t, dir, stringer, "-type", "Const", ".")
   129  	if err != nil {
   130  		t.Fatal(err)
   131  	}
   132  	result, err := os.ReadFile(output)
   133  	if err != nil {
   134  		t.Fatal(err)
   135  	}
   136  	if bytes.Contains(result, protectedConst) {
   137  		t.Fatal("tagged variable appears in untagged run")
   138  	}
   139  	err = os.Remove(output)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	err = runInDir(t, dir, stringer, "-type", "Const", "-tags", "tag", ".")
   144  	if err != nil {
   145  		t.Fatal(err)
   146  	}
   147  	result, err = os.ReadFile(output)
   148  	if err != nil {
   149  		t.Fatal(err)
   150  	}
   151  	if !bytes.Contains(result, protectedConst) {
   152  		t.Fatal("tagged variable does not appear in tagged run")
   153  	}
   154  }
   155  
   156  // TestConstValueChange verifies that if a constant value changes and
   157  // the stringer code is not regenerated, we'll get a compiler error.
   158  func TestConstValueChange(t *testing.T) {
   159  	testenv.NeedsTool(t, "go")
   160  
   161  	stringer := stringerPath(t)
   162  	dir := t.TempDir()
   163  	source := filepath.Join(dir, "day.go")
   164  	err := copy(source, filepath.Join("testdata", "day.go"))
   165  	if err != nil {
   166  		t.Fatal(err)
   167  	}
   168  	stringSource := filepath.Join(dir, "day_string.go")
   169  	// Run stringer in the directory that contains the package files.
   170  	err = runInDir(t, dir, stringer, "-type", "Day", "-output", stringSource)
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  	// Run the binary in the temporary directory as a sanity check.
   175  	err = run(t, "go", "run", stringSource, source)
   176  	if err != nil {
   177  		t.Fatal(err)
   178  	}
   179  	// Overwrite the source file with a version that has changed constants.
   180  	err = copy(source, filepath.Join("testdata", "vary_day.go"))
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	// Unfortunately different compilers may give different error messages,
   185  	// so there's no easy way to verify that the build failed specifically
   186  	// because the constants changed rather than because the vary_day.go
   187  	// file is invalid.
   188  	//
   189  	// Instead we'll just rely on manual inspection of the polluted test
   190  	// output. An alternative might be to check that the error output
   191  	// matches a set of possible error strings emitted by known
   192  	// Go compilers.
   193  	t.Logf("Note: the following messages should indicate an out-of-bounds compiler error\n")
   194  	err = run(t, "go", "build", stringSource, source)
   195  	if err == nil {
   196  		t.Fatal("unexpected compiler success")
   197  	}
   198  }
   199  
   200  var exe struct {
   201  	path string
   202  	err  error
   203  	once sync.Once
   204  }
   205  
   206  func stringerPath(t *testing.T) string {
   207  	testenv.NeedsExec(t)
   208  
   209  	exe.once.Do(func() {
   210  		exe.path, exe.err = os.Executable()
   211  	})
   212  	if exe.err != nil {
   213  		t.Fatal(exe.err)
   214  	}
   215  	return exe.path
   216  }
   217  
   218  // stringerCompileAndRun runs stringer for the named file and compiles and
   219  // runs the target binary in directory dir. That binary will panic if the String method is incorrect.
   220  func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName string) {
   221  	t.Logf("run: %s %s\n", fileName, typeName)
   222  	source := filepath.Join(dir, path.Base(fileName))
   223  	err := copy(source, filepath.Join("testdata", fileName))
   224  	if err != nil {
   225  		t.Fatalf("copying file to temporary directory: %s", err)
   226  	}
   227  	stringSource := filepath.Join(dir, typeName+"_string.go")
   228  	// Run stringer in temporary directory.
   229  	err = run(t, stringer, "-type", typeName, "-output", stringSource, source)
   230  	if err != nil {
   231  		t.Fatal(err)
   232  	}
   233  	// Run the binary in the temporary directory.
   234  	err = run(t, "go", "run", stringSource, source)
   235  	if err != nil {
   236  		t.Fatal(err)
   237  	}
   238  }
   239  
   240  // copy copies the from file to the to file.
   241  func copy(to, from string) error {
   242  	toFd, err := os.Create(to)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	defer toFd.Close()
   247  	fromFd, err := os.Open(from)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	defer fromFd.Close()
   252  	_, err = io.Copy(toFd, fromFd)
   253  	return err
   254  }
   255  
   256  // run runs a single command and returns an error if it does not succeed.
   257  // os/exec should have this function, to be honest.
   258  func run(t testing.TB, name string, arg ...string) error {
   259  	t.Helper()
   260  	return runInDir(t, ".", name, arg...)
   261  }
   262  
   263  // runInDir runs a single command in directory dir and returns an error if
   264  // it does not succeed.
   265  func runInDir(t testing.TB, dir, name string, arg ...string) error {
   266  	t.Helper()
   267  	cmd := testenv.Command(t, name, arg...)
   268  	cmd.Dir = dir
   269  	cmd.Env = append(os.Environ(), "GO111MODULE=auto")
   270  	out, err := cmd.CombinedOutput()
   271  	if len(out) > 0 {
   272  		t.Logf("%s", out)
   273  	}
   274  	if err != nil {
   275  		return fmt.Errorf("%v: %v", cmd, err)
   276  	}
   277  	return nil
   278  }