Skip to content

Commit bbd06db

Browse files
committed
dashboard/app: test coverage /file link
1. Init coveragedb client once and propagate it through context to enable mocking. 2. Always init coverage handlers. It simplifies testing. 3. Read webGit and coveragedb client from ctx to make it mockable. 4. Use int for file line number and int64 for merged coverage. 5. Add tests.
1 parent fa5c255 commit bbd06db

File tree

21 files changed

+373
-86
lines changed

21 files changed

+373
-86
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/api.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"github.com/google/syzkaller/pkg/asset"
2727
"github.com/google/syzkaller/pkg/auth"
2828
"github.com/google/syzkaller/pkg/coveragedb"
29-
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
3029
"github.com/google/syzkaller/pkg/debugtracer"
3130
"github.com/google/syzkaller/pkg/email"
3231
"github.com/google/syzkaller/pkg/gcs"
@@ -105,6 +104,7 @@ var maxCrashes = func() int {
105104
func handleJSON(fn JSONHandler) http.Handler {
106105
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107106
c := appengine.NewContext(r)
107+
c = SetCoverageDBClient(c, coverageDBClient)
108108
reply, err := fn(c, r)
109109
if err != nil {
110110
status := logErrorPrepareStatus(c, err)
@@ -1948,12 +1948,7 @@ func apiSaveCoverage(c context.Context, payload io.Reader) (interface{}, error)
19481948
sss = service.List()
19491949
log.Infof(c, "found %d subsystems for %s namespace", len(sss), descr.Namespace)
19501950
}
1951-
client, err := spannerclient.NewClient(c, appengine.AppID(context.Background()))
1952-
if err != nil {
1953-
return 0, fmt.Errorf("coveragedb.NewClient() failed: %s", err.Error())
1954-
}
1955-
defer client.Close()
1956-
rowsCreated, err := coveragedb.SaveMergeResult(c, client, descr, jsonDec, sss)
1951+
rowsCreated, err := coveragedb.SaveMergeResult(c, GetCoverageDBClient(c), descr, jsonDec, sss)
19571952
if err != nil {
19581953
log.Errorf(c, "error storing coverage for ns %s, date %s: %v",
19591954
descr.Namespace, descr.DateTo.String(), err)

dashboard/app/batch_coverage.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func handleBatchCoverage(w http.ResponseWriter, r *http.Request) {
4343
if err != nil {
4444
log.Errorf(ctx, "failed nsDataAvailable(%s): %s", ns, err)
4545
}
46-
periodsMerged, rowsMerged, err := coveragedb.NsDataMerged(ctx, "syzkaller", ns)
46+
periodsMerged, rowsMerged, err := coveragedb.NsDataMerged(ctx, coverageDBClient, ns)
4747
if err != nil {
4848
log.Errorf(ctx, "failed coveragedb.NsDataMerged(%s): %s", ns, err)
4949
}
@@ -154,7 +154,7 @@ func nsDataAvailable(ctx context.Context, ns string) ([]coveragedb.TimePeriod, [
154154

155155
func handleBatchCoverageClean(w http.ResponseWriter, r *http.Request) {
156156
ctx := context.Background()
157-
totalDeleted, err := coveragedb.DeleteGarbage(ctx)
157+
totalDeleted, err := coveragedb.DeleteGarbage(ctx, coverageDBClient)
158158
if err != nil {
159159
errMsg := fmt.Sprintf("failed to coveragedb.DeleteGarbage: %s", err.Error())
160160
log.Errorf(ctx, "%s", errMsg)

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: 63 additions & 11 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

@@ -19,6 +20,33 @@ import (
1920
"github.com/google/syzkaller/pkg/validator"
2021
)
2122

23+
var coverageDBClient spannerclient.SpannerClient
24+
25+
func initCoverageDB() {
26+
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
27+
if projectID == "" {
28+
// It is a test environment.
29+
// Use SetCoverageDBClient to specify the coveragedb mock or emulator in every test.
30+
return
31+
}
32+
var err error
33+
coverageDBClient, err = spannerclient.NewClient(context.Background(), projectID)
34+
if err != nil {
35+
panic("spanner.NewClient: " + err.Error())
36+
}
37+
}
38+
39+
var keyCoverageDBClient = "coveragedb client key"
40+
41+
func SetCoverageDBClient(ctx context.Context, client spannerclient.SpannerClient) context.Context {
42+
return context.WithValue(ctx, &keyCoverageDBClient, client)
43+
}
44+
45+
func GetCoverageDBClient(ctx context.Context) spannerclient.SpannerClient {
46+
client, _ := ctx.Value(&keyCoverageDBClient).(spannerclient.SpannerClient)
47+
return client
48+
}
49+
2250
type funcStyleBodyJS func(
2351
ctx context.Context, client spannerclient.SpannerClient,
2452
scope *cover.SelectScope, onlyUnique bool, sss, managers []string,
@@ -37,6 +65,10 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
3765
if err != nil {
3866
return err
3967
}
68+
nsConfig := getNsConfig(c, hdr.Namespace)
69+
if nsConfig.Coverage == nil {
70+
return ErrClientNotFound
71+
}
4072
ss := r.FormValue("subsystem")
4173
manager := r.FormValue("manager")
4274

@@ -76,15 +108,9 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
76108

77109
onlyUnique := r.FormValue("unique-only") == "1"
78110

79-
spannerClient, err := spannerclient.NewClient(c, "syzkaller")
80-
if err != nil {
81-
return fmt.Errorf("spanner.NewClient: %s", err.Error())
82-
}
83-
defer spannerClient.Close()
84-
85111
var style template.CSS
86112
var body, js template.HTML
87-
if style, body, js, err = f(c, spannerClient,
113+
if style, body, js, err = f(c, GetCoverageDBClient(c),
88114
&cover.SelectScope{
89115
Ns: hdr.Namespace,
90116
Subsystem: ss,
@@ -147,24 +173,35 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
147173
}
148174
onlyUnique := r.FormValue("unique-only") == "1"
149175
mainNsRepo, _ := nsConfig.mainRepoBranch()
150-
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
176+
client := GetCoverageDBClient(c)
177+
if client == nil {
178+
return fmt.Errorf("spannerdb client is nil")
179+
}
180+
hitLines, hitCounts, err := coveragedb.ReadLinesHitCount(c, client, hdr.Namespace, targetCommit, kernelFilePath, manager, tp)
151181
covMap := cover.MakeCovMap(hitLines, hitCounts)
152182
if err != nil {
153183
return fmt.Errorf("coveragedb.ReadLinesHitCount(%s): %w", manager, err)
154184
}
155185
if onlyUnique {
156-
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
186+
// This request is expected to be made second by tests.
187+
// Moving it to goroutine don't forget to change multiManagerCovDBFixture.
188+
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, client, hdr.Namespace, targetCommit, kernelFilePath, "*", tp)
157189
if err != nil {
158190
return fmt.Errorf("coveragedb.ReadLinesHitCount(*): %w", err)
159191
}
160192
covMap = cover.UniqCoverage(cover.MakeCovMap(allHitLines, allHitCounts), covMap)
161193
}
162194

195+
webGit := getWebGit(c) // Get mock if available.
196+
if webGit == nil {
197+
webGit = covermerger.MakeWebGit(makeProxyURIProvider(nsConfig.Coverage.WebGitURI))
198+
}
199+
163200
content, err := cover.RendFileCoverage(
164201
mainNsRepo,
165202
targetCommit,
166203
kernelFilePath,
167-
makeProxyURIProvider(nsConfig.Coverage.WebGitURI),
204+
webGit,
168205
&covermerger.MergeResult{HitCounts: covMap},
169206
cover.DefaultHTMLRenderConfig())
170207
if err != nil {
@@ -175,19 +212,34 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
175212
return nil
176213
}
177214

215+
var keyWebGit = "file content provider"
216+
217+
func setWebGit(ctx context.Context, provider covermerger.FileVersProvider) context.Context {
218+
return context.WithValue(ctx, &keyWebGit, provider)
219+
}
220+
221+
func getWebGit(ctx context.Context) covermerger.FileVersProvider {
222+
res, _ := ctx.Value(&keyWebGit).(covermerger.FileVersProvider)
223+
return res
224+
}
225+
178226
func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Request) error {
179227
hdr, err := commonHeader(c, r, w, "")
180228
if err != nil {
181229
return err
182230
}
231+
nsConfig := getNsConfig(c, hdr.Namespace)
232+
if nsConfig.Coverage == nil {
233+
return ErrClientNotFound
234+
}
183235
periodType := r.FormValue("period")
184236
if periodType == "" {
185237
periodType = coveragedb.QuarterPeriod
186238
}
187239
if periodType != coveragedb.QuarterPeriod && periodType != coveragedb.MonthPeriod {
188240
return fmt.Errorf("only quarter and month are allowed, but received %s instead", periodType)
189241
}
190-
hist, err := MergedCoverage(c, hdr.Namespace, periodType)
242+
hist, err := MergedCoverage(c, GetCoverageDBClient(c), hdr.Namespace, periodType)
191243
if err != nil {
192244
return err
193245
}

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+
}

0 commit comments

Comments
 (0)