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 | |
| parent | 6bab9518e47d67b3c9bba00f213aa3e1637063e1 (diff) | |
syz-cluster: add support for findings
Findings are crashes and build/boot/test errors that happened during the
patch series processing.
| -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 | ||||
| -rw-r--r-- | syz-cluster/dashboard/handler.go | 36 | ||||
| -rw-r--r-- | syz-cluster/dashboard/templates/series.html | 23 | ||||
| -rw-r--r-- | syz-cluster/pkg/api/api.go | 21 | ||||
| -rw-r--r-- | syz-cluster/pkg/api/client.go | 5 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/entities.go | 25 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/finding_repo.go | 65 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/finding_repo_test.go | 71 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/migrations/1_initialize.up.sql | 9 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/session_test_repo.go | 16 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/session_test_repo_test.go | 14 |
13 files changed, 393 insertions, 67 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 +} diff --git a/syz-cluster/dashboard/handler.go b/syz-cluster/dashboard/handler.go index d0dfbf6a6..d99c0338f 100644 --- a/syz-cluster/dashboard/handler.go +++ b/syz-cluster/dashboard/handler.go @@ -21,6 +21,7 @@ type DashboardHandler struct { seriesRepo *db.SeriesRepository sessionRepo *db.SessionRepository sessionTestRepo *db.SessionTestRepository + findingRepo *db.FindingRepository blobStorage blob.Storage templates map[string]*template.Template } @@ -43,6 +44,7 @@ func NewHandler(env *app.AppEnvironment) (*DashboardHandler, error) { seriesRepo: db.NewSeriesRepository(env.Spanner), sessionRepo: db.NewSessionRepository(env.Spanner), sessionTestRepo: db.NewSessionTestRepository(env.Spanner), + findingRepo: db.NewFindingRepository(env.Spanner), }, nil } @@ -67,9 +69,13 @@ func (h *DashboardHandler) seriesList(w http.ResponseWriter, r *http.Request) { } func (h *DashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) { + type SessionTest struct { + *db.FullSessionTest + Findings []*db.Finding + } type SessionData struct { *db.Session - Tests []*db.FullSessionTest + Tests []SessionTest } type SeriesData struct { *db.Series @@ -97,15 +103,27 @@ func (h *DashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) { return } for _, session := range sessions { - tests, err := h.sessionTestRepo.BySession(ctx, session.ID) + rawTests, err := h.sessionTestRepo.BySession(ctx, session.ID) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + return + } + findings, err := h.findingRepo.ListForSession(ctx, session) if err != nil { http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) return } - data.Sessions = append(data.Sessions, SessionData{ + perName := groupFindings(findings) + sessionData := SessionData{ Session: session, - Tests: tests, - }) + } + for _, test := range rawTests { + sessionData.Tests = append(sessionData.Tests, SessionTest{ + FullSessionTest: test, + Findings: perName[test.TestName], + }) + } + data.Sessions = append(data.Sessions, sessionData) } err = h.templates["series.html"].ExecuteTemplate(w, "base.html", data) @@ -114,6 +132,14 @@ func (h *DashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) { } } +func groupFindings(findings []*db.Finding) map[string][]*db.Finding { + ret := map[string][]*db.Finding{} + for _, finding := range findings { + ret[finding.TestName] = append(ret[finding.TestName], finding) + } + return ret +} + // nolint:dupl func (h *DashboardHandler) sessionLog(w http.ResponseWriter, r *http.Request) { ctx := context.Background() diff --git a/syz-cluster/dashboard/templates/series.html b/syz-cluster/dashboard/templates/series.html index 594f33be9..e3afb88a6 100644 --- a/syz-cluster/dashboard/templates/series.html +++ b/syz-cluster/dashboard/templates/series.html @@ -74,10 +74,29 @@ {{range .Tests}} <tr> <td>{{.TestName}}</td> - <td>{{if .Base}}{{.Base.CommitHash}}{{end}}</td> - <td>{{if .Patched}}{{.Patched.CommitHash}}{{end}}</td> + <td>{{if .BaseBuild}}{{.BaseBuild.CommitHash}}{{end}}</td> + <td>{{if .PatchedBuild}}{{.PatchedBuild.CommitHash}}[patched]{{end}}</td> <th>{{.Result}}</th> </tr> + {{if .Findings}} + <tr> + <td colspan="4"> + <table class="table mb-0"> + <thead> + <tr> + <th scope="col">Title</th> + </tr> + </thead> + <tbody> + {{range . .Findings}} + <tr> + <td>{{.Title}}</td> + </tr> + {{end}} + </tbody> + </td> + </tr> + {{end}} {{end}} </tbody> </table> diff --git a/syz-cluster/pkg/api/api.go b/syz-cluster/pkg/api/api.go index ea629e534..510dd1c47 100644 --- a/syz-cluster/pkg/api/api.go +++ b/syz-cluster/pkg/api/api.go @@ -60,12 +60,33 @@ type Build struct { BuildSuccess bool `json:"build_success"` } +const ( + TestRunning string = "running" + TestPassed string = "passed" + TestFailed string = "failed" // TODO: drop it? only mark completion? + TestError string = "error" +) + type TestResult struct { SessionID string `json:"session_id"` BaseBuildID string `json:"base_build_id"` PatchedBuildID string `json:"patched_build_id"` TestName string `json:"test_name"` Result string `json:"result"` + Log []byte `json:"log"` +} + +type BootResult struct { + Success bool `json:"success"` +} + +// Finding is a kernel crash, boot error, etc. found during a test. +type Finding struct { + SessionID string `json:"session_id"` + TestName string `json:"test_name"` + Title string `json:"title"` + Report []byte `json:"report"` + Log []byte `json:"log"` } // For now, there's no reason to obtain these really via a real API call. diff --git a/syz-cluster/pkg/api/client.go b/syz-cluster/pkg/api/client.go index 0d73d5d43..5f4121c9e 100644 --- a/syz-cluster/pkg/api/client.go +++ b/syz-cluster/pkg/api/client.go @@ -64,6 +64,11 @@ func (client Client) UploadTestResult(ctx context.Context, req *TestResult) erro return err } +func (client Client) UploadFinding(ctx context.Context, req *Finding) error { + _, err := postJSON[Finding, any](ctx, client.baseURL+"/findings/upload", req) + return err +} + func getJSON[Resp any](url string) (*Resp, error) { resp, err := http.Get(url) if err != nil { diff --git a/syz-cluster/pkg/db/entities.go b/syz-cluster/pkg/db/entities.go index babaa0eb3..dcaa2cb44 100644 --- a/syz-cluster/pkg/db/entities.go +++ b/syz-cluster/pkg/db/entities.go @@ -70,31 +70,18 @@ func (s *Session) SetFinishedAt(t time.Time) { s.FinishedAt = spanner.NullTime{Time: t, Valid: true} } -const ( - StateSuccess string = "success" - StateFailed string = "failed" - StateInProgress string = "in_progress" -) - -const ( - TestPassed string = "passed" - TestFailed string = "failed" - TestError string = "error" -) - type SessionTest struct { - SessionID string `spanner:"SessionID"` - BaseBuildID string `spanner:"BaseBuildID"` - PatchedBuildID string `spanner:"PatchedBuildID"` - TestName string `spanner:"TestName"` - Result string `spanner:"Result"` + SessionID string `spanner:"SessionID"` + BaseBuildID spanner.NullString `spanner:"BaseBuildID"` + PatchedBuildID spanner.NullString `spanner:"PatchedBuildID"` + TestName string `spanner:"TestName"` + Result string `spanner:"Result"` } type Finding struct { - ID string `spanner:"ID"` SessionID string `spanner:"SessionID"` TestName string `spanner:"TestName"` Title string `spanner:"Title"` ReportURI string `spanner:"ReportURI"` - LogURI string `spanner:"ConsoleLogURI"` + LogURI string `spanner:"LogURI"` } diff --git a/syz-cluster/pkg/db/finding_repo.go b/syz-cluster/pkg/db/finding_repo.go new file mode 100644 index 000000000..93577d32c --- /dev/null +++ b/syz-cluster/pkg/db/finding_repo.go @@ -0,0 +1,65 @@ +// Copyright 2025 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. + +package db + +import ( + "context" + "errors" + + "cloud.google.com/go/spanner" + "google.golang.org/api/iterator" +) + +type FindingRepository struct { + client *spanner.Client +} + +func NewFindingRepository(client *spanner.Client) *FindingRepository { + return &FindingRepository{ + client: client, + } +} + +var ErrFindingExists = errors.New("the finding already exists") + +// Save either adds the finding to the database or returns ErrFindingExists. +func (repo *FindingRepository) Save(ctx context.Context, finding *Finding) error { + _, err := repo.client.ReadWriteTransaction(ctx, + func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { + stmt := spanner.Statement{ + SQL: "SELECT * from `Findings` WHERE `SessionID`=@sessionID " + + "AND `TestName` = @testName AND `Title`=@title", + Params: map[string]interface{}{ + "sessionID": finding.SessionID, + "testName": finding.TestName, + "title": finding.Title, + }, + } + iter := txn.Query(ctx, stmt) + defer iter.Stop() + _, iterErr := iter.Next() + if iterErr == nil { + return ErrFindingExists + } else if iterErr != iterator.Done { + return iterErr + } + m, err := spanner.InsertStruct("Findings", finding) + if err != nil { + return err + } + return txn.BufferWrite([]*spanner.Mutation{m}) + }) + return err +} + +// nolint: dupl +func (repo *FindingRepository) ListForSession(ctx context.Context, session *Session) ([]*Finding, error) { + stmt := spanner.Statement{ + SQL: "SELECT * FROM `Findings` WHERE `SessionID` = @session ORDER BY `TestName`, `Title`", + Params: map[string]interface{}{"session": session.ID}, + } + iter := repo.client.Single().Query(ctx, stmt) + defer iter.Stop() + return readEntities[Finding](iter) +} diff --git a/syz-cluster/pkg/db/finding_repo_test.go b/syz-cluster/pkg/db/finding_repo_test.go new file mode 100644 index 000000000..9afa22ede --- /dev/null +++ b/syz-cluster/pkg/db/finding_repo_test.go @@ -0,0 +1,71 @@ +// Copyright 2025 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. + +package db + +import ( + "testing" + "time" + + "github.com/google/syzkaller/syz-cluster/pkg/api" + "github.com/stretchr/testify/assert" +) + +func TestFindingRepo(t *testing.T) { + client, ctx := NewTransientDB(t) + sessionRepo := NewSessionRepository(client) + seriesRepo := NewSeriesRepository(client) + findingRepo := NewFindingRepository(client) + testsRepo := NewSessionTestRepository(client) + + series := &Series{ExtID: "some-series"} + err := seriesRepo.Insert(ctx, series, nil) + assert.NoError(t, err) + + session := &Session{CreatedAt: time.Now()} + err = sessionRepo.Insert(ctx, series, session) + assert.NoError(t, err) + + // Add test steps. + for _, name := range []string{"first", "second"} { + err = testsRepo.InsertOrUpdate(ctx, &SessionTest{ + SessionID: session.ID, + TestName: name, + Result: api.TestPassed, + }) + assert.NoError(t, err) + } + + // Add findings. + toInsert := []*Finding{ + { + TestName: "first", + Title: "A", + SessionID: session.ID, + }, + { + TestName: "first", + Title: "B", + SessionID: session.ID, + }, + { + TestName: "second", + Title: "A", + SessionID: session.ID, + }, + } + // Insert them all. + for _, finding := range toInsert { + err := findingRepo.Save(ctx, finding) + assert.NoError(t, err, "finding=%q", finding) + } + // Now it should report a duplicate each time. + for _, finding := range toInsert { + err := findingRepo.Save(ctx, finding) + assert.ErrorIs(t, err, ErrFindingExists) + } + + list, err := findingRepo.ListForSession(ctx, session) + assert.NoError(t, err) + assert.Equal(t, toInsert, list) +} diff --git a/syz-cluster/pkg/db/migrations/1_initialize.up.sql b/syz-cluster/pkg/db/migrations/1_initialize.up.sql index 5bbbe93a4..b7dcfea26 100644 --- a/syz-cluster/pkg/db/migrations/1_initialize.up.sql +++ b/syz-cluster/pkg/db/migrations/1_initialize.up.sql @@ -66,16 +66,15 @@ CREATE TABLE SessionTests ( TestName STRING(256) NOT NULL, -- Parameters JSON, -- Test-dependent set of parameters. Result STRING(36) NOT NULL, - BaseBuildID STRING(36) NOT NULL, - PatchedBuildID STRING(36) NOT NULL, + BaseBuildID STRING(36), + PatchedBuildID STRING(36), CONSTRAINT FK_SessionResults FOREIGN KEY (SessionID) REFERENCES Sessions (ID), - CONSTRAINT ResultEnum CHECK (Result IN ('passed', 'failed', 'error')), + CONSTRAINT ResultEnum CHECK (Result IN ('passed', 'failed', 'error', 'running')), CONSTRAINT FK_BaseBuild FOREIGN KEY (BaseBuildID) REFERENCES Builds (ID), CONSTRAINT FK_PatchedBuild FOREIGN KEY (PatchedBuildID) REFERENCES Builds (ID), ) PRIMARY KEY(SessionID, TestName); CREATE TABLE Findings ( - ID STRING(36) NOT NULL, -- UUID SessionID STRING(36) NOT NULL, TestName STRING(256) NOT NULL, Title STRING(256) NOT NULL, @@ -83,4 +82,4 @@ CREATE TABLE Findings ( LogURI STRING(256) NOT NULL, CONSTRAINT FK_SessionCrashes FOREIGN KEY (SessionID) REFERENCES Sessions (ID), CONSTRAINT FK_TestCrashes FOREIGN KEY (SessionID, TestName) REFERENCES SessionTests (SessionID, TestName), -) PRIMARY KEY(ID) +) PRIMARY KEY(SessionID, TestName, Title); diff --git a/syz-cluster/pkg/db/session_test_repo.go b/syz-cluster/pkg/db/session_test_repo.go index 221b219c6..f3eb62268 100644 --- a/syz-cluster/pkg/db/session_test_repo.go +++ b/syz-cluster/pkg/db/session_test_repo.go @@ -20,10 +20,10 @@ func NewSessionTestRepository(client *spanner.Client) *SessionTestRepository { } } -func (repo *SessionTestRepository) Insert(ctx context.Context, test *SessionTest) error { +func (repo *SessionTestRepository) InsertOrUpdate(ctx context.Context, test *SessionTest) error { _, err := repo.client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { - // Check if the series already exists. + // Check if the test already exists. stmt := spanner.Statement{ SQL: "SELECT * from `SessionTests` WHERE `SessionID`=@sessionID AND `TestName` = @testName", Params: map[string]interface{}{ @@ -59,8 +59,8 @@ func (repo *SessionTestRepository) Insert(ctx context.Context, test *SessionTest type FullSessionTest struct { *SessionTest - Base *Build - Patched *Build + BaseBuild *Build + PatchedBuild *Build } func (repo *SessionTestRepository) BySession(ctx context.Context, sessionID string) ([]*FullSessionTest, error) { @@ -80,11 +80,11 @@ func (repo *SessionTestRepository) BySession(ctx context.Context, sessionID stri for _, obj := range list { full := &FullSessionTest{SessionTest: obj} ret = append(ret, full) - if obj.BaseBuildID != "" { - needBuilds[obj.BaseBuildID] = append(needBuilds[obj.BaseBuildID], &full.Base) + if id := obj.BaseBuildID.String(); !obj.BaseBuildID.IsNull() { + needBuilds[id] = append(needBuilds[id], &full.BaseBuild) } - if obj.PatchedBuildID != "" { - needBuilds[obj.PatchedBuildID] = append(needBuilds[obj.PatchedBuildID], &full.Patched) + if id := obj.PatchedBuildID.String(); !obj.PatchedBuildID.IsNull() { + needBuilds[id] = append(needBuilds[id], &full.PatchedBuild) } } if len(needBuilds) > 0 { diff --git a/syz-cluster/pkg/db/session_test_repo_test.go b/syz-cluster/pkg/db/session_test_repo_test.go index 2756f9d4c..2ecd696d3 100644 --- a/syz-cluster/pkg/db/session_test_repo_test.go +++ b/syz-cluster/pkg/db/session_test_repo_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "cloud.google.com/go/spanner" + "github.com/google/syzkaller/syz-cluster/pkg/api" "github.com/stretchr/testify/assert" ) @@ -38,15 +40,19 @@ func TestSessionTestRepository(t *testing.T) { test := &SessionTest{ SessionID: session.ID, TestName: fmt.Sprintf("test %d", i), - BaseBuildID: build1.ID, - PatchedBuildID: build2.ID, - Result: TestPassed, + BaseBuildID: spanner.NullString{StringVal: build1.ID, Valid: true}, + PatchedBuildID: spanner.NullString{StringVal: build2.ID, Valid: true}, + Result: api.TestPassed, } - err = testsRepo.Insert(ctx, test) + err = testsRepo.InsertOrUpdate(ctx, test) assert.NoError(t, err) } list, err := testsRepo.BySession(ctx, session.ID) assert.NoError(t, err) assert.Len(t, list, 2) + for _, test := range list { + assert.NotNil(t, test.BaseBuild) + assert.NotNil(t, test.PatchedBuild) + } } |
