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 }