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