--- /dev/null
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// go command is not available on android
+
+// +build !android
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "go/build"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/internal/testenv"
+)
+
+// This file contains a test that compiles and runs each program in testdata
+// after generating the string method for its type. The rule is that for testdata/x.go
+// we run stringer -type X and then compile and run the program. The resulting
+// binary panics if the String method for X is not correct, including for error cases.
+
+func TestEndToEnd(t *testing.T) {
+ dir, stringer := buildStringer(t)
+ defer os.RemoveAll(dir)
+ // Read the testdata directory.
+ fd, err := os.Open("testdata")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fd.Close()
+ names, err := fd.Readdirnames(-1)
+ if err != nil {
+ t.Fatalf("Readdirnames: %s", err)
+ }
+ // Generate, compile, and run the test programs.
+ for _, name := range names {
+ if !strings.HasSuffix(name, ".go") {
+ t.Errorf("%s is not a Go file", name)
+ continue
+ }
+ if strings.HasPrefix(name, "tag_") || strings.HasPrefix(name, "vary_") {
+ // This file is used for tag processing in TestTags or TestConstValueChange, below.
+ continue
+ }
+ if name == "cgo.go" && !build.Default.CgoEnabled {
+ t.Logf("cgo is not enabled for %s", name)
+ continue
+ }
+ // Names are known to be ASCII and long enough.
+ typeName := fmt.Sprintf("%c%s", name[0]+'A'-'a', name[1:len(name)-len(".go")])
+ stringerCompileAndRun(t, dir, stringer, typeName, name)
+ }
+}
+
+// TestTags verifies that the -tags flag works as advertised.
+func TestTags(t *testing.T) {
+ dir, stringer := buildStringer(t)
+ defer os.RemoveAll(dir)
+ var (
+ protectedConst = []byte("TagProtected")
+ output = filepath.Join(dir, "const_string.go")
+ )
+ for _, file := range []string{"tag_main.go", "tag_tag.go"} {
+ err := copy(filepath.Join(dir, file), filepath.Join("testdata", file))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ // Run stringer in the directory that contains the package files.
+ // We cannot run stringer in the current directory for the following reasons:
+ // - Versions of Go earlier than Go 1.11, do not support absolute directories as a pattern.
+ // - When the current directory is inside a go module, the path will not be considered
+ // a valid path to a package.
+ err := runInDir(dir, stringer, "-type", "Const", ".")
+ if err != nil {
+ t.Fatal(err)
+ }
+ result, err := ioutil.ReadFile(output)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bytes.Contains(result, protectedConst) {
+ t.Fatal("tagged variable appears in untagged run")
+ }
+ err = os.Remove(output)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = runInDir(dir, stringer, "-type", "Const", "-tags", "tag", ".")
+ if err != nil {
+ t.Fatal(err)
+ }
+ result, err = ioutil.ReadFile(output)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Contains(result, protectedConst) {
+ t.Fatal("tagged variable does not appear in tagged run")
+ }
+}
+
+// TestConstValueChange verifies that if a constant value changes and
+// the stringer code is not regenerated, we'll get a compiler error.
+func TestConstValueChange(t *testing.T) {
+ dir, stringer := buildStringer(t)
+ defer os.RemoveAll(dir)
+ source := filepath.Join(dir, "day.go")
+ err := copy(source, filepath.Join("testdata", "day.go"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ stringSource := filepath.Join(dir, "day_string.go")
+ // Run stringer in the directory that contains the package files.
+ err = runInDir(dir, stringer, "-type", "Day", "-output", stringSource)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Run the binary in the temporary directory as a sanity check.
+ err = run("go", "run", stringSource, source)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Overwrite the source file with a version that has changed constants.
+ err = copy(source, filepath.Join("testdata", "vary_day.go"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Unfortunately different compilers may give different error messages,
+ // so there's no easy way to verify that the build failed specifically
+ // because the constants changed rather than because the vary_day.go
+ // file is invalid.
+ //
+ // Instead we'll just rely on manual inspection of the polluted test
+ // output. An alternative might be to check that the error output
+ // matches a set of possible error strings emitted by known
+ // Go compilers.
+ fmt.Fprintf(os.Stderr, "Note: the following messages should indicate an out-of-bounds compiler error\n")
+ err = run("go", "build", stringSource, source)
+ if err == nil {
+ t.Fatal("unexpected compiler success")
+ }
+}
+
+// buildStringer creates a temporary directory and installs stringer there.
+func buildStringer(t *testing.T) (dir string, stringer string) {
+ t.Helper()
+ testenv.NeedsTool(t, "go")
+
+ dir, err := ioutil.TempDir("", "stringer")
+ if err != nil {
+ t.Fatal(err)
+ }
+ stringer = filepath.Join(dir, "stringer.exe")
+ err = run("go", "build", "-o", stringer)
+ if err != nil {
+ t.Fatalf("building stringer: %s", err)
+ }
+ return dir, stringer
+}
+
+// stringerCompileAndRun runs stringer for the named file and compiles and
+// runs the target binary in directory dir. That binary will panic if the String method is incorrect.
+func stringerCompileAndRun(t *testing.T, dir, stringer, typeName, fileName string) {
+ t.Helper()
+ t.Logf("run: %s %s\n", fileName, typeName)
+ source := filepath.Join(dir, fileName)
+ err := copy(source, filepath.Join("testdata", fileName))
+ if err != nil {
+ t.Fatalf("copying file to temporary directory: %s", err)
+ }
+ stringSource := filepath.Join(dir, typeName+"_string.go")
+ // Run stringer in temporary directory.
+ err = run(stringer, "-type", typeName, "-output", stringSource, source)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Run the binary in the temporary directory.
+ err = run("go", "run", stringSource, source)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// copy copies the from file to the to file.
+func copy(to, from string) error {
+ toFd, err := os.Create(to)
+ if err != nil {
+ return err
+ }
+ defer toFd.Close()
+ fromFd, err := os.Open(from)
+ if err != nil {
+ return err
+ }
+ defer fromFd.Close()
+ _, err = io.Copy(toFd, fromFd)
+ return err
+}
+
+// run runs a single command and returns an error if it does not succeed.
+// os/exec should have this function, to be honest.
+func run(name string, arg ...string) error {
+ return runInDir(".", name, arg...)
+}
+
+// runInDir runs a single command in directory dir and returns an error if
+// it does not succeed.
+func runInDir(dir, name string, arg ...string) error {
+ cmd := exec.Command(name, arg...)
+ cmd.Dir = dir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Env = append(os.Environ(), "GO111MODULE=auto")
+ return cmd.Run()
+}