blob: 39b98e69c9aff472768b93e551e3ace473f532b1 [file]
// Copyright 2023 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"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
)
// Test runs the deadcode command on each scenario
// described by a testdata/*.txtar file.
func Test(t *testing.T) {
testenv.NeedsTool(t, "go")
if runtime.GOOS == "android" {
t.Skipf("the dependencies are not available on android")
}
exe := buildDeadcode(t)
matches, err := filepath.Glob("testdata/*.txtar")
if err != nil {
t.Fatal(err)
}
for _, filename := range matches {
t.Run(filename, func(t *testing.T) {
t.Parallel()
ar, err := txtar.ParseFile(filename)
if err != nil {
t.Fatal(err)
}
// Write the archive files to the temp directory.
tmpdir := extractTxtar(t, ar)
// Parse archive comment as directives of these forms:
//
// [!]deadcode args... command-line arguments
// [!]want arg expected/unwanted string in output (or stderr)
//
// Args may be Go-quoted strings.
type testcase struct {
linenum int
args []string
wantErr bool
want map[string]bool // string -> sense
}
var cases []*testcase
var current *testcase
for i, line := range strings.Split(string(ar.Comment), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue // skip blanks and comments
}
words, err := words(line)
if err != nil {
t.Fatalf("cannot break line into words: %v (%s)", err, line)
}
switch kind := words[0]; kind {
case "deadcode", "!deadcode":
current = &testcase{
linenum: i + 1,
want: make(map[string]bool),
args: words[1:],
wantErr: kind[0] == '!',
}
cases = append(cases, current)
case "want", "!want":
if current == nil {
t.Fatalf("'want' directive must be after 'deadcode'")
}
if len(words) != 2 {
t.Fatalf("'want' directive needs argument <<%s>>", line)
}
current.want[words[1]] = kind[0] != '!'
default:
t.Fatalf("%s: invalid directive %q", filename, kind)
}
}
for _, tc := range cases {
t.Run(fmt.Sprintf("L%d", tc.linenum), func(t *testing.T) {
// Run the command.
cmd := exec.Command(exe, tc.args...)
cmd.Stdout = new(bytes.Buffer)
cmd.Stderr = new(bytes.Buffer)
cmd.Dir = tmpdir
cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
var got string
if err := cmd.Run(); err != nil {
if !tc.wantErr {
t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
}
got = fmt.Sprint(cmd.Stderr)
} else {
if tc.wantErr {
t.Fatalf("deadcode succeeded unexpectedly (stdout=%s)", cmd.Stdout)
}
got = fmt.Sprint(cmd.Stdout)
}
// Check each want directive.
for str, sense := range tc.want {
ok := true
if strings.Contains(got, str) != sense {
if sense {
t.Errorf("missing %q", str)
} else {
t.Errorf("unwanted %q", str)
}
ok = false
}
if !ok {
t.Errorf("got: <<%s>>", got)
}
}
})
}
})
}
}
// TestDeterminism checks that the output of the deadcode command
// does not vary from one run to the next. It has a different form
// from the other tests because it is intentionally order-sensitive.
func TestDeterminism(t *testing.T) {
testenv.NeedsTool(t, "go")
const archive = `
-- go.mod --
module example.com
go 1.21
-- main.go --
package main
// These dead functions are all declared on the same line.
func dead1() {}; func dead2() {}; func dead3() {}; func dead4() {}; func dead5() {}; func dead6() {}
// These live functions all have the same signature and are
// address-taken, so they are all candidate callees of the dynamic
// call in main, and all of them reach target by a path of the same
// length. (There is no static path to target.)
var funcs = []func(){live1, live2, live3, live4, live5, live6}
func main() {
for _, f := range funcs {
f()
}
}
func live1() { target() }
func live2() { target() }
func live3() { target() }
func live4() { target() }
func live5() { target() }
func live6() { target() }
func target() {}
`
exe := buildDeadcode(t)
tmpdir := extractTxtar(t, txtar.Parse([]byte(archive)))
for _, args := range [][]string{
{"."},
{"-whylive=example.com.target", "."},
} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
// A single discrepancy is enough to fail.
// (Before the fix, both scenarios were detected
// within the first three runs.)
const runs = 10
var first string
for i := range runs {
cmd := exec.Command(exe, args...)
cmd.Stdout = new(bytes.Buffer)
cmd.Stderr = new(bytes.Buffer)
cmd.Dir = tmpdir
cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
if err := cmd.Run(); err != nil {
t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
}
got := fmt.Sprint(cmd.Stdout)
if i == 0 {
first = got
} else if got != first {
t.Fatalf("output of run #%d differs from run #1:\n"+
"run #1: <<%s>>\nrun #%d: <<%s>>", i+1, first, i+1, got)
}
}
})
}
}
// extractTxtar writes the files of the archive to a new
// temporary directory and returns its name.
//
// TODO(adonovan): move to [testfiles.ExtractTxtarToTmp].
// Many other tests want it.
func extractTxtar(t *testing.T, ar *txtar.Archive) string {
tmpdir := t.TempDir()
for _, f := range ar.Files {
filename := filepath.Join(tmpdir, f.Name)
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filename, f.Data, 0666); err != nil {
t.Fatal(err)
}
}
return tmpdir
}
// buildDeadcode builds the deadcode executable.
// It returns its path, and a cleanup function.
func buildDeadcode(t *testing.T) string {
bin := filepath.Join(t.TempDir(), "deadcode")
if runtime.GOOS == "windows" {
bin += ".exe"
}
cmd := exec.Command("go", "build", "-o", bin)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Building deadcode: %v\n%s", err, out)
}
return bin
}
// words breaks a string into words, respecting
// Go string quotations around words with spaces.
func words(s string) ([]string, error) {
var words []string
for s != "" {
s = strings.TrimSpace(s)
var word string
if s[0] == '"' || s[0] == '`' {
prefix, err := strconv.QuotedPrefix(s)
if err != nil {
return nil, err
}
s = s[len(prefix):]
word, _ = strconv.Unquote(prefix)
} else {
prefix, rest, _ := strings.Cut(s, " ")
s = rest
word = prefix
}
words = append(words, word)
}
return words, nil
}