github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/covermerger/covermerger_test.go (about) 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 covermerger 5 6 import ( 7 "compress/gzip" 8 "context" 9 "encoding/json" 10 "io" 11 "os" 12 "path/filepath" 13 "sort" 14 "strings" 15 "testing" 16 "time" 17 18 "cloud.google.com/go/civil" 19 "cloud.google.com/go/spanner" 20 "github.com/google/syzkaller/pkg/coveragedb" 21 "github.com/google/syzkaller/pkg/coveragedb/mocks" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/mock" 24 "golang.org/x/sync/errgroup" 25 ) 26 27 var testsPath = "testdata/integration" 28 var defaultTestWorkdir = testsPath + "/all/test-workdir-covermerger" 29 30 func TestMergeCSVWriteJSONL_and_coveragedb_SaveMergeResult(t *testing.T) { 31 rc, wc := io.Pipe() 32 eg := errgroup.Group{} 33 eg.Go(func() error { 34 defer wc.Close() 35 totalInstrumented, totalCovered, err := MergeCSVWriteJSONL( 36 testConfig( 37 "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", 38 "fe46a7dd189e25604716c03576d05ac8a5209743", 39 testsPath+"/aesni-intel_glue/test-workdir-covermerger"), 40 &coveragedb.HistoryRecord{ 41 DateTo: civil.DateOf(time.Now()), 42 }, 43 strings.NewReader(readFileOrFail(t, testsPath+"/aesni-intel_glue/bqTable.txt")), 44 wc) 45 assert.Equal(t, 48, totalInstrumented) 46 assert.Equal(t, 45, totalCovered) 47 return err 48 }) 49 eg.Go(func() error { 50 defer rc.Close() 51 gzrc, err := gzip.NewReader(rc) 52 assert.NoError(t, err) 53 defer gzrc.Close() 54 55 spannerMock := mocks.NewSpannerClient(t) 56 spannerMock. 57 On("Apply", mock.Anything, mock.MatchedBy(func(ms []*spanner.Mutation) bool { 58 // 1 file * (5 managers + 1 manager total) x 1 (to update files) + 1 merge_history + 18 functions 59 return len(ms) == 1*(5+1)*1+1+18 60 })). 61 Return(time.Now(), nil). 62 Once() 63 64 decoder := json.NewDecoder(gzrc) 65 decoder.DisallowUnknownFields() 66 67 descr := new(coveragedb.HistoryRecord) 68 assert.NoError(t, decoder.Decode(descr)) 69 70 _, err = coveragedb.SaveMergeResult(context.Background(), spannerMock, descr, decoder) 71 return err 72 }) 73 assert.NoError(t, eg.Wait()) 74 } 75 76 func TestMergerdCoverageRecords(t *testing.T) { 77 tests := []struct { 78 name string 79 input *FileMergeResult 80 wantRecords []*coveragedb.MergedCoverageRecord 81 }{ 82 { 83 name: "file doesn't exist", 84 input: &FileMergeResult{ 85 FilePath: "deleted.c", 86 MergeResult: &MergeResult{ 87 FileExists: false, 88 }, 89 }, 90 wantRecords: nil, 91 }, 92 { 93 name: "two managers merge", 94 input: &FileMergeResult{ 95 FilePath: "file.c", 96 MergeResult: &MergeResult{ 97 FileExists: true, 98 HitCounts: map[int]int64{ 99 1: 5, 100 2: 7, 101 }, 102 LineDetails: map[int][]*FileRecord{ 103 1: { 104 { 105 FilePath: "file.c", 106 RepoCommit: RepoCommit{ 107 Repo: "repo1", 108 Commit: "commit1", 109 }, 110 StartLine: 10, 111 HitCount: 5, 112 Manager: "manager1", 113 }, 114 }, 115 2: { 116 { 117 FilePath: "file.c", 118 RepoCommit: RepoCommit{ 119 Repo: "repo2", 120 Commit: "commit2", 121 }, 122 StartLine: 20, 123 HitCount: 7, 124 Manager: "manager2", 125 }, 126 }, 127 }, 128 }, 129 }, 130 wantRecords: []*coveragedb.MergedCoverageRecord{ 131 { 132 Manager: "*", 133 FilePath: "file.c", 134 FileData: &coveragedb.Coverage{ 135 Instrumented: 2, 136 Covered: 2, 137 LinesInstrumented: []int64{1, 2}, 138 HitCounts: []int64{5, 7}, 139 }, 140 }, 141 { 142 Manager: "manager1", 143 FilePath: "file.c", 144 FileData: &coveragedb.Coverage{ 145 Instrumented: 1, 146 Covered: 1, 147 LinesInstrumented: []int64{1}, 148 HitCounts: []int64{5}, 149 }, 150 }, 151 { 152 Manager: "manager2", 153 FilePath: "file.c", 154 FileData: &coveragedb.Coverage{ 155 Instrumented: 1, 156 Covered: 1, 157 LinesInstrumented: []int64{2}, 158 HitCounts: []int64{7}, 159 }, 160 }, 161 }, 162 }, 163 } 164 for _, test := range tests { 165 t.Run(test.name, func(t *testing.T) { 166 gotRecords, gotFuncs := mergedCoverageRecords(test.input) 167 sort.Slice(gotRecords, func(i, j int) bool { 168 return gotRecords[i].Manager < gotRecords[j].Manager 169 }) 170 assert.Equal(t, test.wantRecords, gotRecords, "records are not equal") 171 assert.Equal(t, 0, len(gotFuncs), "no functions expected") 172 }) 173 } 174 } 175 176 // nolint: lll 177 func TestAggregateStreamData(t *testing.T) { 178 type Test struct { 179 name string 180 workdir string 181 bqTable string 182 simpleAggregation string 183 baseRepo string 184 baseCommit string 185 checkDetails bool 186 } 187 tests := []Test{ 188 { 189 name: "aesni-intel_glue", 190 workdir: testsPath + "/aesni-intel_glue/test-workdir-covermerger", 191 bqTable: readFileOrFail(t, testsPath+"/aesni-intel_glue/bqTable.txt"), 192 simpleAggregation: readFileOrFail(t, testsPath+"/aesni-intel_glue/merge_result.txt"), 193 baseRepo: "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", 194 baseCommit: "fe46a7dd189e25604716c03576d05ac8a5209743", 195 }, 196 { 197 name: "code deleted", 198 workdir: defaultTestWorkdir, 199 bqTable: `timestamp,version,fuzzing_minutes,arch,build_id,manager,kernel_repo,kernel_branch,kernel_commit,file_path,func_name,sl,sc,el,ec,hit_count,inline,pc 200 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,delete_code.c,func1,2,0,2,-1,1,true,1`, 201 simpleAggregation: `{ 202 "delete_code.c": 203 { 204 "HitCounts":{}, 205 "FileExists": true, 206 "LineDetails":{} 207 } 208 }`, 209 baseRepo: "git://repo", 210 baseCommit: "commit2", 211 checkDetails: true, 212 }, 213 { 214 name: "file deleted", 215 workdir: defaultTestWorkdir, 216 bqTable: `timestamp,version,fuzzing_minutes,arch,build_id,manager,kernel_repo,kernel_branch,kernel_commit,file_path,func_name,sl,sc,el,ec,hit_count,inline,pc 217 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,delete_file.c,func1,2,0,2,-1,1,true,1`, 218 simpleAggregation: `{ 219 "delete_file.c": 220 { 221 "FileExists": false 222 } 223 }`, 224 baseRepo: "git://repo", 225 baseCommit: "commit2", 226 checkDetails: true, 227 }, 228 { 229 name: "covered line changed", 230 workdir: defaultTestWorkdir, 231 bqTable: `timestamp,version,fuzzing_minutes,arch,build_id,manager,kernel_repo,kernel_branch,kernel_commit,file_path,func_name,sl,sc,el,ec,hit_count,inline,pc 232 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,change_line.c,func1,2,0,2,-1,1,true,1 233 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,change_line.c,func1,3,0,3,-1,1,true,1`, 234 simpleAggregation: `{ 235 "change_line.c": 236 { 237 "HitCounts":{"3": 1}, 238 "FileExists": true, 239 "LineDetails": 240 { 241 "3": 242 [ 243 { 244 "FilePath":"change_line.c", 245 "FuncName":"func1", 246 "Repo":"git://repo", 247 "Commit":"commit1", 248 "StartLine":3, 249 "HitCount":1, 250 "Manager":"ci-mock" 251 } 252 ] 253 } 254 } 255 }`, 256 baseRepo: "git://repo", 257 baseCommit: "commit2", 258 checkDetails: true, 259 }, 260 { 261 name: "add line", 262 workdir: defaultTestWorkdir, 263 bqTable: `timestamp,version,fuzzing_minutes,arch,build_id,manager,kernel_repo,kernel_branch,kernel_commit,file_path,func_name,sl,sc,el,ec,hit_count,inline,pc 264 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,add_line.c,func1,2,0,2,-1,1,true,1`, 265 simpleAggregation: `{ 266 "add_line.c": 267 { 268 "HitCounts":{"2": 1}, 269 "FileExists": true, 270 "LineDetails": 271 { 272 "2": 273 [ 274 { 275 "FilePath":"add_line.c", 276 "FuncName":"func1", 277 "Repo":"git://repo", 278 "Commit":"commit1", 279 "StartLine":2, 280 "HitCount":1, 281 "Manager":"ci-mock" 282 } 283 ] 284 } 285 } 286 }`, 287 baseRepo: "git://repo", 288 baseCommit: "commit2", 289 checkDetails: true, 290 }, 291 { 292 name: "instrumented lines w/o coverage are reported", 293 workdir: defaultTestWorkdir, 294 bqTable: `timestamp,version,fuzzing_minutes,arch,build_id,manager,kernel_repo,kernel_branch,kernel_commit,file_path,func_name,sl,sc,el,ec,hit_count,inline,pc 295 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,not_changed.c,func1,3,0,3,-1,0,true,1 296 samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit2,not_changed.c,func1,4,0,4,-1,0,true,1`, 297 simpleAggregation: `{ 298 "not_changed.c": 299 { 300 "HitCounts":{"3": 0, "4": 0}, 301 "FileExists": true, 302 "LineDetails": 303 { 304 "3": 305 [ 306 { 307 "FilePath":"not_changed.c", 308 "FuncName":"func1", 309 "Repo":"git://repo", 310 "Commit":"commit1", 311 "StartLine":3, 312 "HitCount":0, 313 "Manager":"ci-mock" 314 } 315 ], 316 "4": 317 [ 318 { 319 "FilePath":"not_changed.c", 320 "FuncName":"func1", 321 "Repo":"git://repo", 322 "Commit":"commit2", 323 "StartLine":4, 324 "HitCount":0, 325 "Manager":"ci-mock" 326 } 327 ] 328 } 329 } 330 }`, 331 baseRepo: "git://repo", 332 baseCommit: "commit2", 333 checkDetails: true, 334 }, 335 } 336 for _, test := range tests { 337 t.Run(test.name, func(t *testing.T) { 338 mergeResultsCh := make(chan *FileMergeResult) 339 doneCh := make(chan bool) 340 go func() { 341 aggregation := make(map[string]*MergeResult) 342 for fmr := range mergeResultsCh { 343 aggregation[fmr.FilePath] = fmr.MergeResult 344 } 345 if !test.checkDetails { 346 ignoreLineDetailsInTest(aggregation) 347 } 348 var expectedAggregation map[string]*MergeResult 349 assert.NoError(t, json.Unmarshal([]byte(test.simpleAggregation), &expectedAggregation)) 350 assert.Equal(t, expectedAggregation, aggregation) 351 doneCh <- true 352 }() 353 assert.NoError(t, MergeCSVData( 354 context.Background(), 355 testConfig(test.baseRepo, test.baseCommit, test.workdir), 356 strings.NewReader(test.bqTable), 357 mergeResultsCh)) 358 close(mergeResultsCh) 359 <-doneCh 360 }) 361 } 362 } 363 364 func ignoreLineDetailsInTest(results map[string]*MergeResult) { 365 for _, mr := range results { 366 mr.LineDetails = nil 367 } 368 } 369 370 type fileVersProviderMock struct { 371 Workdir string 372 } 373 374 func (m *fileVersProviderMock) GetFileVersions(targetFilePath string, repoCommits ...RepoCommit, 375 ) (FileVersions, error) { 376 res := make(FileVersions) 377 for _, repoCommit := range repoCommits { 378 filePath := filepath.Join(m.Workdir, "repos", repoCommit.Commit, targetFilePath) 379 if bytes, err := os.ReadFile(filePath); err == nil { 380 res[repoCommit] = string(bytes) 381 } 382 } 383 return res, nil 384 } 385 386 func readFileOrFail(t *testing.T, path string) string { 387 absPath, err := filepath.Abs(path) 388 assert.Nil(t, err) 389 content, err := os.ReadFile(absPath) 390 assert.Nil(t, err) 391 return string(content) 392 } 393 394 func testConfig(repo, commit, workdir string) *Config { 395 return &Config{ 396 Jobs: 2, 397 skipRepoClone: true, 398 Base: RepoCommit{ 399 Repo: repo, 400 Commit: commit, 401 }, 402 FileVersProvider: &fileVersProviderMock{Workdir: workdir}, 403 } 404 } 405 406 func TestCheckedFuncName(t *testing.T) { 407 tests := []struct { 408 name string 409 input []string 410 want string 411 }{ 412 { 413 name: "empty input", 414 want: "", 415 }, 416 { 417 name: "single func", 418 input: []string{"func1", "func1"}, 419 want: "func1", 420 }, 421 { 422 name: "multi names", 423 input: []string{"", "", "", "func2", "func2", "func1", "func"}, 424 want: "func2", 425 }, 426 } 427 for _, test := range tests { 428 t.Run(test.name, func(t *testing.T) { 429 got := bestFuncName(test.input) 430 assert.Equal(t, test.want, got) 431 }) 432 } 433 }