Skip to content

Commit 4cf81cb

Browse files
committed
pkg/coveragedb: test SaveMergeResult
1. Make interface testable. 2. Add Spanner interfaces. 3. Generate mocks for proxy interfaces. 4. Test SaveMergeResult. 5. Test MergeCSVWriteJSONL and coveragedb.SaveMergeResult integration.
1 parent 390ff24 commit 4cf81cb

File tree

11 files changed

+527
-148
lines changed

11 files changed

+527
-148
lines changed

Makefile

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

241242
generate_rpc:
242243
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: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ 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"
2930
"github.com/google/syzkaller/pkg/debugtracer"
3031
"github.com/google/syzkaller/pkg/email"
3132
"github.com/google/syzkaller/pkg/gcs"
@@ -1944,15 +1945,21 @@ func apiCreateUploadURL(c context.Context, payload io.Reader) (interface{}, erro
19441945
// Second+ records are coveragedb.MergedCoverageRecord.
19451946
func apiSaveCoverage(c context.Context, payload io.Reader) (interface{}, error) {
19461947
descr := new(coveragedb.HistoryRecord)
1947-
if err := json.NewDecoder(payload).Decode(descr); err != nil {
1948-
return 0, fmt.Errorf("json.NewDecoder(dashapi.MergedCoverageDescription).Decode: %w", err)
1948+
jsonDec := json.NewDecoder(payload)
1949+
if err := jsonDec.Decode(descr); err != nil {
1950+
return 0, fmt.Errorf("json.NewDecoder(coveragedb.HistoryRecord).Decode: %w", err)
19491951
}
19501952
var sss []*subsystem.Subsystem
19511953
if service := getNsConfig(c, descr.Namespace).Subsystems.Service; service != nil {
19521954
sss = service.List()
19531955
log.Infof(c, "found %d subsystems for %s namespace", len(sss), descr.Namespace)
19541956
}
1955-
rowsCreated, err := coveragedb.SaveMergeResult(c, appengine.AppID(context.Background()), descr, payload, sss)
1957+
client, err := spannerclient.NewClient(c, appengine.AppID(context.Background()))
1958+
if err != nil {
1959+
return 0, fmt.Errorf("coveragedb.NewClient() failed: %s", err.Error())
1960+
}
1961+
defer client.Close()
1962+
rowsCreated, err := coveragedb.SaveMergeResult(c, client, descr, jsonDec, sss)
19561963
if err != nil {
19571964
log.Errorf(c, "error storing coverage for ns %s, date %s: %v",
19581965
descr.Namespace, descr.DateTo.String(), err)

dashboard/app/entities_spanner.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"cloud.google.com/go/civil"
1212
"cloud.google.com/go/spanner"
1313
"github.com/google/syzkaller/pkg/coveragedb"
14+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1415
"google.golang.org/api/iterator"
1516
)
1617

@@ -25,7 +26,7 @@ type CoverageHistory struct {
2526
// MergedCoverage uses dates, not time.
2627
func MergedCoverage(ctx context.Context, ns, periodType string) (*CoverageHistory, error) {
2728
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
28-
client, err := coveragedb.NewClient(ctx, projectID)
29+
client, err := spannerclient.NewClient(ctx, projectID)
2930
if err != nil {
3031
return nil, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
3132
}

pkg/cover/heatmap.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"cloud.google.com/go/spanner"
1616
"github.com/google/syzkaller/pkg/coveragedb"
17+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1718
_ "github.com/google/syzkaller/pkg/subsystem/lists"
1819
"golang.org/x/exp/maps"
1920
"google.golang.org/api/iterator"
@@ -185,7 +186,7 @@ where
185186

186187
func filesCoverageWithDetails(ctx context.Context, projectID, ns, subsystem string, timePeriods []coveragedb.TimePeriod,
187188
) ([]*fileCoverageWithDetails, error) {
188-
client, err := coveragedb.NewClient(ctx, projectID)
189+
client, err := spannerclient.NewClient(ctx, projectID)
189190
if err != nil {
190191
return nil, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
191192
}
Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
package coveragedb
55

66
import (
7-
"bufio"
87
"context"
98
"encoding/json"
9+
"errors"
1010
"fmt"
1111
"io"
1212
"os"
@@ -15,6 +15,7 @@ import (
1515

1616
"cloud.google.com/go/civil"
1717
"cloud.google.com/go/spanner"
18+
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
1819
"github.com/google/syzkaller/pkg/subsystem"
1920
_ "github.com/google/syzkaller/pkg/subsystem/lists"
2021
"github.com/google/uuid"
@@ -49,11 +50,6 @@ type HistoryRecord struct {
4950
TotalRows int64
5051
}
5152

52-
func NewClient(ctx context.Context, projectID string) (*spanner.Client, error) {
53-
database := "projects/" + projectID + "/instances/syzbot/databases/coverage"
54-
return spanner.NewClient(ctx, database)
55-
}
56-
5753
type Coverage struct {
5854
Instrumented int64
5955
Covered int64
@@ -76,27 +72,26 @@ type MergedCoverageRecord struct {
7672
FileData *Coverage
7773
}
7874

79-
func SaveMergeResult(ctx context.Context, projectID string, descr *HistoryRecord, jsonl io.Reader,
75+
func SaveMergeResult(ctx context.Context, client spannerclient.SpannerClient, descr *HistoryRecord, dec *json.Decoder,
8076
sss []*subsystem.Subsystem) (int, error) {
81-
client, err := NewClient(ctx, projectID)
82-
if err != nil {
83-
return 0, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
84-
}
85-
defer client.Close()
8677
var rowsCreated int
87-
8878
ssMatcher := subsystem.MakePathMatcher(sss)
8979
ssCache := make(map[string][]string)
9080

9181
session := uuid.New().String()
9282
mutations := []*spanner.Mutation{}
9383

94-
jsonlScanner := bufio.NewScanner(jsonl)
95-
96-
for jsonlScanner.Scan() {
97-
var mcr MergedCoverageRecord
98-
if err := json.Unmarshal([]byte(jsonlScanner.Text()), &mcr); err != nil {
99-
return rowsCreated, fmt.Errorf("json.Unmarshal(MergedCoverageRecord): %w", err)
84+
var mcr MergedCoverageRecord
85+
for {
86+
err := dec.Decode(&mcr)
87+
if err == io.EOF {
88+
break
89+
}
90+
if err != nil {
91+
return rowsCreated, fmt.Errorf("dec.Decode(MergedCoverageRecord): %w", err)
92+
}
93+
if mcr.FileData == nil {
94+
return rowsCreated, errors.New("field MergedCoverageRecord.FileData can't be nil")
10095
}
10196
mutations = append(mutations, fileRecordMutation(session, &mcr))
10297
subsystems := fileSubsystems(mcr.FilePath, ssMatcher, ssCache)
@@ -105,8 +100,8 @@ func SaveMergeResult(ctx context.Context, projectID string, descr *HistoryRecord
105100
// This includes both explicit mutations of the fields (6 fields * 1k records = 6k mutations)
106101
// and implicit index mutations.
107102
// We keep the number of records low enough for the number of explicit mutations * 10 does not exceed the limit.
108-
if len(mutations) > 1000 {
109-
if _, err = client.Apply(ctx, mutations); err != nil {
103+
if len(mutations) >= 1000 {
104+
if _, err := client.Apply(ctx, mutations); err != nil {
110105
return rowsCreated, fmt.Errorf("failed to spanner.Apply(inserts): %s", err.Error())
111106
}
112107
rowsCreated += len(mutations)
@@ -115,9 +110,10 @@ func SaveMergeResult(ctx context.Context, projectID string, descr *HistoryRecord
115110
}
116111

117112
mutations = append(mutations, historyMutation(session, descr))
118-
if _, err = client.Apply(ctx, mutations); err != nil {
113+
if _, err := client.Apply(ctx, mutations); err != nil {
119114
return rowsCreated, fmt.Errorf("failed to spanner.Apply(inserts): %s", err.Error())
120115
}
116+
rowsCreated += len(mutations)
121117
return rowsCreated, nil
122118
}
123119

@@ -150,7 +146,7 @@ where
150146
func ReadLinesHitCount(ctx context.Context, ns, commit, file string, tp TimePeriod,
151147
) (map[int]int, error) {
152148
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
153-
client, err := NewClient(ctx, projectID)
149+
client, err := spannerclient.NewClient(ctx, projectID)
154150
if err != nil {
155151
return nil, fmt.Errorf("spanner.NewClient: %w", err)
156152
}
@@ -236,7 +232,7 @@ func fileSubsystems(filePath string, ssMatcher *subsystem.PathMatcher, ssCache m
236232
}
237233

238234
func NsDataMerged(ctx context.Context, projectID, ns string) ([]TimePeriod, []int64, error) {
239-
client, err := NewClient(ctx, projectID)
235+
client, err := spannerclient.NewClient(ctx, projectID)
240236
if err != nil {
241237
return nil, nil, fmt.Errorf("spanner.NewClient() failed: %s", err.Error())
242238
}
@@ -292,7 +288,7 @@ func NsDataMerged(ctx context.Context, projectID, ns string) ([]TimePeriod, []in
292288
// Returns the number of orphaned file entries successfully deleted.
293289
func DeleteGarbage(ctx context.Context) (int64, error) {
294290
batchSize := 10_000
295-
client, err := NewClient(ctx, os.Getenv("GOOGLE_CLOUD_PROJECT"))
291+
client, err := spannerclient.NewClient(ctx, os.Getenv("GOOGLE_CLOUD_PROJECT"))
296292
if err != nil {
297293
return 0, fmt.Errorf("coveragedb.NewClient: %w", err)
298294
}
@@ -339,7 +335,7 @@ func DeleteGarbage(ctx context.Context) (int64, error) {
339335
return totalDeleted.Load(), nil
340336
}
341337

342-
func goSpannerDelete(ctx context.Context, batch []spanner.Key, eg *errgroup.Group, client *spanner.Client,
338+
func goSpannerDelete(ctx context.Context, batch []spanner.Key, eg *errgroup.Group, client spannerclient.SpannerClient,
343339
totalDeleted *atomic.Int64) {
344340
ks := spanner.KeySetFromKeys(batch...)
345341
ksSize := len(batch)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2024 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 coveragedb
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"io"
10+
"strings"
11+
"testing"
12+
"time"
13+
14+
"cloud.google.com/go/spanner"
15+
"github.com/google/syzkaller/pkg/coveragedb/mocks"
16+
"github.com/google/syzkaller/pkg/subsystem"
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/mock"
19+
)
20+
21+
//go:generate ../../tools/mockery.sh --name SpannerClient -r
22+
23+
type spannerMockTune func(*testing.T, *mocks.SpannerClient)
24+
25+
func TestSaveMergeResult(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
sss []*subsystem.Subsystem
29+
jsonl io.Reader
30+
descr *HistoryRecord
31+
mockTune spannerMockTune
32+
wantErr bool
33+
wantRows int
34+
}{
35+
{
36+
name: "empty jsonl",
37+
jsonl: strings.NewReader(`{}`),
38+
wantErr: true,
39+
},
40+
{
41+
name: "wrong jsonl content",
42+
jsonl: strings.NewReader(`{a}`),
43+
wantErr: true,
44+
},
45+
{
46+
name: "1 record, Ok",
47+
jsonl: strings.NewReader(`{"FileData":{}}`),
48+
descr: &HistoryRecord{},
49+
wantRows: 3, // 1 in files, 1 in file_subsystems and 1 in merge_history
50+
mockTune: func(t *testing.T, m *mocks.SpannerClient) {
51+
m.
52+
On("Apply", mock.Anything, mock.Anything).
53+
Return(time.Now(), nil).
54+
Once()
55+
},
56+
},
57+
{
58+
name: "2 records, Ok",
59+
jsonl: strings.NewReader(` {"FileData":{}}
60+
{"FileData":{}}`),
61+
descr: &HistoryRecord{},
62+
wantRows: 5,
63+
mockTune: func(t *testing.T, m *mocks.SpannerClient) {
64+
m.
65+
On("Apply",
66+
mock.Anything,
67+
mock.MatchedBy(func(ms []*spanner.Mutation) bool {
68+
// 2 in files, 2 in file_subsystems and 1 in merge_history
69+
return len(ms) == 5
70+
})).
71+
Return(time.Now(), nil).
72+
Once()
73+
},
74+
},
75+
{
76+
name: "2k records, Ok",
77+
jsonl: strings.NewReader(strings.Repeat("{\"FileData\":{}}\n", 2000)),
78+
descr: &HistoryRecord{},
79+
wantRows: 4001,
80+
mockTune: func(t *testing.T, m *mocks.SpannerClient) {
81+
m.
82+
On("Apply",
83+
mock.Anything,
84+
mock.MatchedBy(func(ms []*spanner.Mutation) bool {
85+
// 2k in files, 2k in file_subsystems
86+
return len(ms) == 1000
87+
})).
88+
Return(time.Now(), nil).
89+
Times(4).
90+
On("Apply",
91+
mock.Anything,
92+
mock.MatchedBy(func(ms []*spanner.Mutation) bool {
93+
// And 1 in merge_history.
94+
return len(ms) == 1
95+
})).
96+
Return(time.Now(), nil).
97+
Once()
98+
},
99+
},
100+
}
101+
for _, test := range tests {
102+
t.Run(test.name, func(t *testing.T) {
103+
t.Parallel()
104+
spannerMock := mocks.NewSpannerClient(t)
105+
if test.mockTune != nil {
106+
test.mockTune(t, spannerMock)
107+
}
108+
gotRows, err := SaveMergeResult(
109+
context.Background(),
110+
spannerMock,
111+
test.descr,
112+
json.NewDecoder(test.jsonl), test.sss)
113+
if test.wantErr {
114+
assert.Error(t, err)
115+
} else {
116+
assert.NoError(t, err)
117+
}
118+
assert.Equal(t, test.wantRows, gotRows)
119+
})
120+
}
121+
}

0 commit comments

Comments
 (0)