Skip to content

Commit 4a7efa0

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 4a7efa0

File tree

21 files changed

+379
-86
lines changed

21 files changed

+379
-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: 65 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,37 @@ 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(
181+
c, client, hdr.Namespace, targetCommit, kernelFilePath, manager, tp)
151182
covMap := cover.MakeCovMap(hitLines, hitCounts)
152183
if err != nil {
153184
return fmt.Errorf("coveragedb.ReadLinesHitCount(%s): %w", manager, err)
154185
}
155186
if onlyUnique {
156-
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(c, hdr.Namespace, targetCommit, manager, kernelFilePath, tp)
187+
// This request is expected to be made second by tests.
188+
// Moving it to goroutine don't forget to change multiManagerCovDBFixture.
189+
allHitLines, allHitCounts, err := coveragedb.ReadLinesHitCount(
190+
c, client, hdr.Namespace, targetCommit, kernelFilePath, "*", tp)
157191
if err != nil {
158192
return fmt.Errorf("coveragedb.ReadLinesHitCount(*): %w", err)
159193
}
160194
covMap = cover.UniqCoverage(cover.MakeCovMap(allHitLines, allHitCounts), covMap)
161195
}
162196

197+
webGit := getWebGit(c) // Get mock if available.
198+
if webGit == nil {
199+
webGit = covermerger.MakeWebGit(makeProxyURIProvider(nsConfig.Coverage.WebGitURI))
200+
}
201+
163202
content, err := cover.RendFileCoverage(
164203
mainNsRepo,
165204
targetCommit,
166205
kernelFilePath,
167-
makeProxyURIProvider(nsConfig.Coverage.WebGitURI),
206+
webGit,
168207
&covermerger.MergeResult{HitCounts: covMap},
169208
cover.DefaultHTMLRenderConfig())
170209
if err != nil {
@@ -175,19 +214,34 @@ func handleFileCoverage(c context.Context, w http.ResponseWriter, r *http.Reques
175214
return nil
176215
}
177216

217+
var keyWebGit = "file content provider"
218+
219+
func setWebGit(ctx context.Context, provider covermerger.FileVersProvider) context.Context {
220+
return context.WithValue(ctx, &keyWebGit, provider)
221+
}
222+
223+
func getWebGit(ctx context.Context) covermerger.FileVersProvider {
224+
res, _ := ctx.Value(&keyWebGit).(covermerger.FileVersProvider)
225+
return res
226+
}
227+
178228
func handleCoverageGraph(c context.Context, w http.ResponseWriter, r *http.Request) error {
179229
hdr, err := commonHeader(c, r, w, "")
180230
if err != nil {
181231
return err
182232
}
233+
nsConfig := getNsConfig(c, hdr.Namespace)
234+
if nsConfig.Coverage == nil {
235+
return ErrClientNotFound
236+
}
183237
periodType := r.FormValue("period")
184238
if periodType == "" {
185239
periodType = coveragedb.QuarterPeriod
186240
}
187241
if periodType != coveragedb.QuarterPeriod && periodType != coveragedb.MonthPeriod {
188242
return fmt.Errorf("only quarter and month are allowed, but received %s instead", periodType)
189243
}
190-
hist, err := MergedCoverage(c, hdr.Namespace, periodType)
244+
hist, err := MergedCoverage(c, GetCoverageDBClient(c), hdr.Namespace, periodType)
191245
if err != nil {
192246
return err
193247
}

dashboard/app/coverage_test.go

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

0 commit comments

Comments
 (0)