Source file src/cmd/compile/internal/loopvar/loopvar_test.go

     1  // Copyright 2023 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 loopvar_test
     6  
     7  import (
     8  	"internal/testenv"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  )
    16  
    17  type testcase struct {
    18  	lvFlag      string // ==-2, -1, 0, 1, 2
    19  	buildExpect string // message, if any
    20  	expectRC    int
    21  	files       []string
    22  }
    23  
    24  var for_files = []string{
    25  	"for_esc_address.go",             // address of variable
    26  	"for_esc_closure.go",             // closure of variable
    27  	"for_esc_minimal_closure.go",     // simple closure of variable
    28  	"for_esc_method.go",              // method value of variable
    29  	"for_complicated_esc_address.go", // modifies loop index in body
    30  }
    31  
    32  var range_files = []string{
    33  	"range_esc_address.go",         // address of variable
    34  	"range_esc_closure.go",         // closure of variable
    35  	"range_esc_minimal_closure.go", // simple closure of variable
    36  	"range_esc_method.go",          // method value of variable
    37  }
    38  
    39  var cases = []testcase{
    40  	{"-1", "", 11, for_files[:1]},
    41  	{"0", "", 0, for_files[:1]},
    42  	{"1", "", 0, for_files[:1]},
    43  	{"2", "loop variable i now per-iteration,", 0, for_files},
    44  
    45  	{"-1", "", 11, range_files[:1]},
    46  	{"0", "", 0, range_files[:1]},
    47  	{"1", "", 0, range_files[:1]},
    48  	{"2", "loop variable i now per-iteration,", 0, range_files},
    49  
    50  	{"1", "", 0, []string{"for_nested.go"}},
    51  }
    52  
    53  // TestLoopVar checks that the GOEXPERIMENT and debug flags behave as expected.
    54  func TestLoopVarGo1_21(t *testing.T) {
    55  	switch runtime.GOOS {
    56  	case "linux", "darwin":
    57  	default:
    58  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
    59  	}
    60  	switch runtime.GOARCH {
    61  	case "amd64", "arm64":
    62  	default:
    63  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
    64  	}
    65  
    66  	testenv.MustHaveGoBuild(t)
    67  	gocmd := testenv.GoToolPath(t)
    68  	tmpdir := t.TempDir()
    69  	output := filepath.Join(tmpdir, "foo.exe")
    70  
    71  	for i, tc := range cases {
    72  		for _, f := range tc.files {
    73  			source := f
    74  			cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-lang=go1.21 -d=loopvar="+tc.lvFlag, source)
    75  			cmd.Env = append(cmd.Env, "GOEXPERIMENT=loopvar", "HOME="+tmpdir)
    76  			cmd.Dir = "testdata"
    77  			t.Logf("File %s loopvar=%s expect '%s' exit code %d", f, tc.lvFlag, tc.buildExpect, tc.expectRC)
    78  			b, e := cmd.CombinedOutput()
    79  			if e != nil {
    80  				t.Error(e)
    81  			}
    82  			if tc.buildExpect != "" {
    83  				s := string(b)
    84  				if !strings.Contains(s, tc.buildExpect) {
    85  					t.Errorf("File %s test %d expected to match '%s' with \n-----\n%s\n-----", f, i, tc.buildExpect, s)
    86  				}
    87  			}
    88  			// run what we just built.
    89  			cmd = testenv.Command(t, output)
    90  			b, e = cmd.CombinedOutput()
    91  			if tc.expectRC != 0 {
    92  				if e == nil {
    93  					t.Errorf("Missing expected error, file %s, case %d", f, i)
    94  				} else if ee, ok := (e).(*exec.ExitError); !ok || ee.ExitCode() != tc.expectRC {
    95  					t.Error(e)
    96  				} else {
    97  					// okay
    98  				}
    99  			} else if e != nil {
   100  				t.Error(e)
   101  			}
   102  		}
   103  	}
   104  }
   105  
   106  func TestLoopVarInlinesGo1_21(t *testing.T) {
   107  	switch runtime.GOOS {
   108  	case "linux", "darwin":
   109  	default:
   110  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   111  	}
   112  	switch runtime.GOARCH {
   113  	case "amd64", "arm64":
   114  	default:
   115  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   116  	}
   117  
   118  	testenv.MustHaveGoBuild(t)
   119  	gocmd := testenv.GoToolPath(t)
   120  	tmpdir := t.TempDir()
   121  
   122  	root := "cmd/compile/internal/loopvar/testdata/inlines"
   123  
   124  	f := func(pkg string) string {
   125  		// This disables the loopvar change, except for the specified package.
   126  		// The effect should follow the package, even though everything (except "c")
   127  		// is inlined.
   128  		cmd := testenv.Command(t, gocmd, "run", "-gcflags="+root+"/...=-lang=go1.21", "-gcflags="+pkg+"=-d=loopvar=1", root)
   129  		cmd.Env = append(cmd.Env, "GOEXPERIMENT=noloopvar", "HOME="+tmpdir)
   130  		cmd.Dir = filepath.Join("testdata", "inlines")
   131  
   132  		b, e := cmd.CombinedOutput()
   133  		if e != nil {
   134  			t.Error(e)
   135  		}
   136  		return string(b)
   137  	}
   138  
   139  	a := f(root + "/a")
   140  	b := f(root + "/b")
   141  	c := f(root + "/c")
   142  	m := f(root)
   143  
   144  	t.Log(a)
   145  	t.Log(b)
   146  	t.Log(c)
   147  	t.Log(m)
   148  
   149  	if !strings.Contains(a, "f, af, bf, abf, cf sums = 100, 45, 100, 100, 100") {
   150  		t.Errorf("Did not see expected value of a")
   151  	}
   152  	if !strings.Contains(b, "f, af, bf, abf, cf sums = 100, 100, 45, 45, 100") {
   153  		t.Errorf("Did not see expected value of b")
   154  	}
   155  	if !strings.Contains(c, "f, af, bf, abf, cf sums = 100, 100, 100, 100, 45") {
   156  		t.Errorf("Did not see expected value of c")
   157  	}
   158  	if !strings.Contains(m, "f, af, bf, abf, cf sums = 45, 100, 100, 100, 100") {
   159  		t.Errorf("Did not see expected value of m")
   160  	}
   161  }
   162  
   163  func countMatches(s, re string) int {
   164  	slice := regexp.MustCompile(re).FindAllString(s, -1)
   165  	return len(slice)
   166  }
   167  
   168  func TestLoopVarHashes(t *testing.T) {
   169  	// This behavior does not depend on Go version (1.21 or greater)
   170  	switch runtime.GOOS {
   171  	case "linux", "darwin":
   172  	default:
   173  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   174  	}
   175  	switch runtime.GOARCH {
   176  	case "amd64", "arm64":
   177  	default:
   178  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   179  	}
   180  
   181  	testenv.MustHaveGoBuild(t)
   182  	gocmd := testenv.GoToolPath(t)
   183  	tmpdir := t.TempDir()
   184  
   185  	root := "cmd/compile/internal/loopvar/testdata/inlines"
   186  
   187  	f := func(hash string) string {
   188  		// This disables the loopvar change, except for the specified hash pattern.
   189  		// -trimpath is necessary so we get the same answer no matter where the
   190  		// Go repository is checked out. This is not normally a concern since people
   191  		// do not normally rely on the meaning of specific hashes.
   192  		cmd := testenv.Command(t, gocmd, "run", "-trimpath", root)
   193  		cmd.Env = append(cmd.Env, "GOCOMPILEDEBUG=loopvarhash="+hash, "HOME="+tmpdir)
   194  		cmd.Dir = filepath.Join("testdata", "inlines")
   195  
   196  		b, _ := cmd.CombinedOutput()
   197  		// Ignore the error, sometimes it's supposed to fail, the output test will catch it.
   198  		return string(b)
   199  	}
   200  
   201  	for _, arg := range []string{"v001100110110110010100100", "vx336ca4"} {
   202  		m := f(arg)
   203  		t.Log(m)
   204  
   205  		mCount := countMatches(m, "loopvarhash triggered cmd/compile/internal/loopvar/testdata/inlines/main.go:27:6: .* 001100110110110010100100")
   206  		otherCount := strings.Count(m, "loopvarhash")
   207  		if mCount < 1 {
   208  			t.Errorf("%s: did not see triggered main.go:27:6", arg)
   209  		}
   210  		if mCount != otherCount {
   211  			t.Errorf("%s: too many matches", arg)
   212  		}
   213  		mCount = countMatches(m, "cmd/compile/internal/loopvar/testdata/inlines/main.go:27:6: .* \\[bisect-match 0x7802e115b9336ca4\\]")
   214  		otherCount = strings.Count(m, "[bisect-match ")
   215  		if mCount < 1 {
   216  			t.Errorf("%s: did not see bisect-match for main.go:27:6", arg)
   217  		}
   218  		if mCount != otherCount {
   219  			t.Errorf("%s: too many matches", arg)
   220  		}
   221  
   222  		// This next test carefully dodges a bug-to-be-fixed with inlined locations for ir.Names.
   223  		if !strings.Contains(m, ", 100, 100, 100, 100") {
   224  			t.Errorf("%s: did not see expected value of m run", arg)
   225  		}
   226  	}
   227  }
   228  
   229  // TestLoopVarVersionEnableFlag checks for loopvar transformation enabled by command line flag (1.22).
   230  func TestLoopVarVersionEnableFlag(t *testing.T) {
   231  	switch runtime.GOOS {
   232  	case "linux", "darwin":
   233  	default:
   234  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   235  	}
   236  	switch runtime.GOARCH {
   237  	case "amd64", "arm64":
   238  	default:
   239  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   240  	}
   241  
   242  	testenv.MustHaveGoBuild(t)
   243  	gocmd := testenv.GoToolPath(t)
   244  
   245  	// loopvar=3 logs info but does not change loopvarness
   246  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt.go")
   247  	cmd.Dir = filepath.Join("testdata")
   248  
   249  	b, err := cmd.CombinedOutput()
   250  	m := string(b)
   251  
   252  	t.Log(m)
   253  
   254  	yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
   255  	nCount := strings.Count(m, "shared")
   256  
   257  	if yCount != 1 {
   258  		t.Errorf("yCount=%d != 1", yCount)
   259  	}
   260  	if nCount > 0 {
   261  		t.Errorf("nCount=%d > 0", nCount)
   262  	}
   263  	if err != nil {
   264  		t.Errorf("err=%v != nil", err)
   265  	}
   266  }
   267  
   268  // TestLoopVarVersionEnableGoBuild checks for loopvar transformation enabled by go:build version (1.22).
   269  func TestLoopVarVersionEnableGoBuild(t *testing.T) {
   270  	switch runtime.GOOS {
   271  	case "linux", "darwin":
   272  	default:
   273  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   274  	}
   275  	switch runtime.GOARCH {
   276  	case "amd64", "arm64":
   277  	default:
   278  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   279  	}
   280  
   281  	testenv.MustHaveGoBuild(t)
   282  	gocmd := testenv.GoToolPath(t)
   283  
   284  	// loopvar=3 logs info but does not change loopvarness
   285  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt-122.go")
   286  	cmd.Dir = filepath.Join("testdata")
   287  
   288  	b, err := cmd.CombinedOutput()
   289  	m := string(b)
   290  
   291  	t.Log(m)
   292  
   293  	yCount := strings.Count(m, "opt-122.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-122.go:31)")
   294  	nCount := strings.Count(m, "shared")
   295  
   296  	if yCount != 1 {
   297  		t.Errorf("yCount=%d != 1", yCount)
   298  	}
   299  	if nCount > 0 {
   300  		t.Errorf("nCount=%d > 0", nCount)
   301  	}
   302  	if err != nil {
   303  		t.Errorf("err=%v != nil", err)
   304  	}
   305  }
   306  
   307  // TestLoopVarVersionDisableFlag checks for loopvar transformation DISABLED by command line version (1.21).
   308  func TestLoopVarVersionDisableFlag(t *testing.T) {
   309  	switch runtime.GOOS {
   310  	case "linux", "darwin":
   311  	default:
   312  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   313  	}
   314  	switch runtime.GOARCH {
   315  	case "amd64", "arm64":
   316  	default:
   317  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   318  	}
   319  
   320  	testenv.MustHaveGoBuild(t)
   321  	gocmd := testenv.GoToolPath(t)
   322  
   323  	// loopvar=3 logs info but does not change loopvarness
   324  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt.go")
   325  	cmd.Dir = filepath.Join("testdata")
   326  
   327  	b, err := cmd.CombinedOutput()
   328  	m := string(b)
   329  
   330  	t.Log(m) // expect error
   331  
   332  	yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
   333  	nCount := strings.Count(m, "shared")
   334  
   335  	if yCount != 0 {
   336  		t.Errorf("yCount=%d != 0", yCount)
   337  	}
   338  	if nCount > 0 {
   339  		t.Errorf("nCount=%d > 0", nCount)
   340  	}
   341  	if err == nil { // expect error
   342  		t.Errorf("err=%v == nil", err)
   343  	}
   344  }
   345  
   346  // TestLoopVarVersionDisableGoBuild checks for loopvar transformation DISABLED by go:build version (1.21).
   347  func TestLoopVarVersionDisableGoBuild(t *testing.T) {
   348  	switch runtime.GOOS {
   349  	case "linux", "darwin":
   350  	default:
   351  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   352  	}
   353  	switch runtime.GOARCH {
   354  	case "amd64", "arm64":
   355  	default:
   356  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   357  	}
   358  
   359  	testenv.MustHaveGoBuild(t)
   360  	gocmd := testenv.GoToolPath(t)
   361  
   362  	// loopvar=3 logs info but does not change loopvarness
   363  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt-121.go")
   364  	cmd.Dir = filepath.Join("testdata")
   365  
   366  	b, err := cmd.CombinedOutput()
   367  	m := string(b)
   368  
   369  	t.Log(m) // expect error
   370  
   371  	yCount := strings.Count(m, "opt-121.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-121.go:31)")
   372  	nCount := strings.Count(m, "shared")
   373  
   374  	if yCount != 0 {
   375  		t.Errorf("yCount=%d != 0", yCount)
   376  	}
   377  	if nCount > 0 {
   378  		t.Errorf("nCount=%d > 0", nCount)
   379  	}
   380  	if err == nil { // expect error
   381  		t.Errorf("err=%v == nil", err)
   382  	}
   383  }
   384  

View as plain text