github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/cover/html.go (about) 1 // Copyright 2018 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package cover 5 6 import ( 7 "bufio" 8 "bytes" 9 _ "embed" 10 "encoding/csv" 11 "encoding/json" 12 "fmt" 13 "html" 14 "html/template" 15 "io" 16 "math" 17 "os" 18 "path/filepath" 19 "sort" 20 "strconv" 21 "strings" 22 23 "github.com/google/syzkaller/pkg/cover/backend" 24 "github.com/google/syzkaller/pkg/mgrconfig" 25 ) 26 27 type HandlerParams struct { 28 Progs []Prog 29 Filter map[uint64]struct{} 30 Debug bool 31 Force bool 32 } 33 34 func (rg *ReportGenerator) DoHTML(w io.Writer, params HandlerParams) error { 35 var progs = fixUpPCs(params.Progs, params.Filter) 36 files, err := rg.prepareFileMap(progs, params.Force, params.Debug) 37 if err != nil { 38 return err 39 } 40 d := &templateData{ 41 Root: new(templateDir), 42 RawCover: rg.rawCoverEnabled, 43 } 44 haveProgs := len(progs) > 1 || progs[0].Data != "" 45 fileOpenErr := fmt.Errorf("failed to open/locate any source file") 46 for fname, file := range files { 47 pos := d.Root 48 path := "" 49 for { 50 if path != "" { 51 path += "/" 52 } 53 sep := strings.IndexByte(fname, filepath.Separator) 54 if sep == -1 { 55 path += fname 56 break 57 } 58 dir := fname[:sep] 59 path += dir 60 if pos.Dirs == nil { 61 pos.Dirs = make(map[string]*templateDir) 62 } 63 if pos.Dirs[dir] == nil { 64 pos.Dirs[dir] = &templateDir{ 65 templateBase: templateBase{ 66 Path: path, 67 Name: dir, 68 }, 69 } 70 } 71 pos = pos.Dirs[dir] 72 fname = fname[sep+1:] 73 } 74 f := &templateFile{ 75 templateBase: templateBase{ 76 Path: path, 77 Name: fname, 78 Total: file.totalPCs, 79 Covered: file.coveredPCs, 80 }, 81 HasFunctions: len(file.functions) != 0, 82 } 83 pos.Files = append(pos.Files, f) 84 if file.coveredPCs == 0 { 85 continue 86 } 87 addFunctionCoverage(file, d) 88 contents := "" 89 lines, err := parseFile(file.filename) 90 if err == nil { 91 contents = fileContents(file, lines, haveProgs) 92 fileOpenErr = nil 93 } else { 94 // We ignore individual errors of opening/locating source files 95 // because there is a number of reasons when/why it can happen. 96 // We fail only if we can't open/locate any single source file. 97 // syz-ci can mess state of source files (https://github.com/google/syzkaller/issues/1770), 98 // or bazel lies about location of auto-generated files, 99 // or a used can update source files with git pull/checkout. 100 contents = html.EscapeString(err.Error()) 101 if fileOpenErr != nil { 102 fileOpenErr = err 103 } 104 } 105 d.Contents = append(d.Contents, template.HTML(contents)) 106 f.Index = len(d.Contents) - 1 107 } 108 if fileOpenErr != nil { 109 return fileOpenErr 110 } 111 for _, prog := range progs { 112 d.Progs = append(d.Progs, templateProg{ 113 Sig: prog.Sig, 114 Content: template.HTML(html.EscapeString(prog.Data)), 115 }) 116 } 117 118 processDir(d.Root) 119 return coverTemplate.Execute(w, d) 120 } 121 122 type lineCoverExport struct { 123 Module string `json:",omitempty"` 124 Filename string 125 Covered []int `json:",omitempty"` 126 Uncovered []int `json:",omitempty"` 127 Both []int `json:",omitempty"` 128 } 129 130 func (rg *ReportGenerator) DoLineJSON(w io.Writer, params HandlerParams) error { 131 var progs = fixUpPCs(params.Progs, params.Filter) 132 files, err := rg.prepareFileMap(progs, params.Force, params.Debug) 133 if err != nil { 134 return err 135 } 136 var entries []lineCoverExport 137 for _, file := range files { 138 lines, err := parseFile(file.filename) 139 if err != nil { 140 // Ignore and continue onto the next file. 141 continue 142 } 143 entries = append(entries, fileLineContents(file, lines)) 144 } 145 encoder := json.NewEncoder(w) 146 encoder.SetIndent("", "\t") 147 if err := encoder.Encode(entries); err != nil { 148 return fmt.Errorf("encoding [%v] entries failed: %w", len(entries), err) 149 } 150 return nil 151 } 152 153 func fileLineContents(file *file, lines [][]byte) lineCoverExport { 154 lce := lineCoverExport{ 155 Module: file.module, 156 Filename: file.filename, 157 } 158 lineCover := perLineCoverage(file.covered, file.uncovered) 159 for i, ln := range lines { 160 start := 0 161 cover := append(lineCover[i+1], lineCoverChunk{End: backend.LineEnd}) 162 for _, cov := range cover { 163 end := min(cov.End-1, len(ln)) 164 if end == start { 165 continue 166 } 167 if cov.Covered && cov.Uncovered { 168 lce.Both = append(lce.Both, i+1) 169 } else if cov.Covered { 170 lce.Covered = append(lce.Covered, i+1) 171 } else if cov.Uncovered { 172 lce.Uncovered = append(lce.Uncovered, i+1) 173 } 174 } 175 } 176 return lce 177 } 178 179 func (rg *ReportGenerator) DoRawCoverFiles(w io.Writer, params HandlerParams) error { 180 progs := fixUpPCs(params.Progs, params.Filter) 181 if err := rg.symbolizePCs(uniquePCs(progs...)); err != nil { 182 return err 183 } 184 185 resFrames := rg.Frames 186 187 sort.Slice(resFrames, func(i, j int) bool { 188 fl, fr := resFrames[i], resFrames[j] 189 if fl.PC == fr.PC { 190 return !fl.Inline && fr.Inline // non-inline first 191 } 192 return fl.PC < fr.PC 193 }) 194 195 buf := bufio.NewWriter(w) 196 fmt.Fprintf(buf, "PC,Module,Offset,Filename,Inline,StartLine,EndLine\n") 197 for _, frame := range resFrames { 198 offset := frame.PC - frame.Module.Addr 199 fmt.Fprintf(buf, "0x%x,%v,0x%x,%v,%v,%v,%v\n", 200 frame.PC, frame.Module.Name, offset, frame.Name, frame.Inline, frame.StartLine, frame.EndLine) 201 } 202 buf.Flush() 203 return nil 204 } 205 206 type CoverageInfo struct { 207 FilePath string `json:"file_path"` 208 FuncName string `json:"func_name"` 209 StartLine int `json:"sl"` 210 StartCol int `json:"sc"` 211 EndLine int `json:"el"` 212 EndCol int `json:"ec"` 213 HitCount int `json:"hit_count"` 214 Inline bool `json:"inline"` 215 PC uint64 `json:"pc"` 216 } 217 218 // DoCoverJSONL is a handler for "/cover?jsonl=1". 219 func (rg *ReportGenerator) DoCoverJSONL(w io.Writer, params HandlerParams) error { 220 if rg.CallbackPoints != nil { 221 if err := rg.symbolizePCs(rg.CallbackPoints); err != nil { 222 return fmt.Errorf("failed to symbolize PCs(): %w", err) 223 } 224 } 225 progs := fixUpPCs(params.Progs, params.Filter) 226 if err := rg.symbolizePCs(uniquePCs(progs...)); err != nil { 227 return err 228 } 229 pcProgCount := make(map[uint64]int) 230 for _, prog := range progs { 231 for _, pc := range prog.PCs { 232 pcProgCount[pc]++ 233 } 234 } 235 encoder := json.NewEncoder(w) 236 for _, frame := range rg.Frames { 237 endCol := frame.Range.EndCol 238 if endCol == backend.LineEnd { 239 endCol = -1 240 } 241 covInfo := &CoverageInfo{ 242 FilePath: frame.Name, 243 FuncName: frame.FuncName, 244 StartLine: frame.Range.StartLine, 245 StartCol: frame.Range.StartCol, 246 EndLine: frame.Range.EndLine, 247 EndCol: endCol, 248 HitCount: pcProgCount[frame.PC], 249 Inline: frame.Inline, 250 PC: frame.PC, 251 } 252 if err := encoder.Encode(covInfo); err != nil { 253 return fmt.Errorf("failed to json.Encode(): %w", err) 254 } 255 } 256 return nil 257 } 258 259 type ProgramCoverage struct { 260 Repo string `json:"repo,omitempty"` 261 Commit string `json:"commit,omitempty"` 262 Program string `json:"program"` 263 CoveredFiles []*FileCoverage `json:"coverage"` 264 } 265 266 type FileCoverage struct { 267 Repo string `json:"repo,omitempty"` 268 Commit string `json:"commit,omitempty"` 269 FilePath string `json:"file_path"` 270 Functions []*FuncCoverage `json:"functions"` 271 } 272 273 type FuncCoverage struct { 274 FuncName string `json:"func_name"` 275 Blocks []*Block `json:"blocks"` 276 } 277 278 type Block struct { 279 HitCount int `json:"hit_count,omitempty"` 280 FromLine int `json:"from_line"` 281 FromCol int `json:"from_column"` 282 ToLine int `json:"to_line"` 283 ToCol int `json:"to_column"` 284 } 285 286 // DoCoverPrograms returns the corpus programs with the associated coverage. 287 // The result is a jsonl stream. 288 // Each line is a single ProgramCoverage record. 289 func (rg *ReportGenerator) DoCoverPrograms(w io.Writer, params HandlerParams) error { 290 if rg.CallbackPoints != nil { 291 if err := rg.symbolizePCs(rg.CallbackPoints); err != nil { 292 return fmt.Errorf("failed to symbolize PCs(): %w", err) 293 } 294 } 295 pcToFrames := map[uint64][]*backend.Frame{} 296 for _, frame := range rg.Frames { 297 pcToFrames[frame.PC] = append(pcToFrames[frame.PC], frame) 298 } 299 encoder := json.NewEncoder(w) 300 for _, prog := range params.Progs { 301 fileFuncFrames := map[string]map[string][]*backend.Frame{} 302 for _, pc := range uniquePCs(prog) { 303 for _, frame := range pcToFrames[pc] { 304 if fileFuncFrames[frame.Name] == nil { 305 fileFuncFrames[frame.Name] = map[string][]*backend.Frame{} 306 } 307 frames := fileFuncFrames[frame.Name][frame.FuncName] 308 frames = append(frames, frame) 309 fileFuncFrames[frame.Name][frame.FuncName] = frames 310 } 311 } 312 313 var progCoverage []*FileCoverage 314 for filePath, functions := range fileFuncFrames { 315 var expFuncs []*FuncCoverage 316 for funcName, frames := range functions { 317 var expCoveredBlocks []*Block 318 for _, frame := range frames { 319 endCol := frame.EndCol 320 if endCol == backend.LineEnd { 321 endCol = -1 322 } 323 expCoveredBlocks = append(expCoveredBlocks, &Block{ 324 HitCount: 1, 325 FromCol: frame.StartCol, 326 FromLine: frame.StartLine, 327 ToCol: endCol, 328 ToLine: frame.EndLine, 329 }) 330 } 331 expFuncs = append(expFuncs, &FuncCoverage{ 332 FuncName: funcName, 333 Blocks: expCoveredBlocks, 334 }) 335 } 336 progCoverage = append(progCoverage, &FileCoverage{ 337 FilePath: filePath, 338 Functions: expFuncs, 339 }) 340 } 341 342 if err := encoder.Encode(&ProgramCoverage{ 343 Program: prog.Data, 344 CoveredFiles: progCoverage, 345 }); err != nil { 346 return fmt.Errorf("encoder.Encode: %w", err) 347 } 348 } 349 return nil 350 } 351 352 func (rg *ReportGenerator) DoRawCover(w io.Writer, params HandlerParams) error { 353 progs := fixUpPCs(params.Progs, params.Filter) 354 var pcs []uint64 355 if len(progs) == 1 && rg.rawCoverEnabled { 356 pcs = append([]uint64{}, progs[0].PCs...) 357 } else { 358 uniquePCs := make(map[uint64]bool) 359 for _, prog := range progs { 360 for _, pc := range prog.PCs { 361 if uniquePCs[pc] { 362 continue 363 } 364 uniquePCs[pc] = true 365 pcs = append(pcs, pc) 366 } 367 } 368 sort.Slice(pcs, func(i, j int) bool { 369 return pcs[i] < pcs[j] 370 }) 371 } 372 373 buf := bufio.NewWriter(w) 374 for _, pc := range pcs { 375 fmt.Fprintf(buf, "0x%x\n", pc) 376 } 377 buf.Flush() 378 return nil 379 } 380 381 func (rg *ReportGenerator) DoFilterPCs(w io.Writer, params HandlerParams) error { 382 progs := fixUpPCs(params.Progs, params.Filter) 383 var pcs []uint64 384 uniquePCs := make(map[uint64]bool) 385 for _, prog := range progs { 386 for _, pc := range prog.PCs { 387 if uniquePCs[pc] { 388 continue 389 } 390 uniquePCs[pc] = true 391 if _, ok := params.Filter[pc]; ok { 392 pcs = append(pcs, pc) 393 } 394 } 395 } 396 sort.Slice(pcs, func(i, j int) bool { 397 return pcs[i] < pcs[j] 398 }) 399 400 buf := bufio.NewWriter(w) 401 for _, pc := range pcs { 402 fmt.Fprintf(buf, "0x%x\n", pc) 403 } 404 buf.Flush() 405 return nil 406 } 407 408 type fileStats struct { 409 Name string 410 Module string 411 CoveredLines int 412 TotalLines int 413 CoveredPCs int 414 TotalPCs int 415 TotalFunctions int 416 CoveredFunctions int 417 CoveredPCsInFunctions int 418 TotalPCsInCoveredFunctions int 419 TotalPCsInFunctions int 420 } 421 422 var csvFilesHeader = []string{ 423 "Module", 424 "Filename", 425 "CoveredLines", 426 "TotalLines", 427 "CoveredPCs", 428 "TotalPCs", 429 "TotalFunctions", 430 "CoveredPCsInFunctions", 431 "TotalPCsInFunctions", 432 "TotalPCsInCoveredFunctions", 433 } 434 435 func (rg *ReportGenerator) convertToStats(progs []Prog) ([]fileStats, error) { 436 files, err := rg.prepareFileMap(progs, false, false) 437 if err != nil { 438 return nil, err 439 } 440 441 var data []fileStats 442 for fname, file := range files { 443 lines, err := parseFile(file.filename) 444 if err != nil { 445 fmt.Printf("failed to open/locate %s\n", file.filename) 446 continue 447 } 448 totalFuncs := len(file.functions) 449 var coveredInFunc int 450 var pcsInFunc int 451 var pcsInCoveredFunc int 452 var coveredFunc int 453 for _, function := range file.functions { 454 coveredInFunc += function.covered 455 if function.covered != 0 { 456 pcsInCoveredFunc += function.pcs 457 coveredFunc++ 458 } 459 pcsInFunc += function.pcs 460 } 461 totalLines := len(lines) 462 var coveredLines int 463 for _, line := range file.lines { 464 if len(line.progCount) != 0 { 465 coveredLines++ 466 } 467 } 468 data = append(data, fileStats{ 469 Name: fname, 470 Module: file.module, 471 CoveredLines: coveredLines, 472 TotalLines: totalLines, 473 CoveredPCs: file.coveredPCs, 474 TotalPCs: file.totalPCs, 475 TotalFunctions: totalFuncs, 476 CoveredFunctions: coveredFunc, 477 CoveredPCsInFunctions: coveredInFunc, 478 TotalPCsInFunctions: pcsInFunc, 479 TotalPCsInCoveredFunctions: pcsInCoveredFunc, 480 }) 481 } 482 483 return data, nil 484 } 485 486 func (rg *ReportGenerator) DoFileCover(w io.Writer, params HandlerParams) error { 487 var progs = fixUpPCs(params.Progs, params.Filter) 488 data, err := rg.convertToStats(progs) 489 if err != nil { 490 return err 491 } 492 493 sort.SliceStable(data, func(i, j int) bool { 494 return data[i].Name < data[j].Name 495 }) 496 497 writer := csv.NewWriter(w) 498 defer writer.Flush() 499 if err := writer.Write(csvFilesHeader); err != nil { 500 return err 501 } 502 503 var d [][]string 504 for _, dt := range data { 505 d = append(d, []string{ 506 dt.Module, 507 dt.Name, 508 strconv.Itoa(dt.CoveredLines), 509 strconv.Itoa(dt.TotalLines), 510 strconv.Itoa(dt.CoveredPCs), 511 strconv.Itoa(dt.TotalPCs), 512 strconv.Itoa(dt.TotalFunctions), 513 strconv.Itoa(dt.CoveredPCsInFunctions), 514 strconv.Itoa(dt.TotalPCsInFunctions), 515 strconv.Itoa(dt.TotalPCsInCoveredFunctions), 516 }) 517 } 518 return writer.WriteAll(d) 519 } 520 521 func groupCoverByFilePrefixes(datas []fileStats, subsystems []mgrconfig.Subsystem) map[string]map[string]string { 522 d := make(map[string]map[string]string) 523 524 for _, subsystem := range subsystems { 525 var coveredLines int 526 var totalLines int 527 var coveredPCsInFile int 528 var totalPCsInFile int 529 var totalFuncs int 530 var coveredFuncs int 531 var coveredPCsInFuncs int 532 var pcsInCoveredFuncs int 533 var pcsInFuncs int 534 var percentLines float64 535 var percentPCsInFile float64 536 var percentPCsInFunc float64 537 var percentInCoveredFunc float64 538 var percentCoveredFunc float64 539 540 for _, path := range subsystem.Paths { 541 if strings.HasPrefix(path, "-") { 542 continue 543 } 544 excludes := buildExcludePaths(path, subsystem.Paths) 545 for _, data := range datas { 546 if !strings.HasPrefix(data.Name, path) || isExcluded(data.Name, excludes) { 547 continue 548 } 549 coveredLines += data.CoveredLines 550 totalLines += data.TotalLines 551 coveredPCsInFile += data.CoveredPCs 552 totalPCsInFile += data.TotalPCs 553 totalFuncs += data.TotalFunctions 554 coveredFuncs += data.CoveredFunctions 555 coveredPCsInFuncs += data.CoveredPCsInFunctions 556 pcsInFuncs += data.TotalPCsInFunctions 557 pcsInCoveredFuncs += data.TotalPCsInCoveredFunctions 558 } 559 } 560 561 if totalLines != 0 { 562 percentLines = 100.0 * float64(coveredLines) / float64(totalLines) 563 } 564 if totalPCsInFile != 0 { 565 percentPCsInFile = 100.0 * float64(coveredPCsInFile) / float64(totalPCsInFile) 566 } 567 if pcsInFuncs != 0 { 568 percentPCsInFunc = 100.0 * float64(coveredPCsInFuncs) / float64(pcsInFuncs) 569 } 570 if pcsInCoveredFuncs != 0 { 571 percentInCoveredFunc = 100.0 * float64(coveredPCsInFuncs) / float64(pcsInCoveredFuncs) 572 } 573 if totalFuncs != 0 { 574 percentCoveredFunc = 100.0 * float64(coveredFuncs) / float64(totalFuncs) 575 } 576 577 d[subsystem.Name] = map[string]string{ 578 "name": subsystem.Name, 579 "lines": fmt.Sprintf("%v / %v / %.2f%%", coveredLines, totalLines, percentLines), 580 "PCsInFiles": fmt.Sprintf("%v / %v / %.2f%%", coveredPCsInFile, totalPCsInFile, percentPCsInFile), 581 "Funcs": fmt.Sprintf("%v / %v / %.2f%%", coveredFuncs, totalFuncs, percentCoveredFunc), 582 "PCsInFuncs": fmt.Sprintf("%v / %v / %.2f%%", coveredPCsInFuncs, pcsInFuncs, percentPCsInFunc), 583 "PCsInCoveredFuncs": fmt.Sprintf("%v / %v / %.2f%%", coveredPCsInFuncs, pcsInCoveredFuncs, percentInCoveredFunc), 584 } 585 } 586 587 return d 588 } 589 590 func buildExcludePaths(prefix string, paths []string) []string { 591 var excludes []string 592 for _, path := range paths { 593 if strings.HasPrefix(path, "-") && strings.HasPrefix(path[1:], prefix) { 594 excludes = append(excludes, path[1:]) 595 } 596 } 597 return excludes 598 } 599 600 func isExcluded(path string, excludes []string) bool { 601 for _, exclude := range excludes { 602 if strings.HasPrefix(path, exclude) { 603 return true 604 } 605 } 606 return false 607 } 608 609 func (rg *ReportGenerator) DoSubsystemCover(w io.Writer, params HandlerParams) error { 610 var progs = fixUpPCs(params.Progs, params.Filter) 611 data, err := rg.convertToStats(progs) 612 if err != nil { 613 return err 614 } 615 616 d := groupCoverByFilePrefixes(data, rg.subsystem) 617 618 return coverTableTemplate.Execute(w, d) 619 } 620 621 func groupCoverByModule(datas []fileStats) map[string]map[string]string { 622 d := make(map[string]map[string]string) 623 624 coveredLines := make(map[string]int) 625 totalLines := make(map[string]int) 626 coveredPCsInFile := make(map[string]int) 627 totalPCsInFile := make(map[string]int) 628 totalFuncs := make(map[string]int) 629 coveredFuncs := make(map[string]int) 630 coveredPCsInFuncs := make(map[string]int) 631 pcsInCoveredFuncs := make(map[string]int) 632 pcsInFuncs := make(map[string]int) 633 percentLines := make(map[string]float64) 634 percentPCsInFile := make(map[string]float64) 635 percentPCsInFunc := make(map[string]float64) 636 percentInCoveredFunc := make(map[string]float64) 637 percentCoveredFunc := make(map[string]float64) 638 639 for _, data := range datas { 640 coveredLines[data.Module] += data.CoveredLines 641 totalLines[data.Module] += data.TotalLines 642 coveredPCsInFile[data.Module] += data.CoveredPCs 643 totalPCsInFile[data.Module] += data.TotalPCs 644 totalFuncs[data.Module] += data.TotalFunctions 645 coveredFuncs[data.Module] += data.CoveredFunctions 646 coveredPCsInFuncs[data.Module] += data.CoveredPCsInFunctions 647 pcsInFuncs[data.Module] += data.TotalPCsInFunctions 648 pcsInCoveredFuncs[data.Module] += data.TotalPCsInCoveredFunctions 649 } 650 651 for m := range totalLines { 652 if totalLines[m] != 0 { 653 percentLines[m] = 100.0 * float64(coveredLines[m]) / float64(totalLines[m]) 654 } 655 if totalPCsInFile[m] != 0 { 656 percentPCsInFile[m] = 100.0 * float64(coveredPCsInFile[m]) / float64(totalPCsInFile[m]) 657 } 658 if pcsInFuncs[m] != 0 { 659 percentPCsInFunc[m] = 100.0 * float64(coveredPCsInFuncs[m]) / float64(pcsInFuncs[m]) 660 } 661 if pcsInCoveredFuncs[m] != 0 { 662 percentInCoveredFunc[m] = 100.0 * float64(coveredPCsInFuncs[m]) / float64(pcsInCoveredFuncs[m]) 663 } 664 if totalFuncs[m] != 0 { 665 percentCoveredFunc[m] = 100.0 * float64(coveredFuncs[m]) / float64(totalFuncs[m]) 666 } 667 d[m] = map[string]string{ 668 "name": m, 669 "lines": fmt.Sprintf("%v / %v / %.2f%%", 670 coveredLines[m], totalLines[m], percentLines[m]), 671 "PCsInFiles": fmt.Sprintf("%v / %v / %.2f%%", 672 coveredPCsInFile[m], totalPCsInFile[m], percentPCsInFile[m]), 673 "Funcs": fmt.Sprintf("%v / %v / %.2f%%", 674 coveredFuncs[m], totalFuncs[m], percentCoveredFunc[m]), 675 "PCsInFuncs": fmt.Sprintf("%v / %v / %.2f%%", 676 coveredPCsInFuncs[m], pcsInFuncs[m], percentPCsInFunc[m]), 677 "PCsInCoveredFuncs": fmt.Sprintf("%v / %v / %.2f%%", 678 coveredPCsInFuncs[m], pcsInCoveredFuncs[m], percentInCoveredFunc[m]), 679 } 680 } 681 682 return d 683 } 684 685 func (rg *ReportGenerator) DoModuleCover(w io.Writer, params HandlerParams) error { 686 var progs = fixUpPCs(params.Progs, params.Filter) 687 data, err := rg.convertToStats(progs) 688 if err != nil { 689 return err 690 } 691 692 d := groupCoverByModule(data) 693 694 return coverTableTemplate.Execute(w, d) 695 } 696 697 var csvHeader = []string{ 698 "Module", 699 "Filename", 700 "Function", 701 "Covered PCs", 702 "Total PCs", 703 } 704 705 func (rg *ReportGenerator) DoFuncCover(w io.Writer, params HandlerParams) error { 706 var progs = fixUpPCs(params.Progs, params.Filter) 707 files, err := rg.prepareFileMap(progs, params.Force, params.Debug) 708 if err != nil { 709 return err 710 } 711 var data [][]string 712 for fname, file := range files { 713 for _, function := range file.functions { 714 data = append(data, []string{ 715 file.module, 716 fname, 717 function.name, 718 strconv.Itoa(function.covered), 719 strconv.Itoa(function.pcs), 720 }) 721 } 722 } 723 sort.Slice(data, func(i, j int) bool { 724 if data[i][0] != data[j][0] { 725 return data[i][0] < data[j][0] 726 } 727 return data[i][1] < data[j][1] 728 }) 729 writer := csv.NewWriter(w) 730 defer writer.Flush() 731 if err := writer.Write(csvHeader); err != nil { 732 return err 733 } 734 return writer.WriteAll(data) 735 } 736 737 func fixUpPCs(progs []Prog, coverFilter map[uint64]struct{}) []Prog { 738 if coverFilter != nil { 739 for i, prog := range progs { 740 var nPCs []uint64 741 for _, pc := range prog.PCs { 742 if _, ok := coverFilter[pc]; ok { 743 nPCs = append(nPCs, pc) 744 } 745 } 746 progs[i].PCs = nPCs 747 } 748 } 749 return progs 750 } 751 752 func fileContents(file *file, lines [][]byte, haveProgs bool) string { 753 var buf bytes.Buffer 754 lineCover := perLineCoverage(file.covered, file.uncovered) 755 htmlReplacer := strings.NewReplacer(">", ">", "<", "<", "&", "&", "\t", " ") 756 buf.WriteString("<table><tr><td class='count'>") 757 for i := range lines { 758 if haveProgs { 759 prog, count := "", " " 760 if line := file.lines[i+1]; len(line.progCount) != 0 { 761 prog = fmt.Sprintf("onclick='onProgClick(%v, this)'", line.progIndex) 762 count = fmt.Sprintf("% 5v", len(line.progCount)) 763 buf.WriteString(fmt.Sprintf("<span %v>%v</span> ", prog, count)) 764 } 765 buf.WriteByte('\n') 766 } 767 } 768 buf.WriteString("</td><td>") 769 for i := range lines { 770 buf.WriteString(fmt.Sprintf("%d\n", i+1)) 771 } 772 buf.WriteString("</td><td>") 773 for i, ln := range lines { 774 start := 0 775 cover := append(lineCover[i+1], lineCoverChunk{End: backend.LineEnd}) 776 for _, cov := range cover { 777 end := min(cov.End-1, len(ln)) 778 if end == start { 779 continue 780 } 781 chunk := htmlReplacer.Replace(string(ln[start:end])) 782 start = end 783 class := "" 784 if cov.Covered && cov.Uncovered { 785 class = "both" 786 } else if cov.Covered { 787 class = "covered" 788 } else if cov.Uncovered { 789 class = "uncovered" 790 } else { 791 buf.WriteString(chunk) 792 continue 793 } 794 buf.WriteString(fmt.Sprintf("<span class='%v'>%v</span>", class, chunk)) 795 } 796 buf.WriteByte('\n') 797 } 798 buf.WriteString("</td></tr></table>") 799 return buf.String() 800 } 801 802 type lineCoverChunk struct { 803 End int 804 Covered bool 805 Uncovered bool 806 } 807 808 func perLineCoverage(covered, uncovered []backend.Range) map[int][]lineCoverChunk { 809 lines := make(map[int][]lineCoverChunk) 810 for _, r := range covered { 811 mergeRange(lines, r, true) 812 } 813 for _, r := range uncovered { 814 mergeRange(lines, r, false) 815 } 816 return lines 817 } 818 819 func mergeRange(lines map[int][]lineCoverChunk, r backend.Range, covered bool) { 820 // Don't panic on broken debug info, it is frequently broken. 821 r.EndLine = max(r.EndLine, r.StartLine) 822 if r.EndLine == r.StartLine && r.EndCol <= r.StartCol { 823 r.EndCol = backend.LineEnd 824 } 825 for line := r.StartLine; line <= r.EndLine; line++ { 826 start := 0 827 if line == r.StartLine { 828 start = r.StartCol 829 } 830 end := backend.LineEnd 831 if line == r.EndLine { 832 end = r.EndCol 833 } 834 ln := lines[line] 835 if ln == nil { 836 ln = append(ln, lineCoverChunk{End: backend.LineEnd}) 837 } 838 lines[line] = mergeLine(ln, start, end, covered) 839 } 840 } 841 842 func mergeLine(chunks []lineCoverChunk, start, end int, covered bool) []lineCoverChunk { 843 var res []lineCoverChunk 844 chunkStart := 0 845 for _, chunk := range chunks { 846 if chunkStart >= end || chunk.End <= start { 847 res = append(res, chunk) 848 } else if covered && chunk.Covered || !covered && chunk.Uncovered { 849 res = append(res, chunk) 850 } else if chunkStart >= start && chunk.End <= end { 851 if covered { 852 chunk.Covered = true 853 } else { 854 chunk.Uncovered = true 855 } 856 res = append(res, chunk) 857 } else { 858 if chunkStart < start { 859 res = append(res, lineCoverChunk{start, chunk.Covered, chunk.Uncovered}) 860 } 861 mid := min(end, chunk.End) 862 res = append(res, lineCoverChunk{mid, chunk.Covered || covered, chunk.Uncovered || !covered}) 863 if chunk.End > end { 864 res = append(res, lineCoverChunk{chunk.End, chunk.Covered, chunk.Uncovered}) 865 } 866 } 867 chunkStart = chunk.End 868 } 869 return res 870 } 871 872 func addFunctionCoverage(file *file, data *templateData) { 873 var buf bytes.Buffer 874 var coveredTotal int 875 var TotalInCoveredFunc int 876 for _, function := range file.functions { 877 percentage := "" 878 coveredTotal += function.covered 879 if function.covered > 0 { 880 percentage = fmt.Sprintf("%v%%", Percent(function.covered, function.pcs)) 881 TotalInCoveredFunc += function.pcs 882 } else { 883 percentage = "---" 884 } 885 buf.WriteString(fmt.Sprintf("<span class='hover'>%v", function.name)) 886 buf.WriteString(fmt.Sprintf("<span class='cover hover'>%v", percentage)) 887 buf.WriteString(fmt.Sprintf("<span class='cover-right'>of %v", strconv.Itoa(function.pcs))) 888 buf.WriteString("</span></span></span><br>\n") 889 } 890 buf.WriteString("-----------<br>\n") 891 buf.WriteString("<span class='hover'>SUMMARY") 892 percentInCoveredFunc := "" 893 if TotalInCoveredFunc > 0 { 894 percentInCoveredFunc = fmt.Sprintf("%v%%", Percent(coveredTotal, TotalInCoveredFunc)) 895 } else { 896 percentInCoveredFunc = "---" 897 } 898 buf.WriteString(fmt.Sprintf("<span class='cover hover'>%v", percentInCoveredFunc)) 899 buf.WriteString(fmt.Sprintf("<span class='cover-right'>of %v", strconv.Itoa(TotalInCoveredFunc))) 900 buf.WriteString("</span></span></span><br>\n") 901 data.Functions = append(data.Functions, template.HTML(buf.String())) 902 } 903 904 func processDir(dir *templateDir) { 905 for len(dir.Dirs) == 1 && len(dir.Files) == 0 { 906 for _, child := range dir.Dirs { 907 dir.Name += "/" + child.Name 908 dir.Files = child.Files 909 dir.Dirs = child.Dirs 910 } 911 } 912 sort.Slice(dir.Files, func(i, j int) bool { 913 return dir.Files[i].Name < dir.Files[j].Name 914 }) 915 for _, f := range dir.Files { 916 dir.Total += f.Total 917 dir.Covered += f.Covered 918 f.Percent = Percent(f.Covered, f.Total) 919 } 920 for _, child := range dir.Dirs { 921 processDir(child) 922 dir.Total += child.Total 923 dir.Covered += child.Covered 924 } 925 dir.Percent = Percent(dir.Covered, dir.Total) 926 if dir.Covered == 0 { 927 dir.Dirs = nil 928 dir.Files = nil 929 } 930 } 931 932 func Percent[T int | int64](covered, total T) T { 933 if total == 0 { 934 return 0 935 } 936 f := math.Ceil(float64(covered) / float64(total) * 100) 937 if f == 100 && covered < total { 938 f = 99 939 } 940 return T(f) 941 } 942 943 func parseFile(fn string) ([][]byte, error) { 944 data, err := os.ReadFile(fn) 945 if err != nil { 946 return nil, err 947 } 948 var lines [][]byte 949 for { 950 idx := bytes.IndexByte(data, '\n') 951 if idx == -1 { 952 break 953 } 954 lines = append(lines, data[:idx]) 955 data = data[idx+1:] 956 } 957 if len(data) != 0 { 958 lines = append(lines, data) 959 } 960 return lines, nil 961 } 962 963 type templateData struct { 964 Root *templateDir 965 Contents []template.HTML 966 Progs []templateProg 967 Functions []template.HTML 968 RawCover bool 969 } 970 971 type templateProg struct { 972 Sig string 973 Content template.HTML 974 } 975 976 type templateBase struct { 977 Name string 978 Path string 979 Total int 980 Covered int 981 Percent int 982 } 983 984 type templateDir struct { 985 templateBase 986 Dirs map[string]*templateDir 987 Files []*templateFile 988 } 989 990 type templateFile struct { 991 templateBase 992 Index int 993 HasFunctions bool 994 } 995 996 //go:embed templates/cover.html 997 var templatesCover string 998 999 var coverTemplate = template.Must(template.New("").Parse(templatesCover)) 1000 1001 //go:embed templates/cover-table.html 1002 var templatesCoverTable string 1003 var coverTableTemplate = template.Must(template.New("coverTable").Parse(templatesCoverTable))