github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/dashboard/app/coverage.go (about)

     1  // Copyright 2025 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 main
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"html/template"
    10  	"net/http"
    11  	"os"
    12  	"slices"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"cloud.google.com/go/civil"
    17  	"github.com/google/syzkaller/pkg/cover"
    18  	"github.com/google/syzkaller/pkg/coveragedb"
    19  	"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
    20  	"github.com/google/syzkaller/pkg/covermerger"
    21  	"github.com/google/syzkaller/pkg/html/urlutil"
    22  	"github.com/google/syzkaller/pkg/validator"
    23  	"google.golang.org/appengine/v2"
    24  	"google.golang.org/appengine/v2/log"
    25  )
    26  
    27  var coverageDBClient spannerclient.SpannerClient
    28  
    29  func initCoverageDB() {
    30  	if !appengine.IsAppEngine() {
    31  		// It is a test environment.
    32  		// Use setCoverageDBClient to specify the coveragedb mock or emulator in every test.
    33  		return
    34  	}
    35  	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    36  	var err error
    37  	coverageDBClient, err = spannerclient.NewClient(context.Background(), projectID)
    38  	if err != nil {
    39  		panic("spanner.NewClient: " + err.Error())
    40  	}
    41  }
    42  
    43  var keyCoverageDBClient = "coveragedb client key"
    44  
    45  func getCoverageDBClient(ctx context.Context) spannerclient.SpannerClient {
    46  	ctxClient, _ := ctx.Value(&keyCoverageDBClient).(spannerclient.SpannerClient)
    47  	if ctxClient == nil && coverageDBClient == nil {
    48  		panic("attempt to get coverage db client before it was set in tests")
    49  	}
    50  	if ctxClient != nil {
    51  		return ctxClient
    52  	}
    53  	return coverageDBClient
    54  }
    55  
    56  type funcStyleBodyJS func(
    57  	ctx context.Context, client spannerclient.SpannerClient,
    58  	scope *coveragedb.SelectScope, onlyUnique bool, sss, managers []string, dataFilters cover.Format,
    59  ) (template.CSS, template.HTML, template.HTML, error)
    60  
    61  type coverageHeatmapParams struct {
    62  	manager    string
    63  	subsystem  string
    64  	onlyUnique bool
    65  	periodType string
    66  	nPeriods   int
    67  	dateTo     civil.Date
    68  	cover.Format
    69  }
    70  
    71  const minPeriodsOnThePage = 1
    72  const maxPeriodsOnThePage = 12
    73  
    74  func makeHeatmapParams(ctx context.Context, r *http.Request) (*coverageHeatmapParams, error) {
    75  	onlyUnique := getParam[bool](r, UniqueOnly.ParamName(), false)
    76  	periodType := getParam[string](r, PeriodType.ParamName())
    77  	if !slices.Contains(coveragedb.AllPeriods, periodType) {
    78  		return nil, fmt.Errorf("only {%s} are allowed, but received %s instead, %w",
    79  			strings.Join(coveragedb.AllPeriods, ", "), periodType, ErrClientBadRequest)
    80  	}
    81  	nPeriods := getParam[int](r, PeriodCount.ParamName(), 4)
    82  	if nPeriods > maxPeriodsOnThePage || nPeriods < minPeriodsOnThePage {
    83  		return nil, fmt.Errorf("periods_count is wrong, expected [%d, %d]",
    84  			minPeriodsOnThePage, maxPeriodsOnThePage)
    85  	}
    86  
    87  	return &coverageHeatmapParams{
    88  		manager:    getParam[string](r, ManagerName.ParamName()),
    89  		subsystem:  getParam[string](r, SubsystemName.ParamName()),
    90  		onlyUnique: onlyUnique,
    91  		periodType: periodType,
    92  		nPeriods:   nPeriods,
    93  		dateTo:     getParam[civil.Date](r, DateTo.ParamName(), civil.DateOf(timeNow(ctx))),
    94  		Format: cover.Format{
    95  			DropCoveredLines0:         onlyUnique,
    96  			OrderByCoveredLinesDrop:   getParam[bool](r, OrderByCoverDrop.ParamName()),
    97  			FilterMinCoveredLinesDrop: getParam[int](r, MinCoverLinesDrop.ParamName()),
    98  		},
    99  	}, nil
   100  }
   101  
   102  func getParam[T int | string | bool | civil.Date](r *http.Request, name string, orDefault ...T) T {
   103  	var def T
   104  	if len(orDefault) > 0 {
   105  		def = orDefault[0]
   106  	}
   107  	if r.FormValue(name) == "" {
   108  		return def
   109  	}
   110  	var t T
   111  	return extractVal(t, r.FormValue(name)).(T)
   112  }
   113  
   114  func extractVal(t interface{}, val string) interface{} {
   115  	switch t.(type) {
   116  	case int:
   117  		res, _ := strconv.Atoi(val)
   118  		return res
   119  	case string:
   120  		return val
   121  	case bool:
   122  		res, _ := strconv.ParseBool(val)
   123  		return res
   124  	case civil.Date:
   125  		res, _ := civil.ParseDate(val)
   126  		return res
   127  	}
   128  	panic("unsupported type")
   129  }
   130  
   131  func handleCoverageHeatmap(c context.Context, w http.ResponseWriter, r *http.Request) error {
   132  	hdr, err := commonHeader(c, r, w, "")
   133  	if err != nil {
   134  		return err
   135  	}
   136  	params, err := makeHeatmapParams(c, r)
   137  	if err != nil {
   138  		return fmt.Errorf("%s: %w", err.Error(), ErrClientBadRequest)
   139  	}
   140  	if getParam[bool](r, "jsonl") {
   141  		ns := hdr.Namespace
   142  		repo, _ := getNsConfig(c, ns).mainRepoBranch()
   143  		w.Header().Set("Content-Type", "application/json")
   144  		return writeExtAPICoverageFor(c, w, ns, repo, params)
   145  	}
   146  	return handleHeatmap(c, w, hdr, params, cover.DoHeatMapStyleBodyJS)
   147  }
   148  
   149  func handleSubsystemsCoverageHeatmap(c context.Context, w http.ResponseWriter, r *http.Request) error {
   150  	hdr, err := commonHeader(c, r, w, "")
   151  	if err != nil {
   152  		return err
   153  	}
   154  	params, err := makeHeatmapParams(c, r)
   155  	if err != nil {
   156  		return fmt.Errorf("%s: %w", err.Error(), ErrClientBadRequest)
   157  	}
   158  	return handleHeatmap(c, w, hdr, params, cover.DoSubsystemsHeatMapStyleBodyJS)
   159  }
   160  
   161  type covPageParam string
   162  
   163  func (p covPageParam) ParamName() string {
   164  	return string(p)
   165  }
   166  
   167  const (
   168  	// keep-sorted start
   169  	CommitHash        = covPageParam("commit")
   170  	DateTo            = covPageParam("dateto")
   171  	FilePath          = covPageParam("filepath")
   172  	ManagerName       = covPageParam("manager")
   173  	MinCoverLinesDrop = covPageParam("min-cover-lines-drop")
   174  	OrderByCoverDrop  = covPageParam("order-by-cover-lines-drop")
   175  	PeriodCount       = covPageParam("period_count")
   176  	PeriodType        = covPageParam("period")
   177  	SubsystemName     = covPageParam("subsystem")
   178  	UniqueOnly        = covPageParam("unique-only")
   179  	// keep-sorted end
   180  )
   181  
   182  func coveragePageLink(ns, periodType, dateTo string, minDrop, periodCount int, orderByCoverDrop bool) string {
   183  	if periodType == "" {
   184  		periodType = coveragedb.MonthPeriod
   185  	}
   186  	url := "/" + ns + "/coverage"
   187  	url = urlutil.SetParam(url, PeriodType.ParamName(), periodType)
   188  	if periodCount != 0 {
   189  		url = urlutil.SetParam(url, PeriodCount.ParamName(), strconv.Itoa(periodCount))
   190  	}
   191  	if dateTo != "" {
   192  		url = urlutil.SetParam(url, DateTo.ParamName(), dateTo)
   193  	}
   194  	if minDrop > 0 {
   195  		url = urlutil.SetParam(url, MinCoverLinesDrop.ParamName(), strconv.Itoa(minDrop))
   196  	}
   197  	if orderByCoverDrop {
   198  		url = urlutil.SetParam(url, OrderByCoverDrop.ParamName(), "1")
   199  	}
   200  	return url
   201  }
   202  
   203  func handleHeatmap(c context.Context, w http.ResponseWriter, hdr *uiHeader, p *coverageHeatmapParams,
   204  	f funcStyleBodyJS) error {
   205  	nsConfig := getNsConfig(c, hdr.Namespace)
   206  	if nsConfig.Coverage == nil {
   207  		return ErrClientNotFound
   208  	}
   209  
   210  	periods, err := coveragedb.GenNPeriodsTill(p.nPeriods, p.dateTo, p.periodType)
   211  	if err != nil {
   212  		return fmt.Errorf("%s: %w", err.Error(), ErrClientBadRequest)
   213  	}
   214  	managers, err := CachedManagerList(c, hdr.Namespace)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	var subsystems []string
   219  	if ssService := getNsConfig(c, hdr.Namespace).Subsystems.Service; ssService != nil {
   220  		for _, s := range ssService.List() {
   221  			subsystems = append(subsystems, s.Name)
   222  		}
   223  	}
   224  	slices.Sort(managers)
   225  	slices.Sort(subsystems)
   226  
   227  	var style template.CSS
   228  	var body, js template.HTML
   229  	if style, body, js, err = f(c, getCoverageDBClient(c),
   230  		&coveragedb.SelectScope{
   231  			Ns:        hdr.Namespace,
   232  			Subsystem: p.subsystem,
   233  			Manager:   p.manager,
   234  			Periods:   periods,
   235  		},
   236  		p.onlyUnique, subsystems, managers, p.Format,
   237  	); err != nil {
   238  		return fmt.Errorf("failed to generate heatmap: %w", err)
   239  	}
   240  	return serveTemplate(w, "custom_content.html", struct {
   241  		Header *uiHeader
   242  		*cover.StyleBodyJS
   243  	}{
   244  		Header: hdr,
   245  		StyleBodyJS: &cover.StyleBodyJS{
   246  			Style: style,
   247  			Body:  body,
   248  			JS:    js,
   249  		},
   250  	})
   251  }
   252  
   253  func makeProxyURIProvider(url string) covermerger.FuncProxyURI {
   254  	return func(filePath, commit string) string {
   255  		// Parameter format=TEXT is ignored by git servers but is processed by gerrit servers.
   256  		// Gerrit returns base64 encoded data.
   257  		// Git return the plain text data.
   258  		return fmt.Sprintf("%s/%s/%s?format=TEXT", url, commit, filePath)
   259  	}
   260  }
   261  
   262  func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Request) error {
   263  	hdr, err := commonHeader(c, r, w, "")
   264  	if err != nil {
   265  		return err
   266  	}
   267  	nsConfig := getNsConfig(c, hdr.Namespace)
   268  	if nsConfig.Coverage == nil || nsConfig.Coverage.WebGitURI == "" {
   269  		return ErrClientNotFound
   270  	}
   271  	dateToStr := r.FormValue(DateTo.ParamName())
   272  	periodType := r.FormValue(PeriodType.ParamName())
   273  	targetCommit := r.FormValue(CommitHash.ParamName())
   274  	kernelFilePath := r.FormValue(FilePath.ParamName())
   275  	manager := r.FormValue(ManagerName.ParamName())
   276  	if err := validator.AnyError("input validation failed",
   277  		validator.TimePeriodType(periodType, PeriodType.ParamName()),
   278  		validator.CommitHash(targetCommit, CommitHash.ParamName()),
   279  		validator.KernelFilePath(kernelFilePath, FilePath.ParamName()),
   280  		validator.AnyOk(
   281  			validator.Allowlisted(manager, []string{"", "*"}, ManagerName.ParamName()),
   282  			validator.ManagerName(manager, ManagerName.ParamName())),
   283  	); err != nil {
   284  		return fmt.Errorf("%w: %w", err, ErrClientBadRequest)
   285  	}
   286  	targetDate, err := civil.ParseDate(dateToStr)
   287  	if err != nil {
   288  		return fmt.Errorf("%w: civil.ParseDate(%s): %w", ErrClientBadRequest, dateToStr, err)
   289  	}
   290  	tp, err := coveragedb.MakeTimePeriod(targetDate, periodType)
   291  	if err != nil {
   292  		return fmt.Errorf("coveragedb.MakeTimePeriod: %w", err)
   293  	}
   294  	mainNsRepo, _ := nsConfig.mainRepoBranch()
   295  	client := getCoverageDBClient(c)
   296  	if client == nil {
   297  		return fmt.Errorf("spannerdb client is nil")
   298  	}
   299  	hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(
   300  		c, client, hdr.Namespace, targetCommit, kernelFilePath, manager, tp)
   301  	covMap := coveragedb.MakeCovMap(hitLines, hitCounts)
   302  	if err != nil {
   303  		return fmt.Errorf("coveragedb.ReadLinesHitCount(%s): %w", manager, err)
   304  	}
   305  	if getParam[bool](r, UniqueOnly.ParamName()) {
   306  		// This request is expected to be made second by tests.
   307  		// Moving it to goroutine don't forget to change multiManagerCovDBFixture.
   308  		allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(
   309  			c, client, hdr.Namespace, targetCommit, kernelFilePath, "*", tp)
   310  		if err != nil {
   311  			return fmt.Errorf("coveragedb.ReadLinesHitCount(*): %w", err)
   312  		}
   313  		covMap = coveragedb.UniqCoverage(coveragedb.MakeCovMap(allHitLines, allHitCounts), covMap)
   314  	}
   315  
   316  	webGit := getWebGit(c) // Get mock if available.
   317  	if webGit == nil {
   318  		webGit = covermerger.MakeWebGit(makeProxyURIProvider(nsConfig.Coverage.WebGitURI))
   319  	}
   320  
   321  	content, err := cover.RendFileCoverage(
   322  		mainNsRepo,
   323  		targetCommit,
   324  		kernelFilePath,
   325  		webGit,
   326  		&covermerger.MergeResult{HitCounts: covMap},
   327  		cover.DefaultHTMLRenderConfig())
   328  	if err != nil {
   329  		return fmt.Errorf("cover.RendFileCoverage: %w", err)
   330  	}
   331  	w.Header().Set("Content-Type", "text/html")
   332  	w.Write([]byte(content))
   333  	return nil
   334  }
   335  
   336  var keyWebGit = "file content provider"
   337  
   338  func setWebGit(ctx context.Context, provider covermerger.FileVersProvider) context.Context {
   339  	return context.WithValue(ctx, &keyWebGit, provider)
   340  }
   341  
   342  func getWebGit(ctx context.Context) covermerger.FileVersProvider {
   343  	res, _ := ctx.Value(&keyWebGit).(covermerger.FileVersProvider)
   344  	return res
   345  }
   346  
   347  func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Request) error {
   348  	hdr, err := commonHeader(c, r, w, "")
   349  	if err != nil {
   350  		return err
   351  	}
   352  	nsConfig := getNsConfig(c, hdr.Namespace)
   353  	if nsConfig.Coverage == nil {
   354  		return ErrClientNotFound
   355  	}
   356  	periodType := r.FormValue(PeriodType.ParamName())
   357  	if periodType == "" {
   358  		periodType = coveragedb.QuarterPeriod
   359  	}
   360  	if periodType != coveragedb.QuarterPeriod && periodType != coveragedb.MonthPeriod {
   361  		return fmt.Errorf("only quarter and month are allowed, but received %s instead", periodType)
   362  	}
   363  	hist, err := MergedCoverage(c, getCoverageDBClient(c), hdr.Namespace, periodType)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	periodEndDates, err := coveragedb.GenNPeriodsTill(12, civil.DateOf(timeNow(c)), periodType)
   368  	if err != nil {
   369  		return err
   370  	}
   371  	cols := []uiGraphColumn{}
   372  	for _, periodEndDate := range periodEndDates {
   373  		date := periodEndDate.DateTo.String()
   374  		if _, ok := hist.covered[date]; !ok || hist.instrumented[date] == 0 {
   375  			cols = append(cols, uiGraphColumn{Hint: date, Vals: []uiGraphValue{{IsNull: true}}})
   376  		} else {
   377  			val := float32(hist.covered[date]) / float32(hist.instrumented[date])
   378  			cols = append(cols, uiGraphColumn{
   379  				Hint:       date,
   380  				Annotation: val,
   381  				Vals:       []uiGraphValue{{Val: val}},
   382  			})
   383  		}
   384  	}
   385  	data := &uiHistogramPage{
   386  		Title:  hdr.Namespace + " coverage",
   387  		Header: hdr,
   388  		Graph: &uiGraph{
   389  			Headers: []uiGraphHeader{
   390  				{Name: "Total", Color: "Red"},
   391  			},
   392  			Columns: cols,
   393  		},
   394  	}
   395  	return serveTemplate(w, "graph_histogram.html", data)
   396  }
   397  
   398  func handleUpdateCoverDBSubsystems(w http.ResponseWriter, r *http.Request) {
   399  	ctx := r.Context()
   400  	for ns, nsConfig := range getConfig(ctx).Namespaces {
   401  		service := nsConfig.Subsystems.Service
   402  		if service == nil {
   403  			continue
   404  		}
   405  		sss := service.List()
   406  		updatedRecords, err := coveragedb.RegenerateSubsystems(ctx, ns, sss, coverageDBClient)
   407  		if err != nil {
   408  			httpErr := fmt.Errorf("ns %s: %w", ns, err)
   409  			log.Errorf(ctx, "%s", httpErr.Error())
   410  			http.Error(w, httpErr.Error(), http.StatusInternalServerError)
   411  			return
   412  		}
   413  		log.Infof(ctx, "%s: %v records updated\n", ns, updatedRecords)
   414  		fmt.Fprintf(w, "%s: %v records updated\n", ns, updatedRecords)
   415  	}
   416  }