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(">", "&gt;", "<", "&lt;", "&", "&amp;", "\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))