blob: 4929da13146278c42fac72cf220ef6ef3067f283 [file]
package analysis
import (
"encoding/json"
"sort"
"sync"
"time"
"github.com/rcrowley/go-metrics"
"github.com/skia-dev/glog"
"go.skia.org/infra/go/timer"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/expstorage"
"go.skia.org/infra/golden/go/filediffstore"
"go.skia.org/infra/golden/go/ignore"
"go.skia.org/infra/golden/go/storage"
"go.skia.org/infra/golden/go/types"
ptypes "go.skia.org/infra/perf/go/types"
)
const (
NEW_TILE_CACHE_KEY = "newtile"
IGNORED_TILE_CACHE_KEY = "ignoredtile"
)
var (
// The number of times we've successfully loaded and processed a tile.
runsCounter metrics.Counter
// The number of times an error has ocurred when trying to load a tile.
errorTileLoadingCounter metrics.Counter
)
func init() {
runsCounter = metrics.NewRegisteredCounter("analysis.runs", metrics.DefaultRegistry)
errorTileLoadingCounter = metrics.NewRegisteredCounter("analysis.errors", metrics.DefaultRegistry)
}
type PathToURLConverter func(string) string
// LabeledTrace stores a Trace with labels and digests. CommitIds, Digests and
// Labels are of the same length, identical indices refer to the same digest.
type LabeledTrace struct {
Params map[string]string
CommitIds []int
Digests []string
Labels []types.Label
Id int
IgnoreRules []*ignore.IgnoreRule
}
func NewLabeledTrace(params map[string]string, capacity int, traceId int) *LabeledTrace {
return &LabeledTrace{
Params: params,
CommitIds: make([]int, 0, capacity),
Digests: make([]string, 0, capacity),
Labels: make([]types.Label, 0, capacity),
Id: traceId,
IgnoreRules: []*ignore.IgnoreRule{},
}
}
// addLabledDigests adds the given tripples of commitIds, digests and labels to this LabeledTrace.
func (lt *LabeledTrace) addLabeledDigests(commitIds []int, digests []string, labels []types.Label) {
lt.CommitIds = append(lt.CommitIds, commitIds...)
lt.Digests = append(lt.Digests, digests...)
lt.Labels = append(lt.Labels, labels...)
}
// addIgnoreRules attaches the ignore rules that match his trace.
func (lt *LabeledTrace) addIgnoreRules(newRules []*ignore.IgnoreRule) {
lt.IgnoreRules = append(lt.IgnoreRules, newRules...)
}
// LabeledTile aggregates the traces of a tile and provides a slice of commits
// that the commitIds in LabeledTrace refer to.
// LabeledTile and LabeledTrace store the cannonical information
// extracted from the unterlying tile store. The (redundant) output data is
// derived from these.
type LabeledTile struct {
Commits []*ptypes.Commit
// Traces are indexed by the primary key (test name). This is somewhat
// redundant, but this also output format.
Traces map[string][]*LabeledTrace
// CommitsByDigest maps a corpus and a digest to a list of commit ids.
// i.e. CommitsByDigest[corpus][digest] -> slice with indices of Commits.
CommitsByDigest map[string]map[string][]int
// Keeps track of unique ids for traces within this tile.
TraceIdCounter int
}
func NewLabeledTile() *LabeledTile {
return &LabeledTile{
Commits: []*ptypes.Commit{},
CommitsByDigest: map[string]map[string][]int{},
Traces: map[string][]*LabeledTrace{},
TraceIdCounter: 0,
}
}
// getLabeledTrace is a utility function that returns the testName and a labeled
// trace for the given trace (read from a TileStore). If the LabeledTrace does
// not exist it will be added.
func (t *LabeledTile) getLabeledTrace(trace ptypes.Trace) (string, *LabeledTrace) {
params := trace.Params()
pKey := params[types.PRIMARY_KEY_FIELD]
if _, ok := t.Traces[pKey]; !ok {
// Add the primary key with a single labled trace.
t.Traces[pKey] = []*LabeledTrace{}
}
// Search through the traces associated witht this test.
for _, v := range t.Traces[pKey] {
if util.MapsEqual(v.Params, params) {
return pKey, v
}
}
// If we cannot find the trace in our set of tests we are adding a new
// labeled trace.
newLT := NewLabeledTrace(params, trace.Len(), t.TraceIdCounter)
t.TraceIdCounter++
t.Traces[pKey] = append(t.Traces[pKey], newLT)
return pKey, newLT
}
// LabelCounts is an output type to hold counts for classification labels.
type LabelCounts struct {
Unt []int `json:"unt"` // Untriaged
Pos []int `json:"pos"` // Positive
Neg []int `json:"neg"` // Negative
}
func newLabelCounts(length int) *LabelCounts {
return &LabelCounts{
Unt: make([]int, length),
Pos: make([]int, length),
Neg: make([]int, length),
}
}
// AnalyzeState captures the state of a partition of the incoming data.
// When a tile is read from disk it is partitioned into two tiles: current
// and ignored. current contains everything we want to be able to review
// continuously and ignored contains all ignored traces.
// This struct is the container for one of these partitions and the derived
// information.
type AnalyzeState struct {
// Canonical data structure to hold our information about commits, digests
// and labels.
Tile *LabeledTile
// Index to query the Tile.
Index *LabeledTileIndex
// Output data structures that are derived from Tile.
TestDetails *GUITestDetails
Status *GUIStatus
BlameLists *GUIBlameLists
}
// Analyzer continuously manages tasks like polling for new traces
// on disk and generating diffs between images. It is the primary interface
// to be called by the HTTP frontend.
type Analyzer struct {
storages *storage.Storage
// current contains the state of the digests we are primariliy interested in.
current *AnalyzeState
// ignored contains the state of ignored digests.
ignored *AnalyzeState
// lastRawTile points to the last tile loaded from the tileStore.
lastRawTile *ptypes.Tile
// labeledTileCache caches the labeled tiles extracted tiles.
labeledTileCache util.LRUCache
// converter supplied by the client of the type to convert a path to a URL
pathToURLConverter PathToURLConverter
// Lock to protect the expectations and current* variables.
mutex sync.RWMutex
// Counts the number of times the main event loop has executed.
// This is for testing only.
loopCounter int
}
type LabeledTileCodec int
func (d LabeledTileCodec) Encode(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (d LabeledTileCodec) Decode(data []byte) (interface{}, error) {
var v LabeledTile
err := json.Unmarshal(data, &v)
return &v, err
}
func NewAnalyzer(storages *storage.Storage, puConverter PathToURLConverter, cacheFactory filediffstore.CacheFactory, timeBetweenPolls time.Duration) *Analyzer {
labeledTileCache := cacheFactory("ti", LabeledTileCodec(0))
result := &Analyzer{
storages: storages,
pathToURLConverter: puConverter,
current: &AnalyzeState{},
ignored: &AnalyzeState{},
lastRawTile: nil,
labeledTileCache: labeledTileCache,
}
go result.loop(timeBetweenPolls)
return result
}
// ListTestDetails returns a list of triage details based on the supplied
// query. It's complementary to GetTestDetails which returns a single test
// detail.
// TODO(stephana): This should provide pagination since the list is potentially
// very long. If we don't add pagination, this should be merged with
// GetTestDetail.
func (a *Analyzer) ListTestDetails(query map[string][]string) (*GUITestDetails, error) {
a.mutex.RLock()
defer a.mutex.RUnlock()
if len(query) == 0 {
return a.current.TestDetails, nil
}
effectiveQuery := make(map[string][]string, len(query))
foundUntriaged := a.getUntriagedTestDetails(query, effectiveQuery, true)
tests := make([]*GUITestDetail, 0, len(foundUntriaged))
for testName, untriaged := range foundUntriaged {
testDetail := a.current.TestDetails.lookup(testName)
tests = append(tests, &GUITestDetail{
Name: testName,
Untriaged: untriaged,
Positive: testDetail.Positive,
Negative: testDetail.Negative,
})
}
// Sort the test details.
sort.Sort(GUITestDetailSortable(tests))
return &GUITestDetails{
Commits: a.current.TestDetails.Commits,
AllParams: a.current.Index.getAllParams(query),
Query: effectiveQuery,
Tests: tests,
Blames: a.current.BlameLists.Blames,
}, nil
}
// GetTestDetails returns the untriaged, positive and negative digests for a
// specific test with the necessary information (diff metrics, image urls) to
// assign a label to the untriaged digests.
// If query is not empty then we will return traces that match the query.
// If the query is empty and testName is not empty we will return the
// traces of the corresponding test.If both query and testName are empty
// we will return all traces.
// TODO (stephana): If the result is too big we should add pagination.
func (a *Analyzer) GetTestDetails(testName string, query map[string][]string) (*GUITestDetails, error) {
a.mutex.RLock()
defer a.mutex.RUnlock()
var effectiveQuery map[string][]string
testDetail := a.current.TestDetails.lookup(testName)
untriaged := testDetail.Untriaged
if len(query) > 0 {
effectiveQuery = map[string][]string{}
// Filter by only this test.
query[types.PRIMARY_KEY_FIELD] = []string{testName}
foundUntriaged := a.getUntriagedTestDetails(query, effectiveQuery, false)
delete(effectiveQuery, types.PRIMARY_KEY_FIELD)
// Only consider the result if some query parameters were valid.
if len(effectiveQuery) > 0 {
if temp, ok := foundUntriaged[testName]; ok {
untriaged = temp
} else {
untriaged = map[string]*GUIUntriagedDigest{}
}
}
}
return &GUITestDetails{
Commits: a.current.TestDetails.Commits,
CommitsByDigest: map[string]map[string][]int{testName: a.current.TestDetails.CommitsByDigest[testName]},
AllParams: a.current.Index.getAllParams(query),
Query: effectiveQuery,
Blames: map[string][]*BlameDistribution{testName: a.current.BlameLists.Blames[testName]},
Tests: []*GUITestDetail{
&GUITestDetail{
Name: testName,
Untriaged: untriaged,
Positive: testDetail.Positive,
Negative: testDetail.Negative,
},
},
}, nil
}
// SetDigestLabels sets the labels for the given digest and records the user
// that made the classification.
func (a *Analyzer) SetDigestLabels(labeledTestDigests map[string]types.TestClassification, userId string) (*GUITestDetails, error) {
a.mutex.Lock()
defer a.mutex.Unlock()
if err := a.storages.ExpectationsStore.AddChange(labeledTestDigests, userId); err != nil {
return nil, err
}
expectations, err := a.storages.ExpectationsStore.Get()
if err != nil {
return nil, err
}
// Let's update our knowledge of the labels.
a.updateDerivedOutputs(labeledTestDigests, expectations, a.current)
a.updateDerivedOutputs(labeledTestDigests, expectations, a.ignored)
result := make([]*GUITestDetail, 0, len(labeledTestDigests))
for testName := range labeledTestDigests {
result = append(result, a.current.TestDetails.lookup(testName))
}
return &GUITestDetails{
Commits: a.current.TestDetails.Commits,
AllParams: a.current.Index.getAllParams(nil),
Tests: result,
}, nil
}
func (a *Analyzer) GetStatus() *GUIStatus {
return a.current.Status
}
// ListIgnoreRules returns all current ignore rules.
func (a *Analyzer) ListIgnoreRules() ([]*ignore.IgnoreRule, error) {
rules, err := a.storages.IgnoreStore.List()
if err != nil {
return nil, err
}
// TODO(stephana): Inject Count and other statistics about the
// ignored traces. This will be based on LabeledTrace.IgnoreRules.
return rules, nil
}
// AddIgnoreRule adds a new ignore rule and recalculates the new state of the
// system.
func (a *Analyzer) AddIgnoreRule(ignoreRule *ignore.IgnoreRule) error {
if err := a.storages.IgnoreStore.Create(ignoreRule); err != nil {
return err
}
a.processTile(false, false)
return nil
}
// UpdateIgnoreRule updates an existing ignore rule and recalculates the new state of the
// system.
func (a *Analyzer) UpdateIgnoreRule(ruleId int, ignoreRule *ignore.IgnoreRule) error {
if err := a.storages.IgnoreStore.Update(ruleId, ignoreRule); err != nil {
return err
}
a.processTile(false, false)
return nil
}
// DeleteIgnoreRule deletes the ignore rule and recalculates the state of the
// system.
func (a *Analyzer) DeleteIgnoreRule(ruleId int, user string) error {
count, err := a.storages.IgnoreStore.Delete(ruleId, user)
if err != nil {
return err
}
if count > 0 {
a.processTile(false, false)
}
return nil
}
// GetBlameList returns the blame lists for the test identified by testName or
// nil if there is no such test.
func (a *Analyzer) GetBlameList(testName string) *GUIBlameLists {
ret, ok := a.current.BlameLists.Blames[testName]
if !ok {
return nil
}
return &GUIBlameLists{
Commits: a.current.BlameLists.Commits,
Blames: map[string][]*BlameDistribution{testName: ret},
}
}
// loop is the main event loop.
func (a *Analyzer) loop(timeBetweenPolls time.Duration) {
// Process the tile with caching. If the result is false that means
// no tile was loaded from disk and we need to do another run with
// the latest tile.
if !a.processTile(true, true) {
a.processTile(false, true)
}
for _ = range time.Tick(timeBetweenPolls) {
a.processTile(false, true)
}
}
// getCachedTiles returns the last instances of current and ignored.
func (a *Analyzer) getCachedTiles() (*LabeledTile, *LabeledTile) {
if newLabeledTile, ok := a.labeledTileCache.Get(NEW_TILE_CACHE_KEY); ok {
if ignoredLabeledTile, ok := a.labeledTileCache.Get(IGNORED_TILE_CACHE_KEY); ok {
return newLabeledTile.(*LabeledTile), ignoredLabeledTile.(*LabeledTile)
}
}
return nil, nil
}
// cacheTiles stores the latest instances of current and ignored.
func (a *Analyzer) cacheTiles(newLabeledTile *LabeledTile, ignoredLabeledTile *LabeledTile) {
a.labeledTileCache.Add(NEW_TILE_CACHE_KEY, newLabeledTile)
a.labeledTileCache.Add(IGNORED_TILE_CACHE_KEY, ignoredLabeledTile)
}
// processTile loads a tile (built by the ingest process) and partitions it
// into two labeled tiles one with the traces of interest and the traces we
// are ignoring.
// The flags indicate whether to use the cached labeled tiles (during startup)
// and whether to reload the tile from disk or use lastRawTile instead.
// The return value indicates whether a raw tile was loaded from disk.
func (a *Analyzer) processTile(useCached bool, reloadRawTile bool) bool {
loadedTile := false
defer timer.New("processTile").Stop()
glog.Infof("Starting processtile: %t - %t", useCached, reloadRawTile)
var newLabeledTile, ignoredLabeledTile *LabeledTile = nil, nil
var err error
// Load the labeled tiles from cache. This will require no new diffs
// since they have already been calculated for the cached tile.
if useCached {
newLabeledTile, ignoredLabeledTile = a.getCachedTiles()
}
// If we don't have labeled tiles at this point we need to get a raw tile
// and build a labeled tile.
if newLabeledTile == nil {
// Either use a tile already in memory or load a new one.
var tile *ptypes.Tile
if reloadRawTile || (a.lastRawTile == nil) {
tile, err = a.storages.GetLastTileTrimmed(true)
if err != nil {
glog.Errorf("Error retrieving trimmed tile: %s\n", err)
errorTileLoadingCounter.Inc(1)
return false
}
glog.Infof("TileLen: %d", tile.LastCommitIndex()+1)
a.lastRawTile = tile
loadedTile = true
glog.Infoln("Loaded new tile from disk.")
} else {
tile = a.lastRawTile
glog.Infoln("Reusing last raw tile.")
}
newLabeledTile, ignoredLabeledTile = a.partitionRawTile(tile)
a.prepDiffsForLabeledTile(newLabeledTile)
a.prepDiffsForLabeledTile(ignoredLabeledTile)
a.cacheTiles(newLabeledTile, ignoredLabeledTile)
}
glog.Infof("Tests in newLabeledTiles : %d", len(newLabeledTile.Traces))
glog.Infof("Tests in ignoredLabeledTiles: %d", len(ignoredLabeledTile.Traces))
// Protect the tile and expectations with the write lock.
a.mutex.Lock()
defer a.mutex.Unlock()
// Retrieve the current expectations.
expectations, err := a.storages.ExpectationsStore.Get()
if err != nil {
glog.Errorf("Error retrieving expectations: %s", err)
return false
}
a.setDerivedOutputs(newLabeledTile, expectations, a.current, false)
a.setDerivedOutputs(ignoredLabeledTile, expectations, a.ignored, false)
glog.Info("Done processing tiles.")
runsCounter.Inc(1)
a.loopCounter++
return loadedTile
}
// completeDiffs forces the DiffStore to precalculate the diffs
// between all digests (within a test).
// TODO(stephana): This is currently not used, but will be enabled once
// the filediffstore can efficiently handle complete diffs.
func (a *Analyzer) completeDiffs(labeledTile *LabeledTile) {
var wg sync.WaitGroup
for _, traces := range labeledTile.Traces {
wg.Add(1)
go func(traces []*LabeledTrace) {
digestsList := make([][]string, 0, len(traces))
for _, t := range traces {
digestsList = append(digestsList, t.Digests)
}
allDigests := util.UnionStrings(digestsList...)
a.storages.DiffStore.CalculateDiffs(allDigests)
wg.Done()
}(traces)
}
wg.Wait()
}
// prepDiffsForLabeledTile forces the DiffStore to precalculate the diffs
// between new digests. We do this outside the locking so that when we are
// inside the lock almost all diffs will be cached already.
func (a *Analyzer) prepDiffsForLabeledTile(labeledTile *LabeledTile) {
glog.Infof("Starting prep diffs")
// Get the current expectations.
a.mutex.RLock()
expectations, err := a.storages.ExpectationsStore.Get()
a.mutex.RUnlock()
if err != nil {
glog.Errorf("Unable to read expectations: %s", err)
return
}
// Make a dummy call to setDerivedOutputs to force a diff on new
// digest pairs.
tempState := AnalyzeState{}
a.setDerivedOutputs(labeledTile, expectations, &tempState, true)
glog.Infof("Done prep diffs")
}
// partitionRawTile partitions the input tile into two tiles (current and ignored)
// and derives the output data structures for both.
func (a *Analyzer) partitionRawTile(tile *ptypes.Tile) (*LabeledTile, *LabeledTile) {
glog.Info("Processing tile into LabeledTile ...")
// Shared between both tiles.
tileLen := tile.LastCommitIndex() + 1
// Set the up the result tile and a tile for ignored traces.
resultTile := NewLabeledTile()
resultTile.Commits = tile.Commits[:tileLen]
resultCommitsByDigestMap := map[string]map[string]map[int]bool{}
ignoredTile := NewLabeledTile()
ignoredTile.Commits = tile.Commits[:tileLen]
ignoredCommitsByDigestMap := map[string]map[string]map[int]bool{}
// Get the digests that are unavailable, e.g. they cannot be fetched
// from GS or they are not valid images.
unavailableDigests := a.storages.DiffStore.UnavailableDigests()
glog.Infof("Unavailable digests: %v", unavailableDigests)
// Get the rule matcher to find traces to ignore.
ruleMatcher, err := a.storages.IgnoreStore.BuildRuleMatcher()
if err != nil {
glog.Errorf("Unable to build rule matcher: %s", err)
}
// Note: We are assumming that the number and order of traces will change
// over time.
var targetTile *LabeledTile
var commitsByDigestMap map[string]map[string]map[int]bool
for _, v := range tile.Traces {
// Determine if this tile is to be in the result or the ignored tile.
matchedRules, isIgnored := ruleMatcher(v.Params())
if isIgnored {
targetTile = ignoredTile
commitsByDigestMap = ignoredCommitsByDigestMap
} else {
targetTile = resultTile
commitsByDigestMap = resultCommitsByDigestMap
}
tempCommitIds := make([]int, 0, tileLen)
tempLabels := make([]types.Label, 0, tileLen)
tempDigests := make([]string, 0, tileLen)
gTrace := v.(*ptypes.GoldenTrace)
testName := gTrace.Params()[types.PRIMARY_KEY_FIELD]
// Iterate over the digests in this trace.
for i, v := range gTrace.Values[:tileLen] {
if (v != ptypes.MISSING_DIGEST) && !unavailableDigests[v] {
tempCommitIds = append(tempCommitIds, i)
tempDigests = append(tempDigests, v)
tempLabels = append(tempLabels, types.UNTRIAGED)
// Keep track of the commits by digest.
if _, ok := commitsByDigestMap[testName]; !ok {
commitsByDigestMap[testName] = map[string]map[int]bool{v: map[int]bool{i: true}}
} else if _, ok := commitsByDigestMap[testName][v]; !ok {
commitsByDigestMap[testName][v] = map[int]bool{i: true}
} else {
commitsByDigestMap[testName][v][i] = true
}
}
}
// Only consider traces that are not empty.
if len(tempLabels) > 0 {
// Label the digests and add them to the labeled traces.
_, targetLabeledTrace := targetTile.getLabeledTrace(v)
targetLabeledTrace.addLabeledDigests(tempCommitIds, tempDigests, tempLabels)
if isIgnored {
targetLabeledTrace.addIgnoreRules(matchedRules)
}
}
}
getCommitsByDigest(resultTile, resultCommitsByDigestMap)
getCommitsByDigest(ignoredTile, ignoredCommitsByDigestMap)
glog.Info("Done processing tile into LabeledTile.")
return resultTile, ignoredTile
}
func getCommitsByDigest(labeledTile *LabeledTile, commitsByDigestMap map[string]map[string]map[int]bool) {
for testName, cbd := range commitsByDigestMap {
labeledTile.CommitsByDigest[testName] = make(map[string][]int, len(cbd))
for d, commitIds := range cbd {
labeledTile.CommitsByDigest[testName][d] = util.KeysOfIntSet(commitIds)
sort.Ints(labeledTile.CommitsByDigest[testName][d])
}
}
}
// setDerivedOutputs derives the output data from the given tile and
// updates the outputs and tile in the analyzer.
func (a *Analyzer) setDerivedOutputs(labeledTile *LabeledTile, expectations *expstorage.Expectations, state *AnalyzeState, prep bool) {
// Assign all the labels.
for testName, traces := range labeledTile.Traces {
for _, trace := range traces {
labelDigests(testName, trace.Digests, trace.Labels, expectations)
}
}
// Generate the lookup index for the tile and get all parameters.
state.Index = NewLabeledTileIndex(labeledTile)
state.Tile = labeledTile
state.TestDetails = a.getTestDetails(state)
// Don't calculate these during prep runs.
if !prep {
state.Status = calcStatus(state)
state.BlameLists = getBlameLists(state.Tile)
}
}
// updateLabels iterates over the traces in of the tiles that have changed and
// labels them according to our current expecatations.
// updateDerivedOutputs
func (a *Analyzer) updateDerivedOutputs(labeledTestDigests map[string]types.TestClassification, expectations *expstorage.Expectations, state *AnalyzeState) {
// Update the labels of the traces that have changed.
for testName := range labeledTestDigests {
if traces, ok := state.Tile.Traces[testName]; ok {
for _, trace := range traces {
// Note: This is potentially slower than using labels in
// labeledTestDigests directly, but it keeps the code simpler.
labelDigests(testName, trace.Digests, trace.Labels, expectations)
}
}
}
// Update all the output data structures.
// TODO(stephana): Evaluate whether the counts are really useful or if they can be removed.
// If we need them uncomment the following line and implement the corresponding function.
//a.updateOutputCounts(labeledTestDigests)
// Update the tests that have changed and the status.
a.updateTestDetails(labeledTestDigests, state)
state.Status = calcStatus(state)
}
// labelDigest assignes a label to the given digests based on the expectations.
// Its assumes that targetLabels are pre-initialized, usualy with UNTRIAGED,
// because it will not change the label if the given test and digest cannot be
// found.
func labelDigests(testName string, digests []string, targetLabels []types.Label, expectations *expstorage.Expectations) {
for idx, digest := range digests {
if test, ok := expectations.Tests[testName]; ok {
if foundLabel, ok := test[digest]; ok {
targetLabels[idx] = foundLabel
}
}
}
}
// getUntriagedTestDetails returns the untriaged digests of a specific test that
// match the given query. In addition to the digests it returns the query
// that was used to retrieve them.
func (a *Analyzer) getUntriagedTestDetails(query, effectiveQuery map[string][]string, includeAllTests bool) map[string]map[string]*GUIUntriagedDigest {
traces, startCommitId, endCommitId, showHead := a.current.Index.query(query, effectiveQuery)
endCommitId++
if len(effectiveQuery) == 0 {
return nil
}
ret := make(map[string]map[string]*GUIUntriagedDigest, len(a.current.TestDetails.Tests))
// This includes an empty list for tests that we have not found.
if includeAllTests {
for _, testName := range a.current.Index.getTestNames(query) {
ret[testName] = nil
}
}
if !showHead {
for _, trace := range traces {
testName := trace.Params[types.PRIMARY_KEY_FIELD]
current := a.current.TestDetails.lookup(testName).Untriaged
startIdx := sort.SearchInts(trace.CommitIds, startCommitId)
endIdx := sort.SearchInts(trace.CommitIds, endCommitId)
if (endIdx < len(trace.CommitIds)) && (trace.CommitIds[endIdx] == endCommitId) {
endIdx++
}
for idx := startIdx; idx < endIdx; idx++ {
if trace.Labels[idx] == types.UNTRIAGED {
if found, ok := ret[testName]; !ok || found == nil {
ret[testName] = make(map[string]*GUIUntriagedDigest, len(current))
}
ret[testName][trace.Digests[idx]] = current[trace.Digests[idx]]
}
}
}
} else {
for _, trace := range traces {
lastIdx := len(trace.Labels) - 1
if (lastIdx >= 0) && (trace.Labels[lastIdx] == types.UNTRIAGED) {
testName := trace.Params[types.PRIMARY_KEY_FIELD]
current := a.current.TestDetails.lookup(testName).Untriaged
if found, ok := ret[testName]; !ok || found == nil {
ret[testName] = map[string]*GUIUntriagedDigest{}
}
ret[testName][trace.Digests[lastIdx]] = current[trace.Digests[lastIdx]]
}
}
}
return ret
}