Skip to content

Commit a3e20fd

Browse files
committed
dashboard/app: test coverage /file link
1. Init 1 coveragedb client per AppEngine instance. 2. Always init coverage handlers. It simplifies testing. 3. Read webGit from ctx to make it mockable. 4. Read coveragedb client from ctx to make it mockable. 5. Use int for file line number and int64 for merged coverage.
1 parent de67257 commit a3e20fd

File tree

17 files changed

+354
-44
lines changed

17 files changed

+354
-44
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ generate_go: format_cpp
236236
$(GO) generate ./executor ./pkg/ifuzz ./pkg/build ./pkg/rpcserver
237237
$(GO) generate ./vm/proxyapp
238238
$(GO) generate ./pkg/coveragedb
239+
$(GO) generate ./pkg/covermerger
239240

240241
generate_rpc:
241242
flatc -o pkg/flatrpc --warnings-as-errors --gen-object-api --filename-suffix "" --go --gen-onefile --go-namespace flatrpc pkg/flatrpc/flatrpc.fbs

dashboard/app/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ func installConfig(cfg *GlobalConfig) {
425425
initAPIHandlers()
426426
initKcidb()
427427
initBatchProcessors()
428+
initCoverageDB()
428429
}
429430

430431
var contextConfigKey = "Updated config (to be used during tests). Use only in tests!"

dashboard/app/coverage.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"html/template"
1010
"net/http"
11+
"os"
1112
"slices"
1213
"strconv"
1314

@@ -17,8 +18,25 @@ import (
1718
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1819
"github.com/google/syzkaller/pkg/covermerger"
1920
"github.com/google/syzkaller/pkg/validator"
21+
"golang.org/x/exp/maps"
2022
)
2123

24+
var coverageDBClient spannerclient.SpannerClient
25+
26+
func initCoverageDB() {
27+
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
28+
if projectID == "" {
29+
// It is a test environment.
30+
// Use SetCoverageDBClient to specify the coveragedb mock or emulator in every test.
31+
return
32+
}
33+
var err error
34+
coverageDBClient, err = spannerclient.NewClient(context.Background(), projectID)
35+
if err != nil {
36+
panic("spanner.NewClient: " + err.Error())
37+
}
38+
}
39+
2240
type funcStyleBodyJS func(
2341
ctx context.Context, client spannerclient.SpannerClient,
2442
scope *cover.SelectScope, onlyUnique bool, sss, managers []string,
@@ -37,6 +55,10 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
3755
if err != nil {
3856
return err
3957
}
58+
nsConfig := getNsConfig(c, hdr.Namespace)
59+
if nsConfig.Coverage == nil {
60+
return ErrClientNotFound
61+
}
4062
ss := r.FormValue("subsystem")
4163
manager := r.FormValue("manager")
4264

@@ -127,7 +149,8 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
127149
targetCommit := r.FormValue("commit")
128150
kernelFilePath := r.FormValue("filepath")
129151
manager := r.FormValue("manager")
130-
allowedManagers, err := CachedManagerList(c, hdr.Namespace)
152+
// It is easier to test than the CachedManagerList.
153+
allowedManagers := maps.Keys(nsConfig.Managers)
131154
if err != nil {
132155
return fmt.Errorf("CachedManagerList: %w", err)
133156
}
@@ -150,24 +173,31 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
150173
}
151174
onlyUnique := r.FormValue("unique-only") == "1"
152175
mainNsRepo, _ := nsConfig.mainRepoBranch()
153-
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
176+
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, kernelFilePath, manager, tp)
154177
covMap := cover.MakeCovMap(hitLines, hitCounts)
155178
if err != nil {
156179
return fmt.Errorf("coveragedb.ReadLinesHitCount(%s): %w", manager, err)
157180
}
158181
if onlyUnique {
159-
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
182+
// This request is expected to be made second by tests.
183+
// Moving it to goroutine don't forget to change multiManagerCovDBFixture.
184+
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, kernelFilePath, "*", tp)
160185
if err != nil {
161186
return fmt.Errorf("coveragedb.ReadLinesHitCount(*): %w", err)
162187
}
163188
covMap = cover.UniqCoverage(cover.MakeCovMap(allHitLines, allHitCounts), covMap)
164189
}
165190

191+
webGit := getWebGit(c) // Get mock if available.
192+
if webGit == nil {
193+
webGit = covermerger.MakeWebGit(makeProxyURIProvider(nsConfig.Coverage.WebGitURI))
194+
}
195+
166196
content, err := cover.RendFileCoverage(
167197
mainNsRepo,
168198
targetCommit,
169199
kernelFilePath,
170-
makeProxyURIProvider(nsConfig.Coverage.WebGitURI),
200+
webGit,
171201
&covermerger.MergeResult{HitCounts: covMap},
172202
cover.DefaultHTMLRenderConfig())
173203
if err != nil {
@@ -178,11 +208,26 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
178208
return nil
179209
}
180210

211+
var keyWebGit = "file content provider"
212+
213+
func setWebGit(ctx context.Context, provider covermerger.FileVersProvider) context.Context {
214+
return context.WithValue(ctx, &keyWebGit, provider)
215+
}
216+
217+
func getWebGit(ctx context.Context) covermerger.FileVersProvider {
218+
res, _ := ctx.Value(&keyWebGit).(covermerger.FileVersProvider)
219+
return res
220+
}
221+
181222
func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Request) error {
182223
hdr, err := commonHeader(c, r, w, "")
183224
if err != nil {
184225
return err
185226
}
227+
nsConfig := getNsConfig(c, hdr.Namespace)
228+
if nsConfig.Coverage == nil {
229+
return ErrClientNotFound
230+
}
186231
periodType := r.FormValue("period")
187232
if periodType == "" {
188233
periodType = coveragedb.QuarterPeriod

dashboard/app/coverage_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
"strings"
8+
"testing"
9+
10+
"github.com/google/syzkaller/pkg/coveragedb"
11+
"github.com/google/syzkaller/pkg/coveragedb/mocks"
12+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
13+
"github.com/google/syzkaller/pkg/covermerger"
14+
mergermocks "github.com/google/syzkaller/pkg/covermerger/mocks"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/mock"
17+
"google.golang.org/api/iterator"
18+
)
19+
20+
func TestFileCoverage(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
covDB func(t *testing.T) spannerclient.SpannerClient
24+
fileProv covermerger.FileVersProvider
25+
url string
26+
wantInRes []string
27+
}{
28+
{
29+
name: "empty db",
30+
covDB: func(t *testing.T) spannerclient.SpannerClient { return emptyCoverageDBFixture(t, 1) },
31+
fileProv: staticFileProvider(t),
32+
url: "/test2/graph/coverage/file?dateto=2025-01-31&period=month&commit=c0e75905caf368e19aab585d20151500e750de89&filepath=virt/kvm/kvm_main.c",
33+
wantInRes: []string{"1 line1"},
34+
},
35+
{
36+
name: "regular db",
37+
covDB: func(t *testing.T) spannerclient.SpannerClient { return coverageDBFixture(t) },
38+
fileProv: staticFileProvider(t),
39+
url: "/test2/graph/coverage/file?dateto=2025-01-31&period=month&commit=c0e75905caf368e19aab585d20151500e750de89&filepath=virt/kvm/kvm_main.c",
40+
wantInRes: []string{
41+
"4 1 line1",
42+
"5 2 line2",
43+
"6 3 line3"},
44+
},
45+
{
46+
name: "multimanager db",
47+
covDB: func(t *testing.T) spannerclient.SpannerClient { return multiManagerCovDBFixture(t) },
48+
fileProv: staticFileProvider(t),
49+
url: "/test2/graph/coverage/file?dateto=2025-01-31&period=month&commit=c0e75905caf368e19aab585d20151500e750de89&filepath=virt/kvm/kvm_main.c&manager=special-cc-manager&unique-only=1",
50+
wantInRes: []string{
51+
" 0 1 line1", // Covered, is not unique.
52+
" 5 2 line2", // Covered and is unique.
53+
" 3 line3", // Covered only by "*" managers.
54+
},
55+
},
56+
}
57+
for _, test := range tests {
58+
t.Run(test.name, func(t *testing.T) {
59+
c := NewCtx(t)
60+
defer c.Close()
61+
c.setCoverageMocks("test2", test.covDB(t), test.fileProv)
62+
fileCovPage, err := c.GET(test.url)
63+
assert.NoError(t, err)
64+
got := string(fileCovPage)
65+
for _, want := range test.wantInRes {
66+
if !strings.Contains(got, want) {
67+
t.Errorf(`"%s" wasn't found in "%s"'`, want, got)
68+
}
69+
}
70+
})
71+
}
72+
}
73+
74+
func staticFileProvider(t *testing.T) covermerger.FileVersProvider {
75+
m := mergermocks.NewFileVersProvider(t)
76+
m.On("GetFileVersions", mock.Anything, mock.Anything).
77+
Return(func(targetFilePath string, repoCommits ...covermerger.RepoCommit,
78+
) covermerger.FileVersions {
79+
res := covermerger.FileVersions{}
80+
for _, rc := range repoCommits {
81+
res[rc] = `line1
82+
line2
83+
line3`
84+
}
85+
return res
86+
}, nil)
87+
return m
88+
}
89+
90+
func emptyCoverageDBFixture(t *testing.T, times int) spannerclient.SpannerClient {
91+
mRowIterator := mocks.NewRowIterator(t)
92+
mRowIterator.On("Stop").Return().Times(times)
93+
mRowIterator.On("Next").
94+
Return(nil, iterator.Done).Times(times)
95+
96+
mTran := mocks.NewReadOnlyTransaction(t)
97+
mTran.On("Query", mock.Anything, mock.Anything).
98+
Return(mRowIterator).Times(times)
99+
100+
m := mocks.NewSpannerClient(t)
101+
m.On("Single").
102+
Return(mTran).Times(times)
103+
return m
104+
}
105+
106+
func coverageDBFixture(t *testing.T) spannerclient.SpannerClient {
107+
mRowIt := newRowIteratorMock(t, []*coveragedb.LinesCoverage{{
108+
LinesInstrumented: []int64{1, 2, 3},
109+
HitCounts: []int64{4, 5, 6},
110+
}})
111+
112+
mTran := mocks.NewReadOnlyTransaction(t)
113+
mTran.On("Query", mock.Anything, mock.Anything).
114+
Return(mRowIt).Once()
115+
116+
m := mocks.NewSpannerClient(t)
117+
m.On("Single").
118+
Return(mTran).Once()
119+
return m
120+
}
121+
122+
func multiManagerCovDBFixture(t *testing.T) spannerclient.SpannerClient {
123+
mReadFullCoverageTran := mocks.NewReadOnlyTransaction(t)
124+
mReadFullCoverageTran.On("Query", mock.Anything, mock.Anything).
125+
Return(newRowIteratorMock(t, []*coveragedb.LinesCoverage{{
126+
LinesInstrumented: []int64{1, 2, 3},
127+
HitCounts: []int64{4, 5, 6},
128+
}})).Once()
129+
130+
mReadPartialCoverageTran := mocks.NewReadOnlyTransaction(t)
131+
mReadPartialCoverageTran.On("Query", mock.Anything, mock.Anything).
132+
Return(newRowIteratorMock(t, []*coveragedb.LinesCoverage{{
133+
LinesInstrumented: []int64{1, 2},
134+
HitCounts: []int64{3, 5},
135+
}})).Once()
136+
137+
m := mocks.NewSpannerClient(t)
138+
// The order matters. Full coverage is fetched second.
139+
m.On("Single").
140+
Return(mReadPartialCoverageTran).Once()
141+
m.On("Single").
142+
Return(mReadFullCoverageTran).Once()
143+
144+
return m
145+
}
146+
147+
func newRowIteratorMock(t *testing.T, cov []*coveragedb.LinesCoverage,
148+
) *mocks.RowIterator {
149+
m := mocks.NewRowIterator(t)
150+
m.On("Stop").Once().Return()
151+
for _, item := range cov {
152+
mRow := mocks.NewRow(t)
153+
mRow.On("ToStruct", mock.Anything).
154+
Run(func(args mock.Arguments) {
155+
arg := args.Get(0).(*coveragedb.LinesCoverage)
156+
*arg = *item
157+
}).
158+
Return(nil).Once()
159+
160+
m.On("Next").
161+
Return(mRow, nil).Once()
162+
}
163+
164+
m.On("Next").
165+
Return(nil, iterator.Done).Once()
166+
return m
167+
}

dashboard/app/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616
"time"
1717

18+
"github.com/google/syzkaller/pkg/coveragedb"
1819
"github.com/google/syzkaller/pkg/html"
1920
"google.golang.org/appengine/v2"
2021
"google.golang.org/appengine/v2/log"
@@ -32,6 +33,7 @@ func handlerWrapper(fn contextHandler) http.Handler {
3233
func handleContext(fn contextHandler) http.Handler {
3334
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3435
c := appengine.NewContext(r)
36+
c = coveragedb.SetCoverageDBClient(c, coverageDBClient)
3537
c = context.WithValue(c, &currentURLKey, r.URL.RequestURI())
3638
authorizedUser, _ := userAccessLevel(currentUser(c), "", getConfig(c))
3739
if !authorizedUser {

dashboard/app/main.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,11 @@ func initHTTPHandlers() {
6565
http.Handle("/"+ns+"/graph/fuzzing", handlerWrapper(handleGraphFuzzing))
6666
http.Handle("/"+ns+"/graph/crashes", handlerWrapper(handleGraphCrashes))
6767
http.Handle("/"+ns+"/graph/found-bugs", handlerWrapper(handleFoundBugsGraph))
68-
if nsConfig.Coverage != nil {
69-
http.Handle("/"+ns+"/graph/coverage/file", handlerWrapper(handleFileCoverage))
70-
http.Handle("/"+ns+"/graph/coverage", handlerWrapper(handleCoverageGraph))
71-
http.Handle("/"+ns+"/graph/coverage_heatmap", handlerWrapper(handleCoverageHeatmap))
72-
if nsConfig.Subsystems.Service != nil {
73-
http.Handle("/"+ns+"/graph/coverage_subsystems_heatmap", handlerWrapper(handleSubsystemsCoverageHeatmap))
74-
}
68+
http.Handle("/"+ns+"/graph/coverage/file", handlerWrapper(handleFileCoverage))
69+
http.Handle("/"+ns+"/graph/coverage", handlerWrapper(handleCoverageGraph))
70+
http.Handle("/"+ns+"/graph/coverage_heatmap", handlerWrapper(handleCoverageHeatmap))
71+
if nsConfig.Subsystems.Service != nil {
72+
http.Handle("/"+ns+"/graph/coverage_subsystems_heatmap", handlerWrapper(handleSubsystemsCoverageHeatmap))
7573
}
7674
http.Handle("/"+ns+"/repos", handlerWrapper(handleRepos))
7775
http.Handle("/"+ns+"/bug-summaries", handlerWrapper(handleBugSummaries))

dashboard/app/util_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import (
2828
"github.com/google/go-cmp/cmp"
2929
"github.com/google/syzkaller/dashboard/api"
3030
"github.com/google/syzkaller/dashboard/dashapi"
31+
"github.com/google/syzkaller/pkg/coveragedb"
32+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
33+
"github.com/google/syzkaller/pkg/covermerger"
3134
"github.com/google/syzkaller/pkg/email"
3235
"github.com/google/syzkaller/pkg/subsystem"
3336
"google.golang.org/appengine/v2/aetest"
@@ -226,6 +229,20 @@ func (c *Ctx) setSubsystems(ns string, list []*subsystem.Subsystem, rev int) {
226229
}
227230
}
228231

232+
func (c *Ctx) setCoverageMocks(ns string, dbClientMock spannerclient.SpannerClient,
233+
fileProvMock covermerger.FileVersProvider) {
234+
c.transformContext = func(ctx context.Context) context.Context {
235+
newConfig := replaceNamespaceConfig(ctx, ns, func(cfg *Config) *Config {
236+
ret := *cfg
237+
ret.Coverage = &CoverageConfig{WebGitURI: "test-git"}
238+
return &ret
239+
})
240+
ctxWithSpanner := coveragedb.SetCoverageDBClient(ctx, dbClientMock)
241+
ctxWithSpannerAndFileProvider := setWebGit(ctxWithSpanner, fileProvMock)
242+
return contextWithConfig(ctxWithSpannerAndFileProvider, newConfig)
243+
}
244+
}
245+
229246
func (c *Ctx) setKernelRepos(ns string, list []KernelRepo) {
230247
c.transformContext = func(c context.Context) context.Context {
231248
newConfig := replaceNamespaceConfig(c, ns, func(cfg *Config) *Config {

pkg/cover/file.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ func DefaultHTMLRenderConfig() *CoverageRenderConfig {
4040
}
4141
}
4242

43-
func RendFileCoverage(repo, forCommit, filePath string, proxy covermerger.FuncProxyURI,
43+
func RendFileCoverage(repo, forCommit, filePath string, fileProvider covermerger.FileVersProvider,
4444
mr *covermerger.MergeResult, renderConfig *CoverageRenderConfig) (string, error) {
4545
repoCommit := covermerger.RepoCommit{Repo: repo, Commit: forCommit}
46-
files, err := covermerger.MakeWebGit(proxy).GetFileVersions(filePath, repoCommit)
46+
files, err := fileProvider.GetFileVersions(filePath, repoCommit)
4747
if err != nil {
4848
return "", fmt.Errorf("failed to GetFileVersions: %w", err)
4949
}

0 commit comments

Comments
 (0)