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