1// Copyright 2013 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
5package main_test
6
7import (
8	"bytes"
9	"errors"
10	"fmt"
11	"internal/testenv"
12	"io/ioutil"
13	"log"
14	"os"
15	"os/exec"
16	"path"
17	"path/filepath"
18	"regexp"
19	"runtime"
20	"strconv"
21	"strings"
22	"sync"
23	"testing"
24)
25
26const dataDir = "testdata"
27
28var binary string
29
30// We implement TestMain so remove the test binary when all is done.
31func TestMain(m *testing.M) {
32	os.Exit(testMain(m))
33}
34
35func testMain(m *testing.M) int {
36	dir, err := ioutil.TempDir("", "vet_test")
37	if err != nil {
38		fmt.Fprintln(os.Stderr, err)
39		return 1
40	}
41	defer os.RemoveAll(dir)
42	binary = filepath.Join(dir, "testvet.exe")
43
44	return m.Run()
45}
46
47var (
48	buildMu sync.Mutex // guards following
49	built   = false    // We have built the binary.
50	failed  = false    // We have failed to build the binary, don't try again.
51)
52
53func Build(t *testing.T) {
54	buildMu.Lock()
55	defer buildMu.Unlock()
56	if built {
57		return
58	}
59	if failed {
60		t.Skip("cannot run on this environment")
61	}
62	testenv.MustHaveGoBuild(t)
63	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", binary)
64	output, err := cmd.CombinedOutput()
65	if err != nil {
66		failed = true
67		fmt.Fprintf(os.Stderr, "%s\n", output)
68		t.Fatal(err)
69	}
70	built = true
71}
72
73func vetCmd(t *testing.T, arg, pkg string) *exec.Cmd {
74	cmd := exec.Command(testenv.GoToolPath(t), "vet", "-vettool="+binary, arg, path.Join("cmd/vet/testdata", pkg))
75	cmd.Env = os.Environ()
76	return cmd
77}
78
79func TestVet(t *testing.T) {
80	t.Parallel()
81	Build(t)
82	for _, pkg := range []string{
83		"asm",
84		"assign",
85		"atomic",
86		"bool",
87		"buildtag",
88		"cgo",
89		"composite",
90		"copylock",
91		"deadcode",
92		"httpresponse",
93		"lostcancel",
94		"method",
95		"nilfunc",
96		"print",
97		"rangeloop",
98		"shift",
99		"structtag",
100		"testingpkg",
101		// "testtag" has its own test
102		"unmarshal",
103		"unsafeptr",
104		"unused",
105	} {
106		pkg := pkg
107		t.Run(pkg, func(t *testing.T) {
108			t.Parallel()
109
110			// Skip cgo test on platforms without cgo.
111			if pkg == "cgo" && !cgoEnabled(t) {
112				return
113			}
114
115			cmd := vetCmd(t, "-printfuncs=Warn,Warnf", pkg)
116
117			// The asm test assumes amd64.
118			if pkg == "asm" {
119				if runtime.Compiler == "gccgo" {
120					t.Skip("asm test assumes gc")
121				}
122				cmd.Env = append(cmd.Env, "GOOS=linux", "GOARCH=amd64")
123			}
124
125			dir := filepath.Join("testdata", pkg)
126			gos, err := filepath.Glob(filepath.Join(dir, "*.go"))
127			if err != nil {
128				t.Fatal(err)
129			}
130			asms, err := filepath.Glob(filepath.Join(dir, "*.s"))
131			if err != nil {
132				t.Fatal(err)
133			}
134			var files []string
135			files = append(files, gos...)
136			files = append(files, asms...)
137
138			errchk(cmd, files, t)
139		})
140	}
141}
142
143func cgoEnabled(t *testing.T) bool {
144	// Don't trust build.Default.CgoEnabled as it is false for
145	// cross-builds unless CGO_ENABLED is explicitly specified.
146	// That's fine for the builders, but causes commands like
147	// 'GOARCH=386 go test .' to fail.
148	// Instead, we ask the go command.
149	cmd := exec.Command(testenv.GoToolPath(t), "list", "-f", "{{context.CgoEnabled}}")
150	out, _ := cmd.CombinedOutput()
151	return string(out) == "true\n"
152}
153
154func errchk(c *exec.Cmd, files []string, t *testing.T) {
155	output, err := c.CombinedOutput()
156	if _, ok := err.(*exec.ExitError); !ok {
157		t.Logf("vet output:\n%s", output)
158		t.Fatal(err)
159	}
160	fullshort := make([]string, 0, len(files)*2)
161	for _, f := range files {
162		fullshort = append(fullshort, f, filepath.Base(f))
163	}
164	err = errorCheck(string(output), false, fullshort...)
165	if err != nil {
166		t.Errorf("error check failed: %s", err)
167	}
168}
169
170// TestTags verifies that the -tags argument controls which files to check.
171func TestTags(t *testing.T) {
172	t.Parallel()
173	Build(t)
174	for tag, wantFile := range map[string]int{
175		"testtag":     1, // file1
176		"x testtag y": 1,
177		"othertag":    2,
178	} {
179		tag, wantFile := tag, wantFile
180		t.Run(tag, func(t *testing.T) {
181			t.Parallel()
182			t.Logf("-tags=%s", tag)
183			cmd := vetCmd(t, "-tags="+tag, "tagtest")
184			output, err := cmd.CombinedOutput()
185
186			want := fmt.Sprintf("file%d.go", wantFile)
187			dontwant := fmt.Sprintf("file%d.go", 3-wantFile)
188
189			// file1 has testtag and file2 has !testtag.
190			if !bytes.Contains(output, []byte(filepath.Join("tagtest", want))) {
191				t.Errorf("%s: %s was excluded, should be included", tag, want)
192			}
193			if bytes.Contains(output, []byte(filepath.Join("tagtest", dontwant))) {
194				t.Errorf("%s: %s was included, should be excluded", tag, dontwant)
195			}
196			if t.Failed() {
197				t.Logf("err=%s, output=<<%s>>", err, output)
198			}
199		})
200	}
201}
202
203// All declarations below were adapted from test/run.go.
204
205// errorCheck matches errors in outStr against comments in source files.
206// For each line of the source files which should generate an error,
207// there should be a comment of the form // ERROR "regexp".
208// If outStr has an error for a line which has no such comment,
209// this function will report an error.
210// Likewise if outStr does not have an error for a line which has a comment,
211// or if the error message does not match the <regexp>.
212// The <regexp> syntax is Perl but it's best to stick to egrep.
213//
214// Sources files are supplied as fullshort slice.
215// It consists of pairs: full path to source file and its base name.
216func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
217	var errs []error
218	out := splitOutput(outStr, wantAuto)
219	// Cut directory name.
220	for i := range out {
221		for j := 0; j < len(fullshort); j += 2 {
222			full, short := fullshort[j], fullshort[j+1]
223			out[i] = strings.ReplaceAll(out[i], full, short)
224		}
225	}
226
227	var want []wantedError
228	for j := 0; j < len(fullshort); j += 2 {
229		full, short := fullshort[j], fullshort[j+1]
230		want = append(want, wantedErrors(full, short)...)
231	}
232	for _, we := range want {
233		var errmsgs []string
234		if we.auto {
235			errmsgs, out = partitionStrings("<autogenerated>", out)
236		} else {
237			errmsgs, out = partitionStrings(we.prefix, out)
238		}
239		if len(errmsgs) == 0 {
240			errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
241			continue
242		}
243		matched := false
244		n := len(out)
245		for _, errmsg := range errmsgs {
246			// Assume errmsg says "file:line: foo".
247			// Cut leading "file:line: " to avoid accidental matching of file name instead of message.
248			text := errmsg
249			if i := strings.Index(text, " "); i >= 0 {
250				text = text[i+1:]
251			}
252			if we.re.MatchString(text) {
253				matched = true
254			} else {
255				out = append(out, errmsg)
256			}
257		}
258		if !matched {
259			errs = append(errs, fmt.Errorf("%s:%d: no match for %#q in:\n\t%s", we.file, we.lineNum, we.reStr, strings.Join(out[n:], "\n\t")))
260			continue
261		}
262	}
263
264	if len(out) > 0 {
265		errs = append(errs, fmt.Errorf("Unmatched Errors:"))
266		for _, errLine := range out {
267			errs = append(errs, fmt.Errorf("%s", errLine))
268		}
269	}
270
271	if len(errs) == 0 {
272		return nil
273	}
274	if len(errs) == 1 {
275		return errs[0]
276	}
277	var buf bytes.Buffer
278	fmt.Fprintf(&buf, "\n")
279	for _, err := range errs {
280		fmt.Fprintf(&buf, "%s\n", err.Error())
281	}
282	return errors.New(buf.String())
283}
284
285func splitOutput(out string, wantAuto bool) []string {
286	// gc error messages continue onto additional lines with leading tabs.
287	// Split the output at the beginning of each line that doesn't begin with a tab.
288	// <autogenerated> lines are impossible to match so those are filtered out.
289	var res []string
290	for _, line := range strings.Split(out, "\n") {
291		line = strings.TrimSuffix(line, "\r") // normalize Windows output
292		if strings.HasPrefix(line, "\t") {
293			res[len(res)-1] += "\n" + line
294		} else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
295			continue
296		} else if strings.TrimSpace(line) != "" {
297			res = append(res, line)
298		}
299	}
300	return res
301}
302
303// matchPrefix reports whether s starts with file name prefix followed by a :,
304// and possibly preceded by a directory name.
305func matchPrefix(s, prefix string) bool {
306	i := strings.Index(s, ":")
307	if i < 0 {
308		return false
309	}
310	j := strings.LastIndex(s[:i], "/")
311	s = s[j+1:]
312	if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
313		return false
314	}
315	if s[len(prefix)] == ':' {
316		return true
317	}
318	return false
319}
320
321func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
322	for _, s := range strs {
323		if matchPrefix(s, prefix) {
324			matched = append(matched, s)
325		} else {
326			unmatched = append(unmatched, s)
327		}
328	}
329	return
330}
331
332type wantedError struct {
333	reStr   string
334	re      *regexp.Regexp
335	lineNum int
336	auto    bool // match <autogenerated> line
337	file    string
338	prefix  string
339}
340
341var (
342	errRx       = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`)
343	errAutoRx   = regexp.MustCompile(`// (?:GC_)?ERRORAUTO (.*)`)
344	errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
345	lineRx      = regexp.MustCompile(`LINE(([+-])([0-9]+))?`)
346)
347
348// wantedErrors parses expected errors from comments in a file.
349func wantedErrors(file, short string) (errs []wantedError) {
350	cache := make(map[string]*regexp.Regexp)
351
352	src, err := ioutil.ReadFile(file)
353	if err != nil {
354		log.Fatal(err)
355	}
356	for i, line := range strings.Split(string(src), "\n") {
357		lineNum := i + 1
358		if strings.Contains(line, "////") {
359			// double comment disables ERROR
360			continue
361		}
362		var auto bool
363		m := errAutoRx.FindStringSubmatch(line)
364		if m != nil {
365			auto = true
366		} else {
367			m = errRx.FindStringSubmatch(line)
368		}
369		if m == nil {
370			continue
371		}
372		all := m[1]
373		mm := errQuotesRx.FindAllStringSubmatch(all, -1)
374		if mm == nil {
375			log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line)
376		}
377		for _, m := range mm {
378			replacedOnce := false
379			rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
380				if replacedOnce {
381					return m
382				}
383				replacedOnce = true
384				n := lineNum
385				if strings.HasPrefix(m, "LINE+") {
386					delta, _ := strconv.Atoi(m[5:])
387					n += delta
388				} else if strings.HasPrefix(m, "LINE-") {
389					delta, _ := strconv.Atoi(m[5:])
390					n -= delta
391				}
392				return fmt.Sprintf("%s:%d", short, n)
393			})
394			re := cache[rx]
395			if re == nil {
396				var err error
397				re, err = regexp.Compile(rx)
398				if err != nil {
399					log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
400				}
401				cache[rx] = re
402			}
403			prefix := fmt.Sprintf("%s:%d", short, lineNum)
404			errs = append(errs, wantedError{
405				reStr:   rx,
406				re:      re,
407				prefix:  prefix,
408				auto:    auto,
409				lineNum: lineNum,
410				file:    short,
411			})
412		}
413	}
414
415	return
416}
417