blob: 64cfdb77d99f2b844135ec73cca9ee75bb7734aa [file] [log] [blame]
// 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 loopvar_test
import (
"internal/testenv"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
)
type testcase struct {
lvFlag string // ==-2, -1, 0, 1, 2
buildExpect string // message, if any
expectRC int
files []string
}
var for_files = []string{
"for_esc_address.go", // address of variable
"for_esc_closure.go", // closure of variable
"for_esc_minimal_closure.go", // simple closure of variable
"for_esc_method.go", // method value of variable
"for_complicated_esc_address.go", // modifies loop index in body
}
var range_files = []string{
"range_esc_address.go", // address of variable
"range_esc_closure.go", // closure of variable
"range_esc_minimal_closure.go", // simple closure of variable
"range_esc_method.go", // method value of variable
}
var cases = []testcase{
{"-1", "", 11, for_files[:1]},
{"0", "", 0, for_files[:1]},
{"1", "", 0, for_files[:1]},
{"2", "loop variable i now per-iteration,", 0, for_files},
{"-1", "", 11, range_files[:1]},
{"0", "", 0, range_files[:1]},
{"1", "", 0, range_files[:1]},
{"2", "loop variable i now per-iteration,", 0, range_files},
{"1", "", 0, []string{"for_nested.go"}},
}
// TestLoopVar checks that the GOEXPERIMENT and debug flags behave as expected.
func TestLoopVarGo1_21(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
tmpdir := t.TempDir()
output := filepath.Join(tmpdir, "foo.exe")
for i, tc := range cases {
for _, f := range tc.files {
source := f
cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-lang=go1.21 -d=loopvar="+tc.lvFlag, source)
cmd.Env = append(cmd.Env, "GOEXPERIMENT=loopvar", "HOME="+tmpdir)
cmd.Dir = "testdata"
t.Logf("File %s loopvar=%s expect '%s' exit code %d", f, tc.lvFlag, tc.buildExpect, tc.expectRC)
b, e := cmd.CombinedOutput()
if e != nil {
t.Error(e)
}
if tc.buildExpect != "" {
s := string(b)
if !strings.Contains(s, tc.buildExpect) {
t.Errorf("File %s test %d expected to match '%s' with \n-----\n%s\n-----", f, i, tc.buildExpect, s)
}
}
// run what we just built.
cmd = testenv.Command(t, output)
b, e = cmd.CombinedOutput()
if tc.expectRC != 0 {
if e == nil {
t.Errorf("Missing expected error, file %s, case %d", f, i)
} else if ee, ok := (e).(*exec.ExitError); !ok || ee.ExitCode() != tc.expectRC {
t.Error(e)
} else {
// okay
}
} else if e != nil {
t.Error(e)
}
}
}
}
func TestLoopVarInlinesGo1_21(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
tmpdir := t.TempDir()
root := "cmd/compile/internal/loopvar/testdata/inlines"
f := func(pkg string) string {
// This disables the loopvar change, except for the specified package.
// The effect should follow the package, even though everything (except "c")
// is inlined.
cmd := testenv.Command(t, gocmd, "run", "-gcflags="+root+"/...=-lang=go1.21", "-gcflags="+pkg+"=-d=loopvar=1", root)
cmd.Env = append(cmd.Env, "GOEXPERIMENT=noloopvar", "HOME="+tmpdir)
cmd.Dir = filepath.Join("testdata", "inlines")
b, e := cmd.CombinedOutput()
if e != nil {
t.Error(e)
}
return string(b)
}
a := f(root + "/a")
b := f(root + "/b")
c := f(root + "/c")
m := f(root)
t.Logf(a)
t.Logf(b)
t.Logf(c)
t.Logf(m)
if !strings.Contains(a, "f, af, bf, abf, cf sums = 100, 45, 100, 100, 100") {
t.Errorf("Did not see expected value of a")
}
if !strings.Contains(b, "f, af, bf, abf, cf sums = 100, 100, 45, 45, 100") {
t.Errorf("Did not see expected value of b")
}
if !strings.Contains(c, "f, af, bf, abf, cf sums = 100, 100, 100, 100, 45") {
t.Errorf("Did not see expected value of c")
}
if !strings.Contains(m, "f, af, bf, abf, cf sums = 45, 100, 100, 100, 100") {
t.Errorf("Did not see expected value of m")
}
}
func countMatches(s, re string) int {
slice := regexp.MustCompile(re).FindAllString(s, -1)
return len(slice)
}
func TestLoopVarHashes(t *testing.T) {
// This behavior does not depend on Go version (1.21 or greater)
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
tmpdir := t.TempDir()
root := "cmd/compile/internal/loopvar/testdata/inlines"
f := func(hash string) string {
// This disables the loopvar change, except for the specified hash pattern.
// -trimpath is necessary so we get the same answer no matter where the
// Go repository is checked out. This is not normally a concern since people
// do not normally rely on the meaning of specific hashes.
cmd := testenv.Command(t, gocmd, "run", "-trimpath", root)
cmd.Env = append(cmd.Env, "GOCOMPILEDEBUG=loopvarhash="+hash, "HOME="+tmpdir)
cmd.Dir = filepath.Join("testdata", "inlines")
b, _ := cmd.CombinedOutput()
// Ignore the error, sometimes it's supposed to fail, the output test will catch it.
return string(b)
}
for _, arg := range []string{"v001100110110110010100100", "vx336ca4"} {
m := f(arg)
t.Logf(m)
mCount := countMatches(m, "loopvarhash triggered cmd/compile/internal/loopvar/testdata/inlines/main.go:27:6: .* 001100110110110010100100")
otherCount := strings.Count(m, "loopvarhash")
if mCount < 1 {
t.Errorf("%s: did not see triggered main.go:27:6", arg)
}
if mCount != otherCount {
t.Errorf("%s: too many matches", arg)
}
mCount = countMatches(m, "cmd/compile/internal/loopvar/testdata/inlines/main.go:27:6: .* \\[bisect-match 0x7802e115b9336ca4\\]")
otherCount = strings.Count(m, "[bisect-match ")
if mCount < 1 {
t.Errorf("%s: did not see bisect-match for main.go:27:6", arg)
}
if mCount != otherCount {
t.Errorf("%s: too many matches", arg)
}
// This next test carefully dodges a bug-to-be-fixed with inlined locations for ir.Names.
if !strings.Contains(m, ", 100, 100, 100, 100") {
t.Errorf("%s: did not see expected value of m run", arg)
}
}
}
// TestLoopVarVersionEnableFlag checks for loopvar transformation enabled by command line flag (1.22).
func TestLoopVarVersionEnableFlag(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
// loopvar=3 logs info but does not change loopvarness
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt.go")
cmd.Dir = filepath.Join("testdata")
b, err := cmd.CombinedOutput()
m := string(b)
t.Logf(m)
yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
nCount := strings.Count(m, "shared")
if yCount != 1 {
t.Errorf("yCount=%d != 1", yCount)
}
if nCount > 0 {
t.Errorf("nCount=%d > 0", nCount)
}
if err != nil {
t.Errorf("err=%v != nil", err)
}
}
// TestLoopVarVersionEnableGoBuild checks for loopvar transformation enabled by go:build version (1.22).
func TestLoopVarVersionEnableGoBuild(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
// loopvar=3 logs info but does not change loopvarness
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt-122.go")
cmd.Dir = filepath.Join("testdata")
b, err := cmd.CombinedOutput()
m := string(b)
t.Logf(m)
yCount := strings.Count(m, "opt-122.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-122.go:31)")
nCount := strings.Count(m, "shared")
if yCount != 1 {
t.Errorf("yCount=%d != 1", yCount)
}
if nCount > 0 {
t.Errorf("nCount=%d > 0", nCount)
}
if err != nil {
t.Errorf("err=%v != nil", err)
}
}
// TestLoopVarVersionDisableFlag checks for loopvar transformation DISABLED by command line version (1.21).
func TestLoopVarVersionDisableFlag(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
// loopvar=3 logs info but does not change loopvarness
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt.go")
cmd.Dir = filepath.Join("testdata")
b, err := cmd.CombinedOutput()
m := string(b)
t.Logf(m) // expect error
yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
nCount := strings.Count(m, "shared")
if yCount != 0 {
t.Errorf("yCount=%d != 0", yCount)
}
if nCount > 0 {
t.Errorf("nCount=%d > 0", nCount)
}
if err == nil { // expect error
t.Errorf("err=%v == nil", err)
}
}
// TestLoopVarVersionDisableGoBuild checks for loopvar transformation DISABLED by go:build version (1.21).
func TestLoopVarVersionDisableGoBuild(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
}
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
}
testenv.MustHaveGoBuild(t)
gocmd := testenv.GoToolPath(t)
// loopvar=3 logs info but does not change loopvarness
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt-121.go")
cmd.Dir = filepath.Join("testdata")
b, err := cmd.CombinedOutput()
m := string(b)
t.Logf(m) // expect error
yCount := strings.Count(m, "opt-121.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-121.go:31)")
nCount := strings.Count(m, "shared")
if yCount != 0 {
t.Errorf("yCount=%d != 0", yCount)
}
if nCount > 0 {
t.Errorf("nCount=%d > 0", nCount)
}
if err == nil { // expect error
t.Errorf("err=%v == nil", err)
}
}