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 }