Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions syz-cluster/dashboard/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func (h *dashboardHandler) seriesList(w http.ResponseWriter, r *http.Request) er
WithFindings: r.FormValue("with_findings") != "",
Limit: perPage,
Offset: offset,
Name: r.FormValue("name"),
},
// If the filters are changed, the old offset value is irrelevant.
FilterFormURL: urlutil.DropParam(baseURL, "offset", ""),
Expand Down Expand Up @@ -171,6 +172,7 @@ func (h *dashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) er
*db.Series
Patches []*db.Patch
Sessions []SessionData
Versions []*db.Series
TotalPatches int
}
var data SeriesData
Expand All @@ -187,6 +189,11 @@ func (h *dashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) er
return fmt.Errorf("failed to query patches: %w", err)
}
data.TotalPatches = len(data.Patches)
// Note: There may be some false positives, but there's no straightforward way to filter them out.
data.Versions, err = h.seriesRepo.ListAllVersions(ctx, data.Series.Title)
if err != nil {
return fmt.Errorf("failed to query all series versions: %w", err)
}
sessions, err := h.sessionRepo.ListForSeries(ctx, data.Series)
if err != nil {
return fmt.Errorf("failed to query sessions: %w", err)
Expand Down
4 changes: 4 additions & 0 deletions syz-cluster/dashboard/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<label for="inputCc">Cc'd</label>
<input type="text" name="cc" class="form-control mb-3" value="{{.Filter.Cc}}" id="inputCc">
</div>
<div class="col-auto col-sm-3">
<label for="inputName">Patch/Series Name</label>
<input type="text" name="name" class="form-control mb-3" value="{{.Filter.Name}}" id="inputName">
</div>
<div class="col-auto col-sm-3">
<label for="inputStatus">Status</label>
<select id="inputStatus" class="form-control mb-3" name="status">
Expand Down
11 changes: 10 additions & 1 deletion syz-cluster/dashboard/templates/series.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ <h2>Patch Series</h2>
</tr>
<tr>
<th>Version</th>
<td>{{.Version}}</td>
<td>
<select class="form-select" onchange="window.location.href=this.value;">
{{$currentSeriesVersion := .Version}}
{{range .Versions}}
<option value="/series/{{.ID}}" {{if eq .Version $currentSeriesVersion}}selected{{end}}>
Version {{.Version}}
</option>
{{end}}
</select>
</td>
</tr>
<tr>
<th>Cc</th>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Revert search by patch and series names
DROP INDEX SeriesIndex;
ALTER TABLE Series DROP COLUMN TitleTokens;
DROP INDEX PatchesIndex;
ALTER TABLE Patches DROP COLUMN TitleTokens;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Enable search by patch and series names
ALTER TABLE Series ADD COLUMN TitleTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Title)) HIDDEN;
CREATE SEARCH INDEX SeriesIndex ON Series(TitleTokens);
ALTER TABLE Patches ADD COLUMN TitleTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Title)) HIDDEN;
CREATE SEARCH INDEX PatchesIndex ON Patches(TitleTokens);
53 changes: 41 additions & 12 deletions syz-cluster/pkg/db/series_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -132,6 +133,7 @@ type SeriesFilter struct {
WithFindings bool
Limit int
Offset int
Name string
}

// ListLatest() returns the list of series ordered by the decreasing PublishedAt value.
Expand All @@ -141,41 +143,57 @@ func (repo *SeriesRepository) ListLatest(ctx context.Context, filter SeriesFilte
defer ro.Close()

stmt := spanner.Statement{
SQL: "SELECT Series.* FROM Series WHERE 1=1",
SQL: "SELECT Series.* FROM Series",
Params: map[string]any{},
}
var conds []string

if !maxPublishedAt.IsZero() {
stmt.SQL += " AND PublishedAt < @toTime"
conds = append(conds, "PublishedAt < @toTime")
stmt.Params["toTime"] = maxPublishedAt
}
if filter.Cc != "" {
stmt.SQL += " AND @cc IN UNNEST(Cc)"
conds = append(conds, "@cc IN UNNEST(Cc)")
stmt.Params["cc"] = filter.Cc
}
if filter.Name != "" {
conds = append(conds, `ID IN(
SELECT ID FROM SERIES
WHERE SEARCH(Series.TitleTokens, @name)
UNION DISTINCT
SELECT SeriesID FROM Patches
WHERE SEARCH(Patches.TitleTokens, @name)
)`)
stmt.Params["name"] = filter.Name
}
if filter.Status != SessionStatusAny {
// It could have been an INNER JOIN in the main query, but let's favor the simpler code
// in this function.
// The optimizer should transform the query to a JOIN anyway.
stmt.SQL += " AND EXISTS(SELECT 1 FROM Sessions WHERE"
var statusCond = "EXISTS(SELECT 1 FROM Sessions WHERE"
switch filter.Status {
case SessionStatusWaiting:
stmt.SQL += " Sessions.SeriesID = Series.ID AND Sessions.StartedAt IS NULL"
statusCond += " Sessions.SeriesID = Series.ID AND Sessions.StartedAt IS NULL"
case SessionStatusInProgress:
stmt.SQL += " Sessions.ID = Series.LatestSessionID AND Sessions.FinishedAt IS NULL"
statusCond += " Sessions.ID = Series.LatestSessionID AND Sessions.FinishedAt IS NULL"
case SessionStatusFinished:
stmt.SQL += " Sessions.ID = Series.LatestSessionID AND Sessions.FinishedAt IS NOT NULL" +
statusCond += " Sessions.ID = Series.LatestSessionID AND Sessions.FinishedAt IS NOT NULL" +
" AND Sessions.SkipReason IS NULL"
case SessionStatusSkipped:
stmt.SQL += " Sessions.ID = Series.LatestSessionID AND Sessions.SkipReason IS NOT NULL"
statusCond += " Sessions.ID = Series.LatestSessionID AND Sessions.SkipReason IS NOT NULL"
default:
return nil, fmt.Errorf("unknown status value: %q", filter.Status)
}
stmt.SQL += ")"
statusCond += ")"
conds = append(conds, statusCond)
}
if filter.WithFindings {
stmt.SQL += " AND Series.LatestSessionID IS NOT NULL AND EXISTS(" +
"SELECT 1 FROM Findings WHERE " +
"Findings.SessionID = Series.LatestSessionID AND Findings.InvalidatedAt IS NULL)"
conds = append(conds, "Series.LatestSessionID IS NOT NULL AND EXISTS("+
"SELECT 1 FROM Findings WHERE "+
"Findings.SessionID = Series.LatestSessionID AND Findings.InvalidatedAt IS NULL)")
}
if len(conds) != 0 {
stmt.SQL += " WHERE " + strings.Join(conds, " AND ")
}
stmt.SQL += " ORDER BY PublishedAt DESC, ID"
if filter.Limit > 0 {
Expand Down Expand Up @@ -210,6 +228,17 @@ func (repo *SeriesRepository) ListLatest(ctx context.Context, filter SeriesFilte
return ret, nil
}

func (repo *SeriesRepository) ListAllVersions(ctx context.Context, title string) ([]*Series, error) {
ro := repo.client.ReadOnlyTransaction()
defer ro.Close()
return readEntities[Series](ctx, ro, spanner.Statement{
SQL: "SELECT ID, Version FROM SERIES where Title = @title ORDER BY Version",
Params: map[string]any{
"title": title,
},
})
}

func (repo *SeriesRepository) querySessions(ctx context.Context, ro *spanner.ReadOnlyTransaction,
seriesList []*SeriesWithSession) error {
idToSeries := map[string]*SeriesWithSession{}
Expand Down
88 changes: 88 additions & 0 deletions syz-cluster/pkg/db/series_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,94 @@ func TestSeriesRepositoryList(t *testing.T) {
})
}

// nolint: dupl
func TestSeriesRepositorySearch(t *testing.T) {
client, ctx := NewTransientDB(t)
repo := NewSeriesRepository(client)

series1 := &Series{
ExtID: "series-search-1",
Title: "Kernel Series for ARM64",
PublishedAt: time.Date(2020, time.January, 1, 1, 0, 0, 0, time.UTC),
}
patches1 := []*Patch{
{
Title: "arm64: patch for CPU",
Seq: 1,
},
{
Title: "arm64: another patch for memory",
Seq: 2,
},
}
err := repo.Insert(ctx, series1, func() ([]*Patch, error) {
return patches1, nil
})
assert.NoError(t, err)

series2 := &Series{
ExtID: "series-search-2",
Title: "X86 Specific Patch Series",
PublishedAt: time.Date(2020, time.January, 1, 2, 0, 0, 0, time.UTC),
}
patches2 := []*Patch{
{
Title: "x86: new feature",
Seq: 1,
},
}
err = repo.Insert(ctx, series2, func() ([]*Patch, error) {
return patches2, nil
})
assert.NoError(t, err)

series3 := &Series{
ExtID: "series-search-3",
Title: "Generic Bug Fixes",
PublishedAt: time.Date(2020, time.January, 1, 3, 0, 0, 0, time.UTC),
}
patches3 := []*Patch{
{
Title: "net: fix double free",
Seq: 1,
},
}
err = repo.Insert(ctx, series3, func() ([]*Patch, error) {
return patches3, nil
})
assert.NoError(t, err)

t.Run("by_series_name", func(t *testing.T) {
list, err := repo.ListLatest(ctx, SeriesFilter{Name: "Kernel Series"}, time.Time{})
assert.NoError(t, err)
assert.Len(t, list, 1)
assert.Equal(t, series1.Title, list[0].Series.Title)
})
t.Run("by_patch_name", func(t *testing.T) {
list, err := repo.ListLatest(ctx, SeriesFilter{Name: "double free"}, time.Time{})
assert.NoError(t, err)
assert.Len(t, list, 1)
assert.Equal(t, series3.Title, list[0].Series.Title)
})
t.Run("no_match", func(t *testing.T) {
list, err := repo.ListLatest(ctx, SeriesFilter{Name: "nonexistent"}, time.Time{})
assert.NoError(t, err)
assert.Len(t, list, 0)
})
t.Run("empty_search_string", func(t *testing.T) {
list, err := repo.ListLatest(ctx, SeriesFilter{Name: ""}, time.Time{})
assert.NoError(t, err)
assert.Len(t, list, 3) // All series should be returned if search strings are empty.
})
t.Run("search_across_series_and_patch", func(t *testing.T) {
list, err := repo.ListLatest(ctx, SeriesFilter{Name: "patch"}, time.Time{})
assert.NoError(t, err)
assert.Len(t, list, 2)
assert.Equal(t, series2.Title, list[0].Series.Title)
assert.Equal(t, series1.Title, list[1].Series.Title)
})
}

func TestSeriesRepositoryUpdate(t *testing.T) {
client, ctx := NewTransientDB(t)
repo := NewSeriesRepository(client)
Expand Down
Loading