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  }