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