blob: 685c21ba4dfbb0780135f09d307473398666c346 [file] [log] [blame]
package expectations
import (
"fmt"
"sort"
"strings"
"sync"
"go.skia.org/infra/golden/go/types"
)
// Expectations captures the expectations for a set of tests and digests as
// labels (Positive/Negative/Untriaged).
// Put another way, this data structure keeps track if a digest (image) is
// drawn correctly, incorrectly, or newly-seen for a given test.
// Expectations is thread safe.
type Expectations struct {
mutex sync.RWMutex
// TODO(kjlubick) Consider storing this as
// map[digestGrouping]Label where
// type digestGrouping struct {
// digest types.Digest
// grouping types.TestName
// }
labels map[types.TestName]map[types.Digest]Label
}
// Baseline is a simplified view of the Expectations, suitable for JSON encoding. A Baseline only
// has entries with positive and negative labels (i.e. no untriaged entries).
type Baseline map[types.TestName]map[types.Digest]Label
// ReadOnly is an interface with the non-mutating functions of Expectations.
// By using this instead of Expectations, we can make fewer copies, helping performance.
type ReadOnly interface {
Classifier
// ForAll will iterate through all entries in Expectations and call the callback with them.
// Iteration will stop if a non-nil error is returned (and will be forwarded to the caller).
ForAll(fn func(types.TestName, types.Digest, Label) error) error
// Empty returns true iff NumTests() == 0
Empty() bool
// NumTests returns the number of tests that Expectations knows about.
NumTests() int
// Len returns the number of test/digest pairs stored.
Len() int
}
// Classifier is a simple interface for querying expectations.
type Classifier interface {
// Classification returns the label for the given test/digest pair. By definition,
// this will return Untriaged if there isn't already a classification set.
Classification(test types.TestName, digest types.Digest) Label
}
// Set sets the label for a test_name/digest pair. If the pair already exists,
// it will be over written.
func (e *Expectations) Set(testName types.TestName, digest types.Digest, label Label) {
e.mutex.Lock()
defer e.mutex.Unlock()
e.ensureInit()
if digests, ok := e.labels[testName]; ok {
if label == Untriaged {
delete(digests, digest)
} else {
digests[digest] = label
}
} else {
if label != Untriaged {
e.labels[testName] = map[types.Digest]Label{digest: label}
}
}
}
// setDigests is a convenience function to set the expectations of a set of digests for a
// given test_name. Callers should have the write mutex locked.
func (e *Expectations) setDigests(testName types.TestName, labels map[types.Digest]Label) {
digests, ok := e.labels[testName]
if !ok {
digests = make(map[types.Digest]Label, len(labels))
}
for digest, label := range labels {
if label != Untriaged {
digests[digest] = label
}
}
e.labels[testName] = digests
}
// MergeExpectations adds the given expectations to the current expectations, letting
// the ones provided by the passed in parameter overwrite any existing data. Trying to merge
// two expectations into each other simultaneously may result in a dead-lock.
func (e *Expectations) MergeExpectations(other *Expectations) {
if other == nil {
return
}
other.mutex.RLock()
defer other.mutex.RUnlock()
e.mutex.Lock()
defer e.mutex.Unlock()
e.ensureInit()
for testName, digests := range other.labels {
e.setDigests(testName, digests)
}
}
// ForAll implements the ReadOnly interface.
func (e *Expectations) ForAll(fn func(types.TestName, types.Digest, Label) error) error {
e.mutex.RLock()
defer e.mutex.RUnlock()
for test, digests := range e.labels {
for digest, label := range digests {
err := fn(test, digest, label)
if err != nil {
return err
}
}
}
return nil
}
// DeepCopy makes a deep copy of the current expectations/baseline.
func (e *Expectations) DeepCopy() *Expectations {
ret := Expectations{
labels: make(map[types.TestName]map[types.Digest]Label, len(e.labels)),
}
ret.MergeExpectations(e)
return &ret
}
// Classification implements the ReadOnly interface.
func (e *Expectations) Classification(test types.TestName, digest types.Digest) Label {
e.mutex.RLock()
defer e.mutex.RUnlock()
if label, ok := e.labels[test][digest]; ok {
return label
}
return Untriaged
}
// Empty implements the ReadOnly interface.
func (e *Expectations) Empty() bool {
return e.NumTests() == 0
}
// NumTests implements the ReadOnly interface.
func (e *Expectations) NumTests() int {
if e == nil {
return 0
}
e.mutex.RLock()
defer e.mutex.RUnlock()
return len(e.labels)
}
// Len implements the ReadOnly interface.
func (e *Expectations) Len() int {
if e == nil {
return 0
}
e.mutex.RLock()
defer e.mutex.RUnlock()
n := 0
for _, d := range e.labels {
n += len(d)
}
return n
}
// String returns an alphabetically sorted string representation
// of this object.
func (e *Expectations) String() string {
e.mutex.RLock()
defer e.mutex.RUnlock()
names := make([]string, 0, len(e.labels))
for testName := range e.labels {
names = append(names, string(testName))
}
sort.Strings(names)
s := strings.Builder{}
for _, testName := range names {
digestMap := e.labels[types.TestName(testName)]
digests := make([]string, 0, len(digestMap))
for d := range digestMap {
digests = append(digests, string(d))
}
sort.Strings(digests)
_, _ = fmt.Fprintf(&s, "%s:\n", testName)
for _, d := range digests {
_, _ = fmt.Fprintf(&s, "\t%s : %s\n", d, digestMap[types.Digest(d)])
}
}
return s.String()
}
// AsBaseline returns a copy that has all untriaged digests removed.
func (e *Expectations) AsBaseline() Baseline {
e.mutex.RLock()
defer e.mutex.RUnlock()
n := Expectations{
labels: map[types.TestName]map[types.Digest]Label{},
}
for testName, digests := range e.labels {
for d, c := range digests {
if c != Untriaged {
n.Set(testName, d, c)
}
}
}
return n.labels
}
// ensureInit expects that the write mutex is held prior to entry.
func (e *Expectations) ensureInit() {
if e.labels == nil {
e.labels = map[types.TestName]map[types.Digest]Label{}
}
}
// JoinedExp represents a chain of ReadOnly that could contain Labels.
// The Expectations at the beginning of the list override those that follow.
type JoinedExp []ReadOnly
// Join returns a Classifier that combines the given ReadOnly. If multiple ReadOnly have a
// Label for a given Grouping (Test+Digest), the order of the ReadOnly will break the tie by
// using the ReadOnly which was provided first.
func Join(first, second ReadOnly, others ...ReadOnly) JoinedExp {
rv := []ReadOnly{first, second}
rv = append(rv, others...)
return rv
}
// Classification returns the first non-untriaged label for the given
// test and digest. If none of the given ReadOnly have a match, Untriaged is returned.
func (e JoinedExp) Classification(test types.TestName, digest types.Digest) Label {
for _, exp := range e {
if label := exp.Classification(test, digest); label != Untriaged {
return label
}
}
return Untriaged
}
// EmptyClassifier returns a Classifier which returns Untriaged for given input.
// Mostly used for testing.
func EmptyClassifier() Classifier {
return JoinedExp{}
}