github.com/grafana/pyroscope@v1.18.0/pkg/symbolizer/symbolizer_test.go (about) 1 package symbolizer 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "context" 7 "fmt" 8 "io" 9 "os" 10 "strings" 11 "testing" 12 13 "github.com/go-kit/log" 14 "github.com/prometheus/client_golang/prometheus" 15 "github.com/prometheus/client_golang/prometheus/testutil" 16 "github.com/stretchr/testify/mock" 17 "github.com/stretchr/testify/require" 18 19 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 20 "github.com/grafana/pyroscope/lidia" 21 "github.com/grafana/pyroscope/pkg/model" 22 "github.com/grafana/pyroscope/pkg/tenant" 23 "github.com/grafana/pyroscope/pkg/test/mocks/mockobjstore" 24 "github.com/grafana/pyroscope/pkg/test/mocks/mocksymbolizer" 25 "github.com/grafana/pyroscope/pkg/validation" 26 ) 27 28 type symbolizerInputs struct { 29 Registry *prometheus.Registry 30 Limits Limits 31 } 32 33 func newSymbolizerTest(t *testing.T, inp *symbolizerInputs) (*Symbolizer, *mocksymbolizer.MockDebuginfodClient, *mockobjstore.MockBucket) { 34 t.Helper() 35 mockClient := mocksymbolizer.NewMockDebuginfodClient(t) 36 mockBucket := mockobjstore.NewMockBucket(t) 37 38 if inp == nil { 39 inp = &symbolizerInputs{} 40 } 41 42 if inp.Limits == nil { 43 inp.Limits = validation.MockDefaultOverrides() 44 } 45 46 if inp.Registry == nil { 47 inp.Registry = prometheus.NewRegistry() 48 } 49 50 s, err := New( 51 log.NewNopLogger(), 52 Config{MaxDebuginfodConcurrency: 1}, 53 inp.Registry, 54 mockBucket, 55 inp.Limits, 56 ) 57 require.NoError(t, err) 58 s.client = mockClient 59 60 return s, mockClient, mockBucket 61 } 62 63 // TestSymbolizePprof tests symbolization using testdata/symbols.debug which contains: 64 // 65 // 0x1500 -> (contains both functions) 66 // - main (/usr/src/stress-1.0.7-1/src/stress.c:87) 67 // - fprintf (/usr/include/x86_64-linux-gnu/bits/stdio2.h:77) 68 // 69 // 0x3c5a -> atoll_b (/usr/src/stress-1.0.7-1/src/stress.c:632) 70 // 0x2745 -> main (/usr/src/stress-1.0.7-1/src/stress.c:87) 71 func TestSymbolizePprof(t *testing.T) { 72 tests := []struct { 73 name string 74 profile *googlev1.Profile 75 setupMock func(*mocksymbolizer.MockDebuginfodClient, *mockobjstore.MockBucket) 76 wantErr bool 77 validate func(*testing.T, *googlev1.Profile) 78 }{ 79 { 80 name: "already symbolized mapping", 81 profile: &googlev1.Profile{ 82 Mapping: []*googlev1.Mapping{{ 83 HasFunctions: true, 84 HasFilenames: true, 85 HasLineNumbers: true, 86 }}, 87 Location: []*googlev1.Location{{ 88 MappingId: 1, 89 Line: []*googlev1.Line{{ 90 FunctionId: 0, 91 Line: 42, 92 }}, 93 }}, 94 Function: []*googlev1.Function{{ 95 Name: 1, 96 Filename: 2, 97 }}, 98 StringTable: []string{"", "main", "main.go"}, 99 }, 100 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) {}, 101 validate: func(t *testing.T, p *googlev1.Profile) { 102 require.True(t, p.Mapping[0].HasFunctions) 103 require.True(t, p.Mapping[0].HasFilenames) 104 require.True(t, p.Mapping[0].HasLineNumbers) 105 }, 106 }, 107 { 108 name: "needs symbolization single address", 109 profile: &googlev1.Profile{ 110 Mapping: []*googlev1.Mapping{{ 111 BuildId: 1, 112 MemoryStart: 0x0, 113 MemoryLimit: 0x1000000, 114 FileOffset: 0x0, 115 }}, 116 Location: []*googlev1.Location{{ 117 Id: 1, 118 MappingId: 1, 119 Address: 0x1500, 120 }}, 121 StringTable: []string{"", "build-id"}, 122 }, 123 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 124 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return(openTestFile(t), nil).Once() 125 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")).Once() 126 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Return(nil).Once() 127 }, 128 validate: func(t *testing.T, p *googlev1.Profile) { 129 require.True(t, p.Mapping[0].HasFunctions) 130 131 require.Len(t, p.Location[0].Line, 1) 132 133 assertLocationHasFunction(t, p, p.Location[0], "main", "main") 134 }, 135 }, 136 { 137 name: "empty build ID creates fallback symbols", 138 profile: &googlev1.Profile{ 139 Mapping: []*googlev1.Mapping{{ 140 Id: 1, 141 Filename: 2, 142 BuildId: 1, 143 }}, 144 Location: []*googlev1.Location{ 145 {Id: 1, MappingId: 1, Address: 0xa4c}, 146 {Id: 2, MappingId: 1, Address: 0x9f0}, 147 }, 148 StringTable: []string{"", "", "linux-vdso.1.so"}, 149 }, 150 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) {}, 151 validate: func(t *testing.T, p *googlev1.Profile) { 152 require.True(t, p.Mapping[0].HasFunctions) 153 require.Len(t, p.Location[0].Line, 1) 154 require.Len(t, p.Location[1].Line, 1) 155 156 fn1 := p.StringTable[p.Function[p.Location[0].Line[0].FunctionId-1].Name] 157 fn2 := p.StringTable[p.Function[p.Location[1].Line[0].FunctionId-1].Name] 158 require.Contains(t, fn1, "linux-vdso.1.so") 159 require.Contains(t, fn1, "0xa4c") 160 require.Contains(t, fn2, "linux-vdso.1.so") 161 require.Contains(t, fn2, "0x9f0") 162 }, 163 }, 164 { 165 name: "multiple locations per mapping", 166 profile: &googlev1.Profile{ 167 Mapping: []*googlev1.Mapping{{ 168 BuildId: 1, 169 MemoryStart: 0x0, 170 MemoryLimit: 0x1000000, 171 FileOffset: 0x0, 172 }}, 173 Location: []*googlev1.Location{ 174 {Id: 1, MappingId: 1, Address: 0x1500}, 175 {Id: 2, MappingId: 1, Address: 0x3b60}, 176 {Id: 3, MappingId: 1, Address: 0x1440}, 177 }, 178 StringTable: []string{"", "build-id"}, 179 }, 180 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 181 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return(openTestFile(t), nil).Once() 182 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")).Once() 183 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Return(nil).Once() 184 }, 185 validate: func(t *testing.T, p *googlev1.Profile) { 186 require.True(t, p.Mapping[0].HasFunctions) 187 188 // First location (0x1500) - main 189 require.Len(t, p.Location[0].Line, 1) 190 assertLocationHasFunction(t, p, p.Location[0], "main", "main") 191 192 // Second location (0x3b60) - atoll_b 193 require.Len(t, p.Location[1].Line, 1) 194 assertLocationHasFunction(t, p, p.Location[1], "atoll_b", "atoll_b") 195 196 // Third location (0x1440) - main 197 require.Len(t, p.Location[2].Line, 1) 198 assertLocationHasFunction(t, p, p.Location[2], "main", "main") 199 }, 200 }, 201 { 202 name: "preserve existing symbols when HasFunctions=false", 203 // This tests a defensive check against data inconsistency where a mapping has 204 // HasFunctions=false but contains locations with existing symbols. 205 // This scenario should be rare, but we maintain the check for robustness. 206 profile: &googlev1.Profile{ 207 Mapping: []*googlev1.Mapping{{ 208 Id: 1, 209 BuildId: 1, 210 Filename: 2, 211 MemoryStart: 0x0, 212 MemoryLimit: 0x1000000, 213 FileOffset: 0x0, 214 HasFunctions: false, 215 }}, 216 Location: []*googlev1.Location{ 217 { 218 Id: 1, 219 MappingId: 1, 220 Address: 0x1000, 221 Line: []*googlev1.Line{{ 222 FunctionId: 1, 223 Line: 42, 224 }}, 225 }, 226 { 227 Id: 2, 228 MappingId: 1, 229 Address: 0x1500, 230 Line: nil, 231 }, 232 }, 233 Function: []*googlev1.Function{{ 234 Id: 1, 235 Name: 3, 236 }}, 237 StringTable: []string{"", "build-id", "alloy", "existing_function"}, 238 }, 239 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 240 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return(openTestFile(t), nil).Once() 241 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")).Once() 242 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Return(nil).Once() 243 }, 244 validate: func(t *testing.T, p *googlev1.Profile) { 245 require.True(t, p.Mapping[0].HasFunctions) 246 247 require.Len(t, p.Location[0].Line, 1) 248 require.Equal(t, uint64(1), p.Location[0].Line[0].FunctionId) 249 require.Equal(t, "existing_function", p.StringTable[p.Function[0].Name]) 250 251 require.Len(t, p.Location[1].Line, 1) 252 assertLocationHasFunction(t, p, p.Location[1], "main", "main") 253 254 existingFuncStillExists := false 255 for _, str := range p.StringTable { 256 if str == "existing_function" { 257 existingFuncStillExists = true 258 break 259 } 260 } 261 require.True(t, existingFuncStillExists) 262 263 placeholderFound := false 264 for _, str := range p.StringTable { 265 if strings.Contains(str, "!0x") { 266 placeholderFound = true 267 break 268 } 269 } 270 require.False(t, placeholderFound) 271 }, 272 }, 273 } 274 275 for _, tt := range tests { 276 t.Run(tt.name, func(t *testing.T) { 277 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 278 tt.setupMock(mockClient, mockBucket) 279 280 ctx := tenant.InjectTenantID(context.Background(), "tenant") 281 err := s.SymbolizePprof(ctx, tt.profile) 282 if tt.wantErr { 283 require.Error(t, err) 284 return 285 } 286 require.NoError(t, err) 287 288 tt.validate(t, tt.profile) 289 mockClient.AssertExpectations(t) 290 }) 291 } 292 } 293 294 func TestSymbolizationKeepsSequentialFunctionIDs(t *testing.T) { 295 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 296 297 profile := &googlev1.Profile{ 298 Mapping: []*googlev1.Mapping{{BuildId: 1}}, 299 Location: []*googlev1.Location{{Id: 1, MappingId: 1, Address: 0x1500}}, 300 Function: []*googlev1.Function{{Id: 1, Name: 1}}, 301 StringTable: []string{"", "build-id", "existing_func"}, 302 Sample: []*googlev1.Sample{{ 303 LocationId: []uint64{1}, 304 Value: []int64{100}, 305 }}, 306 } 307 308 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")) 309 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return(openTestFile(t), nil) 310 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Return(nil) 311 312 ctx := tenant.InjectTenantID(context.Background(), "tenant") 313 err := s.SymbolizePprof(ctx, profile) 314 require.NoError(t, err) 315 316 // Verify sequential function IDs 317 for i, fn := range profile.Function { 318 require.Equal(t, uint64(i+1), fn.Id) 319 } 320 321 _, err = model.TreeFromBackendProfile(profile, 1000) 322 require.NoError(t, err) 323 } 324 325 func TestSymbolizationWithLidiaData(t *testing.T) { 326 327 const testLidiaZip = "testdata/test_lidia_file.gz" 328 const buildID = "ffcf60c240417166980a43fbbfde486e0b3718e5" 329 330 lidiaData, err := extractGzipFile(t, testLidiaZip) 331 require.NoError(t, err) 332 require.NotEmpty(t, lidiaData) 333 334 // Configure the mock to return the same Lidia data for both Get operations 335 getLidiaData := func() io.ReadCloser { 336 return io.NopCloser(bytes.NewReader(lidiaData)) 337 } 338 339 sym, _, mockBucket := newSymbolizerTest(t, nil) 340 341 mockBucket.On("Get", mock.Anything, buildID).Return(getLidiaData(), nil).Once() 342 mockBucket.On("Get", mock.Anything, buildID).Return(getLidiaData(), nil).Once() 343 344 req := &request{ 345 buildID: buildID, 346 binaryName: "test-binary", 347 locations: []*location{ 348 { 349 address: 0x1b743d6, 350 }, 351 }, 352 } 353 354 ctx := tenant.InjectTenantID(context.Background(), "tenant") 355 sym.symbolize(ctx, req) 356 require.NotEmpty(t, req.locations[0].lines) 357 358 // Second request should also fetch from store 359 req2 := &request{ 360 buildID: buildID, 361 binaryName: "test-binary", 362 locations: []*location{ 363 { 364 address: 0x1b743d6, 365 }, 366 }, 367 } 368 369 sym.symbolize(ctx, req2) 370 require.NotEmpty(t, req2.locations[0].lines) 371 } 372 373 // TestSymbolizeWithObjectStore validates the symbolizer's behavior with the object store 374 func TestSymbolizeWithObjectStore(t *testing.T) { 375 376 elfTestFile := openTestFile(t) 377 elfData, err := io.ReadAll(elfTestFile) 378 elfTestFile.Close() 379 require.NoError(t, err) 380 381 var capturedLidiaData []byte 382 383 ctx := tenant.InjectTenantID(context.Background(), "tenant") 384 385 // 1. First request: Object store miss → fetch from debuginfod → store Lidia data in object store 386 t.Run("store-miss", func(t *testing.T) { 387 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 388 389 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")).Once() 390 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return(io.NopCloser(bytes.NewReader(elfData)), nil).Once() 391 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Run(func(args mock.Arguments) { 392 reader := args.Get(2).(io.Reader) 393 var buf bytes.Buffer 394 teeReader := io.TeeReader(reader, &buf) 395 var err error 396 capturedLidiaData, err = io.ReadAll(teeReader) 397 require.NoError(t, err) 398 }).Return(nil).Once() 399 400 req1 := createRequest(t, "build-id", 0x1500) 401 s.symbolize(ctx, req1) 402 require.NotEmpty(t, req1.locations[0].lines) 403 require.NotEmpty(t, capturedLidiaData) 404 405 mockClient.AssertExpectations(t) 406 mockBucket.AssertExpectations(t) 407 408 }) 409 410 // 2. Second request (same build-id, same address): Object store hit → use cached Lidia data 411 t.Run("store hit, same address", func(t *testing.T) { 412 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 413 414 mockBucket.On("Get", mock.Anything, "build-id").Return( 415 io.NopCloser(bytes.NewReader(capturedLidiaData)), nil, 416 ).Once() 417 418 req2 := createRequest(t, "build-id", 0x1500) 419 s.symbolize(ctx, req2) 420 require.NotEmpty(t, req2.locations[0].lines) 421 422 mockClient.AssertExpectations(t) 423 mockBucket.AssertExpectations(t) 424 }) 425 426 // 3. Third request (same build-id, different address): Object store hit → use cached Lidia data 427 t.Run("store hit, different address", func(t *testing.T) { 428 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 429 mockBucket.On("Get", mock.Anything, "build-id").Return( 430 io.NopCloser(bytes.NewReader(capturedLidiaData)), nil, 431 ).Once() 432 433 req3 := createRequest(t, "build-id", 0x3c5a) 434 s.symbolize(ctx, req3) 435 require.NotEmpty(t, req3.locations[0].lines) 436 437 mockClient.AssertExpectations(t) 438 mockBucket.AssertExpectations(t) 439 }) 440 441 // 4. Fourth request (different build-id): Object store miss → fetch from debuginfod → store Lidia data 442 t.Run("store miss, different build-id", func(t *testing.T) { 443 s, mockClient, mockBucket := newSymbolizerTest(t, nil) 444 445 var capturedLidiaData2 []byte 446 mockBucket.On("Get", mock.Anything, "different-build-id").Return(nil, fmt.Errorf("not found")).Once() 447 mockClient.On("FetchDebuginfo", mock.Anything, "different-build-id").Return(io.NopCloser(bytes.NewReader(elfData)), nil).Once() 448 mockBucket.On("Upload", mock.Anything, "different-build-id", mock.Anything).Run(func(args mock.Arguments) { 449 reader := args.Get(2).(io.Reader) 450 var buf bytes.Buffer 451 teeReader := io.TeeReader(reader, &buf) 452 var err error 453 capturedLidiaData2, err = io.ReadAll(teeReader) 454 require.NoError(t, err) 455 }).Return(nil).Once() 456 457 req4 := createRequest(t, "different-build-id", 0x1500) 458 s.symbolize(ctx, req4) 459 require.NotEmpty(t, req4.locations[0].lines) 460 require.NotEmpty(t, capturedLidiaData2) 461 462 mockClient.AssertExpectations(t) 463 mockBucket.AssertExpectations(t) 464 }) 465 466 } 467 468 func TestSymbolizerMetrics(t *testing.T) { 469 tests := []struct { 470 name string 471 setupMock func(*mocksymbolizer.MockDebuginfodClient, *mockobjstore.MockBucket) 472 setupTest func(*Symbolizer, context.Context) 473 expected map[string]int 474 }{ 475 { 476 name: "successful symbolization with cache", 477 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 478 elfTestFile := openTestFile(t) 479 elfData, err := io.ReadAll(elfTestFile) 480 elfTestFile.Close() 481 require.NoError(t, err) 482 483 preProcessor := &Symbolizer{ 484 logger: log.NewNopLogger(), 485 metrics: newMetrics(nil), 486 } 487 lidiaData, err := preProcessor.processELFData(elfData, 0) // 0 means unlimited 488 require.NoError(t, err) 489 require.NotEmpty(t, lidiaData) 490 491 mockBucket.On("IsObjNotFoundErr", mock.Anything).Return(true).Maybe() 492 mockBucket.On("Name").Return("test-bucket").Maybe() 493 494 mockBucket.On("Get", mock.Anything, "build-id").Return(nil, fmt.Errorf("not found")).Once() 495 496 mockClient.On("FetchDebuginfo", mock.Anything, "build-id").Return( 497 io.NopCloser(bytes.NewReader(elfData)), nil, 498 ).Once() 499 mockBucket.On("Upload", mock.Anything, "build-id", mock.Anything).Return(nil).Once() 500 501 mockBucket.On("Get", mock.Anything, "build-id").Return( 502 io.NopCloser(bytes.NewReader(lidiaData)), nil, 503 ).Once() 504 }, 505 setupTest: func(s *Symbolizer, ctx context.Context) { 506 req1 := createRequest(t, "build-id", 0x1500) 507 s.symbolize(ctx, req1) 508 509 req2 := createRequest(t, "build-id", 0x1500) 510 s.symbolize(ctx, req2) 511 }, 512 expected: map[string]int{ 513 "pyroscope_profile_symbolization_duration_seconds": 0, 514 "pyroscope_debug_symbol_resolution_duration_seconds": 1, 515 "pyroscope_debug_symbol_resolution_errors_total": 0, 516 }, 517 }, 518 { 519 name: "debuginfod error", 520 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 521 mockBucket.On("Get", mock.Anything, "unknown-build-id").Return(nil, fmt.Errorf("not found")).Once() 522 mockClient.On("FetchDebuginfo", mock.Anything, "unknown-build-id"). 523 Return(nil, buildIDNotFoundError{buildID: "unknown-build-id"}).Once() 524 }, 525 setupTest: func(s *Symbolizer, ctx context.Context) { 526 req := createRequest(t, "unknown-build-id", 0x1500) 527 s.symbolize(ctx, req) 528 }, 529 expected: map[string]int{ 530 "pyroscope_profile_symbolization_duration_seconds": 0, 531 "pyroscope_debug_symbol_resolution_duration_seconds": 0, 532 "pyroscope_debug_symbol_resolution_errors_total": 0, 533 }, 534 }, 535 { 536 name: "elf_parsing_error", 537 setupMock: func(mockClient *mocksymbolizer.MockDebuginfodClient, mockBucket *mockobjstore.MockBucket) { 538 invalidData := []byte("invalid elf data") 539 540 mockBucket.On("Get", mock.Anything, "invalid-elf").Return(nil, fmt.Errorf("not found")).Once() 541 mockClient.On("FetchDebuginfo", mock.Anything, "invalid-elf").Return( 542 io.NopCloser(bytes.NewReader(invalidData)), nil, 543 ).Once() 544 }, 545 setupTest: func(s *Symbolizer, ctx context.Context) { 546 req := createRequest(t, "invalid-elf", 0x1500) 547 s.symbolize(ctx, req) 548 }, 549 expected: map[string]int{ 550 "pyroscope_profile_symbolization_duration_seconds": 0, 551 "pyroscope_debug_symbol_resolution_errors_total": 1, 552 }, 553 }, 554 } 555 556 for _, tt := range tests { 557 t.Run(tt.name, func(t *testing.T) { 558 reg := prometheus.NewRegistry() 559 s, mockClient, mockBucket := newSymbolizerTest(t, &symbolizerInputs{Registry: reg}) 560 tt.setupMock(mockClient, mockBucket) 561 562 ctx := tenant.InjectTenantID(context.Background(), "tenant") 563 tt.setupTest(s, ctx) 564 565 for metricName, expectedCount := range tt.expected { 566 count, err := testutil.GatherAndCount(reg, metricName) 567 require.NoError(t, err, "Error gathering metric %s", metricName) 568 require.Equal(t, expectedCount, count, "Metric %s count mismatch", metricName) 569 } 570 571 mockClient.AssertExpectations(t) 572 mockBucket.AssertExpectations(t) 573 }) 574 } 575 } 576 577 func assertLocationHasFunction(t *testing.T, profile *googlev1.Profile, loc *googlev1.Location, 578 functionName, fileName string) { 579 t.Helper() 580 581 found := false 582 583 for _, line := range loc.Line { 584 for _, fn := range profile.Function { 585 if fn.Id == line.FunctionId { 586 name := "<invalid>" 587 if fn.Name >= 0 && int(fn.Name) < len(profile.StringTable) { 588 name = profile.StringTable[fn.Name] 589 } 590 if name == functionName { 591 found = true 592 } 593 } 594 } 595 } 596 597 require.True(t, found, "Function %q not found in location", functionName) 598 599 if found { 600 fileNameFound := false 601 for _, str := range profile.StringTable { 602 if str == fileName { 603 fileNameFound = true 604 break 605 } 606 } 607 require.True(t, fileNameFound, "Filename %q not found in string table", fileName) 608 } 609 610 } 611 612 func openTestFile(t *testing.T) io.ReadCloser { 613 t.Helper() 614 f, err := os.Open("testdata/symbols.debug") 615 require.NoError(t, err) 616 617 data, err := io.ReadAll(f) 618 require.NoError(t, err) 619 f.Close() 620 621 return NewReaderAtCloser(data) 622 } 623 624 func extractGzipFile(t *testing.T, gzipPath string) ([]byte, error) { 625 t.Helper() 626 file, err := os.Open(gzipPath) 627 if err != nil { 628 return nil, err 629 } 630 defer file.Close() 631 632 gzipReader, err := gzip.NewReader(file) 633 if err != nil { 634 return nil, err 635 } 636 defer gzipReader.Close() 637 638 return io.ReadAll(gzipReader) 639 } 640 641 func createRequest(t *testing.T, buildID string, address uint64) *request { 642 t.Helper() 643 return &request{ 644 buildID: buildID, 645 locations: []*location{ 646 { 647 address: address, 648 }, 649 }, 650 } 651 } 652 653 func TestConfigValidate(t *testing.T) { 654 tests := []struct { 655 name string 656 setup func(cfg *Config) 657 wantErr bool 658 }{ 659 { 660 name: "valid config with positive concurrency", 661 setup: func(cfg *Config) { cfg.MaxDebuginfodConcurrency = 10 }, 662 wantErr: false, 663 }, 664 { 665 name: "invalid config with zero concurrency", 666 setup: func(cfg *Config) { cfg.MaxDebuginfodConcurrency = 0 }, 667 wantErr: true, 668 }, 669 { 670 name: "invalid config with negative concurrency", 671 setup: func(cfg *Config) { cfg.MaxDebuginfodConcurrency = -1 }, 672 wantErr: true, 673 }, 674 } 675 676 for _, tt := range tests { 677 t.Run(tt.name, func(t *testing.T) { 678 cfg := Config{} 679 tt.setup(&cfg) 680 err := cfg.Validate() 681 if tt.wantErr { 682 require.Error(t, err) 683 } else { 684 require.NoError(t, err) 685 } 686 }) 687 } 688 } 689 690 // TestUpdateAllSymbolsInProfile verifies that line numbers, file paths, and StartLine 691 // are properly passed through from SourceInfoFrame to the profile. 692 func TestUpdateAllSymbolsInProfile(t *testing.T) { 693 s := &Symbolizer{logger: log.NewNopLogger()} 694 stringMap := make(map[string]int64) 695 696 t.Run("basic symbolization", func(t *testing.T) { 697 profile := &googlev1.Profile{ 698 Mapping: []*googlev1.Mapping{{Id: 1, HasFunctions: false}}, 699 Location: []*googlev1.Location{{Id: 1, MappingId: 1, Address: 0x1500}}, 700 StringTable: []string{""}, 701 Function: []*googlev1.Function{}, 702 } 703 704 symbolizedLocs := []symbolizedLocation{{ 705 loc: profile.Location[0], 706 symLoc: &location{ 707 address: 0x1500, 708 lines: []lidia.SourceInfoFrame{{ 709 LineNumber: 42, FunctionName: "testFunction", FilePath: "/path/to/test.go", 710 }}, 711 }, 712 mapping: profile.Mapping[0], 713 }} 714 715 s.updateAllSymbolsInProfile(profile, symbolizedLocs, stringMap) 716 717 require.True(t, profile.Mapping[0].HasFunctions) 718 require.Len(t, profile.Location[0].Line, 1) 719 require.Len(t, profile.Function, 1) 720 721 line := profile.Location[0].Line[0] 722 fn := profile.Function[0] 723 724 require.Equal(t, int64(42), line.Line) 725 require.Equal(t, int64(42), fn.StartLine) 726 require.Equal(t, "testFunction", profile.StringTable[fn.Name]) 727 require.Equal(t, "/path/to/test.go", profile.StringTable[fn.Filename]) 728 }) 729 730 t.Run("minimum StartLine for same function", func(t *testing.T) { 731 profile := &googlev1.Profile{ 732 Mapping: []*googlev1.Mapping{{Id: 1, HasFunctions: false}}, 733 Location: []*googlev1.Location{ 734 {Id: 1, MappingId: 1, Address: 0x1500}, 735 {Id: 2, MappingId: 1, Address: 0x1600}, 736 }, 737 StringTable: []string{""}, 738 Function: []*googlev1.Function{}, 739 } 740 741 symbolizedLocs := []symbolizedLocation{ 742 { 743 loc: profile.Location[0], 744 symLoc: &location{address: 0x1500, lines: []lidia.SourceInfoFrame{{ 745 LineNumber: 100, FunctionName: "testFunction", FilePath: "/path/to/test.go", 746 }}}, 747 mapping: profile.Mapping[0], 748 }, 749 { 750 loc: profile.Location[1], 751 symLoc: &location{address: 0x1600, lines: []lidia.SourceInfoFrame{{ 752 LineNumber: 50, FunctionName: "testFunction", FilePath: "/path/to/test.go", 753 }}}, 754 mapping: profile.Mapping[0], 755 }, 756 } 757 758 s.updateAllSymbolsInProfile(profile, symbolizedLocs, stringMap) 759 760 require.Len(t, profile.Function, 1) 761 // StartLine properly updated 762 require.Equal(t, int64(50), profile.Function[0].StartLine) 763 require.Equal(t, int64(100), profile.Location[0].Line[0].Line) 764 require.Equal(t, int64(50), profile.Location[1].Line[0].Line) 765 }) 766 }