Source file src/cmd/vet/vet_test.go

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

View as plain text