Repository: google/pprof (go)

Issue:
panics if profile is not found
Hello! I opened https://github.com/golang/go/issues/67626 for the Go project to track consuming a fix; my guess is there should be an issue on this project to track creation of a fix.

### What version of pprof are you using?

I've checked out 9665fa269a300a7e802154f4df8c4e9ee970fc5c from May 20, currently the latest version of the `main` branch.

### What operating system and processor architecture are you using?

linux/amd64

### What did you do?

```
$ go run . ./missing
```

### What did you expect to see?

```
$ go run . ./missing
./missing: open ./missing: no such file or directory
pprof: failed to fetch any source profiles
exit status 2
```

### What did you see instead?

```
$ go run . ./missing
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8906f7]

goroutine 19 [running]:
github.com/google/pprof/internal/driver.fetch({0x7fffec08a721, 0x9}, 0xffffffffc4653600, 0xffffffffc4653600, {0x7fc040045278, 0xc000080210}, {0xa68860, 0xc0000aa840})
	/home/rhysh/pprof/internal/driver/fetch.go:514 +0x2f7
github.com/google/pprof/internal/driver.grabProfile(0xc0001e28c0, {0x7fffec08a721?, 0x0?}, {0x0?, 0x0?}, {0xa69c80, 0xc000032fd0}, {0x7fc040045278, 0xc000080210}, {0xa68860, ...})
	/home/rhysh/pprof/internal/driver/fetch.go:334 +0x131
github.com/google/pprof/internal/driver.concurrentGrab.func1(0xc000234000)
	/home/rhysh/pprof/internal/driver/fetch.go:211 +0x86
created by github.com/google/pprof/internal/driver.concurrentGrab in goroutine 34
	/home/rhysh/pprof/internal/driver/fetch.go:209 +0x8f
exit status 2
```

Bisect says " a5a03c77bf0842e0592d2be8129bca0b2bd3bb60 is the first bad commit".

CC @cuishuang, @aalexand


Pull request description:
Fix panic on missing file.
Fixes #863.

Patch:
diff --git a/internal/driver/fetch.go b/internal/driver/fetch.go
index 95204a3..a94ddf6 100644
--- a/internal/driver/fetch.go
+++ b/internal/driver/fetch.go
@@ -493,7 +493,7 @@ func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http
 	var f io.ReadCloser
 
 	// First determine whether the source is a file, if not, it will be treated as a URL.
-	if _, openErr := os.Stat(source); openErr == nil {
+	if _, err = os.Stat(source); err == nil {
 		if isPerfFile(source) {
 			f, err = convertPerfData(source, ui)
 		} else {


Test patch:
diff --git a/internal/driver/fetch_test.go b/internal/driver/fetch_test.go
index c3d04b6..209fd40 100644
--- a/internal/driver/fetch_test.go
+++ b/internal/driver/fetch_test.go
@@ -191,11 +191,13 @@ func TestFetch(t *testing.T) {
 	const path = "testdata/"
 	type testcase struct {
 		source, execName string
+		wantErr          bool
 	}
 	ts := []testcase{
-		{path + "go.crc32.cpu", ""},
-		{path + "go.nomappings.crash", "/bin/gotest.exe"},
-		{"http://localhost/profile?file=cppbench.cpu", ""},
+		{path + "go.crc32.cpu", "", false},
+		{path + "go.nomappings.crash", "/bin/gotest.exe", false},
+		{"http://localhost/profile?file=cppbench.cpu", "", false},
+		{"./missing", "", true},
 	}
 	// Test that paths with a colon character are recognized as file paths
 	// if the file exists, rather than as a URL. We have to skip this test
@@ -211,24 +213,32 @@ func TestFetch(t *testing.T) {
 		if err != nil {
 			t.Fatalf("create dst file %s failed: %#v", dst, err)
 		}
-		ts = append(ts, testcase{dst, ""})
+		ts = append(ts, testcase{dst, "", false})
 	}
 	for _, tc := range ts {
-		p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, nil, testObj{}, &proftest.TestUI{T: t}, &httpTransport{})
-		if err != nil {
-			t.Fatalf("%s: %s", tc.source, err)
-		}
-		if len(p.Sample) == 0 {
-			t.Errorf("%s: want non-zero samples", tc.source)
-		}
-		if e := tc.execName; e != "" {
-			switch {
-			case len(p.Mapping) == 0 || p.Mapping[0] == nil:
-				t.Errorf("%s: want mapping[0].execName == %s, got no mappings", tc.source, e)
-			case p.Mapping[0].File != e:
-				t.Errorf("%s: want mapping[0].execName == %s, got %s", tc.source, e, p.Mapping[0].File)
+		t.Run(tc.source, func(t *testing.T) {
+			p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, nil, testObj{}, &proftest.TestUI{T: t}, &httpTransport{})
+			if tc.wantErr {
+				if err == nil {
+					t.Fatal("got no error, want an error")
+				}
+				return
 			}
-		}
+			if err != nil {
+				t.Fatalf("got error %v, want no error", err)
+			}
+			if len(p.Sample) == 0 {
+				t.Error("got zero samples, want non-zero")
+			}
+			if e := tc.execName; e != "" {
+				switch {
+				case len(p.Mapping) == 0 || p.Mapping[0] == nil:
+					t.Errorf("got no mappings, want mapping[0].execName == %s", e)
+				case p.Mapping[0].File != e:
+					t.Errorf("got mapping[0].execName == %s, want %s", p.Mapping[0].File, e)
+				}
+			}
+		})
 	}
 }