diff --git a/dryad/conf/conf.go b/dryad/conf/conf.go index 64cae15..871ae69 100644 --- a/dryad/conf/conf.go +++ b/dryad/conf/conf.go @@ -20,7 +20,6 @@ package conf import ( "fmt" "io" - "io/ioutil" "github.com/BurntSushi/toml" @@ -84,9 +83,9 @@ func (g *General) Marshal(w io.Writer) error { return toml.NewEncoder(w).Encode(g) } -// Unmarshal reads TOML representation from r and parses it into g. +// Unmarshal reads TOML representation from r and parses it into g. Function may panic (e.g. when reader is nil). func (g *General) Unmarshal(r io.Reader) error { - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return err } diff --git a/dryad/conf/conf_suite_test.go b/dryad/conf/conf_suite_test.go deleted file mode 100644 index cb1aa15..0000000 --- a/dryad/conf/conf_suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package conf_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestConf(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Conf Suite") -} diff --git a/dryad/conf/conf_test.go b/dryad/conf/conf_test.go index 0c3a93d..beaa5e1 100644 --- a/dryad/conf/conf_test.go +++ b/dryad/conf/conf_test.go @@ -14,21 +14,29 @@ * limitations under the License */ -package conf_test +package conf import ( "bytes" + "errors" + "io" "strings" + "testing" "github.com/SamsungSLAV/boruta" - . "github.com/SamsungSLAV/boruta/dryad/conf" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" ) -var _ = Describe("Conf", func() { - marshaled := `listen_address = ":7175" +type brokenReader struct { + io.Reader +} + +func (*brokenReader) Read(_ []byte) (n int, err error) { + return 0, errors.New("broken reader") +} + +var ( + marshaled = `listen_address = ":7175" boruta_address = "" ssh_address = ":22" sdcard = "/dev/sdX" @@ -40,7 +48,13 @@ stm_path = "/run/stm.socket" name = "boruta-user" groups = [] ` - unmarshaled := &General{ + empty = `listen_address = "" +boruta_address = "" +ssh_address = "" +sdcard = "" +stm_path = "" +` + unmarshaled = &General{ Address: ":7175", SSHAdress: ":22", Caps: boruta.Capabilities(map[string]string{}), @@ -51,27 +65,69 @@ stm_path = "/run/stm.socket" SDcard: "/dev/sdX", STMsocket: "/run/stm.socket", } - var g *General +) - BeforeEach(func() { - g = NewConf() - }) +func TestNewConf(t *testing.T) { + assert.Equal(t, NewConf(), unmarshaled) +} - It("should initially have default configuration", func() { - Expect(g).To(Equal(unmarshaled)) - }) +func TestMarshal(t *testing.T) { + testCases := [...]struct { + name string + conf *General + str string + err error + }{ + {name: "valid", conf: NewConf(), str: marshaled, err: nil}, + {name: "empty", conf: new(General), str: empty, err: nil}, + {name: "nil", conf: nil, str: "", err: nil}, + } + assert := assert.New(t) - It("should encode default configuration", func() { - var w bytes.Buffer - g.Marshal(&w) - result := w.String() - Expect(result).ToNot(BeEmpty()) - Expect(result).To(Equal(marshaled)) - }) + for _, test := range testCases { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + var b bytes.Buffer + assert.ErrorIs(test.conf.Marshal(&b), test.err) + assert.Equal(b.String(), test.str) + }) + } +} - It("should decode default configuration", func() { - g = new(General) - g.Unmarshal(strings.NewReader(marshaled)) - Expect(g).To(Equal(unmarshaled)) - }) -}) +func TestUnmarshal(t *testing.T) { + testCases := [...]struct { + name string + conf *General + read io.Reader + err error + panics bool + }{ + {name: "valid", conf: NewConf(), read: strings.NewReader(marshaled), err: nil}, + {name: "invalid", conf: new(General), read: strings.NewReader(`/4`), err: errors.New(`toml: line 1: expected '.' or '=', but got '/' instead`)}, + {name: "empty", conf: new(General), read: strings.NewReader(empty), err: nil}, + {name: "brokenReader", conf: new(General), read: new(brokenReader), err: errors.New("broken reader")}, + {name: "nil", conf: new(General), read: nil, err: nil, panics: true}, + } + assert := assert.New(t) + + for _, test := range testCases { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + var err error + g := new(General) + if test.panics { + assert.Panics(func() { err = g.Unmarshal(test.read) }) + } else { + assert.NotPanics(func() { err = g.Unmarshal(test.read) }) + } + if test.err != nil { + assert.ErrorContains(err, test.err.Error()) + } else { + assert.NoError(err) + } + assert.Equal(g, test.conf) + }) + } +} diff --git a/filter/filter_test.go b/filter/filter_test.go index acaf107..8fd0511 100644 --- a/filter/filter_test.go +++ b/filter/filter_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package filter import ( + "fmt" "testing" "github.com/SamsungSLAV/boruta" @@ -76,32 +77,36 @@ func TestNewRequest(t *testing.T) { } for _, tcase := range newRequestTests { - filter := NewRequests(tcase.ids, tcase.priorities, tcase.states) - assert.NotNil(filter, tcase.name) + tcase := tcase + t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + filter := NewRequests(tcase.ids, tcase.priorities, tcase.states) + assert.NotNil(filter, tcase.name) - // Verify IDs. - assert.Len(filter.IDs, len(tcase.ids), tcase.name) - if len(tcase.ids) > 0 { - assert.Equal(tcase.ids, filter.IDs, tcase.name) - } else { - assert.Nil(filter.IDs) - } + // Verify IDs. + assert.Len(filter.IDs, len(tcase.ids), tcase.name) + if len(tcase.ids) > 0 { + assert.Equal(tcase.ids, filter.IDs, tcase.name) + } else { + assert.Nil(filter.IDs) + } - // Verify Priorities. - assert.Len(filter.Priorities, len(tcase.priorities), tcase.name) - if len(tcase.priorities) > 0 { - assert.Equal(tcase.priorities, filter.Priorities, tcase.name) - } else { - assert.Nil(filter.Priorities) - } + // Verify Priorities. + assert.Len(filter.Priorities, len(tcase.priorities), tcase.name) + if len(tcase.priorities) > 0 { + assert.Equal(tcase.priorities, filter.Priorities, tcase.name) + } else { + assert.Nil(filter.Priorities) + } - // Verify States. - assert.Len(filter.States, len(tcase.states), tcase.name) - if len(tcase.states) > 0 { - assert.Equal(tcase.expectedStates, filter.States, tcase.name) - } else { - assert.Nil(filter.States) - } + // Verify States. + assert.Len(filter.States, len(tcase.states), tcase.name) + if len(tcase.states) > 0 { + assert.Equal(tcase.expectedStates, filter.States, tcase.name) + } else { + assert.Nil(filter.States) + } + }) } } @@ -203,17 +208,32 @@ func TestRequestMatch(t *testing.T) { }, } - var filter Requests + makeName := func(states []boruta.ReqState, priorities []boruta.Priority, ids []boruta.ReqID) string { + return fmt.Sprintf("States: %v, Priorities: %v, ISs: %v", states, priorities, ids) + } + makeFilter := func(states []boruta.ReqState, priorities []boruta.Priority, ids []boruta.ReqID) Requests { + return Requests{ + States: states, + Priorities: priorities, + IDs: ids, + } + } + for _, stest := range statesTests { - filter.States = stest.states + stest := stest for _, ptest := range priorityTests { - filter.Priorities = ptest.priorities + ptest := ptest for _, idstest := range idsTests { - filter.IDs = idstest.ids - assert.Equal(stest.result && ptest.result && idstest.result, filter.Match(&req)) + idtest := idstest + filter := makeFilter(stest.states, ptest.priorities, idtest.ids) + t.Run(makeName(filter.States, filter.Priorities, filter.IDs), func(t *testing.T) { + t.Parallel() + assert.Equal(stest.result && ptest.result && idtest.result, filter.Match(&req)) + }) } } } + var filter Requests assert.False(filter.Match(nil)) assert.False(filter.Match(5)) } @@ -261,63 +281,80 @@ func TestWorkerMatch(t *testing.T) { other := boruta.Group("other") var tests = [...]struct { + name string worker *boruta.WorkerInfo filter *Workers result bool }{ { + name: "NilGroupsAndCaps", worker: newWorker(groups(all), caps("armv7", "true")), filter: NewWorkers(nil, nil), result: true, }, { + name: "EmptyGroupsAndNilCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(groups(empty), nil), result: false, }, { + name: "NilGroupsAndMatchingCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(nil, caps("aarch64", "true")), result: true, }, { + name: "NilGroupsAndDefaultCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(nil, make(boruta.Capabilities)), result: true, }, { + name: "DefaultGroupsAndNilCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(make(boruta.Groups, 0), nil), result: true, }, { + name: "MatchingOtherGroupsAndMatchingCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(groups(all, other), caps("aarch64", "true")), result: true, }, { + name: "NotMatchingGroupsAndMatchingCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(groups(other), caps("aarch64", "true")), result: false, }, { + name: "MatchingAllGroupsAndMatchingCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(groups(all, other), caps("aarch64", "false")), result: false, }, { + name: "MatchingAllGroupsAndNotMatchingCaps", worker: newWorker(groups(all, some), caps("aarch64", "true")), filter: NewWorkers(groups(all, other), boruta.Capabilities{"foo": "bar"}), result: false, }, + { + name: "DefaultFilterNil", + worker: nil, + filter: new(Workers), + result: false, + }, } for _, tcase := range tests { - assert.Equal(tcase.result, tcase.filter.Match(tcase.worker)) + tcase := tcase + t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + assert.Equal(tcase.result, tcase.filter.Match(tcase.worker)) + }) } - - filter := new(Workers) - assert.False(filter.Match(nil)) - assert.False(filter.Match(5)) + assert.False(new(Workers).Match(5), "WrongType") } diff --git a/requests/queue_test.go b/requests/queue_test.go index 1ee709f..c2c0b56 100644 --- a/requests/queue_test.go +++ b/requests/queue_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ // File requests/queue_test.go contains additional tests for queue.go. Please -// take a look at requests_test.go for initTest() and requestsTests definition. +// take a look at requests_test.go for requestsTests definition. package requests @@ -35,7 +35,15 @@ func TestRemovePanic(t *testing.T) { func TestQueue(t *testing.T) { assert := assert.New(t) queue := newPrioQueue() - var reqs = []struct { + + runTest := func(exists bool, expected boruta.ReqID) { + t.Helper() + reqid, ok := queue.next() + assert.Equal(exists, ok) + assert.Equal(expected, reqid) + } + + reqs := [...]struct { id boruta.ReqID pr boruta.Priority }{ @@ -46,19 +54,15 @@ func TestQueue(t *testing.T) { {boruta.ReqID(5), boruta.Priority(3)}, {boruta.ReqID(6), boruta.Priority(3)}, } - sorted := []boruta.ReqID{boruta.ReqID(2), boruta.ReqID(3), boruta.ReqID(5), boruta.ReqID(6), + sorted := [...]boruta.ReqID{boruta.ReqID(2), boruta.ReqID(3), boruta.ReqID(5), boruta.ReqID(6), boruta.ReqID(1), boruta.ReqID(4)} // Test for empty queue. - reqid, ok := queue.next() - assert.False(ok) - assert.Equal(boruta.ReqID(0), reqid) + runTest(false, boruta.ReqID(0)) // Test if iterator was initialized and queue is empty. queue.initIterator() - reqid, ok = queue.next() - assert.False(ok) - assert.Equal(boruta.ReqID(0), reqid) + runTest(false, boruta.ReqID(0)) queue.releaseIterator() req := requestsTests[0].req @@ -78,26 +82,18 @@ func TestQueue(t *testing.T) { // Check if queue returns request IDs in proper order. queue.initIterator() for _, r := range sorted { - reqid, ok = queue.next() - assert.True(ok) - assert.Equal(r, reqid) + runTest(true, r) } // Check if call to next() after iterating through whole queue returns false. - reqid, ok = queue.next() - assert.False(ok) - assert.Equal(boruta.ReqID(0), reqid) + runTest(false, boruta.ReqID(0)) queue.releaseIterator() // Check if after another initialization next() returns first element. queue.initIterator() - reqid, ok = queue.next() - assert.True(ok) - assert.Equal(sorted[0], reqid) + runTest(true, sorted[0]) // Check call to releaseIterator() when iterator hasn't finished properly // sets next(). queue.releaseIterator() - reqid, ok = queue.next() - assert.False(ok) - assert.Equal(boruta.ReqID(0), reqid) + runTest(false, boruta.ReqID(0)) } diff --git a/requests/requests_requestsmanager_test.go b/requests/requests_requestsmanager_test.go index 35f1bc0..3431269 100644 --- a/requests/requests_requestsmanager_test.go +++ b/requests/requests_requestsmanager_test.go @@ -20,356 +20,463 @@ package requests //go:generate mockgen -package requests -destination=jobsmanager_mock_test.go -write_package_comment=false github.com/SamsungSLAV/boruta/matcher JobsManager import ( - "errors" + "sync" "time" "github.com/SamsungSLAV/boruta" gomock "github.com/golang/mock/gomock" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" ) -var _ = Describe("Requests as RequestsManager", func() { - Describe("With RequestsManager created", func() { - var ctrl *gomock.Controller - var wm *MockWorkersManager - var jm *MockJobsManager - var R *ReqsCollection - testErr := errors.New("Test Error") - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - wm = NewMockWorkersManager(ctrl) - jm = NewMockJobsManager(ctrl) - wm.EXPECT().SetChangeListener(gomock.Any()) - R = NewRequestQueue(wm, jm) - }) - AfterEach(func() { - R.Finish() - ctrl.Finish() - }) +func tryLock(mtx *sync.RWMutex, ch chan bool) { + mtx.Lock() + defer mtx.Unlock() + ch <- true +} + +func tryReceive(ch chan bool) func() bool { + return func() bool { + select { + case <-ch: + return true + default: + return false + } + } +} + +func (s *RequestsTestSuite) TestInitIteration() { + entered := make(chan bool) + mutexUnlocked := tryReceive(entered) + + s.Run("ValidInit", func() { + s.NoError(s.rqueue.InitIteration()) + s.True(s.rqueue.iterating) + + // Verify that mutex is locked. + go tryLock(s.rqueue.mutex, entered) + s.Never(mutexUnlocked, time.Second, 10*time.Millisecond) + + // Release the mutex + s.rqueue.mutex.Unlock() + s.Eventually(mutexUnlocked, time.Second, 10*time.Millisecond) + s.TearDownTest() + }) + + s.Run("InitError", func() { + s.SetupTest() + s.rqueue.mutex.Lock() + s.rqueue.iterating = true + s.rqueue.mutex.Unlock() + s.EqualError(s.rqueue.InitIteration(), boruta.ErrInternalLogicError.Error()) + go tryLock(s.rqueue.mutex, entered) + s.Eventually(mutexUnlocked, time.Second, 10*time.Millisecond) + }) +} + +func (s *RequestsTestSuite) TestTerminateIteration() { + entered := make(chan bool) + mutexUnlocked := tryReceive(entered) + + s.NoError(s.rqueue.InitIteration()) + s.True(s.rqueue.iterating) + s.rqueue.TerminateIteration() + // iterating is set to false and mutex is unlocked + s.False(s.rqueue.iterating) + go tryLock(s.rqueue.mutex, entered) + s.Eventually(mutexUnlocked, time.Second, 10*time.Millisecond) + + // When iterating is false and mutex is locked, then TerminateIteration should unlock the mutex. + s.rqueue.mutex.Lock() + s.rqueue.TerminateIteration() + s.False(s.rqueue.iterating) // iterating hasn't changed + go tryLock(s.rqueue.mutex, entered) + s.Eventually(mutexUnlocked, time.Second, 10*time.Millisecond) +} + +func (s *RequestsTestSuite) TestIteration() { + s.Panics(func() { + s.rqueue.mutex.Lock() + defer s.rqueue.mutex.Unlock() + s.rqueue.Next() + }) - Describe("Iterations", func() { - var entered chan int - testMutex := func() { - R.mutex.Lock() - defer R.mutex.Unlock() - entered <- 1 + verify := []boruta.ReqID{3, 5, 1, 2, 7, 4, 6} + now := time.Now() + tomorrow := now.AddDate(0, 0, 1) + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + insert := func(p boruta.Priority) { + _, err := s.rqueue.NewRequest(boruta.Capabilities{}, p, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + } + insert(3) //1 + insert(3) //2 + insert(1) //3 + insert(5) //4 + insert(1) //5 + insert(5) //6 + insert(3) //7 + + s.Run("IterateOverAll", func() { + reqs := make([]boruta.ReqID, 0, len(verify)) + + s.NoError(s.rqueue.InitIteration()) + for r, ok := s.rqueue.Next(); ok; r, ok = s.rqueue.Next() { + reqs = append(reqs, r) + } + s.rqueue.TerminateIteration() + s.Equal(verify, reqs) + }) + + s.Run("RestartIterations", func() { + for times := 0; times < len(verify); times++ { + reqs := make([]boruta.ReqID, 0, len(verify)) + i := 0 + s.NoError(s.rqueue.InitIteration()) + for r, ok := s.rqueue.Next(); ok && i < times; r, ok = s.rqueue.Next() { + reqs = append(reqs, r) + i++ } - BeforeEach(func() { - entered = make(chan int) - }) - Describe("InitIteration", func() { - It("should init iterations and lock requests mutex", func() { - err := R.InitIteration() - Expect(err).NotTo(HaveOccurred()) - Expect(R.iterating).To(BeTrue()) - - // Verify that mutex is locked. - go testMutex() - Consistently(entered).ShouldNot(Receive()) - - // Release the mutex - R.mutex.Unlock() - Eventually(entered).Should(Receive()) - }) - It("should return error and remain mutex unlocked if iterations are already started", func() { - R.mutex.Lock() - R.iterating = true - R.mutex.Unlock() - - err := R.InitIteration() - Expect(err).To(Equal(boruta.ErrInternalLogicError)) - - // Verify that mutex is not locked. - go testMutex() - Eventually(entered).Should(Receive()) - }) + s.rqueue.TerminateIteration() + s.Equal(verify[:times], reqs) + } + }) + +} + +func (s *RequestsTestSuite) TestVerifyIfReady() { + now := time.Now() + tomorrow := now.AddDate(0, 0, 1) + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + req, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + s.NotZero(req) + noreq := req + 1 + + s.Run("Requests", func() { + testCases := [...]struct { + name string + req boruta.ReqID + t time.Time + ret bool + }{ + {name: "UnknownReqID", req: noreq, t: now, ret: false}, + {name: "DeadlineOK", req: req, t: tomorrow.Add(-time.Hour), ret: true}, + {name: "OnDeadline", req: req, t: tomorrow, ret: false}, + {name: "DeadlinePassed", req: req, t: tomorrow.Add(time.Hour), ret: false}, + {name: "ValidAfterInFuture", req: req, t: now.Add(-time.Hour), ret: false}, + {name: "OnValidAfter", req: req, t: now, ret: true}, + {name: "ValidAfterPassed", req: req, t: now.Add(time.Hour), ret: true}, + // Request is known, in WAIT state and now is between ValidAfter and Deadline. + {name: "ValidReqOnTime", req: req, t: now.Add(12 * time.Hour), ret: true}, + } + + for _, test := range testCases { + s.Run(test.name, func() { + s.T().Parallel() + s.Equalf(test.ret, s.rqueue.VerifyIfReady(test.req, test.t), "test case: %s", test.name) }) - Describe("TerminateIteration", func() { - It("should terminate iterations and unlock requests mutex", func() { - err := R.InitIteration() - Expect(err).NotTo(HaveOccurred()) - - R.TerminateIteration() - Expect(R.iterating).To(BeFalse()) - - // Verify that mutex is not locked. - go testMutex() - Eventually(entered).Should(Receive()) - }) - It("should just release mutex if iterations are not started", func() { - R.mutex.Lock() - - R.TerminateIteration() - - // Verify that mutex is not locked. - go testMutex() - Eventually(entered).Should(Receive()) - }) + } + }) + + s.Run("States", func() { + states := [...]boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, boruta.DONE, boruta.FAILED} + for _, state := range states { + state := state + s.rqueue.mutex.Lock() + s.rqueue.requests[req].State = state + s.rqueue.mutex.Unlock() + s.Run(string(state), func() { + s.T().Parallel() + s.Falsef(s.rqueue.VerifyIfReady(req, now), "state: %s", state) }) + } + }) +} + +func (s *RequestsTestSuite) TestGet() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + + now := time.Now() + req, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + s.NotZero(req) + noreq := req + 1 + + testCases := [...]struct { + name string + id boruta.ReqID + err error + }{ + {name: "Missing", id: noreq, err: boruta.NotFoundError("Request")}, + {name: "Valid", id: req, err: nil}, + } + + for _, test := range testCases { + test := test + s.Run(test.name, func() { + s.T().Parallel() + r, err := s.rqueue.Get(test.id) + s.ErrorIs(test.err, err) + if err != nil { + s.Empty(r) + } else { + s.rqueue.mutex.RLock() + rinfo, ok := s.rqueue.requests[req] + s.rqueue.mutex.RUnlock() + s.Truef(ok, "test case: %s", test.name) + s.Equalf(*rinfo, r, "test case: %s", test.name) + } }) - Describe("Iterating over requests", func() { - verify := []boruta.ReqID{3, 5, 1, 2, 7, 4, 6} - BeforeEach(func() { - now := time.Now() - tomorrow := now.AddDate(0, 0, 1) - wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), testErr).AnyTimes() - insert := func(p boruta.Priority) { - _, err := R.NewRequest(boruta.Capabilities{}, p, boruta.UserInfo{}, now, tomorrow) - Expect(err).NotTo(HaveOccurred()) + } +} + +func (s *RequestsTestSuite) TestTimeout() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + vafter := time.Now() + + s.Run("Requests", func() { + future, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, vafter, tomorrow) + s.NoError(err) + s.NotZero(future) + + // As we want to trigger timeout manually for this request deadlineTimes is replaced with new one. + // After adding request previous one is restored + s.rqueue.mutex.Lock() + deadlineTimes := s.rqueue.deadlineTimes + s.rqueue.deadlineTimes = newRequestTimes() + s.rqueue.mutex.Unlock() + past, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, vafter, time.Now().Add(1*time.Second)) + s.NoError(err) + s.NotZero(past) + s.rqueue.mutex.Lock() + // Restore previous deadlineTimes heap. + s.rqueue.deadlineTimes = deadlineTimes + s.rqueue.mutex.Unlock() + + noreq := past + 1 + + testCases := [...]struct { + name string + req boruta.ReqID + before uint + after uint + sleep time.Duration + state boruta.ReqState + err error + }{ + {name: "MissingRequest", req: noreq, before: 2, after: 2, sleep: 0 * time.Second, state: "", err: boruta.NotFoundError("Request")}, + {name: "DeadlineInFuture", req: future, before: 2, after: 2, sleep: 0 * time.Second, state: boruta.WAIT, err: ErrModificationForbidden}, + {name: "DeadlinePassed", req: past, before: 2, after: 1, sleep: 1 * time.Second, state: boruta.TIMEOUT, err: nil}, + } + + for _, test := range testCases { + test := test + s.Run(test.name, func() { + s.T().Parallel() + s.rqueue.queue.mtx.Lock() + s.Equalf(test.before, s.rqueue.queue.length, "test case: %s", test.name) + s.rqueue.queue.mtx.Unlock() + time.Sleep(test.sleep) + s.ErrorIsf(test.err, s.rqueue.Timeout(test.req), "test case: %s", test.name) + s.rqueue.queue.mtx.Lock() + s.Equalf(test.after, s.rqueue.queue.length, "test case: %s", test.name) + s.rqueue.queue.mtx.Unlock() + s.rqueue.mutex.RLock() + if test.state != "" { + s.Equalf(test.state, s.rqueue.requests[test.req].State, "test case: %s", test.name) } - insert(3) //1 - insert(3) //2 - insert(1) //3 - insert(5) //4 - insert(1) //5 - insert(5) //6 - insert(3) //7 + s.rqueue.mutex.RUnlock() }) - It("should properly iterate over requests", func() { - reqs := make([]boruta.ReqID, 0) + } + }) - R.InitIteration() - for r, ok := R.Next(); ok; r, ok = R.Next() { - reqs = append(reqs, r) - } - R.TerminateIteration() + s.Run("States", func() { + // Timeout should only work on requests in WAIT state. + states := [...]boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, boruta.DONE, boruta.FAILED} + req, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, vafter, tomorrow) + s.NoError(err) + s.NotZero(req) - Expect(reqs).To(Equal(verify)) + for _, state := range states { + state := state + s.rqueue.mutex.Lock() + s.rqueue.requests[req].State = state + s.rqueue.mutex.Unlock() + s.rqueue.queue.mtx.Lock() + qlen := s.rqueue.queue.length + s.rqueue.queue.mtx.Unlock() + s.Run(string(state), func() { + s.T().Parallel() + s.rqueue.queue.mtx.Lock() + s.EqualValuesf(qlen, s.rqueue.queue.length, "state: %s", state) + s.rqueue.queue.mtx.Unlock() + s.ErrorIsf(ErrModificationForbidden, s.rqueue.Timeout(req), "state: %s", state) + s.rqueue.queue.mtx.Lock() + s.EqualValuesf(qlen, s.rqueue.queue.length, "state: %s", state) + s.rqueue.queue.mtx.Unlock() }) - It("should restart iterations in new critical section", func() { - for times := 0; times < len(verify); times++ { - reqs := make([]boruta.ReqID, 0) - i := 0 - R.InitIteration() - for r, ok := R.Next(); ok && i < times; r, ok = R.Next() { - reqs = append(reqs, r) - i++ - } - R.TerminateIteration() - Expect(reqs).To(Equal(verify[:times])) + } + }) +} + +func (s *RequestsTestSuite) TestClose() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + + now := time.Now() + const reqsCnt = 3 + var reqs [reqsCnt]boruta.ReqID + for i := 0; i < reqsCnt; i++ { + req, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + s.NotZero(req) + reqs[i] = req + } + + s.Run("JobsAndRequests", func() { + testWorker := boruta.WorkerUUID("TestWorker") + noreq := reqs[reqsCnt-1] + 1 + testCases := [...]struct { + name string + req boruta.ReqID + job *boruta.JobInfo + before boruta.ReqState + after boruta.ReqState + err error + }{ + {name: "MissingRequest", req: noreq, job: nil, before: "", after: "", err: boruta.NotFoundError("Request")}, + { + name: "MissingJob", + req: reqs[0], + job: nil, + before: boruta.INPROGRESS, + after: boruta.INPROGRESS, + err: boruta.ErrInternalLogicError, + }, + { + name: "JobRunning", + req: reqs[0], + job: &boruta.JobInfo{Timeout: tomorrow}, + before: boruta.INPROGRESS, + after: boruta.INPROGRESS, + err: ErrModificationForbidden, + }, + { + name: "CloseAndReleaseWorker", + req: reqs[1], + job: &boruta.JobInfo{Timeout: now.AddDate(0, 0, -1), WorkerUUID: testWorker}, + before: boruta.INPROGRESS, + after: boruta.DONE, + err: nil, + }, + } + + // Successful Close will finish the job, so Finish will be called only once. + s.jm.EXPECT().Finish(testWorker, true).Times(1) + for _, test := range testCases { + test := test + s.Run(test.name, func() { + s.T().Parallel() + s.rqueue.mutex.Lock() + rinfo, ok := s.rqueue.requests[test.req] + if ok { + rinfo.State = test.before + rinfo.Job = test.job } - }) - It("should panic if Next is called without InitIteration", func() { - wrap := func() { - R.mutex.Lock() - defer R.mutex.Unlock() - R.Next() + s.rqueue.mutex.Unlock() + s.ErrorIsf(test.err, s.rqueue.Close(test.req), "test case: %s", test.name) + if test.after != "" { + s.rqueue.mutex.RLock() + s.Equalf(test.after, rinfo.State, "test case: %s", test.name) + s.rqueue.mutex.RUnlock() } - Expect(wrap).To(Panic()) - }) - }) - Describe("With request in the queue", func() { - var now, tomorrow time.Time - var req, noreq boruta.ReqID - var rinfo *boruta.ReqInfo - BeforeEach(func() { - now = time.Now() - tomorrow = now.AddDate(0, 0, 1) - wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), testErr).AnyTimes() - var err error - req, err = R.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) - Expect(err).NotTo(HaveOccurred()) - var ok bool - R.mutex.Lock() - rinfo, ok = R.requests[req] - R.mutex.Unlock() - Expect(ok).To(BeTrue()) - noreq = req + 1 }) - Describe("VerifyIfReady", func() { - It("should fail if reqID is unknown", func() { - Expect(R.VerifyIfReady(noreq, now)).To(BeFalse()) - }) - It("should fail if state is not WAIT", func() { - states := []boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, - boruta.DONE, boruta.FAILED} - for _, s := range states { - R.mutex.Lock() - rinfo.State = s - R.mutex.Unlock() - Expect(R.VerifyIfReady(req, now)).To(BeFalse(), "state = %v", s) - } - }) - It("should fail if Deadline is reached or passed", func() { - Expect(R.VerifyIfReady(req, tomorrow.Add(-time.Hour))).To(BeTrue()) - Expect(R.VerifyIfReady(req, tomorrow)).To(BeFalse()) - Expect(R.VerifyIfReady(req, tomorrow.Add(time.Hour))).To(BeFalse()) - }) - It("should fail if ValidAfter is in future", func() { - Expect(R.VerifyIfReady(req, now.Add(-time.Hour))).To(BeFalse()) - Expect(R.VerifyIfReady(req, now)).To(BeTrue()) - Expect(R.VerifyIfReady(req, now.Add(time.Hour))).To(BeTrue()) - }) - It("should succeed if request is known, in WAIT state and now is between ValidAfter and Deadline", func() { - Expect(R.VerifyIfReady(req, now.Add(12*time.Hour))).To(BeTrue()) - }) - }) - Describe("Get", func() { - It("should fail if reqID is unknown", func() { - r, err := R.Get(noreq) - Expect(err).To(Equal(boruta.NotFoundError("Request"))) - Expect(r).To(Equal(boruta.ReqInfo{})) - }) - It("should succeed if reqID is valid", func() { - r, err := R.Get(req) - Expect(err).NotTo(HaveOccurred()) - Expect(r).To(Equal(*rinfo)) - }) - }) - Describe("Timeout", func() { - It("should fail if reqID is unknown", func() { - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Timeout(noreq) - Expect(err).To(Equal(boruta.NotFoundError("Request"))) - Expect(R.queue.length).To(Equal(uint(1))) - }) - It("should fail if request is not in WAIT state", func() { - R.mutex.Lock() - rinfo.Deadline = now.Add(-time.Hour) - R.mutex.Unlock() - Expect(R.queue.length).To(Equal(uint(1))) - states := []boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, - boruta.DONE, boruta.FAILED} - for _, s := range states { - R.mutex.Lock() - rinfo.State = s - R.mutex.Unlock() - err := R.Timeout(req) - Expect(err).To(Equal(ErrModificationForbidden), "state = %v", s) - Expect(R.queue.length).To(Equal(uint(1)), "state = %v", s) - } - }) - It("should fail if deadline is in the future", func() { - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Timeout(req) - Expect(err).To(Equal(ErrModificationForbidden)) - Expect(R.queue.length).To(Equal(uint(1))) - }) - It("should pass if deadline is past", func() { - R.mutex.Lock() - rinfo.Deadline = now.Add(-time.Hour) - R.mutex.Unlock() - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Timeout(req) - Expect(err).NotTo(HaveOccurred()) - Expect(rinfo.State).To(Equal(boruta.TIMEOUT)) - Expect(R.queue.length).To(BeZero()) - }) + } + }) + + // Only requests that are INPROGRESS can be closed. + s.Run("RequestStates", func() { + states := [...]boruta.ReqState{boruta.WAIT, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, boruta.DONE, boruta.FAILED} + for _, state := range states { + state := state + s.rqueue.mutex.Lock() + s.rqueue.requests[reqs[2]].State = state + s.rqueue.mutex.Unlock() + s.Run(string(state), func() { + s.T().Parallel() + s.ErrorIsf(ErrModificationForbidden, s.rqueue.Close(reqs[2]), "state: %s", state) }) - Describe("Close", func() { - It("should fail if reqID is unknown", func() { - err := R.Close(noreq) - Expect(err).To(Equal(boruta.NotFoundError("Request"))) - }) - It("should fail if request is not in INPROGRESS state", func() { - states := []boruta.ReqState{boruta.WAIT, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, - boruta.DONE, boruta.FAILED} - for _, state := range states { - R.mutex.Lock() - rinfo.State = state - R.mutex.Unlock() - - err := R.Close(req) - Expect(err).To(Equal(ErrModificationForbidden), "state = %s", state) - } - }) - It("should fail if request has no job assigned", func() { - R.mutex.Lock() - rinfo.State = boruta.INPROGRESS - Expect(rinfo.Job).To(BeNil()) - R.mutex.Unlock() - - err := R.Close(req) - Expect(err).To(Equal(boruta.ErrInternalLogicError)) - }) - It("should fail if job's is not yet timed out", func() { - R.mutex.Lock() - rinfo.State = boruta.INPROGRESS - rinfo.Job = &boruta.JobInfo{ - Timeout: time.Now().AddDate(0, 0, 1), - } - R.mutex.Unlock() - - err := R.Close(req) - Expect(err).To(Equal(ErrModificationForbidden)) - }) - It("should close request and release worker", func() { - testWorker := boruta.WorkerUUID("TestWorker") - R.mutex.Lock() - rinfo.State = boruta.INPROGRESS - rinfo.Job = &boruta.JobInfo{ - Timeout: time.Now().AddDate(0, 0, -1), - WorkerUUID: testWorker, - } - R.mutex.Unlock() - gomock.InOrder( - jm.EXPECT().Finish(testWorker, true), - ) - err := R.Close(req) - Expect(err).NotTo(HaveOccurred()) - Expect(rinfo.State).To(Equal(boruta.DONE)) - }) + } + }) +} + +func (s *RequestsTestSuite) TestRun() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + now := time.Now() + req, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + req2, err := s.rqueue.NewRequest(boruta.Capabilities{}, 3, boruta.UserInfo{}, now, tomorrow) + s.NoError(err) + testWorker := boruta.WorkerUUID("TestWorker") + noreq := req2 + 1 + + doChecks := func(s *RequestsTestSuite, r boruta.ReqID, worker boruta.WorkerUUID, after uint, iter bool, err error) { + s.T().Helper() + s.NoError(s.rqueue.InitIteration()) + s.EqualValues(2, s.rqueue.queue.length) + s.True(s.rqueue.iterating) + s.ErrorIs(err, s.rqueue.Run(r, worker)) + rinfo, ok := s.rqueue.requests[r] + if ok && rinfo.Job != nil { + s.Greater(rinfo.Job.Timeout, time.Now()) + } + s.Equal(iter, s.rqueue.iterating) + s.Equal(after, s.rqueue.queue.length) + s.rqueue.TerminateIteration() + } + + s.Run("States", func() { + // Only requests that are WAIT can be run. + states := [...]boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, boruta.DONE, boruta.FAILED} + for _, state := range states { + state := state + s.rqueue.mutex.Lock() + s.rqueue.requests[req2].State = state + s.rqueue.mutex.Unlock() + s.Run(string(state), func() { + s.T().Parallel() + doChecks(s, req2, testWorker, 2, true, ErrModificationForbidden) }) - Describe("Run", func() { - testWorker := boruta.WorkerUUID("TestWorker") - - It("should fail if reqID is unknown", func() { - R.mutex.Lock() - defer R.mutex.Unlock() - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Run(noreq, testWorker) - Expect(err).To(Equal(boruta.NotFoundError("Request"))) - Expect(R.queue.length).To(Equal(uint(1))) - }) - It("should fail if reqID is unknown during iteration", func() { - R.InitIteration() - defer R.TerminateIteration() - Expect(R.iterating).To(BeTrue()) - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Run(noreq, testWorker) - Expect(err).To(Equal(boruta.NotFoundError("Request"))) - Expect(R.iterating).To(BeTrue()) - Expect(R.queue.length).To(Equal(uint(1))) - }) - It("should fail if request is not in WAIT state", func() { - states := []boruta.ReqState{boruta.INPROGRESS, boruta.CANCEL, boruta.TIMEOUT, boruta.INVALID, - boruta.DONE, boruta.FAILED} - for _, state := range states { - R.InitIteration() - Expect(R.queue.length).To(Equal(uint(1)), "state = %s", state) - rinfo.State = state - err := R.Run(req, boruta.WorkerUUID("TestWorker")) - Expect(err).To(Equal(ErrModificationForbidden), "state = %s", state) - Expect(R.queue.length).To(Equal(uint(1)), "state = %s", state) - R.TerminateIteration() - } - }) - It("should start progress for valid reqID", func() { - R.InitIteration() - defer R.TerminateIteration() - Expect(R.queue.length).To(Equal(uint(1))) - err := R.Run(req, testWorker) - Expect(err).NotTo(HaveOccurred()) - Expect(rinfo.State).To(Equal(boruta.INPROGRESS)) - Expect(rinfo.Job.Timeout).To(BeTemporally(">", time.Now())) - Expect(R.queue.length).To(BeZero()) - }) - It("should start progress and break iterations when iterating", func() { - R.InitIteration() - defer R.TerminateIteration() - Expect(R.queue.length).To(Equal(uint(1))) - Expect(R.iterating).To(BeTrue()) - err := R.Run(req, testWorker) - Expect(err).NotTo(HaveOccurred()) - Expect(rinfo.State).To(Equal(boruta.INPROGRESS)) - Expect(rinfo.Job.Timeout).To(BeTemporally(">", time.Now())) - Expect(R.iterating).To(BeFalse()) - Expect(R.queue.length).To(BeZero()) - }) + } + }) + s.Run("UnknownID", func() { + s.rqueue.mutex.Lock() + defer s.rqueue.mutex.Unlock() + s.rqueue.queue.mtx.Lock() + s.EqualValues(2, s.rqueue.queue.length) + s.rqueue.queue.mtx.Unlock() + s.ErrorIs(boruta.NotFoundError("Request"), s.rqueue.Run(noreq, testWorker)) + s.rqueue.queue.mtx.Lock() + s.EqualValues(2, s.rqueue.queue.length) + s.rqueue.queue.mtx.Unlock() + }) + s.Run("Iterating", func() { + testCases := [...]struct { + name string + req boruta.ReqID + worker boruta.WorkerUUID + after uint + iter bool + err error + }{ + {name: "UnknownID", req: noreq, worker: testWorker, after: 2, iter: true, err: boruta.NotFoundError("Request")}, + {name: "ValidRequest", req: req, worker: testWorker, after: 1, iter: false, err: nil}, + } + for _, test := range testCases { + test := test + s.Run(test.name, func() { + doChecks(s, test.req, test.worker, test.after, test.iter, test.err) }) - }) + } }) -}) +} diff --git a/requests/requests_suite_test.go b/requests/requests_suite_test.go index d2b1f66..e492c67 100644 --- a/requests/requests_suite_test.go +++ b/requests/requests_suite_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,45 @@ package requests import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - + "errors" "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" ) -func TestRequests(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Requests Suite") +var ( + zeroTime time.Time + now = time.Now().UTC() + yesterday = now.AddDate(0, 0, -1).UTC() + tomorrow = now.AddDate(0, 0, 1).UTC() + nextWeek = now.AddDate(0, 0, 7).UTC() +) + +type RequestsTestSuite struct { + suite.Suite + ctrl *gomock.Controller + wm *MockWorkersManager + jm *MockJobsManager + testErr error + rqueue *ReqsCollection +} + +func (s *RequestsTestSuite) SetupTest() { + s.ctrl = gomock.NewController(s.T()) + s.wm = NewMockWorkersManager(s.ctrl) + s.jm = NewMockJobsManager(s.ctrl) + s.testErr = errors.New("Test Error") + s.wm.EXPECT().SetChangeListener(gomock.Any()) + s.rqueue = NewRequestQueue(s.wm, s.jm) +} + +func (s *RequestsTestSuite) TearDownTest() { + s.rqueue.Finish() + s.ctrl.Finish() +} + +func TestRequestsTestSuite(t *testing.T) { + suite.Run(t, new(RequestsTestSuite)) } diff --git a/requests/requests_test.go b/requests/requests_test.go index 0f46cc7..76601c2 100644 --- a/requests/requests_test.go +++ b/requests/requests_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,7 @@ package requests import ( - "errors" "net" - "testing" "time" "github.com/SamsungSLAV/boruta" @@ -30,23 +28,18 @@ import ( ) var ( - owner boruta.UserInfo - job boruta.JobInfo - zeroTime time.Time - caps = make(boruta.Capabilities) - now = time.Now().UTC() - lastWeek = now.AddDate(0, 0, -7).UTC() - yesterday = now.AddDate(0, 0, -1).UTC() - tomorrow = now.AddDate(0, 0, 1).UTC() - nextWeek = now.AddDate(0, 0, 7).UTC() + owner boruta.UserInfo + job boruta.JobInfo + caps = make(boruta.Capabilities) ) var requestsTests = [...]struct { - req boruta.ReqInfo - err error + name string + req boruta.ReqInfo + err error }{ { - // valid request + name: "ValidRequest", req: boruta.ReqInfo{ ID: boruta.ReqID(1), Priority: boruta.Priority((boruta.HiPrio + boruta.LoPrio) / 2), @@ -60,7 +53,7 @@ var requestsTests = [...]struct { err: nil, }, { - // request with invalid priority + name: "InvalidPriority", req: boruta.ReqInfo{ ID: boruta.ReqID(0), Priority: boruta.Priority(boruta.LoPrio + 1), @@ -74,7 +67,7 @@ var requestsTests = [...]struct { err: ErrPriority, }, { - // request with ValidAfter date newer then Deadline + name: "ValidAfterNewerThenDeadline", req: boruta.ReqInfo{ ID: boruta.ReqID(0), Priority: boruta.Priority((boruta.HiPrio + boruta.LoPrio) / 2), @@ -88,7 +81,7 @@ var requestsTests = [...]struct { err: ErrInvalidTimeRange, }, { - // request with Deadline set in the past. + name: "DeadlineInThePast", req: boruta.ReqInfo{ ID: boruta.ReqID(0), Priority: boruta.Priority((boruta.HiPrio + boruta.LoPrio) / 2), @@ -103,285 +96,282 @@ var requestsTests = [...]struct { }, } -func initTest(t *testing.T) (*assert.Assertions, *ReqsCollection, *gomock.Controller, *MockJobsManager) { - ctrl := gomock.NewController(t) - wm := NewMockWorkersManager(ctrl) - jm := NewMockJobsManager(ctrl) - testErr := errors.New("Test Error") - wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), - testErr).AnyTimes() - wm.EXPECT().SetChangeListener(gomock.Any()) - return assert.New(t), NewRequestQueue(wm, jm), ctrl, jm -} - -func finiTest(rqueue *ReqsCollection, ctrl *gomock.Controller) { - rqueue.Finish() - ctrl.Finish() +func (s *RequestsTestSuite) TestNewRequestQueue() { + s.rqueue.mutex.RLock() + defer s.rqueue.mutex.RUnlock() + s.Zero(len(s.rqueue.requests)) + s.NotNil(s.rqueue.queue) + s.Zero(s.rqueue.queue.length) } -func TestNewRequestQueue(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestNewRequest() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() - rqueue.mutex.RLock() - defer rqueue.mutex.RUnlock() - assert.Zero(len(rqueue.requests)) - assert.NotNil(rqueue.queue) - assert.Zero(rqueue.queue.length) -} - -func TestNewRequest(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) - - for _, test := range requestsTests { - reqid, err := rqueue.NewRequest(test.req.Caps, test.req.Priority, - test.req.Owner, test.req.ValidAfter, test.req.Deadline) - assert.Equal(test.req.ID, reqid) - assert.Equal(test.err, err) - } + s.Run("TimesAndPriorities", func() { + for _, test := range requestsTests { + test := test + s.Run(test.name, func() { + s.T().Parallel() + reqid, err := s.rqueue.NewRequest(test.req.Caps, test.req.Priority, + test.req.Owner, test.req.ValidAfter, test.req.Deadline) + s.Equalf(test.req.ID, reqid, "test case: %s", test.name) + s.Equalf(test.err, err, "test case: %s", test.name) + }) + } - req := requestsTests[0].req - req.Deadline = zeroTime - req.ValidAfter = zeroTime - start := time.Now() - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, - req.ValidAfter, req.Deadline) - stop := time.Now() - assert.Nil(err) - rqueue.mutex.RLock() - defer rqueue.mutex.RUnlock() - res := rqueue.requests[reqid] - assert.True(start.Before(res.ValidAfter) && stop.After(res.ValidAfter)) - start = start.AddDate(0, 1, 0) - stop = stop.AddDate(0, 1, 0) - assert.True(start.Before(res.Deadline) && stop.After(res.Deadline)) - assert.EqualValues(2, rqueue.queue.length) + }) + s.Run("ZeroTimes", func() { + req := requestsTests[0].req + start := time.Now() + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, + zeroTime, zeroTime) + stop := time.Now() + s.Nil(err) + s.NotZero(reqid) + s.rqueue.mutex.RLock() + defer s.rqueue.mutex.RUnlock() + res := s.rqueue.requests[reqid] + s.True(start.Before(res.ValidAfter) && stop.After(res.ValidAfter)) + start = start.AddDate(0, 1, 0) + stop = stop.AddDate(0, 1, 0) + s.True(start.Before(res.Deadline) && stop.After(res.Deadline)) + s.EqualValues(2, s.rqueue.queue.length) + }) } -func TestCloseRequest(t *testing.T) { - assert, rqueue, ctrl, jm := initTest(t) - defer finiTest(rqueue, ctrl) - +func (s *RequestsTestSuite) TestCloseRequest() { req := requestsTests[0].req jobInfo := boruta.JobInfo{ WorkerUUID: "Test WorkerUUID", } + const reqsCnt = 4 + var reqs [reqsCnt]boruta.ReqID + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() // Add valid request to the queue. - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - - // Cancel previously added request. - rqueue.mutex.RLock() - assert.EqualValues(1, rqueue.queue.length) - rqueue.mutex.RUnlock() - err = rqueue.CloseRequest(reqid) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(boruta.ReqState(boruta.CANCEL), rqueue.requests[reqid].State) - assert.Zero(rqueue.queue.length) - rqueue.mutex.RUnlock() - - // Try to close non-existent request. - err = rqueue.CloseRequest(boruta.ReqID(2)) - assert.Equal(boruta.NotFoundError("Request"), err) - - // Add another valid request. - reqid, err = rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - assert.EqualValues(2, reqid) - // Simulate situation where request was assigned a worker and job has begun. - reqinfo, err := rqueue.GetRequestInfo(reqid) - assert.Nil(err) - rqueue.mutex.Lock() - rqueue.requests[reqid].State = boruta.INPROGRESS - rqueue.requests[reqid].Job = &jobInfo - rqueue.queue.removeRequest(&reqinfo) - rqueue.mutex.Unlock() - // Close request. - gomock.InOrder( - jm.EXPECT().Finish(jobInfo.WorkerUUID, true), - ) - err = rqueue.CloseRequest(reqid) - assert.Nil(err) - rqueue.mutex.RLock() - assert.EqualValues(2, len(rqueue.requests)) - assert.Equal(boruta.ReqState(boruta.DONE), rqueue.requests[reqid].State) - rqueue.mutex.RUnlock() - - // Add another another valid request. - reqid, err = rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - assert.EqualValues(3, reqid) - // Simulate situation where request is in PROGRESS state, but no job for it exists. - reqinfo, err = rqueue.GetRequestInfo(reqid) - assert.Nil(err) - rqueue.mutex.Lock() - rqueue.requests[reqid].State = boruta.INPROGRESS - rqueue.requests[reqid].Job = nil - rqueue.queue.removeRequest(&reqinfo) - rqueue.mutex.Unlock() - // Close request. - err = rqueue.CloseRequest(reqid) - assert.Nil(err) - rqueue.mutex.RLock() - assert.EqualValues(3, len(rqueue.requests)) - assert.Equal(boruta.ReqState(boruta.DONE), rqueue.requests[reqid].State) - rqueue.mutex.RUnlock() - - // Simulation for the rest of states. - states := [...]boruta.ReqState{boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, boruta.DONE, - boruta.FAILED} - reqid, err = rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - assert.EqualValues(4, reqid) - reqinfo, err = rqueue.GetRequestInfo(reqid) - assert.Nil(err) - rqueue.mutex.Lock() - rqueue.queue.removeRequest(&reqinfo) - rqueue.mutex.Unlock() - for i := range states { - rqueue.mutex.Lock() - rqueue.requests[reqid].State = states[i] - rqueue.mutex.Unlock() - err = rqueue.CloseRequest(reqid) - assert.EqualValues(ErrModificationForbidden, err) + for i := 0; i < reqsCnt; i++ { + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) + s.Nil(err) + s.NotZero(reqid) + reqs[i] = reqid } + s.rqueue.queue.mtx.Lock() + s.EqualValues(reqsCnt, s.rqueue.queue.length) + s.rqueue.queue.mtx.Unlock() + + s.Run("Requests", func() { + noreq := reqs[reqsCnt-1] + 1 + testCases := [...]struct { + name string + reqid boruta.ReqID + state boruta.ReqState + sub uint + err error + }{ + {name: "MissingRequest", reqid: noreq, state: "", err: boruta.NotFoundError("Request")}, + {name: "Cancel", reqid: reqs[0], state: boruta.CANCEL, sub: 1, err: nil}, + {name: "Done", reqid: reqs[1], state: boruta.DONE, sub: 0, err: nil}, // Request is in progress and job is assigned. + {name: "NoJob", reqid: reqs[2], state: boruta.DONE, sub: 0, err: nil}, // Request is in progress but job is missing. + } + + s.jm.EXPECT().Finish(jobInfo.WorkerUUID, true) + s.rqueue.mutex.Lock() + s.rqueue.requests[reqs[1]].State = boruta.INPROGRESS + s.rqueue.requests[reqs[1]].Job = &jobInfo + s.rqueue.requests[reqs[2]].State = boruta.INPROGRESS + s.rqueue.requests[reqs[2]].Job = nil + s.rqueue.mutex.Unlock() + for i := 1; i <= 2; i++ { // Requests that are in progress are removed from priority queue. + reqinfo, err := s.rqueue.GetRequestInfo(reqs[i]) + s.NoError(err) + s.rqueue.queue.removeRequest(&reqinfo) + } + + for _, test := range testCases { + test := test + s.Run(test.name, func() { + s.T().Parallel() + if test.state == boruta.CANCEL { + time.Sleep(time.Second) + } + s.rqueue.queue.mtx.Lock() + qlen := s.rqueue.queue.length + s.rqueue.queue.mtx.Unlock() + // Adjust number of elements in the priority queue after closing. + qlen -= test.sub + s.ErrorIsf(test.err, s.rqueue.CloseRequest(test.reqid), "test case: %s", test.name) + s.rqueue.queue.mtx.Lock() + s.Equalf(qlen, s.rqueue.queue.length, "test case: %s", test.name) + s.rqueue.queue.mtx.Unlock() + s.rqueue.mutex.RLock() + s.EqualValuesf(reqsCnt, len(s.rqueue.requests), "test case: %s", test.name) + s.rqueue.mutex.RUnlock() + if test.err == nil { + s.rqueue.mutex.RLock() + s.Equalf(test.state, s.rqueue.requests[test.reqid].State, "test case: %s", test.name) + s.rqueue.mutex.RUnlock() + } + }) + } + }) - rqueue.mutex.RLock() - defer rqueue.mutex.RUnlock() - assert.EqualValues(4, len(rqueue.requests)) - assert.EqualValues(0, rqueue.queue.length) + // Simulation for the rest of states. + s.Run("States", func() { + states := [...]boruta.ReqState{boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, boruta.DONE, boruta.FAILED} + reqid := reqs[len(reqs)-1] + reqinfo, err := s.rqueue.GetRequestInfo(reqid) + s.Nil(err) + s.rqueue.queue.removeRequest(&reqinfo) + for _, state := range states { + state := state + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].State = state + s.rqueue.mutex.Unlock() + s.Run(string(state), func() { + s.T().Parallel() + s.EqualValuesf(ErrModificationForbidden, s.rqueue.CloseRequest(reqid), "state: %s", state) + }) + } + }) + s.rqueue.mutex.RLock() + // rqueue shouldn't change + s.EqualValues(reqsCnt, len(s.rqueue.requests)) + s.rqueue.mutex.RUnlock() + s.rqueue.queue.mtx.Lock() + // Priority queue should be empty. + s.EqualValues(0, s.rqueue.queue.length) + s.rqueue.queue.mtx.Unlock() } -func TestUpdateRequest(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestUpdateRequest() { tmp := requestsTests[0].req + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() // Add valid request. - reqid, err := rqueue.NewRequest(tmp.Caps, tmp.Priority, tmp.Owner, tmp.ValidAfter, tmp.Deadline) - assert.Nil(err) - rqueue.mutex.RLock() - req := rqueue.requests[reqid] - rqueue.mutex.RUnlock() - reqBefore, err := rqueue.GetRequestInfo(reqid) - assert.Nil(err) - reqUpdate := new(boruta.ReqInfo) - rqueue.mutex.RLock() - *reqUpdate = *req - rqueue.mutex.RUnlock() - - // Check noop. - err = rqueue.UpdateRequest(nil) - assert.Nil(err) - reqUpdate.ValidAfter = zeroTime - reqUpdate.Deadline = zeroTime - reqUpdate.Priority = boruta.Priority(0) - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(req, &reqBefore) - // Check request that doesn't exist. - *reqUpdate = *req - rqueue.mutex.RUnlock() - reqUpdate.ID++ - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(boruta.NotFoundError("Request"), err) - rqueue.mutex.RLock() - reqUpdate.ID = req.ID - // Change Priority only. - reqUpdate.Priority = req.Priority - 1 - rqueue.mutex.RUnlock() - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(reqUpdate.Priority, req.Priority) - rqueue.mutex.RUnlock() - // Change ValidAfter only. - reqUpdate.ValidAfter = yesterday - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(reqUpdate.ValidAfter, req.ValidAfter) - rqueue.mutex.RUnlock() - // Change Deadline only. - reqUpdate.Deadline = tomorrow.AddDate(0, 0, 1).UTC() - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(reqUpdate.Deadline, req.Deadline) - rqueue.mutex.RUnlock() - // Change Priority, ValidAfter and Deadline. - reqUpdate.Deadline = tomorrow - reqUpdate.ValidAfter = time.Now().Add(time.Hour) - reqUpdate.Priority = boruta.LoPrio - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(reqUpdate, req) - rqueue.mutex.RUnlock() - // Change values to the same ones that are already set. - err = rqueue.UpdateRequest(reqUpdate) - assert.Nil(err) - rqueue.mutex.RLock() - assert.Equal(reqUpdate, req) - rqueue.mutex.RUnlock() - // Change Priority to illegal value. - reqUpdate.Priority = boruta.LoPrio + 1 - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(ErrPriority, err) - rqueue.mutex.RLock() - reqUpdate.Priority = req.Priority - rqueue.mutex.RUnlock() - //Change Deadline to illegal value. - reqUpdate.Deadline = yesterday - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(ErrDeadlineInThePast, err) - reqUpdate.Deadline = time.Now().Add(time.Minute) - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(ErrInvalidTimeRange, err) - // Change ValidAfer to illegal value. - rqueue.mutex.RLock() - reqUpdate.ValidAfter = req.Deadline.Add(time.Hour) - rqueue.mutex.RUnlock() - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(ErrInvalidTimeRange, err) + reqid, err := s.rqueue.NewRequest(tmp.Caps, tmp.Priority, tmp.Owner, tmp.ValidAfter, tmp.Deadline) + s.NoError(err) + s.NotZero(reqid) + s.rqueue.mutex.RLock() + req := s.rqueue.requests[reqid] + s.rqueue.mutex.RUnlock() + + s.Run("Requests", func() { + s.rqueue.mutex.RLock() + reqUpdate := *req + s.rqueue.mutex.RUnlock() + reqBefore, err := s.rqueue.GetRequestInfo(reqid) + s.NoError(err) + // Check noop. + s.NoError(s.rqueue.UpdateRequest(nil)) + reqUpdate.ValidAfter = zeroTime + reqUpdate.Deadline = zeroTime + reqUpdate.Priority = boruta.Priority(0) + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(req, &reqBefore) + // Check request that doesn't exist. + reqUpdate = *req + s.rqueue.mutex.RUnlock() + reqUpdate.ID++ + s.ErrorIs(boruta.NotFoundError("Request"), s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + reqUpdate.ID = req.ID + // Change Priority only. + reqUpdate.Priority = req.Priority - 1 + s.rqueue.mutex.RUnlock() + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(reqUpdate.Priority, req.Priority) + s.rqueue.mutex.RUnlock() + // Change ValidAfter only. + reqUpdate.ValidAfter = yesterday + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(reqUpdate.ValidAfter, req.ValidAfter) + s.rqueue.mutex.RUnlock() + // Change Deadline only. + reqUpdate.Deadline = tomorrow.AddDate(0, 0, 1).UTC() + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(reqUpdate.Deadline, req.Deadline) + s.rqueue.mutex.RUnlock() + // Change Priority, ValidAfter and Deadline. + reqUpdate.Deadline = tomorrow + reqUpdate.ValidAfter = time.Now().Add(time.Hour) + reqUpdate.Priority = boruta.LoPrio + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(reqUpdate, *req) + s.rqueue.mutex.RUnlock() + // Change values to the same ones that are already set. + s.NoError(s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + s.Equal(reqUpdate, *req) + s.rqueue.mutex.RUnlock() + // Change Priority to illegal value. + reqUpdate.Priority = boruta.LoPrio + 1 + s.ErrorIs(ErrPriority, s.rqueue.UpdateRequest(&reqUpdate)) + s.rqueue.mutex.RLock() + reqUpdate.Priority = req.Priority + s.rqueue.mutex.RUnlock() + //Change Deadline to illegal value. + reqUpdate.Deadline = yesterday + s.ErrorIs(ErrDeadlineInThePast, s.rqueue.UpdateRequest(&reqUpdate)) + reqUpdate.Deadline = time.Now().Add(time.Minute) + s.ErrorIs(ErrInvalidTimeRange, s.rqueue.UpdateRequest(&reqUpdate)) + // Change ValidAfer to illegal value. + s.rqueue.mutex.RLock() + reqUpdate.ValidAfter = req.Deadline.Add(time.Hour) + s.rqueue.mutex.RUnlock() + s.ErrorIs(ErrInvalidTimeRange, s.rqueue.UpdateRequest(&reqUpdate)) + }) // Try to change values for other changes. - states := [...]boruta.ReqState{boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, boruta.DONE, - boruta.FAILED, boruta.INPROGRESS} - for _, state := range states { - rqueue.mutex.Lock() - rqueue.requests[reqid].State = state - rqueue.mutex.Unlock() - err = rqueue.UpdateRequest(reqUpdate) - assert.Equal(ErrModificationForbidden, err) - } + s.Run("States", func() { + states := [...]boruta.ReqState{boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, boruta.DONE, boruta.FAILED, boruta.INPROGRESS} + reqUpdate := *req + for _, state := range states { + state := state + s.Run(string(state), func() { + s.T().Parallel() + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].State = state + s.rqueue.mutex.Unlock() + s.ErrorIsf(ErrModificationForbidden, s.rqueue.UpdateRequest(&reqUpdate), "state: %s", state) + }) + } + }) } -func TestGetRequestInfo(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestGetRequestInfo() { + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() req := requestsTests[0].req req.Job = nil - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - - // Get request information for existing request. - reqUpdate, err := rqueue.GetRequestInfo(reqid) - assert.Nil(err) - assert.Equal(req, reqUpdate) - - // Try to get information for non-existent request. - req3, err := rqueue.GetRequestInfo(boruta.ReqID(2)) - assert.Equal(boruta.NotFoundError("Request"), err) - assert.Zero(req3) + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) + s.Nil(err) + s.NotZero(reqid) + noreq := reqid + 1 + + testCases := [...]struct { + name string + id boruta.ReqID + req boruta.ReqInfo + err error + }{ + {name: "MissingRequest", id: noreq, req: boruta.ReqInfo{}, err: boruta.NotFoundError("Request")}, + {name: "ValidRequest", id: reqid, req: req, err: nil}, + } + + for _, test := range testCases { + s.Run(test.name, func() { + s.T().Parallel() + r, err := s.rqueue.GetRequestInfo(test.id) + s.Equalf(test.req, r, "test case: %s", test.name) + s.ErrorIsf(test.err, err, "test case: %s", test.name) + }) + } } -func TestListRequests(t *testing.T) { - _, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestListRequests() { req := requestsTests[0].req const reqsCnt = 16 si := &boruta.SortInfo{ @@ -392,27 +382,28 @@ func TestListRequests(t *testing.T) { getResults := func(ids ...int) (res []*boruta.ReqInfo) { res = make([]*boruta.ReqInfo, len(ids)) for idx, id := range ids { - res[idx] = rqueue.requests[boruta.ReqID(id)] + res[idx] = s.rqueue.requests[boruta.ReqID(id)] } return } + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() // Add few requests. for i := 0; i < reqsCnt; i++ { - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) if err != nil { - t.Fatal("unable to create new request:", err) + s.T().Fatal("unable to create new request:", err) } if i%2 == 1 { - rqueue.mutex.Lock() - rqueue.requests[reqid].Priority++ - rqueue.mutex.Unlock() + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].Priority++ + s.rqueue.mutex.Unlock() } if i > 1 { - rqueue.mutex.Lock() - rqueue.requests[reqid].State = boruta.DONE - rqueue.mutex.Unlock() + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].State = boruta.DONE + s.rqueue.mutex.Unlock() } } @@ -868,40 +859,39 @@ func TestListRequests(t *testing.T) { checkReqs := func(assert *assert.Assertions, name string, reqs []*boruta.ReqInfo, resp []boruta.ReqInfo) { + s.T().Helper() assert.Equal(len(reqs), len(resp), name) for i := range resp { - assert.Equal(reqs[i], &resp[i], name) + assert.Equalf(reqs[i], &resp[i], "test case: %s", name) } } for _, test := range filterTests { test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - assert := assert.New(t) - resp, info, err := rqueue.ListRequests(test.f, test.s, test.p) - assert.Equal(test.err, err, test.name) - assert.Equal(test.info, info, test.name) - checkReqs(assert, test.name, test.result, resp) + s.Run(test.name, func() { + s.T().Parallel() + resp, info, err := s.rqueue.ListRequests(test.f, test.s, test.p) + s.Equalf(test.err, err, "test case: %s", test.name) + s.Equalf(test.info, info, "test case: %s", test.name) + checkReqs(s.Assert(), test.name, test.result, resp) }) } name := "nil interface" - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - resp, info, err := rqueue.ListRequests(nil, nil, nil) - assert.Nil(err) - assert.Equal(&boruta.ListInfo{ + s.Run(name, func() { + resp, info, err := s.rqueue.ListRequests(nil, nil, nil) + s.Nil(err) + s.Equal(&boruta.ListInfo{ TotalItems: 16, RemainingItems: 0, }, info) - checkReqs(assert, name, getResults(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + checkReqs(s.Assert(), name, getResults(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16), resp) }) name = "big queue" // As tests are running in parallel rqueue modification would make some of previously defined // tests fail. - ctrl2 := gomock.NewController(t) + ctrl2 := gomock.NewController(s.T()) defer ctrl2.Finish() wm := NewMockWorkersManager(ctrl2) jm := NewMockJobsManager(ctrl2) @@ -910,101 +900,139 @@ func TestListRequests(t *testing.T) { wm.EXPECT().SetChangeListener(gomock.Any()) rqueue2 := NewRequestQueue(wm, jm) for i := 0; i < int(boruta.MaxPageLimit)+reqsCnt; i++ { - rqueue2.NewRequest(req.Caps, req.Priority, req.Owner, tomorrow, nextWeek) + _, err := rqueue2.NewRequest(req.Caps, req.Priority, req.Owner, tomorrow, nextWeek) + s.Nil(err) } - assert.Equal(t, int(boruta.MaxPageLimit)+reqsCnt, len(rqueue2.requests)) - t.Run(name, func(t *testing.T) { - assert := assert.New(t) + s.Equal(int(boruta.MaxPageLimit)+reqsCnt, len(rqueue2.requests)) + s.Run(name, func() { resp, info, err := rqueue2.ListRequests(nil, nil, &boruta.RequestsPaginator{Limit: 0}) - assert.Nil(err) - assert.EqualValues(int(boruta.MaxPageLimit)+reqsCnt, info.TotalItems) - assert.EqualValues(boruta.MaxPageLimit, len(resp)) - assert.EqualValues(reqsCnt, info.RemainingItems) + s.Nil(err) + s.EqualValues(int(boruta.MaxPageLimit)+reqsCnt, info.TotalItems) + s.EqualValues(boruta.MaxPageLimit, len(resp)) + s.EqualValues(reqsCnt, info.RemainingItems) }) } -func TestAcquireWorker(t *testing.T) { - assert, rqueue, ctrl, jm := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestAcquireWorker() { req := requestsTests[0].req empty := boruta.AccessInfo{} - testErr := errors.New("Test Error") - // Add valid request. - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - - states := [...]boruta.ReqState{boruta.WAIT, boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, - boruta.DONE, boruta.FAILED, boruta.INPROGRESS} - for _, state := range states { - rqueue.mutex.Lock() - rqueue.requests[reqid].State = state - rqueue.mutex.Unlock() - ainfo, err := rqueue.AcquireWorker(reqid) - assert.Equal(ErrWorkerNotAssigned, err) - assert.Equal(empty, ainfo) - } + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() + // Add valid requests. + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) + s.Nil(err) + s.NotZero(reqid) + reqid2, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) + s.Nil(err) + s.NotZero(reqid2) + noreq := reqid2 + 1 - // Try to acquire worker for non-existing request. - ainfo, err := rqueue.AcquireWorker(boruta.ReqID(2)) - assert.Equal(boruta.NotFoundError("Request"), err) - assert.Equal(empty, ainfo) + s.Run("States", func() { + states := [...]boruta.ReqState{boruta.WAIT, boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, + boruta.DONE, boruta.FAILED, boruta.INPROGRESS} + for _, state := range states { + state := state + s.Run(string(state), func() { + s.T().Parallel() + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].State = state + s.rqueue.mutex.Unlock() + ainfo, err := s.rqueue.AcquireWorker(reqid) + s.Equalf(ErrWorkerNotAssigned, err, "state: %s", state) + s.Equalf(empty, ainfo, "state: %s", state) + }) + } + }) - // Try to acquire worker when jobs.Get() fails. - jobInfo := boruta.JobInfo{ - WorkerUUID: "Test WorkerUUID", - } - rqueue.mutex.Lock() - rqueue.requests[reqid].Job = &jobInfo - rqueue.mutex.Unlock() - ignoredJob := &workers.Job{Req: boruta.ReqID(0xBAD)} - jm.EXPECT().Get(jobInfo.WorkerUUID).Return(ignoredJob, testErr) - ainfo, err = rqueue.AcquireWorker(reqid) - assert.Equal(testErr, err) - assert.Equal(empty, ainfo) - - // AcquireWorker to succeed needs JobInfo to be set. It also needs to be - // in INPROGRESS state, which was set in the loop. - job := &workers.Job{ - Access: boruta.AccessInfo{Addr: &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4)}}, - } - rqueue.mutex.Lock() - rqueue.requests[reqid].Job = &jobInfo - rqueue.mutex.Unlock() - jm.EXPECT().Get(jobInfo.WorkerUUID).Return(job, nil) - ainfo, err = rqueue.AcquireWorker(reqid) - assert.Nil(err) - assert.Equal(job.Access, ainfo) + s.Run("Requests", func() { + testCases := [...]struct { + name string + id boruta.ReqID + job *boruta.JobInfo + mockJob *workers.Job + err error + }{ + {name: "MissingRequest", id: noreq, job: nil, mockJob: nil, err: boruta.NotFoundError("Request")}, + { + name: "JobError", + id: reqid, + job: &boruta.JobInfo{WorkerUUID: "Worker1"}, + mockJob: &workers.Job{Req: boruta.ReqID(0xBAD)}, + err: s.testErr, + }, + { + name: "ValidRequest", + id: reqid2, + job: &boruta.JobInfo{WorkerUUID: "Worker2"}, + mockJob: &workers.Job{Access: boruta.AccessInfo{Addr: &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4)}}}, + err: nil, + }, + } + + for _, test := range testCases { + test := test + if test.job != nil { + s.rqueue.mutex.Lock() + s.rqueue.requests[test.id].Job = test.job + s.rqueue.requests[test.id].State = boruta.INPROGRESS + s.rqueue.mutex.Unlock() + s.jm.EXPECT().Get(test.job.WorkerUUID).Return(test.mockJob, test.err) + } + s.Run(test.name, func() { + s.T().Parallel() + _, err := s.rqueue.AcquireWorker(test.id) + s.Equalf(test.err, err, "test case: %s", test.name) + }) + } + }) } -func TestProlongAccess(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) +func (s *RequestsTestSuite) TestProlongAccess() { req := requestsTests[0].req + s.wm.EXPECT().TakeBestMatchingWorker(gomock.Any(), gomock.Any()).Return(boruta.WorkerUUID(""), s.testErr).AnyTimes() // Add valid request. - reqid, err := rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) - assert.Nil(err) - - states := [...]boruta.ReqState{boruta.WAIT, boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, - boruta.DONE, boruta.FAILED, boruta.INPROGRESS} - for _, state := range states { - rqueue.mutex.Lock() - rqueue.requests[reqid].State = state - rqueue.mutex.Unlock() - err = rqueue.ProlongAccess(reqid) - assert.Equal(ErrWorkerNotAssigned, err) - } + reqid, err := s.rqueue.NewRequest(req.Caps, req.Priority, req.Owner, req.ValidAfter, req.Deadline) + s.Nil(err) + s.NotZero(reqid) - // Try to prolong access of job for non-existing request. - err = rqueue.ProlongAccess(boruta.ReqID(2)) - assert.Equal(boruta.NotFoundError("Request"), err) - - // ProlongAccess to succeed needs JobInfo to be set. It also needs to be - // in INPROGRESS state, which was set in the loop. - rqueue.mutex.Lock() - rqueue.requests[reqid].Job = new(boruta.JobInfo) - rqueue.mutex.Unlock() - err = rqueue.ProlongAccess(reqid) - assert.Nil(err) + s.Run("States", func() { + states := [...]boruta.ReqState{boruta.WAIT, boruta.INVALID, boruta.CANCEL, boruta.TIMEOUT, + boruta.DONE, boruta.FAILED, boruta.INPROGRESS} + for _, state := range states { + state := state + s.Run(string(state), func() { + s.T().Parallel() + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].State = state + s.rqueue.mutex.Unlock() + s.Equalf(ErrWorkerNotAssigned, s.rqueue.ProlongAccess(reqid), "state: %s", state) + }) + } + }) + + s.Run("Requests", func() { + testCases := [...]struct { + name string + id boruta.ReqID + err error + }{ + {name: "Missing", id: reqid + 1, err: boruta.NotFoundError("Request")}, + {name: "Valid", id: reqid, err: nil}, + } + // ProlongAccess to succeed needs JobInfo to be set. It also needs to be + // in INPROGRESS state. + s.rqueue.mutex.Lock() + s.rqueue.requests[reqid].Job = new(boruta.JobInfo) + s.rqueue.requests[reqid].State = boruta.INPROGRESS + s.rqueue.mutex.Unlock() + + for _, test := range testCases { + test := test + s.Run(test.name, func() { + s.T().Parallel() + s.ErrorIsf(test.err, s.rqueue.ProlongAccess(test.id), "test case: %s", test.name) + }) + } + }) } diff --git a/requests/requests_workerchange_test.go b/requests/requests_workerchange_test.go index 4ba5b15..07f11f5 100644 --- a/requests/requests_workerchange_test.go +++ b/requests/requests_workerchange_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2018-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,110 +17,108 @@ package requests import ( - "errors" "time" "github.com/SamsungSLAV/boruta" "github.com/SamsungSLAV/boruta/workers" +) - gomock "github.com/golang/mock/gomock" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" +var ( + noWorker = boruta.WorkerUUID("") + testCapabilities = boruta.Capabilities{"key": "value"} + testPriority = (boruta.HiPrio + boruta.LoPrio) / 2 + testUser = boruta.UserInfo{Groups: []boruta.Group{"Test Group"}} + trigger = make(chan int, 1) ) -var _ = Describe("Requests as WorkerChange", func() { - var ctrl *gomock.Controller - var wm *MockWorkersManager - var jm *MockJobsManager - var R *ReqsCollection - testErr := errors.New("Test Error") - testWorker := boruta.WorkerUUID("Test Worker UUID") - noWorker := boruta.WorkerUUID("") - testCapabilities := boruta.Capabilities{"key": "value"} - testPriority := (boruta.HiPrio + boruta.LoPrio) / 2 - testUser := boruta.UserInfo{Groups: []boruta.Group{"Test Group"}} - now := time.Now() - tomorrow := now.AddDate(0, 0, 1) - trigger := make(chan int, 1) +func setTrigger(val int) { trigger <- val } - setTrigger := func(val int) { - trigger <- val - } - eventuallyTrigger := func(val int) { - EventuallyWithOffset(1, trigger).Should(Receive(Equal(val))) +func eventuallyTrigger(val int) func() bool { + return func() bool { + select { + case v := <-trigger: + return val == v + default: + return false + } } - eventuallyState := func(reqid boruta.ReqID, state boruta.ReqState) { - EventuallyWithOffset(1, func() boruta.ReqState { - info, err := R.GetRequestInfo(reqid) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - ExpectWithOffset(1, info).NotTo(BeNil()) - return info.State - }).Should(Equal(state)) +} + +func eventuallyState(s *RequestsTestSuite, reqid boruta.ReqID, state boruta.ReqState) func() bool { + return func() bool { + s.T().Helper() + info, err := s.rqueue.GetRequestInfo(reqid) + s.NoError(err) + s.NotZero(reqid) + return info.State == state } +} - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - wm = NewMockWorkersManager(ctrl) - jm = NewMockJobsManager(ctrl) - wm.EXPECT().SetChangeListener(gomock.Any()) - R = NewRequestQueue(wm, jm) - }) - AfterEach(func() { - R.Finish() - ctrl.Finish() +// ValidMatcher should do nothing if there are no waiting requests. +func (s *RequestsTestSuite) TestOnWorkerIdleNoRequests() { + testWorker := boruta.WorkerUUID("Test Worker UUID") + s.rqueue.OnWorkerIdle(testWorker) +} + +func (s *RequestsTestSuite) TestOnWorkerIdleMatchRequest() { + testWorker := boruta.WorkerUUID("Test Worker UUID") + // Add Request. Use trigger to wait for ValidMatcher goroutine to match worker. + s.wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).DoAndReturn(func(boruta.Groups, boruta.Capabilities) (boruta.WorkerUUID, error) { + setTrigger(1) + return noWorker, s.testErr }) + reqid, err := s.rqueue.NewRequest(testCapabilities, testPriority, testUser, now, tomorrow) + s.NoError(err) + s.NotZero(reqid) + s.Eventually(eventuallyTrigger(1), time.Second, 10*time.Millisecond) - Describe("OnWorkerIdle", func() { - It("ValidMatcher should do nothing if there are no waiting requests", func() { - R.OnWorkerIdle(testWorker) - }) - It("ValidMatcher should try matching request", func() { - // Add Request. Use trigger to wait for ValidMatcher goroutine to match worker. - wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).Return(noWorker, testErr).Do(func(boruta.Groups, boruta.Capabilities) { - setTrigger(1) - }) - reqid, err := R.NewRequest(testCapabilities, testPriority, testUser, now, tomorrow) - Expect(err).NotTo(HaveOccurred()) - Expect(reqid).NotTo(BeZero()) - eventuallyTrigger(1) + // Test. Use trigger to wait for ValidMatcher goroutine to match worker. + s.wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).DoAndReturn(func(boruta.Groups, boruta.Capabilities) (boruta.WorkerUUID, error) { + setTrigger(2) + return noWorker, s.testErr + }) + s.rqueue.OnWorkerIdle(testWorker) + s.Eventually(eventuallyTrigger(2), time.Second, 10*time.Millisecond) - // Test. Use trigger to wait for ValidMatcher goroutine to match worker. - wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).Return(noWorker, testErr).Do(func(boruta.Groups, boruta.Capabilities) { - setTrigger(2) - }) - R.OnWorkerIdle(testWorker) - eventuallyTrigger(2) - }) + // Updating request should also try to match worker. + s.wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).DoAndReturn(func(boruta.Groups, boruta.Capabilities) (boruta.WorkerUUID, error) { + setTrigger(3) + return noWorker, s.testErr }) - Describe("OnWorkerFail", func() { - It("should return if jobs.Get fails", func() { - jm.EXPECT().Get(testWorker).Return(nil, testErr) - R.OnWorkerFail(testWorker) - }) - It("should panic if failing worker was processing unknown Job", func() { - noReq := boruta.ReqID(0) - job := workers.Job{Req: noReq} - jm.EXPECT().Get(testWorker).Return(&job, nil) - Expect(func() { - R.OnWorkerFail(testWorker) - }).To(Panic()) - }) - It("should set request to FAILED state if call succeeds", func() { - // Add Request. Use trigger to wait for ValidMatcher goroutine to match worker. - wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).Return(noWorker, testErr).Do(func(boruta.Groups, boruta.Capabilities) { - setTrigger(3) - }) - reqid, err := R.NewRequest(testCapabilities, testPriority, testUser, now, tomorrow) - Expect(err).NotTo(HaveOccurred()) - Expect(reqid).NotTo(BeZero()) - eventuallyTrigger(3) + rinfo, err := s.rqueue.GetRequestInfo(reqid) + s.NoError(err) + s.NotEmpty(rinfo) + rinfo.Priority = rinfo.Priority + 1 + err = s.rqueue.UpdateRequest(&rinfo) + s.NoError(err) + s.Eventually(eventuallyTrigger(3), time.Second, 10*time.Millisecond) +} + +func (s *RequestsTestSuite) TestOnWorkerFailed() { + testWorker := boruta.WorkerUUID("Test Worker UUID") + // When jobs.Get() fails OnWorkerFail should just return (without panic nor calling jobs.Finish()). + s.jm.EXPECT().Get(testWorker).Return(nil, s.testErr) + s.NotPanics(func() { s.rqueue.OnWorkerFail(testWorker) }) + + // OnWorkerFail should panick when jobs.Get() returns unknown request ID. + noReq := boruta.ReqID(0) + job := workers.Job{Req: noReq} + s.jm.EXPECT().Get(testWorker).Return(&job, nil) + s.Panics(func() { s.rqueue.OnWorkerFail(testWorker) }) - // Test. - job := workers.Job{Req: reqid} - jm.EXPECT().Get(testWorker).Return(&job, nil) - jm.EXPECT().Finish(testWorker, false) - R.OnWorkerFail(testWorker) - eventuallyState(reqid, boruta.FAILED) - }) + // When call succeeds request should be in failed state. + s.wm.EXPECT().TakeBestMatchingWorker(testUser.Groups, testCapabilities).DoAndReturn(func(boruta.Groups, boruta.Capabilities) (boruta.WorkerUUID, error) { + setTrigger(4) + return noWorker, s.testErr }) -}) + reqid, err := s.rqueue.NewRequest(testCapabilities, testPriority, testUser, now, tomorrow) + s.NoError(err) + s.NotZero(reqid) + // Wait until match is done. + s.Eventually(eventuallyTrigger(4), time.Second, 10*time.Millisecond) + job.Req = reqid + s.jm.EXPECT().Get(testWorker).Return(&job, nil) + s.jm.EXPECT().Finish(testWorker, false) + s.NotPanics(func() { s.rqueue.OnWorkerFail(testWorker) }) + s.Eventually(eventuallyState(s, reqid, boruta.FAILED), time.Second, 10*time.Millisecond) +} diff --git a/requests/sorter_test.go b/requests/sorter_test.go index d4ce1eb..b584b76 100644 --- a/requests/sorter_test.go +++ b/requests/sorter_test.go @@ -17,12 +17,20 @@ package requests import ( + "strings" "testing" "github.com/SamsungSLAV/boruta" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -var validReqs = []boruta.ReqInfo{ +type SorterTestSuite struct { + suite.Suite + sorter *sorter +} + +var validReqs = [...]boruta.ReqInfo{ { ID: 1, Priority: 4, @@ -53,138 +61,120 @@ var validReqs = []boruta.ReqInfo{ }, } -func TestNewSorter(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) - - sorter, err := newSorter(nil) - assert.Nil(err) - assert.Equal("", sorter.item) - assert.Equal(boruta.SortOrderAsc, sorter.order) - - si := new(boruta.SortInfo) - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("", sorter.item) - assert.Equal(boruta.SortOrderAsc, sorter.order) - - si.Item = "foobar" - si.Order = boruta.SortOrderAsc - sorter, err = newSorter(si) - assert.Equal(boruta.ErrWrongSortItem, err) - assert.Nil(sorter) - - si.Item = "" - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("", sorter.item) - assert.Equal(boruta.SortOrderAsc, sorter.order) - - si.Item = "id" - si.Order = boruta.SortOrderDesc - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("id", sorter.item) - assert.Equal(boruta.SortOrderDesc, sorter.order) - - si.Item = "priority" - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("priority", sorter.item) - assert.Equal(boruta.SortOrderDesc, sorter.order) - - // newSorter should be case insensitive. - si.Item = "DeadLine" - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("deadline", sorter.item) - assert.Equal(boruta.SortOrderDesc, sorter.order) - - si.Item = "validafter" - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("validafter", sorter.item) - assert.Equal(boruta.SortOrderDesc, sorter.order) - - si.Item = "STATE" - sorter, err = newSorter(si) - assert.Nil(err) - assert.Equal("state", sorter.item) - assert.Equal(boruta.SortOrderDesc, sorter.order) +func (s *SorterTestSuite) SetupTest() { + s.sorter = new(sorter) + s.sorter.reqs = make([]boruta.ReqInfo, len(validReqs)) + copy(s.sorter.reqs, validReqs[:]) } -func TestSorterLen(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) - s := new(sorter) - s.reqs = make([]boruta.ReqInfo, len(validReqs)) - copy(s.reqs, validReqs) - assert.Equal(len(validReqs), s.Len()) +func TestNewSorter(t *testing.T) { + assert := assert.New(t) + + testCases := [...]struct { + name string + si *boruta.SortInfo + order boruta.SortOrder + err error + }{ + {name: "NilSortInfo", si: nil, order: boruta.SortOrderAsc, err: nil}, + {name: "EmptySortInfo", si: new(boruta.SortInfo), order: boruta.SortOrderAsc, err: nil}, + {name: "EmptySortItem", si: &boruta.SortInfo{Item: "", Order: boruta.SortOrderAsc}, order: boruta.SortOrderAsc, err: nil}, + {name: "IdSortItem", si: &boruta.SortInfo{Item: "id", Order: boruta.SortOrderDesc}, order: boruta.SortOrderDesc, err: nil}, + { + name: "PrioritySortItem", + si: &boruta.SortInfo{Item: "priority", Order: boruta.SortOrderDesc}, + order: boruta.SortOrderDesc, + err: nil, + }, + { + name: "CaseInsencitiveSortItem", + si: &boruta.SortInfo{Item: "DeadLine", Order: boruta.SortOrderDesc}, + order: boruta.SortOrderDesc, + err: nil, + }, + { + name: "ValidAfterSortItem", + si: &boruta.SortInfo{Item: "ValidAfter", Order: boruta.SortOrderAsc}, + order: boruta.SortOrderAsc, + err: nil, + }, + { + name: "StateSortItem", + si: &boruta.SortInfo{Item: "STATE", Order: boruta.SortOrderAsc}, + order: boruta.SortOrderAsc, + err: nil, + }, + { + name: "WrongSortItem", + si: &boruta.SortInfo{Item: "foobar", Order: boruta.SortOrderAsc}, + order: boruta.SortOrderAsc, + err: boruta.ErrWrongSortItem, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + sorter, err := newSorter(test.si) + assert.Equalf(test.err, err, "test case: %s", test.name) + if err != nil { + assert.Nilf(sorter, "test case: %s", test.name) + } else { + assert.Equalf(test.order, sorter.order, "test case: %s", test.name) + if test.si != nil { + assert.Equalf(strings.ToLower(test.si.Item), sorter.item, "test case: %s", test.name) + } + } + }) + } } -func TestSorterSwap(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) - s := new(sorter) - s.reqs = make([]boruta.ReqInfo, len(validReqs)) - copy(s.reqs, validReqs) - s.Swap(0, 1) - assert.Equal(s.reqs[0], validReqs[1]) - assert.Equal(s.reqs[1], validReqs[0]) +func (s *SorterTestSuite) TestSorterLen() { + s.Equal(len(validReqs), s.sorter.Len()) } -func TestSorterLess(t *testing.T) { - assert, rqueue, ctrl, _ := initTest(t) - defer finiTest(rqueue, ctrl) - s := new(sorter) - s.reqs = make([]boruta.ReqInfo, len(validReqs)) - copy(s.reqs, validReqs) - s.item = "panic" - assert.Panics(func() { s.Less(0, 1) }) - - s.item = "" - assert.True(s.Less(0, 1)) - s.order = boruta.SortOrderDesc - assert.False(s.Less(0, 1)) - - s.item = "id" - assert.False(s.Less(0, 1)) - s.order = boruta.SortOrderAsc - assert.True(s.Less(0, 1)) - - s.item = "priority" - assert.True(s.Less(1, 2)) - // equal priorities, sort by ID - assert.True(s.Less(0, 2)) - s.order = boruta.SortOrderDesc - assert.False(s.Less(1, 2)) - // equal priorities, sort by ID - assert.False(s.Less(0, 2)) - - s.item = "deadline" - assert.False(s.Less(1, 2)) - // equal deadlines, sort by ID - assert.False(s.Less(0, 1)) - s.order = boruta.SortOrderAsc - assert.True(s.Less(1, 2)) - // equal deadlines, sort by ID - assert.True(s.Less(0, 1)) +func (s *SorterTestSuite) TestSorterSwap() { + s.sorter.Swap(0, 1) + s.Equal(s.sorter.reqs[0], validReqs[1]) + s.Equal(s.sorter.reqs[1], validReqs[0]) +} - s.item = "validafter" - assert.True(s.Less(0, 2)) - // equal validafters, sort by ID - assert.True(s.Less(0, 3)) - s.order = boruta.SortOrderDesc - assert.False(s.Less(0, 2)) - // equal validafters, sort by ID - assert.False(s.Less(0, 3)) +func (s *SorterTestSuite) TestSorterLess() { + testCases := [...]struct { + name, item string + i, j int + }{ + {name: "EmptyItem", item: "", i: 0, j: 1}, + {name: "ID", item: "id", i: 0, j: 1}, + {name: "Priority", item: "priority", i: 1, j: 2}, + {name: "PriorityEqual", item: "priority", i: 0, j: 2}, // sort by IO + {name: "Deadline", item: "deadline", i: 1, j: 2}, + {name: "DeadlineEqual", item: "deadline", i: 0, j: 1}, // sort by IO + {name: "ValidAfter", item: "validafter", i: 0, j: 2}, + {name: "ValidAfterEqual", item: "validafter", i: 0, j: 3}, // sort by IO + {name: "State", item: "state", i: 1, j: 0}, + {name: "StateEqual", item: "state", i: 0, j: 3}, // sort by IO + } + + s.Run("panic", func() { + s.sorter.item = "panic" + s.Panics(func() { s.sorter.Less(0, 1) }) + }) + + for _, test := range testCases { + test := test + sorter := s.sorter + sorter.item = test.item + s.Run(test.name, func() { + sorter.order = boruta.SortOrderAsc + s.Truef(sorter.Less(test.i, test.j), "test case: %s", test.name) + sorter.order = boruta.SortOrderDesc + s.Falsef(sorter.Less(test.i, test.j), "test case: %s", test.name) + }) + } +} - s.item = "state" - assert.True(s.Less(0, 1)) - // equal states, sort by ID - assert.False(s.Less(0, 3)) - s.order = boruta.SortOrderAsc - assert.False(s.Less(0, 1)) - // equal states, sort by ID - assert.True(s.Less(0, 3)) +func TestSorterTestSuite(t *testing.T) { + suite.Run(t, new(SorterTestSuite)) } diff --git a/requests/times_test.go b/requests/times_test.go index 1a53a85..09b61aa 100644 --- a/requests/times_test.go +++ b/requests/times_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,69 @@ package requests import ( + "os" "runtime" "runtime/debug" + "strconv" + "testing" "time" "github.com/SamsungSLAV/boruta" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" ) +const loopRoutineName = "github.com/SamsungSLAV/boruta/requests.(*requestTimes).loop" + +func countGoRoutine(name string) int { + runtime.GC() + + counter := 0 + p := make([]runtime.StackRecord, 1) + n, _ := runtime.GoroutineProfile(p) + p = make([]runtime.StackRecord, 2*n) + runtime.GoroutineProfile(p) + for _, s := range p { + for _, f := range s.Stack() { + if f != 0 { + if runtime.FuncForPC(f).Name() == name { + counter++ + } + } + } + } + return counter +} + +func getLen(t *requestTimes) int { + t.mutex.Lock() + defer t.mutex.Unlock() + return t.times.Len() +} + +func getMin(t *requestTimes) requestTime { + t.mutex.Lock() + defer t.mutex.Unlock() + return t.times.Min() +} + +func prepareRequestTime(after time.Duration, req boruta.ReqID) requestTime { + d := time.Duration(after) + n := time.Now().Add(d) + return requestTime{time: n, req: req} +} + +func chanIsOpen(ch chan bool) bool { + if ch == nil { + return false + } + select { + case _, open := <-ch: + return open + default: + return true + } +} + type TestMatcher struct { Counter int Notified []boruta.ReqID @@ -39,205 +93,205 @@ func (m *TestMatcher) Notify(reqs []boruta.ReqID) { m.Notified = append(m.Notified, reqs...) } -var _ = Describe("Times", func() { - loopRoutineName := "github.com/SamsungSLAV/boruta/requests.(*requestTimes).loop" +type TimesTestSuite struct { + suite.Suite + t *requestTimes + baseCount int +} + +func (s *TimesTestSuite) SetupSuite() { debug.SetGCPercent(1) - var t *requestTimes - var baseCount int - - countGoRoutine := func(name string) int { - runtime.GC() - - counter := 0 - p := make([]runtime.StackRecord, 1) - n, _ := runtime.GoroutineProfile(p) - p = make([]runtime.StackRecord, 2*n) - runtime.GoroutineProfile(p) - for _, s := range p { - for _, f := range s.Stack() { - if f != 0 { - if runtime.FuncForPC(f).Name() == name { - counter++ - } - } - } - } - return counter +} + +func (s *TimesTestSuite) SetupTest() { + s.baseCount = countGoRoutine(loopRoutineName) + s.t = newRequestTimes() +} + +func (s *TimesTestSuite) TearDownTest() { + if s.t != nil { + s.t.finish() } - getLen := func() int { - t.mutex.Lock() - defer t.mutex.Unlock() - return t.times.Len() + s.Equal(s.baseCount, countGoRoutine(loopRoutineName)) +} + +func (s *TimesTestSuite) TearDownSuite() { + p := 100 + env, found := os.LookupEnv("GOGC") + if found { + tmp, err := strconv.Atoi(env) + if err == nil { + p = tmp + } } - getMin := func() requestTime { - t.mutex.Lock() - defer t.mutex.Unlock() - return t.times.Min() + debug.SetGCPercent(p) +} + +func (s *TimesTestSuite) TestNewRequestTimes() { + // newRequestTimes shoul return valid, zeroed instance + assertProperlyInitialized := func(t *requestTimes) { + s.T().Helper() + s.NotNil(t) + s.NotNil(t.times) + s.Zero(getLen(t)) + s.NotNil(t.timer) + s.Nil(t.matcher) + s.NotNil(t.mutex) + s.True(chanIsOpen(t.stop)) } - prepareRequestTime := func(after time.Duration, req boruta.ReqID) requestTime { - d := time.Duration(after) - n := time.Now().Add(d) - return requestTime{time: n, req: req} + + assertProperlyInitialized(s.t) + s.Equal(s.baseCount+1, countGoRoutine(loopRoutineName)) + + t := newRequestTimes() + defer t.finish() + assertProperlyInitialized(t) + s.NotSame(s.t, t) + s.NotSame(s.t.times, t.times) + s.NotSame(s.t.timer, t.timer) + s.NotSame(s.t.mutex, t.mutex) + s.NotSame(s.t.stop, t.stop) + s.Equal(s.baseCount+2, countGoRoutine(loopRoutineName)) +} + +func (s *TimesTestSuite) TestFinish() { + finishAndCheck := func() { + s.T().Helper() + s.t.finish() + s.False(chanIsOpen(s.t.stop)) + s.Equal(s.baseCount, countGoRoutine(loopRoutineName)) } - BeforeEach(func() { - baseCount = countGoRoutine(loopRoutineName) - t = newRequestTimes() - }) - AfterEach(func() { - if t != nil { - t.finish() - } - Expect(countGoRoutine(loopRoutineName)).To(Equal(baseCount)) - }) - Describe("newRequestTimes", func() { - It("should init all fields", func() { - Expect(t).NotTo(BeNil(), "t") - Expect(t.times).NotTo(BeNil()) - Expect(t.times.Len()).To(BeZero()) - Expect(t.timer).NotTo(BeNil()) - Expect(t.matcher).To(BeNil()) - Expect(t.mutex).NotTo(BeNil()) - Expect(t.stop).NotTo(BeClosed()) - Expect(countGoRoutine(loopRoutineName)).To(Equal(baseCount + 1)) - }) - It("should create separate object in every call", func() { - t2 := newRequestTimes() - defer t2.finish() - - Expect(t).NotTo(BeIdenticalTo(t2)) - Expect(t.times).NotTo(BeIdenticalTo(t2.times)) - Expect(t.timer).NotTo(BeIdenticalTo(t2.timer)) - Expect(t.mutex).NotTo(BeIdenticalTo(t2.mutex)) - Expect(t.stop).NotTo(BeIdenticalTo(t2.stop)) - Expect(countGoRoutine(loopRoutineName)).To(Equal(baseCount + 2)) - }) - }) - Describe("finish", func() { - It("should work with unused empty structure", func() { - t.finish() - - Expect(t.stop).To(BeClosed()) - Expect(countGoRoutine(loopRoutineName)).To(Equal(baseCount)) - // Avoid extra finish in AfterEach. - t = nil - }) - It("should work when times heap is not empty", func() { - t.insert(prepareRequestTime(time.Minute, 1)) - t.insert(prepareRequestTime(time.Hour, 2)) - t.insert(prepareRequestTime(0, 3)) - - t.finish() - - Expect(t.stop).To(BeClosed()) - Expect(countGoRoutine(loopRoutineName)).To(Equal(baseCount)) - // Avoid extra finish in AfterEach. - t = nil - }) + s.Run("Empty", func() { + s.Zero(getLen(s.t)) + finishAndCheck() }) - Describe("insert", func() { - It("should insert single time", func() { - r100m := prepareRequestTime(100*time.Millisecond, 100) - - t.insert(r100m) - Expect(getLen()).To(Equal(1)) - Expect(getMin()).To(Equal(r100m)) - - Eventually(getLen).Should(BeZero()) - }) - It("should insert multiple times", func() { - r100m := prepareRequestTime(100*time.Millisecond, 100) - r200m := prepareRequestTime(200*time.Millisecond, 200) - r500m := prepareRequestTime(500*time.Millisecond, 500) - r800m := prepareRequestTime(800*time.Millisecond, 800) - - t.insert(r100m) - t.insert(r200m) - t.insert(r100m) - t.insert(r800m) - Expect(getLen()).To(Equal(4)) - Expect(getMin()).To(Equal(r100m)) - - // Expect process() to remove 2 elements after 100 ms [100 ms]. - Eventually(getLen).Should(Equal(2)) - Expect(getMin()).To(Equal(r200m)) - - // Expect process() to remove 1 element after another 100 ms [200 ms]. - Eventually(getLen).Should(Equal(1)) - Expect(getMin()).To(Equal(r800m)) - - t.insert(r500m) - Expect(getLen()).To(Equal(2)) - Expect(getMin()).To(Equal(r500m)) - - // Expect process() to remove 1 element after another 300 ms [500 ms]. - Eventually(getLen).Should(Equal(1)) - Expect(getMin()).To(Equal(r800m)) - - // Expect process() to remove 1 element after another 300 ms [800 ms]. - Eventually(getLen).Should(BeZero()) - }) - }) - Describe("setMatcher", func() { - It("should set matcher", func() { - var m TestMatcher - - Expect(t.matcher).To(BeNil()) - t.setMatcher(&m) - Expect(t.matcher).To(Equal(&m)) - Expect(m.Counter).To(BeZero()) - }) - It("should notify matcher", func() { - var m TestMatcher - t.setMatcher(&m) - - rid := boruta.ReqID(100) - t.insert(prepareRequestTime(100*time.Millisecond, rid)) - - Expect(m.Counter).To(BeZero()) - Expect(m.Notified).To(BeNil()) - - // Expect process() to remove 1 element after 100 ms [100 ms]. - Eventually(getLen).Should(BeZero()) - Expect(m.Counter).To(Equal(1)) - Expect(len(m.Notified)).To(Equal(1)) - Expect(m.Notified).To(ContainElement(rid)) - - }) + + s.Run("NotEmpty", func() { + s.SetupTest() + s.t.insert(prepareRequestTime(time.Minute, 1)) + s.t.insert(prepareRequestTime(time.Hour, 2)) + s.t.insert(prepareRequestTime(0, 3)) + s.NotZero(getLen(s.t)) + finishAndCheck() + s.t = nil // Don't call finish in test teardown. }) - Describe("process", func() { - It("should be run once for same times", func() { - var m TestMatcher - r100m := prepareRequestTime(100*time.Millisecond, 0) - reqs := []boruta.ReqID{101, 102, 103, 104, 105} - - t.setMatcher(&m) - for _, r := range reqs { - r100m.req = r - t.insert(r100m) - } - Expect(m.Counter).To(BeZero()) - - // Expect process() to remove all elements after 100 ms [100 ms]. - Eventually(getLen).Should(BeZero()) - Expect(m.Counter).To(Equal(1)) - Expect(len(m.Notified)).To(Equal(len(reqs))) - Expect(m.Notified).To(ConsistOf(reqs)) - }) +} + +func (s *TimesTestSuite) TestInsert() { + r100m := prepareRequestTime(100*time.Millisecond, 100) + // Check one element. + s.Run("OneElement", func() { + s.t.insert(r100m) + s.Equal(1, getLen(s.t)) + s.Equal(r100m, getMin(s.t)) + s.Eventually(func() bool { return getLen(s.t) == 0 }, 120*time.Millisecond, 10*time.Millisecond) }) - Describe("past time", func() { - It("should handle times in the past properly", func() { - var m TestMatcher - t.setMatcher(&m) - - rid := boruta.ReqID(200) - t.insert(prepareRequestTime(-time.Hour, rid)) - - // Expect process() to remove element. - Eventually(getLen).Should(BeZero()) - Expect(m.Counter).To(Equal(1)) - Expect(len(m.Notified)).To(Equal(1)) - Expect(m.Notified).To(ContainElement(rid)) - }) + + s.Run("ManyElements", func() { + r200m := prepareRequestTime(200*time.Millisecond, 200) + r500m := prepareRequestTime(500*time.Millisecond, 500) + r800m := prepareRequestTime(800*time.Millisecond, 800) + s.t.insert(r200m) + s.t.insert(r100m) + s.t.insert(r800m) + s.t.insert(r100m) + + length := getLen(s.t) + s.Equal(r100m, getMin(s.t)) + s.Equal(4, length) + + // Expect process() to remove 2 elements after 100 ms [100 ms]. + length -= 2 + s.Eventually(func() bool { return getLen(s.t) == length }, 110*time.Millisecond, 10*time.Millisecond) + s.Equal(r200m, getMin(s.t)) + + // Expect process() to remove 1 element after another 100 ms [200 ms]. + length -= 1 + s.Eventually(func() bool { return getLen(s.t) == length }, 210*time.Millisecond, 10*time.Millisecond) + s.Equal(r800m, getMin(s.t)) + + // Add another element. + s.t.insert(r500m) + length++ + s.Eventually(func() bool { return getLen(s.t) == length }, 100*time.Millisecond, 10*time.Millisecond) + s.Equal(r500m, getMin(s.t)) + + // Expect process() to remove 1 element after another 300 ms [500 ms]. + length-- + s.Eventually(func() bool { return getLen(s.t) == length }, 310*time.Millisecond, 10*time.Millisecond) + s.Equal(r800m, getMin(s.t)) + + // Expect process() to remove 1 element after another 300 ms [800 ms]. + s.Eventually(func() bool { return getLen(s.t) == 0 }, 310*time.Millisecond, 10*time.Millisecond) }) -}) +} + +func (s *TimesTestSuite) TestSetMatcher() { + var m TestMatcher + + s.Nil(s.t.matcher) + s.t.setMatcher(&m) + s.t.mutex.Lock() + s.Same(&m, s.t.matcher) + s.Zero(m.Counter) + s.t.mutex.Unlock() + + rid := boruta.ReqID(100) + s.t.insert(prepareRequestTime(100*time.Millisecond, rid)) + s.t.mutex.Lock() + s.Zero(m.Counter) + s.Nil(m.Notified) + s.t.mutex.Unlock() + + // process() should remove element after 100ms. + s.Eventually(func() bool { return getLen(s.t) == 0 }, 120*time.Millisecond, 10*time.Millisecond) + s.t.mutex.Lock() + s.Equal(1, m.Counter) + s.Equal(1, len(m.Notified)) + s.Contains(m.Notified, rid) + s.t.mutex.Unlock() +} + +func (s *TimesTestSuite) TestProcess() { + var m TestMatcher + reqs := [...]boruta.ReqID{101, 102, 103, 104, 105} + r100m := prepareRequestTime(100*time.Millisecond, 0) + + s.t.setMatcher(&m) + for _, r := range reqs { + r100m.req = r + s.t.insert(r100m) + } + s.t.mutex.Lock() + s.Zero(m.Counter) + s.t.mutex.Unlock() + + // Expect process() to remove all elements after 100 ms [100 ms]. + s.Eventually(func() bool { return getLen(s.t) == 0 }, 120*time.Millisecond, 10*time.Millisecond) + s.t.mutex.Lock() + s.Equal(1, m.Counter) + s.Equal(len(reqs), len(m.Notified)) + s.ElementsMatch(reqs, m.Notified) + s.t.mutex.Unlock() +} + +func (s *TimesTestSuite) TestPastTime() { + var m TestMatcher + s.t.setMatcher(&m) + + rid := boruta.ReqID(200) + s.t.insert(prepareRequestTime(-time.Hour, rid)) + + // Expect process() to remove element. + s.Eventually(func() bool { return getLen(s.t) == 0 }, 100*time.Millisecond, 10*time.Millisecond) + s.t.mutex.Lock() + s.Equal(1, m.Counter) + s.Equal(1, len(m.Notified)) + s.Contains(m.Notified, rid) + s.t.mutex.Unlock() +} + +func TestTimesTestSuite(t *testing.T) { + suite.Run(t, new(TimesTestSuite)) +} diff --git a/requests/timesheap_test.go b/requests/timesheap_test.go index 8a114c0..bbce1ba 100644 --- a/requests/timesheap_test.go +++ b/requests/timesheap_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved + * Copyright (c) 2017-2022 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,139 +17,115 @@ package requests import ( + "testing" "time" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" ) -var _ = Describe("TimesHeap", func() { - var h *timesHeap - var t []time.Time - var r []requestTime - BeforeEach(func() { - now := time.Now() - t = make([]time.Time, 0) - for i := 0; i < 4; i++ { - t = append(t, now.AddDate(0, 0, i)) - - } - - r = []requestTime{ - {time: t[0], req: 1}, - {time: t[1], req: 2}, - {time: t[2], req: 3}, - {time: t[3], req: 4}, - {time: t[3], req: 5}, - {time: t[1], req: 6}, - } - - h = newTimesHeap() - }) - - Describe("newTimesHeap", func() { - It("should create an empty heap", func() { - Expect(h).NotTo(BeNil()) - Expect(h.Len()).To(BeZero()) - }) - It("should create a new heap every time called", func() { - h2 := newTimesHeap() - - h.Push(r[1]) - h2.Push(r[2]) - - Expect(h.Len()).To(Equal(1)) - Expect(h2.Len()).To(Equal(1)) - Expect(h.Min()).To(Equal(r[1])) - Expect(h2.Min()).To(Equal(r[2])) - }) - }) - - Describe("Len", func() { - It("should return valid heap size", func() { - for i, e := range r { - h.Push(e) - Expect(h.Len()).To(Equal(i+1), "i=%v", i) - } - - for i := len(r) - 1; i >= 0; i-- { - h.Pop() - Expect(h.Len()).To(Equal(i), "i=%v", i) - } - }) - }) - - Describe("Min", func() { - It("should return minimum value of the heap", func() { - toPush := []int{3, 5, 1, 2} - pushMin := []int{3, 5, 1, 1} - for i, e := range toPush { - h.Push(r[e]) - Expect(h.Min()).To(Equal(r[pushMin[i]])) - } - - popMin := []int{5, 2, 3} - for _, e := range popMin { - h.Pop() - Expect(h.Min()).To(Equal(r[e])) - } - }) - It("should panic in case of empty heap", func() { - Expect(func() { - h.Min() - }).Should(Panic()) - }) - }) - - Describe("Pop", func() { - It("should pop minimal element", func() { - toPush := []int{5, 3, 1, 0} - for _, e := range toPush { - h.Push(r[e]) - } - - pushMin := []int{0, 1, 5, 3} - for _, e := range pushMin { - Expect(h.Min()).To(Equal(r[e])) - Expect(h.Pop()).To(Equal(r[e])) - } - }) - It("should decrease heap size by one", func() { - toPush := []int{0, 2, 1, 5} - for _, e := range toPush { - h.Push(r[e]) - } - - popMin := []int{0, 1, 5, 2} - for i, e := range popMin { - Expect(h.Len()).To(Equal(len(popMin) - i)) - Expect(h.Pop()).To(Equal(r[e])) - } - Expect(h.Len()).To(BeZero()) - }) - It("should panic in case of empty heap", func() { - Expect(func() { - h.Pop() - }).Should(Panic()) - }) - }) - - Describe("Push", func() { - It("should add elements to the heap keeping minimum property", func() { - toPush := []int{4, 5, 1, 3} - pushMin := []int{4, 5, 1, 1} - for i, e := range toPush { - h.Push(r[e]) - Expect(h.Min()).To(Equal(r[pushMin[i]])) - } - }) - It("should increase heap size by one", func() { - Expect(h.Len()).To(BeZero()) - toPush := []int{1, 0, 2, 4} - for i, e := range toPush { - h.Push(r[e]) - Expect(h.Len()).To(Equal(i + 1)) - } - }) - }) -}) +type TimesHeapTestSuite struct { + suite.Suite + h *timesHeap + t []time.Time + r []requestTime +} + +func (s *TimesHeapTestSuite) SetupTest() { + now := time.Now() + s.t = make([]time.Time, 0, 4) + for i := 0; i < 4; i++ { + s.t = append(s.t, now.AddDate(0, 0, i)) + } + + s.r = []requestTime{ + {time: s.t[0], req: 1}, + {time: s.t[1], req: 2}, + {time: s.t[2], req: 3}, + {time: s.t[3], req: 4}, + {time: s.t[3], req: 5}, + {time: s.t[1], req: 6}, + } + + s.h = newTimesHeap() +} + +func (s *TimesHeapTestSuite) TestNewTimesHeap() { + // Created heap should be empty. + s.NotNil(s.h) + s.Zero(s.h.Len()) + // It should create a new heap every time called. + h := newTimesHeap() + s.Equal(s.h, h) + s.NotSame(s.h, h) +} + +func (s *TimesHeapTestSuite) TestLen() { + for i, e := range s.r { + s.h.Push(e) + s.Equal(i+1, s.h.Len()) + } + + for i := len(s.r) - 1; i >= 0; i-- { + s.h.Pop() + s.Equal(i, s.h.Len()) + } +} + +func (s *TimesHeapTestSuite) TestMin() { + // Empty heap causes panic. + s.Panics(func() { s.h.Min() }) + + // Return minimum value of the heap. + toPush := [...]int{3, 5, 1, 2} + pushMin := [...]int{3, 5, 1, 1} + s.Zero(s.h.Len()) + for i, e := range toPush { + s.h.Push(s.r[e]) + s.Equal(s.r[pushMin[i]], s.h.Min()) + } + + // Min doesn't remove elements from heap, contraty to Pop. + s.NotZero(s.h.Len()) + + popMin := [...]int{5, 2, 3} + for _, e := range popMin { + s.h.Pop() + s.Equal(s.r[e], s.h.Min()) + } +} + +func (s *TimesHeapTestSuite) TestPop() { + // Empty heap causes panic. + s.Panics(func() { s.h.Pop() }) + + // Pops minial element and decreases heap size by one. + toPush := [...]int{5, 3, 1, 0} + for _, e := range toPush { + s.h.Push(s.r[e]) + } + pushMin := [...]int{0, 1, 5, 3} + for i, e := range pushMin { + s.Equal(s.r[e], s.h.Min()) + // Min shouldn't remove element. + s.Equal(len(pushMin)-i, s.h.Len()) + s.Equal(s.r[e], s.h.Pop()) + // Pop should remove element. + s.Equal(len(pushMin)-i-1, s.h.Len()) + } + s.Zero(s.h.Len()) +} + +func (s *TimesHeapTestSuite) TestPush() { + // Push adds element to heap. Size of heap is increased and minimum property is kept. + toPush := [...]int{4, 5, 1, 3} + pushMin := [...]int{4, 5, 1, 1} + for i, e := range toPush { + s.h.Push(s.r[e]) + s.Equal(s.r[pushMin[i]], s.h.Min()) + s.Equal(i+1, s.h.Len()) + } +} + +func TestTimesHeapTestSuite(t *testing.T) { + suite.Run(t, new(TimesHeapTestSuite)) +} diff --git a/requests/timesheapcontainer_test.go b/requests/timesheapcontainer_test.go index 64d2d11..71805d7 100644 --- a/requests/timesheapcontainer_test.go +++ b/requests/timesheapcontainer_test.go @@ -17,137 +17,130 @@ package requests import ( + "testing" "time" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" ) -var _ = Describe("TimesHeapContainer", func() { - var thc timesHeapContainer - var t0, t1, t2, t3 time.Time - var thcLen int - BeforeEach(func() { - t0 = time.Now() - t1 = t0.AddDate(0, 0, 1) - t2 = t1.AddDate(0, 0, 1) - t3 = t2.AddDate(0, 0, 1) - - thc = []requestTime{ - {time: t0, req: 1}, - {time: t3, req: 2}, - {time: t1, req: 3}, - {time: t2, req: 4}, - {time: t1, req: 1}, - {time: t3, req: 2}, - } - thcLen = len(thc) +type TimesHeapContainerTestSuite struct { + suite.Suite + thc timesHeapContainer + t0, t1, t2, t3 time.Time + thcLen int +} + +func (s *TimesHeapContainerTestSuite) SetupTest() { + s.t0 = time.Now() + s.t1 = s.t0.AddDate(0, 0, 1) + s.t2 = s.t1.AddDate(0, 0, 1) + s.t3 = s.t2.AddDate(0, 0, 1) + + s.thc = []requestTime{ + {time: s.t0, req: 1}, + {time: s.t3, req: 2}, + {time: s.t1, req: 3}, + {time: s.t2, req: 4}, + {time: s.t1, req: 1}, + {time: s.t3, req: 2}, + } + s.thcLen = len(s.thc) +} + +func (s *TimesHeapContainerTestSuite) TestLen() { + var c timesHeapContainer + s.Zero(c.Len()) + + e := make([]requestTime, 0) + c = e + s.Zero(c.Len()) + + s.Equal(s.thcLen, s.thc.Len()) +} + +func (s *TimesHeapContainerTestSuite) TestLess() { + s.True(s.thc.Less(0, 1)) // t0 < t3 + s.False(s.thc.Less(1, 0)) // t3 < t0 + + s.False(s.thc.Less(1, 2)) // t3 < t1 + s.True(s.thc.Less(2, 1)) // t1 < t3 + + s.False(s.thc.Less(3, 3)) // t2 < t2 + + s.False(s.thc.Less(2, 4)) // t1/3 < t1/1 + s.True(s.thc.Less(4, 2)) // t1/1 < t1/3 + + s.False(s.thc.Less(1, 5)) // t3/2 < t3/2 + s.False(s.thc.Less(5, 1)) // t3/2 < t3/2 +} + +func (s *TimesHeapContainerTestSuite) TestSwap() { + // swap different elements + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[1]) + s.Equal(requestTime{time: s.t2, req: 4}, s.thc[3]) + s.thc.Swap(1, 3) + s.Equal(requestTime{time: s.t2, req: 4}, s.thc[1]) + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[3]) + // restore original order + s.thc.Swap(1, 3) + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[1]) + s.Equal(requestTime{time: s.t2, req: 4}, s.thc[3]) + + // swap same values + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[1]) + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[5]) + s.thc.Swap(1, 5) + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[1]) + s.Equal(requestTime{time: s.t3, req: 2}, s.thc[5]) + + // swap in place + s.Equal(requestTime{time: s.t2, req: 4}, s.thc[3]) + s.thc.Swap(3, 3) + s.Equal(requestTime{time: s.t2, req: 4}, s.thc[3]) +} + +func (s *TimesHeapContainerTestSuite) TestPush() { + // Push to empty container. + var c timesHeapContainer + s.Zero(c.Len()) + c.Push(requestTime{time: s.t2, req: 4}) + s.Equal(1, c.Len()) + s.Equal(requestTime{time: s.t2, req: 4}, c[0]) + + // Push to non empty container. + s.Equal(s.thcLen, s.thc.Len()) + s.thc.Push(requestTime{time: s.t0, req: 7}) + s.Equal(s.thcLen+1, s.thc.Len()) + s.Equal(requestTime{time: s.t0, req: 7}, s.thc[s.thcLen]) + + s.thc.Push(requestTime{time: s.t2, req: 5}) + s.Equal(s.thcLen+2, s.thc.Len()) + s.Equal(requestTime{time: s.t2, req: 5}, s.thc[s.thcLen+1]) +} + +func (s *TimesHeapContainerTestSuite) TestPop() { + // Panic when container is empty. + s.Panics(func() { + var c timesHeapContainer + c.Pop() }) - Describe("Len", func() { - It("should work with nil container", func() { - var c timesHeapContainer - Expect(c.Len()).To(BeZero()) - }) - It("should work with empty container", func() { - s := make([]requestTime, 0) - var c timesHeapContainer = s - Expect(c.Len()).To(BeZero()) - }) - It("should work with non-empty container", func() { - Expect(thc.Len()).To(Equal(thcLen)) - }) - }) - - Describe("Less", func() { - It("should compare elements", func() { - Expect(thc.Less(0, 1)).To(BeTrue()) //t0 < t3 - Expect(thc.Less(1, 0)).To(BeFalse()) //t3 < t0 - - Expect(thc.Less(1, 2)).To(BeFalse()) //t3 < t1 - Expect(thc.Less(2, 1)).To(BeTrue()) //t1 < t3 - - Expect(thc.Less(3, 3)).To(BeFalse()) //t2 < t2 - - Expect(thc.Less(2, 4)).To(BeFalse()) //t1/3 < t1/1 - Expect(thc.Less(4, 2)).To(BeTrue()) //t1/1 < t1/3 - - Expect(thc.Less(1, 5)).To(BeFalse()) //t3/2 < t3/2 - Expect(thc.Less(5, 1)).To(BeFalse()) //t3/2 < t3/2 - }) - }) - - Describe("Swap", func() { - It("should swap different elements", func() { - Expect(thc[1]).To(Equal(requestTime{time: t3, req: 2})) - Expect(thc[3]).To(Equal(requestTime{time: t2, req: 4})) - thc.Swap(1, 3) - Expect(thc[1]).To(Equal(requestTime{time: t2, req: 4})) - Expect(thc[3]).To(Equal(requestTime{time: t3, req: 2})) - }) - It("should swap same values", func() { - Expect(thc[1]).To(Equal(requestTime{time: t3, req: 2})) - Expect(thc[5]).To(Equal(requestTime{time: t3, req: 2})) - thc.Swap(1, 5) - Expect(thc[1]).To(Equal(requestTime{time: t3, req: 2})) - Expect(thc[5]).To(Equal(requestTime{time: t3, req: 2})) - }) - It("should swap in place", func() { - Expect(thc[3]).To(Equal(requestTime{time: t2, req: 4})) - thc.Swap(3, 3) - Expect(thc[3]).To(Equal(requestTime{time: t2, req: 4})) - }) - }) - - Describe("Push", func() { - It("should work with empty container", func() { - var c timesHeapContainer - Expect(c.Len()).To(BeZero()) - - c.Push(requestTime{time: t2, req: 4}) + // Pop elements from container + s.Equal(s.thcLen, s.thc.Len()) - Expect(c.Len()).To(Equal(1)) - Expect(c[0]).To(Equal(requestTime{time: t2, req: 4})) - }) - It("should add elements to non empty container", func() { - Expect(thc.Len()).To(Equal(thcLen)) + t := s.thc.Pop() + s.Equal(s.thcLen-1, s.thc.Len()) + s.Equal(requestTime{time: s.t3, req: 2}, t) - thc.Push(requestTime{time: t0, req: 7}) + t = s.thc.Pop() + s.Equal(s.thcLen-2, s.thc.Len()) + s.Equal(requestTime{time: s.t1, req: 1}, t) - Expect(thc.Len()).To(Equal(thcLen + 1)) - Expect(thc[thcLen]).To(Equal(requestTime{time: t0, req: 7})) + t = s.thc.Pop() + s.Equal(s.thcLen-3, s.thc.Len()) + s.Equal(requestTime{time: s.t2, req: 4}, t) +} - thc.Push(requestTime{time: t2, req: 5}) - - Expect(thc.Len()).To(Equal(thcLen + 2)) - Expect(thc[thcLen+1]).To(Equal(requestTime{time: t2, req: 5})) - }) - }) - - Describe("Pop", func() { - It("should pop elements from container", func() { - Expect(thc.Len()).To(Equal(thcLen)) - - t := thc.Pop() - - Expect(thc.Len()).To(Equal(thcLen - 1)) - Expect(t).To(Equal(requestTime{time: t3, req: 2})) - - t = thc.Pop() - - Expect(thc.Len()).To(Equal(thcLen - 2)) - Expect(t).To(Equal(requestTime{time: t1, req: 1})) - - t = thc.Pop() - - Expect(thc.Len()).To(Equal(thcLen - 3)) - Expect(t).To(Equal(requestTime{time: t2, req: 4})) - }) - It("should panic if container is empty", func() { - Expect(func() { - var c timesHeapContainer - c.Pop() - }).Should(Panic()) - }) - }) -}) +func TestTimesHeapContainerTestSuite(t *testing.T) { + suite.Run(t, new(TimesHeapContainerTestSuite)) +}