...
Run Format

Source file src/cmd/vet/vet_test.go

Documentation: cmd/vet

  // Copyright 2013 The Go Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style
  // license that can be found in the LICENSE file.
  
  package main_test
  
  import (
  	"bytes"
  	"errors"
  	"fmt"
  	"internal/testenv"
  	"io/ioutil"
  	"log"
  	"os"
  	"os/exec"
  	"path/filepath"
  	"regexp"
  	"runtime"
  	"strconv"
  	"strings"
  	"sync"
  	"testing"
  )
  
  const (
  	dataDir = "testdata"
  	binary  = "./testvet.exe"
  )
  
  // We implement TestMain so remove the test binary when all is done.
  func TestMain(m *testing.M) {
  	result := m.Run()
  	os.Remove(binary)
  	os.Exit(result)
  }
  
  var (
  	buildMu sync.Mutex // guards following
  	built   = false    // We have built the binary.
  	failed  = false    // We have failed to build the binary, don't try again.
  )
  
  func Build(t *testing.T) {
  	buildMu.Lock()
  	defer buildMu.Unlock()
  	if built {
  		return
  	}
  	if failed {
  		t.Skip("cannot run on this environment")
  	}
  	testenv.MustHaveGoBuild(t)
  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", binary)
  	output, err := cmd.CombinedOutput()
  	if err != nil {
  		failed = true
  		fmt.Fprintf(os.Stderr, "%s\n", output)
  		t.Fatal(err)
  	}
  	built = true
  }
  
  func Vet(t *testing.T, files []string) {
  	flags := []string{
  		"-printfuncs=Warn:1,Warnf:1",
  		"-all",
  		"-shadow",
  	}
  	cmd := exec.Command(binary, append(flags, files...)...)
  	errchk(cmd, files, t)
  }
  
  // TestVet is equivalent to running this:
  // 	go build -o ./testvet
  // 	errorCheck the output of ./testvet -shadow -printfuncs='Warn:1,Warnf:1' testdata/*.go testdata/*.s
  // 	rm ./testvet
  //
  
  // TestVet tests self-contained files in testdata/*.go.
  //
  // If a file contains assembly or has inter-dependencies, it should be
  // in its own test, like TestVetAsm, TestDivergentPackagesExamples,
  // etc below.
  func TestVet(t *testing.T) {
  	Build(t)
  	t.Parallel()
  
  	gos, err := filepath.Glob(filepath.Join(dataDir, "*.go"))
  	if err != nil {
  		t.Fatal(err)
  	}
  	wide := runtime.GOMAXPROCS(0)
  	if wide > len(gos) {
  		wide = len(gos)
  	}
  	batch := make([][]string, wide)
  	for i, file := range gos {
  		// TODO: Remove print.go exception once we require type checking for everything,
  		// and then delete TestVetPrint.
  		if strings.HasSuffix(file, "print.go") {
  			continue
  		}
  		batch[i%wide] = append(batch[i%wide], file)
  	}
  	for i, files := range batch {
  		if len(files) == 0 {
  			continue
  		}
  		files := files
  		t.Run(fmt.Sprint(i), func(t *testing.T) {
  			t.Parallel()
  			t.Logf("files: %q", files)
  			Vet(t, files)
  		})
  	}
  }
  
  func TestVetPrint(t *testing.T) {
  	Build(t)
  	file := filepath.Join("testdata", "print.go")
  	cmd := exec.Command(
  		"go", "vet", "-vettool="+binary,
  		"-printf",
  		"-printfuncs=Warn:1,Warnf:1",
  		file,
  	)
  	errchk(cmd, []string{file}, t)
  }
  
  func TestVetAsm(t *testing.T) {
  	Build(t)
  
  	asmDir := filepath.Join(dataDir, "asm")
  	gos, err := filepath.Glob(filepath.Join(asmDir, "*.go"))
  	if err != nil {
  		t.Fatal(err)
  	}
  	asms, err := filepath.Glob(filepath.Join(asmDir, "*.s"))
  	if err != nil {
  		t.Fatal(err)
  	}
  
  	t.Parallel()
  	Vet(t, append(gos, asms...))
  }
  
  func TestVetDirs(t *testing.T) {
  	t.Parallel()
  	Build(t)
  	for _, dir := range []string{
  		"testingpkg",
  		"divergent",
  		"buildtag",
  		"incomplete", // incomplete examples
  		"cgo",
  	} {
  		dir := dir
  		t.Run(dir, func(t *testing.T) {
  			t.Parallel()
  			gos, err := filepath.Glob(filepath.Join("testdata", dir, "*.go"))
  			if err != nil {
  				t.Fatal(err)
  			}
  			Vet(t, gos)
  		})
  	}
  }
  
  func errchk(c *exec.Cmd, files []string, t *testing.T) {
  	output, err := c.CombinedOutput()
  	if _, ok := err.(*exec.ExitError); !ok {
  		t.Logf("vet output:\n%s", output)
  		t.Fatal(err)
  	}
  	fullshort := make([]string, 0, len(files)*2)
  	for _, f := range files {
  		fullshort = append(fullshort, f, filepath.Base(f))
  	}
  	err = errorCheck(string(output), false, fullshort...)
  	if err != nil {
  		t.Errorf("error check failed: %s", err)
  	}
  }
  
  // TestTags verifies that the -tags argument controls which files to check.
  func TestTags(t *testing.T) {
  	t.Parallel()
  	Build(t)
  	for _, tag := range []string{"testtag", "x testtag y", "x,testtag,y"} {
  		tag := tag
  		t.Run(tag, func(t *testing.T) {
  			t.Parallel()
  			t.Logf("-tags=%s", tag)
  			args := []string{
  				"-tags=" + tag,
  				"-v", // We're going to look at the files it examines.
  				"testdata/tagtest",
  			}
  			cmd := exec.Command(binary, args...)
  			output, err := cmd.CombinedOutput()
  			if err != nil {
  				t.Fatal(err)
  			}
  			// file1 has testtag and file2 has !testtag.
  			if !bytes.Contains(output, []byte(filepath.Join("tagtest", "file1.go"))) {
  				t.Error("file1 was excluded, should be included")
  			}
  			if bytes.Contains(output, []byte(filepath.Join("tagtest", "file2.go"))) {
  				t.Error("file2 was included, should be excluded")
  			}
  		})
  	}
  }
  
  // Issue #21188.
  func TestVetVerbose(t *testing.T) {
  	t.Parallel()
  	Build(t)
  	cmd := exec.Command(binary, "-v", "-all", "testdata/cgo/cgo3.go")
  	out, err := cmd.CombinedOutput()
  	if err != nil {
  		t.Logf("%s", out)
  		t.Error(err)
  	}
  }
  
  // All declarations below were adapted from test/run.go.
  
  // errorCheck matches errors in outStr against comments in source files.
  // For each line of the source files which should generate an error,
  // there should be a comment of the form // ERROR "regexp".
  // If outStr has an error for a line which has no such comment,
  // this function will report an error.
  // Likewise if outStr does not have an error for a line which has a comment,
  // or if the error message does not match the <regexp>.
  // The <regexp> syntax is Perl but its best to stick to egrep.
  //
  // Sources files are supplied as fullshort slice.
  // It consists of pairs: full path to source file and it's base name.
  func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
  	var errs []error
  	out := splitOutput(outStr, wantAuto)
  	// Cut directory name.
  	for i := range out {
  		for j := 0; j < len(fullshort); j += 2 {
  			full, short := fullshort[j], fullshort[j+1]
  			out[i] = strings.Replace(out[i], full, short, -1)
  		}
  	}
  
  	var want []wantedError
  	for j := 0; j < len(fullshort); j += 2 {
  		full, short := fullshort[j], fullshort[j+1]
  		want = append(want, wantedErrors(full, short)...)
  	}
  	for _, we := range want {
  		var errmsgs []string
  		if we.auto {
  			errmsgs, out = partitionStrings("<autogenerated>", out)
  		} else {
  			errmsgs, out = partitionStrings(we.prefix, out)
  		}
  		if len(errmsgs) == 0 {
  			errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
  			continue
  		}
  		matched := false
  		n := len(out)
  		for _, errmsg := range errmsgs {
  			// Assume errmsg says "file:line: foo".
  			// Cut leading "file:line: " to avoid accidental matching of file name instead of message.
  			text := errmsg
  			if i := strings.Index(text, " "); i >= 0 {
  				text = text[i+1:]
  			}
  			if we.re.MatchString(text) {
  				matched = true
  			} else {
  				out = append(out, errmsg)
  			}
  		}
  		if !matched {
  			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")))
  			continue
  		}
  	}
  
  	if len(out) > 0 {
  		errs = append(errs, fmt.Errorf("Unmatched Errors:"))
  		for _, errLine := range out {
  			errs = append(errs, fmt.Errorf("%s", errLine))
  		}
  	}
  
  	if len(errs) == 0 {
  		return nil
  	}
  	if len(errs) == 1 {
  		return errs[0]
  	}
  	var buf bytes.Buffer
  	fmt.Fprintf(&buf, "\n")
  	for _, err := range errs {
  		fmt.Fprintf(&buf, "%s\n", err.Error())
  	}
  	return errors.New(buf.String())
  }
  
  func splitOutput(out string, wantAuto bool) []string {
  	// gc error messages continue onto additional lines with leading tabs.
  	// Split the output at the beginning of each line that doesn't begin with a tab.
  	// <autogenerated> lines are impossible to match so those are filtered out.
  	var res []string
  	for _, line := range strings.Split(out, "\n") {
  		line = strings.TrimSuffix(line, "\r") // normalize Windows output
  		if strings.HasPrefix(line, "\t") {
  			res[len(res)-1] += "\n" + line
  		} else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
  			continue
  		} else if strings.TrimSpace(line) != "" {
  			res = append(res, line)
  		}
  	}
  	return res
  }
  
  // matchPrefix reports whether s starts with file name prefix followed by a :,
  // and possibly preceded by a directory name.
  func matchPrefix(s, prefix string) bool {
  	i := strings.Index(s, ":")
  	if i < 0 {
  		return false
  	}
  	j := strings.LastIndex(s[:i], "/")
  	s = s[j+1:]
  	if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
  		return false
  	}
  	if s[len(prefix)] == ':' {
  		return true
  	}
  	return false
  }
  
  func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
  	for _, s := range strs {
  		if matchPrefix(s, prefix) {
  			matched = append(matched, s)
  		} else {
  			unmatched = append(unmatched, s)
  		}
  	}
  	return
  }
  
  type wantedError struct {
  	reStr   string
  	re      *regexp.Regexp
  	lineNum int
  	auto    bool // match <autogenerated> line
  	file    string
  	prefix  string
  }
  
  var (
  	errRx       = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`)
  	errAutoRx   = regexp.MustCompile(`// (?:GC_)?ERRORAUTO (.*)`)
  	errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
  	lineRx      = regexp.MustCompile(`LINE(([+-])([0-9]+))?`)
  )
  
  // wantedErrors parses expected errors from comments in a file.
  func wantedErrors(file, short string) (errs []wantedError) {
  	cache := make(map[string]*regexp.Regexp)
  
  	src, err := ioutil.ReadFile(file)
  	if err != nil {
  		log.Fatal(err)
  	}
  	for i, line := range strings.Split(string(src), "\n") {
  		lineNum := i + 1
  		if strings.Contains(line, "////") {
  			// double comment disables ERROR
  			continue
  		}
  		var auto bool
  		m := errAutoRx.FindStringSubmatch(line)
  		if m != nil {
  			auto = true
  		} else {
  			m = errRx.FindStringSubmatch(line)
  		}
  		if m == nil {
  			continue
  		}
  		all := m[1]
  		mm := errQuotesRx.FindAllStringSubmatch(all, -1)
  		if mm == nil {
  			log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line)
  		}
  		for _, m := range mm {
  			replacedOnce := false
  			rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
  				if replacedOnce {
  					return m
  				}
  				replacedOnce = true
  				n := lineNum
  				if strings.HasPrefix(m, "LINE+") {
  					delta, _ := strconv.Atoi(m[5:])
  					n += delta
  				} else if strings.HasPrefix(m, "LINE-") {
  					delta, _ := strconv.Atoi(m[5:])
  					n -= delta
  				}
  				return fmt.Sprintf("%s:%d", short, n)
  			})
  			re := cache[rx]
  			if re == nil {
  				var err error
  				re, err = regexp.Compile(rx)
  				if err != nil {
  					log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
  				}
  				cache[rx] = re
  			}
  			prefix := fmt.Sprintf("%s:%d", short, lineNum)
  			errs = append(errs, wantedError{
  				reStr:   rx,
  				re:      re,
  				prefix:  prefix,
  				auto:    auto,
  				lineNum: lineNum,
  				file:    short,
  			})
  		}
  	}
  
  	return
  }
  

View as plain text