Source file src/runtime/pprof/proto_test.go

     1  // Copyright 2016 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 pprof
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"internal/abi"
    13  	"internal/profile"
    14  	"internal/testenv"
    15  	"os"
    16  	"os/exec"
    17  	"reflect"
    18  	"runtime"
    19  	"strings"
    20  	"testing"
    21  	"unsafe"
    22  )
    23  
    24  // translateCPUProfile parses binary CPU profiling stack trace data
    25  // generated by runtime.CPUProfile() into a profile struct.
    26  // This is only used for testing. Real conversions stream the
    27  // data into the profileBuilder as it becomes available.
    28  //
    29  // count is the number of records in data.
    30  func translateCPUProfile(data []uint64, count int) (*profile.Profile, error) {
    31  	var buf bytes.Buffer
    32  	b := newProfileBuilder(&buf)
    33  	tags := make([]unsafe.Pointer, count)
    34  	if err := b.addCPUData(data, tags); err != nil {
    35  		return nil, err
    36  	}
    37  	if err := b.build(); err != nil {
    38  		return nil, err
    39  	}
    40  	return profile.Parse(&buf)
    41  }
    42  
    43  // fmtJSON returns a pretty-printed JSON form for x.
    44  // It works reasonably well for printing protocol-buffer
    45  // data structures like profile.Profile.
    46  func fmtJSON(x any) string {
    47  	js, _ := json.MarshalIndent(x, "", "\t")
    48  	return string(js)
    49  }
    50  
    51  func TestConvertCPUProfileNoSamples(t *testing.T) {
    52  	// A test server with mock cpu profile data.
    53  	var buf bytes.Buffer
    54  
    55  	b := []uint64{3, 0, 500} // empty profile at 500 Hz (2ms sample period)
    56  	p, err := translateCPUProfile(b, 1)
    57  	if err != nil {
    58  		t.Fatalf("translateCPUProfile: %v", err)
    59  	}
    60  	if err := p.Write(&buf); err != nil {
    61  		t.Fatalf("writing profile: %v", err)
    62  	}
    63  
    64  	p, err = profile.Parse(&buf)
    65  	if err != nil {
    66  		t.Fatalf("profile.Parse: %v", err)
    67  	}
    68  
    69  	// Expected PeriodType and SampleType.
    70  	periodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
    71  	sampleType := []*profile.ValueType{
    72  		{Type: "samples", Unit: "count"},
    73  		{Type: "cpu", Unit: "nanoseconds"},
    74  	}
    75  
    76  	checkProfile(t, p, 2000*1000, periodType, sampleType, nil, "")
    77  }
    78  
    79  //go:noinline
    80  func f1() { f1() }
    81  
    82  //go:noinline
    83  func f2() { f2() }
    84  
    85  // testPCs returns two PCs and two corresponding memory mappings
    86  // to use in test profiles.
    87  func testPCs(t *testing.T) (addr1, addr2 uint64, map1, map2 *profile.Mapping) {
    88  	switch runtime.GOOS {
    89  	case "linux", "android", "netbsd":
    90  		// Figure out two addresses from /proc/self/maps.
    91  		mmap, err := os.ReadFile("/proc/self/maps")
    92  		if err != nil {
    93  			t.Fatal(err)
    94  		}
    95  		var mappings []*profile.Mapping
    96  		id := uint64(1)
    97  		parseProcSelfMaps(mmap, func(lo, hi, offset uint64, file, buildID string) {
    98  			mappings = append(mappings, &profile.Mapping{
    99  				ID:      id,
   100  				Start:   lo,
   101  				Limit:   hi,
   102  				Offset:  offset,
   103  				File:    file,
   104  				BuildID: buildID,
   105  			})
   106  			id++
   107  		})
   108  		if len(mappings) < 2 {
   109  			// It is possible for a binary to only have 1 executable
   110  			// region of memory.
   111  			t.Skipf("need 2 or more mappings, got %v", len(mappings))
   112  		}
   113  		addr1 = mappings[0].Start
   114  		map1 = mappings[0]
   115  		addr2 = mappings[1].Start
   116  		map2 = mappings[1]
   117  	case "windows", "darwin", "ios":
   118  		addr1 = uint64(abi.FuncPCABIInternal(f1))
   119  		addr2 = uint64(abi.FuncPCABIInternal(f2))
   120  
   121  		start, end, exe, buildID, err := readMainModuleMapping()
   122  		if err != nil {
   123  			t.Fatal(err)
   124  		}
   125  
   126  		map1 = &profile.Mapping{
   127  			ID:           1,
   128  			Start:        start,
   129  			Limit:        end,
   130  			File:         exe,
   131  			BuildID:      buildID,
   132  			HasFunctions: true,
   133  		}
   134  		map2 = &profile.Mapping{
   135  			ID:           1,
   136  			Start:        start,
   137  			Limit:        end,
   138  			File:         exe,
   139  			BuildID:      buildID,
   140  			HasFunctions: true,
   141  		}
   142  	case "js", "wasip1":
   143  		addr1 = uint64(abi.FuncPCABIInternal(f1))
   144  		addr2 = uint64(abi.FuncPCABIInternal(f2))
   145  	default:
   146  		addr1 = uint64(abi.FuncPCABIInternal(f1))
   147  		addr2 = uint64(abi.FuncPCABIInternal(f2))
   148  		// Fake mapping - HasFunctions will be true because two PCs from Go
   149  		// will be fully symbolized.
   150  		fake := &profile.Mapping{ID: 1, HasFunctions: true}
   151  		map1, map2 = fake, fake
   152  	}
   153  	return
   154  }
   155  
   156  func TestConvertCPUProfile(t *testing.T) {
   157  	addr1, addr2, map1, map2 := testPCs(t)
   158  
   159  	b := []uint64{
   160  		3, 0, 500, // hz = 500
   161  		5, 0, 10, uint64(addr1 + 1), uint64(addr1 + 2), // 10 samples in addr1
   162  		5, 0, 40, uint64(addr2 + 1), uint64(addr2 + 2), // 40 samples in addr2
   163  		5, 0, 10, uint64(addr1 + 1), uint64(addr1 + 2), // 10 samples in addr1
   164  	}
   165  	p, err := translateCPUProfile(b, 4)
   166  	if err != nil {
   167  		t.Fatalf("translating profile: %v", err)
   168  	}
   169  	period := int64(2000 * 1000)
   170  	periodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
   171  	sampleType := []*profile.ValueType{
   172  		{Type: "samples", Unit: "count"},
   173  		{Type: "cpu", Unit: "nanoseconds"},
   174  	}
   175  	samples := []*profile.Sample{
   176  		{Value: []int64{20, 20 * 2000 * 1000}, Location: []*profile.Location{
   177  			{ID: 1, Mapping: map1, Address: addr1},
   178  			{ID: 2, Mapping: map1, Address: addr1 + 1},
   179  		}},
   180  		{Value: []int64{40, 40 * 2000 * 1000}, Location: []*profile.Location{
   181  			{ID: 3, Mapping: map2, Address: addr2},
   182  			{ID: 4, Mapping: map2, Address: addr2 + 1},
   183  		}},
   184  	}
   185  	checkProfile(t, p, period, periodType, sampleType, samples, "")
   186  }
   187  
   188  func checkProfile(t *testing.T, p *profile.Profile, period int64, periodType *profile.ValueType, sampleType []*profile.ValueType, samples []*profile.Sample, defaultSampleType string) {
   189  	t.Helper()
   190  
   191  	if p.Period != period {
   192  		t.Errorf("p.Period = %d, want %d", p.Period, period)
   193  	}
   194  	if !reflect.DeepEqual(p.PeriodType, periodType) {
   195  		t.Errorf("p.PeriodType = %v\nwant = %v", fmtJSON(p.PeriodType), fmtJSON(periodType))
   196  	}
   197  	if !reflect.DeepEqual(p.SampleType, sampleType) {
   198  		t.Errorf("p.SampleType = %v\nwant = %v", fmtJSON(p.SampleType), fmtJSON(sampleType))
   199  	}
   200  	if defaultSampleType != p.DefaultSampleType {
   201  		t.Errorf("p.DefaultSampleType = %v\nwant = %v", p.DefaultSampleType, defaultSampleType)
   202  	}
   203  	// Clear line info since it is not in the expected samples.
   204  	// If we used f1 and f2 above, then the samples will have line info.
   205  	for _, s := range p.Sample {
   206  		for _, l := range s.Location {
   207  			l.Line = nil
   208  		}
   209  	}
   210  	if fmtJSON(p.Sample) != fmtJSON(samples) { // ignore unexported fields
   211  		if len(p.Sample) == len(samples) {
   212  			for i := range p.Sample {
   213  				if !reflect.DeepEqual(p.Sample[i], samples[i]) {
   214  					t.Errorf("sample %d = %v\nwant = %v\n", i, fmtJSON(p.Sample[i]), fmtJSON(samples[i]))
   215  				}
   216  			}
   217  			if t.Failed() {
   218  				t.FailNow()
   219  			}
   220  		}
   221  		t.Fatalf("p.Sample = %v\nwant = %v", fmtJSON(p.Sample), fmtJSON(samples))
   222  	}
   223  }
   224  
   225  var profSelfMapsTests = `
   226  00400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat
   227  0060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat
   228  0060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat
   229  014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
   230  7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
   231  7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   232  7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   233  7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   234  7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   235  7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
   236  7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   237  7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
   238  7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
   239  7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   240  7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   241  7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
   242  7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
   243  7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
   244  ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
   245  ->
   246  00400000 0040b000 00000000 /bin/cat
   247  7f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
   248  7f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
   249  7ffc34343000 7ffc34345000 00000000 [vdso]
   250  ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
   251  
   252  00400000-07000000 r-xp 00000000 00:00 0
   253  07000000-07093000 r-xp 06c00000 00:2e 536754                             /path/to/gobench_server_main
   254  07093000-0722d000 rw-p 06c92000 00:2e 536754                             /path/to/gobench_server_main
   255  0722d000-07b21000 rw-p 00000000 00:00 0
   256  c000000000-c000036000 rw-p 00000000 00:00 0
   257  ->
   258  07000000 07093000 06c00000 /path/to/gobench_server_main
   259  `
   260  
   261  var profSelfMapsTestsWithDeleted = `
   262  00400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat (deleted)
   263  0060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat (deleted)
   264  0060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat (deleted)
   265  014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
   266  7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
   267  7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   268  7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   269  7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   270  7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   271  7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
   272  7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   273  7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
   274  7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
   275  7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   276  7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   277  7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
   278  7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
   279  7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
   280  ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
   281  ->
   282  00400000 0040b000 00000000 /bin/cat
   283  7f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
   284  7f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
   285  7ffc34343000 7ffc34345000 00000000 [vdso]
   286  ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
   287  
   288  00400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat with space
   289  0060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat with space
   290  0060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat with space
   291  014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
   292  7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
   293  7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   294  7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   295  7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   296  7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   297  7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
   298  7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   299  7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
   300  7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
   301  7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   302  7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   303  7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
   304  7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
   305  7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
   306  ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
   307  ->
   308  00400000 0040b000 00000000 /bin/cat with space
   309  7f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
   310  7f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
   311  7ffc34343000 7ffc34345000 00000000 [vdso]
   312  ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
   313  `
   314  
   315  func TestProcSelfMaps(t *testing.T) {
   316  
   317  	f := func(t *testing.T, input string) {
   318  		for tx, tt := range strings.Split(input, "\n\n") {
   319  			in, out, ok := strings.Cut(tt, "->\n")
   320  			if !ok {
   321  				t.Fatal("malformed test case")
   322  			}
   323  			if len(out) > 0 && out[len(out)-1] != '\n' {
   324  				out += "\n"
   325  			}
   326  			var buf strings.Builder
   327  			parseProcSelfMaps([]byte(in), func(lo, hi, offset uint64, file, buildID string) {
   328  				fmt.Fprintf(&buf, "%08x %08x %08x %s\n", lo, hi, offset, file)
   329  			})
   330  			if buf.String() != out {
   331  				t.Errorf("#%d: have:\n%s\nwant:\n%s\n%q\n%q", tx, buf.String(), out, buf.String(), out)
   332  			}
   333  		}
   334  	}
   335  
   336  	t.Run("Normal", func(t *testing.T) {
   337  		f(t, profSelfMapsTests)
   338  	})
   339  
   340  	t.Run("WithDeletedFile", func(t *testing.T) {
   341  		f(t, profSelfMapsTestsWithDeleted)
   342  	})
   343  }
   344  
   345  // TestMapping checks the mapping section of CPU profiles
   346  // has the HasFunctions field set correctly. If all PCs included
   347  // in the samples are successfully symbolized, the corresponding
   348  // mapping entry (in this test case, only one entry) should have
   349  // its HasFunctions field set true.
   350  // The test generates a CPU profile that includes PCs from C side
   351  // that the runtime can't symbolize. See ./testdata/mappingtest.
   352  func TestMapping(t *testing.T) {
   353  	testenv.MustHaveGoRun(t)
   354  	testenv.MustHaveCGO(t)
   355  
   356  	prog := "./testdata/mappingtest/main.go"
   357  
   358  	// GoOnly includes only Go symbols that runtime will symbolize.
   359  	// Go+C includes C symbols that runtime will not symbolize.
   360  	for _, traceback := range []string{"GoOnly", "Go+C"} {
   361  		t.Run("traceback"+traceback, func(t *testing.T) {
   362  			cmd := exec.Command(testenv.GoToolPath(t), "run", prog)
   363  			if traceback != "GoOnly" {
   364  				cmd.Env = append(os.Environ(), "SETCGOTRACEBACK=1")
   365  			}
   366  			cmd.Stderr = new(bytes.Buffer)
   367  
   368  			out, err := cmd.Output()
   369  			if err != nil {
   370  				t.Fatalf("failed to run the test program %q: %v\n%v", prog, err, cmd.Stderr)
   371  			}
   372  
   373  			prof, err := profile.Parse(bytes.NewReader(out))
   374  			if err != nil {
   375  				t.Fatalf("failed to parse the generated profile data: %v", err)
   376  			}
   377  			t.Logf("Profile: %s", prof)
   378  
   379  			hit := make(map[*profile.Mapping]bool)
   380  			miss := make(map[*profile.Mapping]bool)
   381  			for _, loc := range prof.Location {
   382  				if symbolized(loc) {
   383  					hit[loc.Mapping] = true
   384  				} else {
   385  					miss[loc.Mapping] = true
   386  				}
   387  			}
   388  			if len(miss) == 0 {
   389  				t.Log("no location with missing symbol info was sampled")
   390  			}
   391  
   392  			for _, m := range prof.Mapping {
   393  				if miss[m] && m.HasFunctions {
   394  					t.Errorf("mapping %+v has HasFunctions=true, but contains locations with failed symbolization", m)
   395  					continue
   396  				}
   397  				if !miss[m] && hit[m] && !m.HasFunctions {
   398  					t.Errorf("mapping %+v has HasFunctions=false, but all referenced locations from this lapping were symbolized successfully", m)
   399  					continue
   400  				}
   401  			}
   402  
   403  			if traceback == "Go+C" {
   404  				// The test code was arranged to have PCs from C and
   405  				// they are not symbolized.
   406  				// Check no Location containing those unsymbolized PCs contains multiple lines.
   407  				for i, loc := range prof.Location {
   408  					if !symbolized(loc) && len(loc.Line) > 1 {
   409  						t.Errorf("Location[%d] contains unsymbolized PCs and multiple lines: %v", i, loc)
   410  					}
   411  				}
   412  			}
   413  		})
   414  	}
   415  }
   416  
   417  func symbolized(loc *profile.Location) bool {
   418  	if len(loc.Line) == 0 {
   419  		return false
   420  	}
   421  	l := loc.Line[0]
   422  	f := l.Function
   423  	if l.Line == 0 || f == nil || f.Name == "" || f.Filename == "" {
   424  		return false
   425  	}
   426  	return true
   427  }
   428  
   429  // TestFakeMapping tests if at least one mapping exists
   430  // (including a fake mapping), and their HasFunctions bits
   431  // are set correctly.
   432  func TestFakeMapping(t *testing.T) {
   433  	var buf bytes.Buffer
   434  	if err := Lookup("heap").WriteTo(&buf, 0); err != nil {
   435  		t.Fatalf("failed to write heap profile: %v", err)
   436  	}
   437  	prof, err := profile.Parse(&buf)
   438  	if err != nil {
   439  		t.Fatalf("failed to parse the generated profile data: %v", err)
   440  	}
   441  	t.Logf("Profile: %s", prof)
   442  	if len(prof.Mapping) == 0 {
   443  		t.Fatal("want profile with at least one mapping entry, got 0 mapping")
   444  	}
   445  
   446  	hit := make(map[*profile.Mapping]bool)
   447  	miss := make(map[*profile.Mapping]bool)
   448  	for _, loc := range prof.Location {
   449  		if symbolized(loc) {
   450  			hit[loc.Mapping] = true
   451  		} else {
   452  			miss[loc.Mapping] = true
   453  		}
   454  	}
   455  	for _, m := range prof.Mapping {
   456  		if miss[m] && m.HasFunctions {
   457  			t.Errorf("mapping %+v has HasFunctions=true, but contains locations with failed symbolization", m)
   458  			continue
   459  		}
   460  		if !miss[m] && hit[m] && !m.HasFunctions {
   461  			t.Errorf("mapping %+v has HasFunctions=false, but all referenced locations from this lapping were symbolized successfully", m)
   462  			continue
   463  		}
   464  	}
   465  }
   466  
   467  // Make sure the profiler can handle an empty stack trace.
   468  // See issue 37967.
   469  func TestEmptyStack(t *testing.T) {
   470  	b := []uint64{
   471  		3, 0, 500, // hz = 500
   472  		3, 0, 10, // 10 samples with an empty stack trace
   473  	}
   474  	_, err := translateCPUProfile(b, 2)
   475  	if err != nil {
   476  		t.Fatalf("translating profile: %v", err)
   477  	}
   478  }
   479  
   480  var errWrite = errors.New("error from writer")
   481  
   482  type errWriter struct{}
   483  
   484  func (errWriter) Write(p []byte) (int, error) { return 0, errWrite }
   485  
   486  func TestWriteToErr(t *testing.T) {
   487  	err := Lookup("heap").WriteTo(&errWriter{}, 0)
   488  	if !errors.Is(err, errWrite) {
   489  		t.Fatalf("want error from writer, got: %v", err)
   490  	}
   491  }
   492  

View as plain text