1// Copyright 2019 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
5package logopt
6
7import (
8	"internal/testenv"
9	"os"
10	"path/filepath"
11	"runtime"
12	"strings"
13	"testing"
14)
15
16const srcCode = `package x
17type pair struct {a,b int}
18func bar(y *pair) *int {
19	return &y.b
20}
21var a []int
22func foo(w, z *pair) *int {
23	if *bar(w) > 0 {
24		return bar(z)
25	}
26	if a[1] > 0 {
27		a = a[:2]
28	}
29	return &a[0]
30}
31
32// address taking prevents closure inlining
33func n() int {
34	foo := func() int { return 1 }
35	bar := &foo
36	x := (*bar)() + foo()
37	return x
38}
39`
40
41func want(t *testing.T, out string, desired string) {
42	// On Windows, Unicode escapes in the JSON output end up "normalized" elsewhere to /u....,
43	// so "normalize" what we're looking for to match that.
44	s := strings.ReplaceAll(desired, string(os.PathSeparator), "/")
45	if !strings.Contains(out, s) {
46		t.Errorf("did not see phrase %s in \n%s", s, out)
47	}
48}
49
50func wantN(t *testing.T, out string, desired string, n int) {
51	if strings.Count(out, desired) != n {
52		t.Errorf("expected exactly %d occurrences of %s in \n%s", n, desired, out)
53	}
54}
55
56func TestPathStuff(t *testing.T) {
57	sep := string(filepath.Separator)
58	if path, whine := parseLogPath("file:///c:foo"); path != "c:foo" || whine != "" { // good path
59		t.Errorf("path='%s', whine='%s'", path, whine)
60	}
61	if path, whine := parseLogPath("file:///foo"); path != sep+"foo" || whine != "" { // good path
62		t.Errorf("path='%s', whine='%s'", path, whine)
63	}
64	if path, whine := parseLogPath("foo"); path != "" || whine == "" { // BAD path
65		t.Errorf("path='%s', whine='%s'", path, whine)
66	}
67	if sep == "\\" { // On WINDOWS ONLY
68		if path, whine := parseLogPath("C:/foo"); path != "C:\\foo" || whine != "" { // good path
69			t.Errorf("path='%s', whine='%s'", path, whine)
70		}
71		if path, whine := parseLogPath("c:foo"); path != "" || whine == "" { // BAD path
72			t.Errorf("path='%s', whine='%s'", path, whine)
73		}
74		if path, whine := parseLogPath("/foo"); path != "" || whine == "" { // BAD path
75			t.Errorf("path='%s', whine='%s'", path, whine)
76		}
77	} else { // ON UNIX ONLY
78		if path, whine := parseLogPath("/foo"); path != sep+"foo" || whine != "" { // good path
79			t.Errorf("path='%s', whine='%s'", path, whine)
80		}
81	}
82}
83
84func TestLogOpt(t *testing.T) {
85	t.Parallel()
86
87	testenv.MustHaveGoBuild(t)
88
89	dir := fixSlash(t.TempDir()) // Normalize the directory name as much as possible, for Windows testing
90	src := filepath.Join(dir, "file.go")
91	if err := os.WriteFile(src, []byte(srcCode), 0644); err != nil {
92		t.Fatal(err)
93	}
94
95	outfile := filepath.Join(dir, "file.o")
96
97	t.Run("JSON_fails", func(t *testing.T) {
98		// Test malformed flag
99		out, err := testLogOpt(t, "-json=foo", src, outfile)
100		if err == nil {
101			t.Error("-json=foo succeeded unexpectedly")
102		}
103		want(t, out, "option should be")
104		want(t, out, "number")
105
106		// Test a version number that is currently unsupported (and should remain unsupported for a while)
107		out, err = testLogOpt(t, "-json=9,foo", src, outfile)
108		if err == nil {
109			t.Error("-json=0,foo succeeded unexpectedly")
110		}
111		want(t, out, "version must be")
112
113	})
114
115	// replace d (dir)  with t ("tmpdir") and convert path separators to '/'
116	normalize := func(out []byte, d, t string) string {
117		s := string(out)
118		s = strings.ReplaceAll(s, d, t)
119		s = strings.ReplaceAll(s, string(os.PathSeparator), "/")
120		return s
121	}
122
123	// Ensure that <128 byte copies are not reported and that 128-byte copies are.
124	// Check at both 1 and 8-byte alignments.
125	t.Run("Copy", func(t *testing.T) {
126		const copyCode = `package x
127func s128a1(x *[128]int8) [128]int8 {
128	return *x
129}
130func s127a1(x *[127]int8) [127]int8 {
131	return *x
132}
133func s16a8(x *[16]int64) [16]int64 {
134	return *x
135}
136func s15a8(x *[15]int64) [15]int64 {
137	return *x
138}
139`
140		copy := filepath.Join(dir, "copy.go")
141		if err := os.WriteFile(copy, []byte(copyCode), 0644); err != nil {
142			t.Fatal(err)
143		}
144		outcopy := filepath.Join(dir, "copy.o")
145
146		// On not-amd64, test the host architecture and os
147		arches := []string{runtime.GOARCH}
148		goos0 := runtime.GOOS
149		if runtime.GOARCH == "amd64" { // Test many things with "linux" (wasm will get "js")
150			arches = []string{"arm", "arm64", "386", "amd64", "mips", "mips64", "loong64", "ppc64le", "riscv64", "s390x", "wasm"}
151			goos0 = "linux"
152		}
153
154		for _, arch := range arches {
155			t.Run(arch, func(t *testing.T) {
156				goos := goos0
157				if arch == "wasm" {
158					goos = "js"
159				}
160				_, err := testCopy(t, dir, arch, goos, copy, outcopy)
161				if err != nil {
162					t.Error("-json=0,file://log/opt should have succeeded")
163				}
164				logged, err := os.ReadFile(filepath.Join(dir, "log", "opt", "x", "copy.json"))
165				if err != nil {
166					t.Error("-json=0,file://log/opt missing expected log file")
167				}
168				slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
169				t.Logf("%s", slogged)
170				want(t, slogged, `{"range":{"start":{"line":3,"character":2},"end":{"line":3,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
171				want(t, slogged, `{"range":{"start":{"line":9,"character":2},"end":{"line":9,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
172				wantN(t, slogged, `"code":"copy"`, 2)
173			})
174		}
175	})
176
177	// Some architectures don't fault on nil dereference, so nilchecks are eliminated differently.
178	// The N-way copy test also doesn't need to run N-ways N times.
179	if runtime.GOARCH != "amd64" {
180		return
181	}
182
183	t.Run("Success", func(t *testing.T) {
184		// This test is supposed to succeed
185
186		// Note 'file://' is the I-Know-What-I-Am-Doing way of specifying a file, also to deal with corner cases for Windows.
187		_, err := testLogOptDir(t, dir, "-json=0,file://log/opt", src, outfile)
188		if err != nil {
189			t.Error("-json=0,file://log/opt should have succeeded")
190		}
191		logged, err := os.ReadFile(filepath.Join(dir, "log", "opt", "x", "file.json"))
192		if err != nil {
193			t.Error("-json=0,file://log/opt missing expected log file")
194		}
195		// All this delicacy with uriIfy and filepath.Join is to get this test to work right on Windows.
196		slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
197		t.Logf("%s", slogged)
198		// below shows proper nilcheck
199		want(t, slogged, `{"range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}},"severity":3,"code":"nilcheck","source":"go compiler","message":"",`+
200			`"relatedInformation":[{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"}]}`)
201		want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
202		want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
203		// escape analysis explanation
204		want(t, slogged, `{"range":{"start":{"line":7,"character":13},"end":{"line":7,"character":13}},"severity":3,"code":"leak","source":"go compiler","message":"parameter z leaks to ~r0 with derefs=0",`+
205			`"relatedInformation":[`+
206			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:    flow: y = z:"},`+
207			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:      from y := z (assign-pair)"},`+
208			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:    flow: ~r0 = y:"},`+
209			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
210			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:      from y.b (dot of pointer)"},`+
211			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
212			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:      from \u0026y.b (address-of)"},`+
213			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":9},"end":{"line":4,"character":9}}},"message":"inlineLoc"},`+
214			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow:      from ~r0 = \u0026y.b (assign-pair)"},`+
215			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow:    flow: ~r0 = ~r0:"},`+
216			`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow:      from return ~r0 (return)"}]}`)
217	})
218}
219
220func testLogOpt(t *testing.T, flag, src, outfile string) (string, error) {
221	run := []string{testenv.GoToolPath(t), "tool", "compile", "-p=p", flag, "-o", outfile, src}
222	t.Log(run)
223	cmd := testenv.Command(t, run[0], run[1:]...)
224	out, err := cmd.CombinedOutput()
225	t.Logf("%s", out)
226	return string(out), err
227}
228
229func testLogOptDir(t *testing.T, dir, flag, src, outfile string) (string, error) {
230	// Notice the specified import path "x"
231	run := []string{testenv.GoToolPath(t), "tool", "compile", "-p=x", flag, "-o", outfile, src}
232	t.Log(run)
233	cmd := testenv.Command(t, run[0], run[1:]...)
234	cmd.Dir = dir
235	out, err := cmd.CombinedOutput()
236	t.Logf("%s", out)
237	return string(out), err
238}
239
240func testCopy(t *testing.T, dir, goarch, goos, src, outfile string) (string, error) {
241	// Notice the specified import path "x"
242	run := []string{testenv.GoToolPath(t), "tool", "compile", "-p=x", "-json=0,file://log/opt", "-o", outfile, src}
243	t.Log(run)
244	cmd := testenv.Command(t, run[0], run[1:]...)
245	cmd.Dir = dir
246	cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+goos)
247	out, err := cmd.CombinedOutput()
248	t.Logf("%s", out)
249	return string(out), err
250}
251