github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-ci/manager_test.go (about) 1 // Copyright 2023 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 "bytes" 8 "compress/gzip" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/http" 14 "net/http/httptest" 15 "strings" 16 "testing" 17 18 "github.com/google/syzkaller/dashboard/dashapi" 19 "github.com/google/syzkaller/pkg/cover" 20 gcsmocks "github.com/google/syzkaller/pkg/gcs/mocks" 21 "github.com/google/syzkaller/pkg/mgrconfig" 22 "github.com/google/syzkaller/pkg/vcs" 23 "github.com/google/syzkaller/sys/targets" 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/mock" 26 ) 27 28 type dashapiMock struct { 29 mock.Mock 30 } 31 32 func (dm *dashapiMock) BuilderPoll(manager string) (*dashapi.BuilderPollResp, error) { 33 args := dm.Called(manager) 34 return args.Get(0).(*dashapi.BuilderPollResp), args.Error(1) 35 } 36 37 // We don't care about the methods below for now. 38 func (dm *dashapiMock) ReportBuildError(req *dashapi.BuildErrorReq) error { return nil } 39 func (dm *dashapiMock) UploadBuild(build *dashapi.Build) error { return nil } 40 func (dm *dashapiMock) LogError(name, msg string, args ...interface{}) {} 41 func (dm *dashapiMock) CommitPoll() (*dashapi.CommitPollResp, error) { return nil, nil } 42 func (dm *dashapiMock) UploadCommits(commits []dashapi.Commit) error { return nil } 43 44 func TestManagerPollCommits(t *testing.T) { 45 // Mock a repository. 46 baseDir := t.TempDir() 47 repo := vcs.CreateTestRepo(t, baseDir, "") 48 var lastCommit *vcs.Commit 49 for _, title := range []string{ 50 "unrelated commit one", 51 "commit1 title", 52 "unrelated commit two", 53 "commit3 title", 54 `title with fix 55 56 Reported-by: foo+abcd000@bar.com`, 57 "unrelated commit three", 58 } { 59 lastCommit = repo.CommitChange(title) 60 } 61 62 vcsRepo, err := vcs.NewRepo(targets.TestOS, targets.TestArch64, baseDir, vcs.OptPrecious) 63 if err != nil { 64 t.Fatal(err) 65 } 66 67 mock := new(dashapiMock) 68 mgr := Manager{ 69 name: "test-manager", 70 dash: mock, 71 repo: vcsRepo, 72 mgrcfg: &ManagerConfig{}, 73 } 74 75 // Mock BuilderPoll(). 76 commits := []string{ 77 "commit1 title", 78 "commit2 title", 79 "commit3 title", 80 "commit4 title", 81 } 82 // Let's trigger sampling as well. 83 for i := 0; i < 100; i++ { 84 commits = append(commits, fmt.Sprintf("test%d", i)) 85 } 86 mock.On("BuilderPoll", "test-manager").Return(&dashapi.BuilderPollResp{ 87 PendingCommits: commits, 88 ReportEmail: "foo@bar.com", 89 }, nil) 90 91 matches, fixCommits, err := mgr.pollCommits(lastCommit.Hash) 92 if err != nil { 93 t.Fatal(err) 94 } 95 96 foundCommits := map[string]bool{} 97 // Call it several more times to catch all commits. 98 for i := 0; i < 100; i++ { 99 for _, name := range matches { 100 foundCommits[name] = true 101 } 102 matches, _, err = mgr.pollCommits(lastCommit.Hash) 103 if err != nil { 104 t.Fatal(err) 105 } 106 } 107 108 var foundCommitsSlice []string 109 for title := range foundCommits { 110 foundCommitsSlice = append(foundCommitsSlice, title) 111 } 112 assert.ElementsMatch(t, foundCommitsSlice, []string{ 113 "commit1 title", "commit3 title", 114 }) 115 assert.Len(t, fixCommits, 1) 116 commit := fixCommits[0] 117 assert.Equal(t, commit.Title, "title with fix") 118 assert.ElementsMatch(t, commit.BugIDs, []string{"abcd000"}) 119 } 120 121 func TestUploadCoverJSONLToGCS(t *testing.T) { 122 tests := []struct { 123 name string 124 125 inputJSONL string 126 inputNameSuffix string 127 128 inputCompress bool 129 inputPublish bool 130 131 wantGCSFileName string 132 wantGCSFileContent string 133 wantCompressed bool 134 wantPublish bool 135 wantError string 136 }{ 137 { 138 name: "upload single object", 139 inputJSONL: "{}", 140 wantGCSFileName: "test-bucket/test-namespace/mgr-name.jsonl", 141 wantGCSFileContent: "{}\n", 142 }, 143 { 144 name: "upload single object, compress", 145 inputJSONL: "{}", 146 inputCompress: true, 147 wantGCSFileName: "test-bucket/test-namespace/mgr-name.jsonl", 148 wantGCSFileContent: "{}\n", 149 wantCompressed: true, 150 }, 151 { 152 name: "upload single object, publish", 153 inputJSONL: "{}", 154 inputPublish: true, 155 wantGCSFileName: "test-bucket/test-namespace/mgr-name.jsonl", 156 wantGCSFileContent: "{}\n", 157 wantPublish: true, 158 }, 159 { 160 name: "upload single object, unique name", 161 inputJSONL: "{}", 162 inputNameSuffix: "-suffix", 163 wantGCSFileName: "test-bucket/test-namespace/mgr-name-suffix.jsonl", 164 wantGCSFileContent: "{}\n", 165 }, 166 167 { 168 name: "upload single object, error", 169 inputJSONL: "{", 170 wantGCSFileName: "test-bucket/test-namespace/mgr-name.jsonl", 171 wantError: "callback: cover.ProgramCoverage: unexpected EOF", 172 }, 173 } 174 175 for _, test := range tests { 176 t.Run(test.name, func(t *testing.T) { 177 httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 178 w.Write([]byte(test.inputJSONL)) 179 })) 180 defer httpServer.Close() 181 182 testSetverAddrPort, _ := strings.CutPrefix(httpServer.URL, "http://") 183 mgr := Manager{ 184 name: "mgr-name", 185 managercfg: &mgrconfig.Config{ 186 HTTP: testSetverAddrPort, 187 Cover: true, 188 }, 189 mgrcfg: &ManagerConfig{ 190 DashboardClient: "test-namespace", 191 }, 192 } 193 194 gcsMock := gcsmocks.NewClient(t) 195 gotBytes := mockWriteCloser{} 196 197 gcsMock.On("FileWriter", test.wantGCSFileName, "", ""). 198 Return(&gotBytes, nil).Once() 199 gcsMock.On("Close").Return(nil).Once() 200 if test.wantPublish { 201 gcsMock.On("Publish", test.wantGCSFileName). 202 Return(nil).Once() 203 } 204 err := mgr.uploadCoverJSONLToGCS(context.Background(), gcsMock, 205 "/teststream&jsonl=1", 206 "gs://test-bucket", 207 uploadOptions{ 208 nameSuffix: test.inputNameSuffix, 209 publish: test.inputPublish, 210 compress: test.inputCompress, 211 }, 212 func(w io.Writer, dec *json.Decoder) error { 213 var v any 214 if err := dec.Decode(&v); err != nil { 215 return fmt.Errorf("cover.ProgramCoverage: %w", err) 216 } 217 if err := cover.WriteJSLine(w, &v); err != nil { 218 return fmt.Errorf("cover.WriteJSLine: %w", err) 219 } 220 return nil 221 }) 222 if test.wantError != "" { 223 assert.Equal(t, test.wantError, err.Error()) 224 } else { 225 assert.NoError(t, err) 226 } 227 assert.Equal(t, 1, gotBytes.closedTimes) 228 if test.wantCompressed { 229 gzReader, err := gzip.NewReader(&gotBytes.buf) 230 assert.NoError(t, err) 231 defer gzReader.Close() 232 plainBytes := mockWriteCloser{} 233 _, err = io.Copy(&plainBytes, gzReader) 234 assert.NoError(t, err) 235 gotBytes = plainBytes 236 } 237 assert.Equal(t, test.wantGCSFileContent, gotBytes.buf.String()) 238 }) 239 } 240 } 241 242 type mockWriteCloser struct { 243 buf bytes.Buffer 244 closedTimes int 245 } 246 247 func (m *mockWriteCloser) Write(p []byte) (n int, err error) { 248 return m.buf.Write(p) 249 } 250 251 func (m *mockWriteCloser) Close() error { 252 m.closedTimes++ 253 return nil 254 }