blob: a6902597b7e0ed9f15cddd114bcb13de09490c8e [file] [log] [blame]
// Copyright 2018 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 unitchecker_test
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"golang.org/x/tools/go/analysis/passes/assign"
"golang.org/x/tools/go/analysis/passes/findcall"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/unitchecker"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/testfiles"
"golang.org/x/tools/txtar"
)
func TestMain(m *testing.M) {
// child process?
switch os.Getenv("ENTRYPOINT") {
case "vet":
vet()
panic("unreachable")
case "minivet":
minivet()
panic("unreachable")
case "worker":
worker() // see ExampleSeparateAnalysis
panic("unreachable")
}
// test process
flag.Parse()
os.Exit(m.Run())
}
// minivet is a vet-like tool with a few analyzers, for testing.
func minivet() {
unitchecker.Main(
findcall.Analyzer,
printf.Analyzer,
assign.Analyzer,
)
}
// This is a very basic integration test of modular
// analysis with facts using unitchecker under "go vet".
// It fork/execs the main function above.
func TestIntegration(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skipf("skipping fork/exec test on this platform")
}
const src = `
-- go.mod --
module golang.org/fake
go 1.24
-- a/a.go --
package a
func _() {
MyFunc123()
}
func MyFunc123() {}
-- b/b.go --
package b
import "golang.org/fake/a"
func _() {
a.MyFunc123()
MyFunc123()
}
func MyFunc123() {}
-- c/c.go --
package c
func _() {
i := 5
i = i
}
-- d/d.go --
package d
import "fmt"
var (
msg string
_ = fmt.Sprintf(msg)
)
-- d/dgen.go --
// Code generated by hand. DO NOT EDIT.
package d
import "fmt"
var _ = fmt.Sprintf(msg)
`
// Expand archive into tmp tree.
fs, err := txtar.FS(txtar.Parse([]byte(src)))
if err != nil {
t.Fatal(err)
}
tmpdir := testfiles.CopyToTmp(t, fs)
// -- operators --
// vet runs "go vet" with the specified arguments (plus -findcall.name=MyFunc123).
vet := func(t *testing.T, args ...string) (exitcode int, stdout, stderr string) {
cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123")
cmd.Stdout = new(strings.Builder)
cmd.Stderr = new(strings.Builder)
cmd.Args = append(cmd.Args, args...)
cmd.Env = append(os.Environ(), "ENTRYPOINT=minivet")
cmd.Dir = tmpdir
if err := cmd.Run(); err != nil {
exitErr, ok := err.(*exec.ExitError)
if !ok {
t.Fatalf("couldn't exec %v: %v", cmd, err)
}
exitcode = exitErr.ExitCode()
}
// Sanitize filenames; this is imperfect due to
// (e.g.) /private/tmp -> /tmp symlink on macOS.
stdout = strings.ReplaceAll(fmt.Sprint(cmd.Stdout), tmpdir, "TMPDIR")
stderr = strings.ReplaceAll(fmt.Sprint(cmd.Stderr), tmpdir, "TMPDIR")
// Show vet information on failure.
t.Cleanup(func() {
if t.Failed() {
t.Logf("command: %v", cmd)
t.Logf("exit code: %d", exitcode)
t.Logf("stdout: %s", stdout)
t.Logf("stderr: %s", stderr)
}
})
return
}
// exitcode asserts that the exit code was "want".
exitcode := func(t *testing.T, got, want int) {
if got != want {
t.Fatalf("vet tool exit code was %d", got)
}
}
// parseJSON parses the JSON diagnostics into a simple line-oriented form.
parseJSON := func(t *testing.T, stdout string) string {
var v map[string]map[string][]map[string]any
if err := json.Unmarshal([]byte(stdout), &v); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
var res strings.Builder
for pkgpath, v := range v {
for analyzer, v := range v {
for _, v := range v {
fmt.Fprintf(&res, "%s: [%s@%s] %v\n",
v["posn"],
analyzer, pkgpath,
v["message"])
}
}
}
// Show parsed JSON information on failure.
t.Cleanup(func() {
if t.Failed() {
t.Logf("json: %s", &res)
}
})
return res.String()
}
// substring asserts that the labeled output contained the substring.
substring := func(t *testing.T, label, output, substr string) {
if !strings.Contains(output, substr) {
t.Fatalf("%s: expected substring %q", label, substr)
}
}
// contains asserts that the specified file contains the substring.
contains := func(t *testing.T, filename, substr string) {
content, err := os.ReadFile(filepath.Join(tmpdir, filename))
if err != nil {
t.Fatalf("can't read %s: %v", filename, err)
}
t.Cleanup(func() {
if t.Failed() {
t.Logf("content of %s: <<%s>>", filename, content)
}
})
substring(t, filename, string(content), substr)
}
// -- scenarios --
t.Run("a", func(t *testing.T) {
code, _, stderr := vet(t, "golang.org/fake/a")
exitcode(t, code, 1)
substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
})
t.Run("b", func(t *testing.T) {
code, _, stderr := vet(t, "golang.org/fake/b")
exitcode(t, code, 1)
substring(t, "stderr", stderr, "b/b.go:6:13: call of MyFunc123")
substring(t, "stderr", stderr, "b/b.go:7:11: call of MyFunc123")
})
t.Run("c", func(t *testing.T) {
code, _, stderr := vet(t, "golang.org/fake/c")
exitcode(t, code, 1)
substring(t, "stderr", stderr, "c/c.go:5:5: self-assignment of i")
})
t.Run("ab", func(t *testing.T) {
code, _, stderr := vet(t, "golang.org/fake/a", "golang.org/fake/b")
exitcode(t, code, 1)
substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
substring(t, "stderr", stderr, "b/b.go:6:13: call of MyFunc123")
substring(t, "stderr", stderr, "b/b.go:7:11: call of MyFunc123")
})
t.Run("a-json", func(t *testing.T) {
code, stdout, _ := vet(t, "-json", "golang.org/fake/a")
exitcode(t, code, 0)
testenv.NeedsGo1Point(t, 26) // depends on CL 702815 (go vet -json => stdout)
json := parseJSON(t, stdout)
substring(t, "json", json, "a/a.go:4:11: [findcall@golang.org/fake/a] call of MyFunc123")
})
t.Run("c-json", func(t *testing.T) {
code, stdout, _ := vet(t, "-json", "golang.org/fake/c")
exitcode(t, code, 0)
testenv.NeedsGo1Point(t, 26) // depends on CL 702815 (go vet -json => stdout)
json := parseJSON(t, stdout)
substring(t, "json", json, "c/c.go:5:5: [assign@golang.org/fake/c] self-assignment of i")
})
t.Run("a-context", func(t *testing.T) {
code, _, stderr := vet(t, "-c=0", "golang.org/fake/a")
exitcode(t, code, 1)
substring(t, "stderr", stderr, "a/a.go:4:11: call of MyFunc123")
substring(t, "stderr", stderr, "4 MyFunc123")
})
t.Run("d-fix", func(t *testing.T) {
testenv.NeedsGo1Point(t, 26)
code, _, stderr := vet(t, "-fix", "golang.org/fake/d")
exitcode(t, code, 1)
contains(t, "d/d.go", `fmt.Sprintf("%s", msg)`) // fixed
contains(t, "d/dgen.go", `fmt.Sprintf(msg)`) // fix not applied to generated file
substring(t, "stderr", stderr, "applied 1 of 2 fixes; 1 files updated")
})
}