aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-01-15 17:41:31 +0100
committerAleksandr Nogikh <nogikh@google.com>2025-01-22 13:17:53 +0000
commitcc143e38041972ad4dbaff9cfbfd416c29d581b5 (patch)
tree31e07572a22bebdc20c1fc73a6ef9140c03eb3e5
parent6bab9518e47d67b3c9bba00f213aa3e1637063e1 (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.go40
-rw-r--r--syz-cluster/controller/api_test.go66
-rw-r--r--syz-cluster/controller/services.go69
-rw-r--r--syz-cluster/dashboard/handler.go36
-rw-r--r--syz-cluster/dashboard/templates/series.html23
-rw-r--r--syz-cluster/pkg/api/api.go21
-rw-r--r--syz-cluster/pkg/api/client.go5
-rw-r--r--syz-cluster/pkg/db/entities.go25
-rw-r--r--syz-cluster/pkg/db/finding_repo.go65
-rw-r--r--syz-cluster/pkg/db/finding_repo_test.go71
-rw-r--r--syz-cluster/pkg/db/migrations/1_initialize.up.sql9
-rw-r--r--syz-cluster/pkg/db/session_test_repo.go16
-rw-r--r--syz-cluster/pkg/db/session_test_repo_test.go14
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)
+ }
}