diff options
| author | Aleksandr Nogikh <nogikh@google.com> | 2025-01-15 17:41:31 +0100 |
|---|---|---|
| committer | Aleksandr Nogikh <nogikh@google.com> | 2025-01-22 13:17:53 +0000 |
| commit | cc143e38041972ad4dbaff9cfbfd416c29d581b5 (patch) | |
| tree | 31e07572a22bebdc20c1fc73a6ef9140c03eb3e5 /syz-cluster/controller | |
| parent | 6bab9518e47d67b3c9bba00f213aa3e1637063e1 (diff) | |
syz-cluster: add support for findings
Findings are crashes and build/boot/test errors that happened during the
patch series processing.
Diffstat (limited to 'syz-cluster/controller')
| -rw-r--r-- | syz-cluster/controller/api.go | 40 | ||||
| -rw-r--r-- | syz-cluster/controller/api_test.go | 66 | ||||
| -rw-r--r-- | syz-cluster/controller/services.go | 69 |
3 files changed, 151 insertions, 24 deletions
diff --git a/syz-cluster/controller/api.go b/syz-cluster/controller/api.go index 6f1089bd2..9db06c230 100644 --- a/syz-cluster/controller/api.go +++ b/syz-cluster/controller/api.go @@ -1,6 +1,7 @@ // Copyright 2024 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. +// nolint: dupl // The methods look similar, but extracting the common parts will only make the code worse. package main import ( @@ -12,20 +13,21 @@ import ( "github.com/google/syzkaller/syz-cluster/pkg/api" "github.com/google/syzkaller/syz-cluster/pkg/app" - "github.com/google/syzkaller/syz-cluster/pkg/db" ) type ControllerAPI struct { - seriesService *SeriesService - buildService *BuildService - testRepo *db.SessionTestRepository + seriesService *SeriesService + buildService *BuildService + testService *SessionTestService + findingService *FindingService } func NewControllerAPI(env *app.AppEnvironment) *ControllerAPI { return &ControllerAPI{ - seriesService: NewSeriesService(env), - buildService: NewBuildService(env), - testRepo: db.NewSessionTestRepository(env.Spanner), + seriesService: NewSeriesService(env), + buildService: NewBuildService(env), + testService: NewSessionTestService(env), + findingService: NewFindingService(env), } } @@ -36,6 +38,7 @@ func (c ControllerAPI) Mux() *http.ServeMux { mux.HandleFunc("/builds/last", c.getLastBuild) mux.HandleFunc("/builds/upload", c.uploadBuild) mux.HandleFunc("/tests/upload", c.uploadTest) + mux.HandleFunc("/findings/upload", c.uploadFinding) return mux } @@ -85,15 +88,22 @@ func (c ControllerAPI) uploadTest(w http.ResponseWriter, r *http.Request) { return } // TODO: add parameters validation. - err := c.testRepo.Insert(context.Background(), &db.SessionTest{ - SessionID: req.SessionID, - BaseBuildID: req.BaseBuildID, - PatchedBuildID: req.PatchedBuildID, - TestName: req.TestName, - Result: req.Result, - }) + err := c.testService.Save(context.Background(), req) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + reply[interface{}](w, nil) +} + +func (c ControllerAPI) uploadFinding(w http.ResponseWriter, r *http.Request) { + req := parseBody[api.Finding](w, r) + if req == nil { + return + } + // TODO: add parameters validation. + err := c.findingService.Save(context.Background(), req) if err != nil { - // TODO: sometimes it's not StatusInternalServerError. http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } diff --git a/syz-cluster/controller/api_test.go b/syz-cluster/controller/api_test.go index 6076ddc63..bc205392b 100644 --- a/syz-cluster/controller/api_test.go +++ b/syz-cluster/controller/api_test.go @@ -4,6 +4,7 @@ package main import ( + "context" "net/http/httptest" "testing" "time" @@ -39,6 +40,60 @@ func TestAPISuccessfulBuild(t *testing.T) { server := httptest.NewServer(apiServer.Mux()) defer server.Close() + client := api.NewClient(server.URL) + buildInfo, _ := uploadTestBuild(t, client) + info, err := client.LastSuccessfulBuild(ctx, &api.LastBuildReq{ + Arch: buildInfo.Arch, + TreeName: buildInfo.TreeName, + ConfigName: buildInfo.ConfigName, + }) + assert.NoError(t, err) + assert.Equal(t, buildInfo, info) +} + +func TestAPISaveFinding(t *testing.T) { + env, ctx := app.TestEnvironment(t) + apiServer := NewControllerAPI(env) + server := httptest.NewServer(apiServer.Mux()) + defer server.Close() + + series := addTestSeries(t, db.NewSeriesRepository(env.Spanner), env.BlobStorage) + session := addTestSession(t, db.NewSessionRepository(env.Spanner), series) + + client := api.NewClient(server.URL) + _, buildResp := uploadTestBuild(t, client) + err := client.UploadTestResult(ctx, &api.TestResult{ + SessionID: session.ID, + BaseBuildID: buildResp.ID, + TestName: "test", + Result: api.TestRunning, + }) + assert.NoError(t, err) + + t.Run("not existing test", func(t *testing.T) { + err = client.UploadFinding(ctx, &api.Finding{ + SessionID: session.ID, + TestName: "unknown test", + }) + assert.Error(t, err) + }) + + t.Run("must succeed", func(t *testing.T) { + finding := &api.Finding{ + SessionID: session.ID, + TestName: "test", + Report: []byte("report"), + Log: []byte("log"), + } + err = client.UploadFinding(ctx, finding) + assert.NoError(t, err) + // Even if the finding is reported the second time, it must still not fail. + err = client.UploadFinding(ctx, finding) + assert.NoError(t, err) + }) +} + +func uploadTestBuild(t *testing.T, client *api.Client) (*api.Build, *api.UploadBuildResp) { buildInfo := &api.Build{ Arch: "amd64", TreeName: "mainline", @@ -47,17 +102,10 @@ func TestAPISuccessfulBuild(t *testing.T) { CommitDate: time.Date(2020, time.January, 1, 3, 0, 0, 0, time.UTC), BuildSuccess: true, } - client := api.NewClient(server.URL) - ret, err := client.UploadBuild(ctx, &api.UploadBuildReq{ + ret, err := client.UploadBuild(context.Background(), &api.UploadBuildReq{ Build: *buildInfo, }) assert.NoError(t, err) assert.NotEmpty(t, ret.ID) - info, err := client.LastSuccessfulBuild(ctx, &api.LastBuildReq{ - Arch: buildInfo.Arch, - TreeName: buildInfo.TreeName, - ConfigName: buildInfo.ConfigName, - }) - assert.NoError(t, err) - assert.Equal(t, buildInfo, info) + return buildInfo, ret } diff --git a/syz-cluster/controller/services.go b/syz-cluster/controller/services.go index c97ef4f4f..6906f6ccd 100644 --- a/syz-cluster/controller/services.go +++ b/syz-cluster/controller/services.go @@ -4,11 +4,13 @@ package main import ( + "bytes" "context" "errors" "fmt" "io" + "cloud.google.com/go/spanner" "github.com/google/syzkaller/syz-cluster/pkg/api" "github.com/google/syzkaller/syz-cluster/pkg/app" "github.com/google/syzkaller/syz-cluster/pkg/blob" @@ -127,3 +129,70 @@ func (s *BuildService) LastBuild(ctx context.Context, req *api.LastBuildReq) (*a } return resp, nil } + +type SessionTestService struct { + testRepo *db.SessionTestRepository +} + +func NewSessionTestService(env *app.AppEnvironment) *SessionTestService { + return &SessionTestService{ + testRepo: db.NewSessionTestRepository(env.Spanner), + } +} + +func (s *SessionTestService) Save(ctx context.Context, req *api.TestResult) error { + entity := &db.SessionTest{ + SessionID: req.SessionID, + TestName: req.TestName, + Result: req.Result, + } + if req.BaseBuildID != "" { + entity.BaseBuildID = spanner.NullString{StringVal: req.BaseBuildID, Valid: true} + } + if req.PatchedBuildID != "" { + entity.PatchedBuildID = spanner.NullString{StringVal: req.PatchedBuildID, Valid: true} + } + return s.testRepo.InsertOrUpdate(context.Background(), entity) +} + +type FindingService struct { + findingRepo *db.FindingRepository + blobStorage blob.Storage +} + +func NewFindingService(env *app.AppEnvironment) *FindingService { + return &FindingService{ + findingRepo: db.NewFindingRepository(env.Spanner), + blobStorage: env.BlobStorage, + } +} + +func (s *FindingService) Save(ctx context.Context, req *api.Finding) error { + var reportURI, logURI string + var err error + if len(req.Log) > 0 { + logURI, err = s.blobStorage.Store(bytes.NewReader(req.Log)) + if err != nil { + return fmt.Errorf("failed to save the log: %w", err) + } + } + if len(req.Report) > 0 { + reportURI, err = s.blobStorage.Store(bytes.NewReader(req.Report)) + if err != nil { + return fmt.Errorf("failed to save the report: %w", err) + } + } + // TODO: if it's not actually addded, the blob records will be orphaned. + err = s.findingRepo.Save(ctx, &db.Finding{ + SessionID: req.SessionID, + TestName: req.TestName, + Title: req.Title, + ReportURI: reportURI, + LogURI: logURI, + }) + if err == db.ErrFindingExists { + // It's ok, just ignore. + return nil + } + return err +} |
