From aeca4769a755a7d059aec92458fe58ca5b96ee3c Mon Sep 17 00:00:00 2001 From: Maciej Wereski Date: Tue, 13 Dec 2022 17:08:35 +0100 Subject: [PATCH 1/3] Requests: refactor tests Tests in requests packaged are overhauled: * Unification: Ginkgo/Gomega is dropped in favour of Testify. * Tests are mostly organized into Testify suites. * Subtests are used extensively. * Subtests are mostly run in parallel. Some reasons why Testify has been chosen: * It's more popular (widely used) than Ginkgo. * Some of Ginkgo functionality was deprecated ans will be removed in V2. * It's easier for Golang devs to start writing tests in Testify. Signed-off-by: Maciej Wereski --- requests/queue_test.go | 40 +- requests/requests_requestsmanager_test.go | 765 ++++++++++++--------- requests/requests_suite_test.go | 46 +- requests/requests_test.go | 780 +++++++++++----------- requests/requests_workerchange_test.go | 180 +++-- requests/sorter_test.go | 244 ++++--- requests/times_test.go | 444 ++++++------ requests/timesheap_test.go | 242 +++---- requests/timesheapcontainer_test.go | 241 ++++--- 9 files changed, 1578 insertions(+), 1404 deletions(-) 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)) +} From 376696bc39de69043769802d5446c26e9ff2ef1c Mon Sep 17 00:00:00 2001 From: Maciej Wereski Date: Fri, 23 Dec 2022 11:58:30 +0100 Subject: [PATCH 2/3] Filters: run test cases as subtests Signed-off-by: Maciej Wereski --- filter/filter_test.go | 105 ++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 34 deletions(-) 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") } From ab91138b9f60d6d05822e6b24c05c2d1e4b8ed15 Mon Sep 17 00:00:00 2001 From: Maciej Wereski Date: Fri, 24 Feb 2023 21:13:17 +0100 Subject: [PATCH 3/3] Dryad: refactor tests Tests were rewritten from ginkgo to testify, new testcases were added to improve coverage. For reason of dropping ginkgo please see commit aeca476. Additionaly deprecated ioutil function was dropped and documentation was updated with information about potential panic in config Unmarshal. Signed-off-by: Maciej Wereski --- dryad/conf/conf.go | 5 +- dryad/conf/conf_suite_test.go | 29 --------- dryad/conf/conf_test.go | 112 +++++++++++++++++++++++++--------- 3 files changed, 86 insertions(+), 60 deletions(-) delete mode 100644 dryad/conf/conf_suite_test.go 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) + }) + } +}