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