| package expstorage |
| |
| import ( |
| "sync" |
| |
| "github.com/skia-dev/glog" |
| "go.skia.org/infra/go/eventbus" |
| "go.skia.org/infra/golden/go/types" |
| ) |
| |
| // Events emmited by this packacke. |
| const ( |
| // Event emitted when expecations change. |
| // Callback argument: []string with the names of changed tests. |
| EV_EXPSTORAGE_CHANGED = "expstorage:changed" |
| ) |
| |
| // Wraps the set of expectations and provides methods to manipulate them. |
| type Expectations struct { |
| Tests map[string]types.TestClassification `json:"tests"` |
| } |
| |
| // Classification returns the classification for a single digest, returning |
| // types.UNTRIAGED if that test or digest is unknown to Expectations. |
| func (e *Expectations) Classification(test, digest string) types.Label { |
| if label, ok := e.Tests[test][digest]; ok { |
| return label |
| } |
| return types.UNTRIAGED |
| } |
| |
| func NewExpectations() *Expectations { |
| return &Expectations{ |
| Tests: map[string]types.TestClassification{}, |
| } |
| } |
| |
| // Add tests and their labeled digests. |
| func (e *Expectations) AddDigests(testDigests map[string]types.TestClassification) { |
| for testName, digests := range testDigests { |
| if _, ok := e.Tests[testName]; !ok { |
| e.Tests[testName] = map[string]types.Label{} |
| } |
| for digest, label := range digests { |
| e.Tests[testName][digest] = label |
| } |
| } |
| } |
| |
| // RemoveDigests removes the given digests from the expectations. |
| // The key in the input is the test name. |
| func (e *Expectations) RemoveDigests(digests map[string][]string) { |
| for testName, digests := range digests { |
| for _, digest := range digests { |
| delete(e.Tests[testName], digest) |
| } |
| |
| if len(e.Tests[testName]) == 0 { |
| delete(e.Tests, testName) |
| } |
| } |
| } |
| |
| func (e *Expectations) DeepCopy() *Expectations { |
| m := make(map[string]types.TestClassification, len(e.Tests)) |
| for k, v := range e.Tests { |
| m[k] = v.DeepCopy() |
| } |
| return &Expectations{ |
| Tests: m, |
| } |
| } |
| |
| // Delta returns the additions and removals that are necessary to |
| // get from e to right. The results can be passed directly to the |
| // AddChange and RemoveChange functions of the ExpectationsStore. |
| func (e *Expectations) Delta(right *Expectations) (*Expectations, map[string][]string) { |
| addExp := subtract(right, e, nil) |
| removeExp := subtract(e, right, addExp.Tests) |
| |
| // Copy the testnames and digests into the output. |
| ret := make(map[string][]string, len(removeExp.Tests)) |
| for testName, digests := range removeExp.Tests { |
| temp := make([]string, 0, len(digests)) |
| for digest := range digests { |
| temp = append(temp, digest) |
| } |
| ret[testName] = temp |
| } |
| |
| return addExp, ret |
| } |
| |
| // Returns a copy of expA with all values removed that also appear in expB. |
| func subtract(expA, expB *Expectations, exclude map[string]types.TestClassification) *Expectations { |
| ret := make(map[string]types.TestClassification, len(expA.Tests)) |
| for testName, digests := range expA.Tests { |
| for digest, labelA := range digests { |
| if _, ok := exclude[testName][digest]; !ok { |
| if labelB, ok := expB.Tests[testName][digest]; !ok || (labelB != labelA) { |
| if found, ok := ret[testName]; !ok { |
| ret[testName] = map[string]types.Label{digest: labelA} |
| } else { |
| found[digest] = labelA |
| } |
| } |
| } |
| } |
| } |
| return &Expectations{ |
| Tests: ret, |
| } |
| } |
| |
| // Defines the storage interface for expectations. |
| type ExpectationsStore interface { |
| // Get the current classifications for image digests. The keys of the |
| // expectations map are the test names. |
| Get() (exp *Expectations, err error) |
| |
| // AddChange writes the given classified digests to the database and records the |
| // user that made the change. |
| AddChange(changes map[string]types.TestClassification, userId string) error |
| |
| // RemoveChange removes the given digests from the expectations store. |
| // The key in changes is the test name which maps to a list of digests |
| // to remove. |
| RemoveChange(changes map[string][]string) error |
| |
| // QueryLog allows to paginate through the changes in the expecations. |
| // If details is true the result will include a list of triage operations |
| // that were part a change. |
| QueryLog(offset, size int, details bool) ([]*TriageLogEntry, int, error) |
| } |
| |
| // TriageDetails represents one changed digest and the label that was |
| // assigned as part of the triage operation. |
| type TriageDetail struct { |
| TestName string `json:"test_name"` |
| Digest string `json:"digest"` |
| Label string `json:"label"` |
| } |
| |
| // TriageLogEntry represents one change in the expectation store. |
| type TriageLogEntry struct { |
| Name string `json:"name"` |
| TS int64 `json:"ts"` |
| ChangeCount int `json:"changeCount"` |
| Details []*TriageDetail `json:"details"` |
| } |
| |
| // Implements ExpectationsStore in memory for prototyping and testing. |
| type MemExpectationsStore struct { |
| expectations *Expectations |
| readCopy *Expectations |
| eventBus *eventbus.EventBus |
| |
| // Protects expectations. |
| mutex sync.Mutex |
| } |
| |
| // New instance of memory backed expecation storage. |
| func NewMemExpectationsStore(eventBus *eventbus.EventBus) ExpectationsStore { |
| return &MemExpectationsStore{ |
| expectations: NewExpectations(), |
| readCopy: NewExpectations(), |
| eventBus: eventBus, |
| } |
| } |
| |
| // ------------- In-memory implementation |
| // See ExpectationsStore interface. |
| func (m *MemExpectationsStore) Get() (*Expectations, error) { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| |
| return m.readCopy, nil |
| } |
| |
| // See ExpectationsStore interface. |
| func (m *MemExpectationsStore) AddChange(changedTests map[string]types.TestClassification, userId string) error { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| |
| testNames := make([]string, 0, len(changedTests)) |
| for testName, digests := range changedTests { |
| if _, ok := m.expectations.Tests[testName]; !ok { |
| m.expectations.Tests[testName] = map[string]types.Label{} |
| } |
| for d, label := range digests { |
| m.expectations.Tests[testName][d] = label |
| } |
| testNames = append(testNames, testName) |
| } |
| if m.eventBus != nil { |
| m.eventBus.Publish(EV_EXPSTORAGE_CHANGED, testNames) |
| } |
| |
| m.readCopy = m.expectations.DeepCopy() |
| return nil |
| } |
| |
| // RemoveChange, see ExpectationsStore interface. |
| func (m *MemExpectationsStore) RemoveChange(changedDigests map[string][]string) error { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| |
| testNames := make([]string, 0, len(changedDigests)) |
| |
| for testName, digests := range changedDigests { |
| for _, digest := range digests { |
| delete(m.expectations.Tests[testName], digest) |
| if len(m.expectations.Tests[testName]) == 0 { |
| delete(m.expectations.Tests, testName) |
| } |
| } |
| testNames = append(testNames, testName) |
| } |
| if m.eventBus != nil { |
| m.eventBus.Publish(EV_EXPSTORAGE_CHANGED, testNames) |
| } |
| |
| m.readCopy = m.expectations.DeepCopy() |
| return nil |
| } |
| |
| // See ExpectationsStore interface. |
| func (m *MemExpectationsStore) QueryLog(offset, size int, details bool) ([]*TriageLogEntry, int, error) { |
| glog.Fatal("MemExpectation store does not support querying the logs.") |
| return nil, 0, nil |
| } |