Skip to content

Commit e55a2da

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 e55a2da

File tree

17 files changed

+356
-47
lines changed

17 files changed

+356
-47
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: 64 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,36 @@ 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+
40+
var keyCoverageDBClient = "coveragedb client key"
41+
42+
func SetCoverageDBClient(ctx context.Context, client spannerclient.SpannerClient) context.Context {
43+
return context.WithValue(ctx, &keyCoverageDBClient, client)
44+
}
45+
46+
func GetCoverageDBClient(ctx context.Context) spannerclient.SpannerClient {
47+
client, _ := ctx.Value(&keyCoverageDBClient).(spannerclient.SpannerClient)
48+
return client
49+
}
50+
2251
type funcStyleBodyJS func(
2352
ctx context.Context, client spannerclient.SpannerClient,
2453
scope *cover.SelectScope, onlyUnique bool, sss, managers []string,
@@ -37,6 +66,10 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
3766
if err != nil {
3867
return err
3968
}
69+
nsConfig := getNsConfig(c, hdr.Namespace)
70+
if nsConfig.Coverage == nil {
71+
return ErrClientNotFound
72+
}
4073
ss := r.FormValue("subsystem")
4174
manager := r.FormValue("manager")
4275

@@ -127,7 +160,8 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
127160
targetCommit := r.FormValue("commit")
128161
kernelFilePath := r.FormValue("filepath")
129162
manager := r.FormValue("manager")
130-
allowedManagers, err := CachedManagerList(c, hdr.Namespace)
163+
// It is easier to test than the CachedManagerList.
164+
allowedManagers := maps.Keys(nsConfig.Managers)
131165
if err != nil {
132166
return fmt.Errorf("CachedManagerList: %w", err)
133167
}
@@ -150,24 +184,35 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
150184
}
151185
onlyUnique := r.FormValue("unique-only") == "1"
152186
mainNsRepo, _ := nsConfig.mainRepoBranch()
153-
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
187+
client := GetCoverageDBClient(c)
188+
if client == nil {
189+
return fmt.Errorf("spannerdb client is nil")
190+
}
191+
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, client, hdr.Namespace, targetCommit, kernelFilePath, manager, tp)
154192
covMap := cover.MakeCovMap(hitLines, hitCounts)
155193
if err != nil {
156194
return fmt.Errorf("coveragedb.ReadLinesHitCount(%s): %w", manager, err)
157195
}
158196
if onlyUnique {
159-
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
197+
// This request is expected to be made second by tests.
198+
// Moving it to goroutine don't forget to change multiManagerCovDBFixture.
199+
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, client, hdr.Namespace, targetCommit, kernelFilePath, "*", tp)
160200
if err != nil {
161201
return fmt.Errorf("coveragedb.ReadLinesHitCount(*): %w", err)
162202
}
163203
covMap = cover.UniqCoverage(cover.MakeCovMap(allHitLines, allHitCounts), covMap)
164204
}
165205

206+
webGit := getWebGit(c) // Get mock if available.
207+
if webGit == nil {
208+
webGit = covermerger.MakeWebGit(makeProxyURIProvider(nsConfig.Coverage.WebGitURI))
209+
}
210+
166211
content, err := cover.RendFileCoverage(
167212
mainNsRepo,
168213
targetCommit,
169214
kernelFilePath,
170-
makeProxyURIProvider(nsConfig.Coverage.WebGitURI),
215+
webGit,
171216
&covermerger.MergeResult{HitCounts: covMap},
172217
cover.DefaultHTMLRenderConfig())
173218
if err != nil {
@@ -178,11 +223,26 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
178223
return nil
179224
}
180225

226+
var keyWebGit = "file content provider"
227+
228+
func setWebGit(ctx context.Context, provider covermerger.FileVersProvider) context.Context {
229+
return context.WithValue(ctx, &keyWebGit, provider)
230+
}
231+
232+
func getWebGit(ctx context.Context) covermerger.FileVersProvider {
233+
res, _ := ctx.Value(&keyWebGit).(covermerger.FileVersProvider)
234+
return res
235+
}
236+
181237
func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Request) error {
182238
hdr, err := commonHeader(c, r, w, "")
183239
if err != nil {
184240
return err
185241
}
242+
nsConfig := getNsConfig(c, hdr.Namespace)
243+
if nsConfig.Coverage == nil {
244+
return ErrClientNotFound
245+
}
186246
periodType := r.FormValue("period")
187247
if periodType == "" {
188248
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ func handlerWrapper(fn contextHandler) http.Handler {
3232
func handleContext(fn contextHandler) http.Handler {
3333
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3434
c := appengine.NewContext(r)
35+
if coverageDBClient != nil { // Nil in prod.
36+
c = SetCoverageDBClient(c, coverageDBClient)
37+
}
3538
c = context.WithValue(c, &currentURLKey, r.URL.RequestURI())
3639
authorizedUser, _ := userAccessLevel(currentUser(c), "", getConfig(c))
3740
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ 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/spannerclient"
32+
"github.com/google/syzkaller/pkg/covermerger"
3133
"github.com/google/syzkaller/pkg/email"
3234
"github.com/google/syzkaller/pkg/subsystem"
3335
"google.golang.org/appengine/v2/aetest"
@@ -226,6 +228,20 @@ func (c *Ctx) setSubsystems(ns string, list []*subsystem.Subsystem, rev int) {
226228
}
227229
}
228230

231+
func (c *Ctx) setCoverageMocks(ns string, dbClientMock spannerclient.SpannerClient,
232+
fileProvMock covermerger.FileVersProvider) {
233+
c.transformContext = func(ctx context.Context) context.Context {
234+
newConfig := replaceNamespaceConfig(ctx, ns, func(cfg *Config) *Config {
235+
ret := *cfg
236+
ret.Coverage = &CoverageConfig{WebGitURI: "test-git"}
237+
return &ret
238+
})
239+
ctxWithSpanner := SetCoverageDBClient(ctx, dbClientMock)
240+
ctxWithSpannerAndFileProvider := setWebGit(ctxWithSpanner, fileProvMock)
241+
return contextWithConfig(ctxWithSpannerAndFileProvider, newConfig)
242+
}
243+
}
244+
229245
func (c *Ctx) setKernelRepos(ns string, list []KernelRepo) {
230246
c.transformContext = func(c context.Context) context.Context {
231247
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)