...
Run Format

Source file src/cmd/vet/vet_test.go

Documentation: cmd/vet

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

View as plain text