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  }