Add Boruter implementation with tests 18/162018/11
authorLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Tue, 21 Nov 2017 20:34:31 +0000 (21:34 +0100)
committerPawel Wieczorek <p.wieczorek2@samsung.com>
Wed, 18 Apr 2018 16:32:31 +0000 (18:32 +0200)
BoruterImpl implements communication with Boruta. It is used
for acquiring, monitoring and releasing Dryads for Weles' Jobs.

Change-Id: I91667cad4ccac176e4e01c697e2dbfbe7b54cfd3
Signed-off-by: Lukasz Wojciechowski <l.wojciechow@partner.samsung.com>
controller/boruterimpl.go [new file with mode: 0644]
controller/boruterimpl_test.go [new file with mode: 0644]

diff --git a/controller/boruterimpl.go b/controller/boruterimpl.go
new file mode 100644 (file)
index 0000000..25c2866
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ *  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
+ */
+
+// File controller/boruterimpl.go implements Boruter interface
+// for communication with Boruta. Communication is used for acquiring,
+// monitoring and releasing Dryads for Weles' Jobs.
+
+package controller
+
+import (
+       "fmt"
+       "sync"
+       "time"
+
+       "git.tizen.org/tools/boruta"
+       "git.tizen.org/tools/weles"
+       "git.tizen.org/tools/weles/controller/notifier"
+)
+
+// TODO ProlongAccess to Dryad in Boruta, before time expires.
+
+// jobBorutaInfo contains information about status of acquiring Dryad from
+// Boruta for running a single Job.
+type jobBorutaInfo struct {
+       // rid is the Boruta's request ID for the Job.
+       rid boruta.ReqID
+       // status is the current state of the request.
+       status boruta.ReqState
+       // timeout defines Dryad acquirement duration.
+       timeout time.Time
+}
+
+// BoruterImpl is a Handler that is responsible for managing communication
+// with Boruta, acquiring Dryads, prolonging access and releasing them.
+type BoruterImpl struct {
+       // Notifier provides channel for communication with Controller.
+       notifier.Notifier
+       // jobs references module implementing Jobs management.
+       jobs JobsController
+       // boruta is Boruta's client.
+       boruta boruta.Requests
+
+       // info contains information about status of acquiring Dryad from Boruta.
+       info map[weles.JobID]*jobBorutaInfo
+       // rid2Job maps Boruta's RequestID to Weles' JobID.
+       rid2Job map[boruta.ReqID]weles.JobID
+       // mutex protects access to info and rid2Job maps.
+       mutex *sync.Mutex
+       // borutaCheckPeriod defines how often Boruta is asked for requests' status.
+       borutaCheckPeriod time.Duration
+       // finish is channel for stopping internal goroutine.
+       finish chan int
+       // looper waits for internal goroutine running loop to finish.
+       looper sync.WaitGroup
+}
+
+// NewBoruter creates a new BoruterImpl structure setting up references
+// to used Weles and Boruta modules.
+func NewBoruter(j JobsController, b boruta.Requests, period time.Duration) Boruter {
+       ret := &BoruterImpl{
+               Notifier:          notifier.NewNotifier(),
+               jobs:              j,
+               boruta:            b,
+               info:              make(map[weles.JobID]*jobBorutaInfo),
+               rid2Job:           make(map[boruta.ReqID]weles.JobID),
+               mutex:             new(sync.Mutex),
+               borutaCheckPeriod: period,
+               finish:            make(chan int),
+       }
+       ret.looper.Add(1)
+       go ret.loop()
+       return ret
+}
+
+// Finish internal goroutine.
+func (h *BoruterImpl) Finish() {
+       h.finish <- 1
+       h.looper.Wait()
+}
+
+// add registers new Boruta's request ID for the Job to be monitored.
+func (h *BoruterImpl) add(j weles.JobID, r boruta.ReqID) {
+       h.mutex.Lock()
+       defer h.mutex.Unlock()
+
+       h.info[j] = &jobBorutaInfo{
+               rid: r,
+       }
+       h.rid2Job[r] = j
+}
+
+// remove Boruta's request ID for the Job from monitored requests.
+func (h *BoruterImpl) remove(j weles.JobID, r boruta.ReqID) {
+       h.mutex.Lock()
+       defer h.mutex.Unlock()
+
+       delete(h.rid2Job, r)
+       delete(h.info, j)
+}
+
+// pop gets and removes Boruta's request ID for the Job and a Job from monitored set.
+// It returns request ID related to the removed Job
+func (h *BoruterImpl) pop(j weles.JobID) (r boruta.ReqID, err error) {
+       h.mutex.Lock()
+       defer h.mutex.Unlock()
+
+       rinfo, ok := h.info[j]
+       if !ok {
+               return r, weles.ErrJobNotFound
+       }
+       r = rinfo.rid
+       delete(h.rid2Job, r)
+       delete(h.info, j)
+       return
+}
+
+// setProlongTime stores time until Dryad is acquired from Boruta.
+func (h *BoruterImpl) setProlongTime(j weles.JobID, rinfo boruta.ReqInfo) {
+       h.mutex.Lock()
+       defer h.mutex.Unlock()
+       h.info[j].timeout = rinfo.Job.Timeout
+}
+
+// updateStatus analyzes single Boruta's request info and verifies if it is
+// related to any of Weles' Jobs. If so, method returns new status of request
+// and ID of related Job. Otherwise zero-value status is returned.
+func (h *BoruterImpl) updateStatus(rinfo boruta.ReqInfo) (newState boruta.ReqState, j weles.JobID) {
+       h.mutex.Lock()
+       defer h.mutex.Unlock()
+
+       var ok bool
+       j, ok = h.rid2Job[rinfo.ID]
+       if !ok {
+               return
+       }
+       info := h.info[j]
+       if info.status == rinfo.State {
+               return
+       }
+       info.status = rinfo.State
+       newState = rinfo.State
+
+       return
+}
+
+// acquire gets Dryad from Boruta and sets information about it in
+// JobsController. It stores acquired Dryad's expiration time
+// and notifies Controller about getting Dryad for the Job.
+func (h *BoruterImpl) acquire(j weles.JobID, rinfo boruta.ReqInfo) {
+       ai, err := h.boruta.AcquireWorker(rinfo.ID)
+       if err != nil {
+               h.remove(j, rinfo.ID)
+               h.SendFail(j, fmt.Sprintf("Cannot acquire worker from Boruta : %s", err.Error()))
+               return
+       }
+       // TODO acquire username from Boruta.
+       err = h.jobs.SetDryad(j, weles.Dryad{Addr: ai.Addr, Key: ai.Key, Username: "boruta-user"})
+       if err != nil {
+               h.remove(j, rinfo.ID)
+               h.SendFail(j, fmt.Sprintf("Internal Weles error while setting Dryad : %s", err.Error()))
+               return
+       }
+       h.setProlongTime(j, rinfo)
+       h.SendOK(j)
+}
+
+// loop monitors Boruta's requests.
+func (h *BoruterImpl) loop() {
+       defer h.looper.Done()
+       for {
+               select {
+               case <-h.finish:
+                       return
+               case <-time.After(h.borutaCheckPeriod):
+               }
+
+               // TODO use filter with slice of ReqIDs when implemented in Boruta.
+               requests, err := h.boruta.ListRequests(nil)
+               if err != nil {
+                       // TODO log error
+                       continue
+               }
+
+               for _, rinfo := range requests {
+                       status, j := h.updateStatus(rinfo)
+
+                       switch status {
+                       case boruta.INPROGRESS:
+                               h.acquire(j, rinfo)
+                       case boruta.CANCEL:
+                               h.remove(j, rinfo.ID)
+                       case boruta.DONE:
+                               h.remove(j, rinfo.ID)
+                       case boruta.TIMEOUT:
+                               h.remove(j, rinfo.ID)
+                               h.SendFail(j, "Timeout in Boruta.")
+                       case boruta.INVALID:
+                               h.remove(j, rinfo.ID)
+                               h.SendFail(j, "No suitable device in Boruta to run test.")
+                       case boruta.FAILED:
+                               h.remove(j, rinfo.ID)
+                               h.SendFail(j, "Boruta failed during request execution.")
+                       }
+               }
+       }
+}
+
+// getCaps prepares Capabilities for registering new request in Boruta.
+func (h *BoruterImpl) getCaps(config weles.Config) boruta.Capabilities {
+       if config.DeviceType == "" {
+               return boruta.Capabilities{}
+       }
+
+       return boruta.Capabilities{
+               "DeviceType": config.DeviceType,
+       }
+}
+
+// getPriority prepares Priority for registering new request in Boruta.
+func (h *BoruterImpl) getPriority(config weles.Config) boruta.Priority {
+       switch config.Priority {
+       case weles.LOW:
+               return 11
+       case weles.MEDIUM:
+               return 7
+       case weles.HIGH:
+               return 3
+       default:
+               return 7
+       }
+}
+
+// getOwner prepares Owner for registering new request in Boruta.
+func (h *BoruterImpl) getOwner() boruta.UserInfo {
+       return boruta.UserInfo{}
+}
+
+// getValidAfter prepares ValidAfter time for registering new request in Boruta.
+func (h *BoruterImpl) getValidAfter(config weles.Config) time.Time {
+       return time.Now()
+}
+
+// getDeadline prepares Deadline time for registering new request in Boruta.
+func (h *BoruterImpl) getDeadline(config weles.Config) time.Time {
+       const defaultDelay = 24 * time.Hour
+       if config.Timeouts.JobTimeout == weles.ValidPeriod(0) {
+               return time.Now().Add(defaultDelay)
+       }
+
+       return time.Now().Add(time.Duration(config.Timeouts.JobTimeout))
+}
+
+// Request registers new request in Boruta and adds it to monitored requests.
+func (h *BoruterImpl) Request(j weles.JobID) {
+       err := h.jobs.SetStatusAndInfo(j, weles.JOB_WAITING, "")
+       if err != nil {
+               h.SendFail(j, fmt.Sprintf("Internal Weles error while changing Job status : %s", err.Error()))
+               return
+       }
+
+       config, err := h.jobs.GetConfig(j)
+       if err != nil {
+               h.SendFail(j, fmt.Sprintf("Internal Weles error while getting Job config : %s", err.Error()))
+               return
+       }
+
+       caps := h.getCaps(config)
+       priority := h.getPriority(config)
+       owner := h.getOwner()
+       validAfter := h.getValidAfter(config)
+       deadline := h.getDeadline(config)
+
+       r, err := h.boruta.NewRequest(caps, priority, owner, validAfter, deadline)
+       if err != nil {
+               h.SendFail(j, fmt.Sprintf("Failed to create request in Boruta : %s", err.Error()))
+               return
+       }
+
+       h.add(j, r)
+}
+
+// Release returns Dryad to Boruta's pool and closes Boruta's request.
+func (h *BoruterImpl) Release(j weles.JobID) {
+       r, err := h.pop(j)
+       if err != nil {
+               return
+       }
+       h.boruta.CloseRequest(r)
+}
diff --git a/controller/boruterimpl_test.go b/controller/boruterimpl_test.go
new file mode 100644 (file)
index 0000000..6512505
--- /dev/null
@@ -0,0 +1,390 @@
+/*
+ *  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 controller
+
+import (
+       "errors"
+       "net"
+       "sync"
+       "time"
+
+       "git.tizen.org/tools/boruta"
+       "git.tizen.org/tools/weles"
+       cmock "git.tizen.org/tools/weles/controller/mock"
+       "git.tizen.org/tools/weles/controller/notifier"
+       gomock "github.com/golang/mock/gomock"
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("BoruterImpl", func() {
+       var r <-chan notifier.Notification
+       var jc *cmock.MockJobsController
+       var req *cmock.MockRequests
+       var h Boruter
+       var ctrl *gomock.Controller
+       var config weles.Config
+       var caps boruta.Capabilities
+       var priority boruta.Priority
+       j := weles.JobID(0xCAFE)
+       rid := boruta.ReqID(0xD0DA)
+       period := 50 * time.Millisecond
+       jobTimeout := time.Hour
+       owner := boruta.UserInfo{}
+       err := errors.New("test error")
+
+       expectRegistered := func(offset int) {
+               h.(*BoruterImpl).mutex.Lock()
+               defer h.(*BoruterImpl).mutex.Unlock()
+
+               ExpectWithOffset(offset, len(h.(*BoruterImpl).info)).To(Equal(1))
+               info, ok := h.(*BoruterImpl).info[j]
+               ExpectWithOffset(offset, ok).To(BeTrue())
+               ExpectWithOffset(offset, info.rid).To(Equal(rid))
+
+               ExpectWithOffset(offset, len(h.(*BoruterImpl).rid2Job)).To(Equal(1))
+               job, ok := h.(*BoruterImpl).rid2Job[rid]
+               ExpectWithOffset(offset, ok).To(BeTrue())
+               ExpectWithOffset(offset, job).To(Equal(j))
+       }
+       eventuallyEmpty := func(offset int) {
+               EventuallyWithOffset(offset, func() int {
+                       h.(*BoruterImpl).mutex.Lock()
+                       defer h.(*BoruterImpl).mutex.Unlock()
+                       return len(h.(*BoruterImpl).info)
+               }).Should(BeZero())
+               EventuallyWithOffset(offset, func() int {
+                       h.(*BoruterImpl).mutex.Lock()
+                       defer h.(*BoruterImpl).mutex.Unlock()
+                       return len(h.(*BoruterImpl).rid2Job)
+               }).Should(BeZero())
+       }
+       eventuallyNoti := func(offset int, ok bool, msg string) {
+               expectedNotification := notifier.Notification{
+                       JobID: j,
+                       OK:    ok,
+                       Msg:   msg,
+               }
+               EventuallyWithOffset(offset, r).Should(Receive(Equal(expectedNotification)))
+       }
+
+       BeforeEach(func() {
+               ctrl = gomock.NewController(GinkgoT())
+
+               jc = cmock.NewMockJobsController(ctrl)
+               req = cmock.NewMockRequests(ctrl)
+
+               h = NewBoruter(jc, req, period)
+               r = h.Listen()
+
+               config = weles.Config{
+                       DeviceType: "TestDeviceType",
+                       Priority:   "medium",
+                       Timeouts: weles.Timeouts{
+                               JobTimeout: weles.ValidPeriod(jobTimeout),
+                       },
+               }
+               caps = boruta.Capabilities{"DeviceType": "TestDeviceType"}
+               priority = boruta.Priority(7)
+       })
+       AfterEach(func() {
+               h.(*BoruterImpl).Finish()
+               ctrl.Finish()
+       })
+       Describe("NewBoruter", func() {
+               It("should create a new object", func() {
+                       Expect(h).NotTo(BeNil())
+                       Expect(h.(*BoruterImpl).jobs).To(Equal(jc))
+                       Expect(h.(*BoruterImpl).boruta).To(Equal(req))
+                       Expect(h.(*BoruterImpl).info).NotTo(BeNil())
+                       Expect(h.(*BoruterImpl).rid2Job).NotTo(BeNil())
+                       Expect(h.(*BoruterImpl).mutex).NotTo(BeNil())
+                       Expect(h.(*BoruterImpl).borutaCheckPeriod).To(Equal(period))
+               })
+       })
+       Describe("loop", func() {
+               It("should ignore ListRequests errors", func() {
+                       counter := 5
+                       mutex := &sync.Mutex{}
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(caps, priority, owner, gomock.Any(), gomock.Any()).Return(rid, nil)
+                       req.EXPECT().ListRequests(nil).AnyTimes().Return([]boruta.ReqInfo{}, err).Do(func(boruta.ListFilter) {
+                               mutex.Lock()
+                               defer mutex.Unlock()
+                               counter--
+                       })
+
+                       h.Request(j)
+                       Eventually(func() int {
+                               mutex.Lock()
+                               defer mutex.Unlock()
+                               return counter
+                       }).Should(BeNumerically("<", 0))
+
+                       expectRegistered(1)
+               })
+       })
+       Describe("Request", func() {
+               It("should register job successfully", func() {
+                       var va, dl time.Time
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(caps, priority, owner, gomock.Any(), gomock.Any()).Return(rid, nil).Do(
+                               func(c boruta.Capabilities, p boruta.Priority, ui boruta.UserInfo, validAfter time.Time, deadline time.Time) {
+                                       va = validAfter
+                                       dl = deadline
+                               })
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       before := time.Now()
+                       h.Request(j)
+                       after := time.Now()
+
+                       Expect(va).To(BeTemporally(">=", before))
+                       Expect(va).To(BeTemporally("<=", after))
+                       Expect(dl).To(BeTemporally(">=", before.Add(jobTimeout)))
+                       Expect(dl).To(BeTemporally("<=", after.Add(jobTimeout)))
+
+                       expectRegistered(1)
+               })
+               It("should register job successfully even when JobTimout is not defined in Config", func() {
+                       var va, dl time.Time
+                       config.Timeouts.JobTimeout = weles.ValidPeriod(0)
+                       defaultDelay := 24 * time.Hour
+
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(caps, priority, owner, gomock.Any(), gomock.Any()).Return(rid, nil).Do(
+                               func(c boruta.Capabilities, p boruta.Priority, ui boruta.UserInfo, validAfter time.Time, deadline time.Time) {
+                                       va = validAfter
+                                       dl = deadline
+                               })
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       before := time.Now()
+                       h.Request(j)
+                       after := time.Now()
+
+                       Expect(va).To(BeTemporally(">=", before))
+                       Expect(va).To(BeTemporally("<=", after))
+                       Expect(dl).To(BeTemporally(">=", before.Add(defaultDelay)))
+                       Expect(dl).To(BeTemporally("<=", after.Add(defaultDelay)))
+
+                       expectRegistered(1)
+               })
+               It("should fail if NewRequest fails", func() {
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(caps, priority, owner, gomock.Any(), gomock.Any()).Return(boruta.ReqID(0), err)
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       h.Request(j)
+
+                       eventuallyNoti(1, false, "Failed to create request in Boruta : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should fail if GetConfig fails", func() {
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(weles.Config{}, err)
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       h.Request(j)
+
+                       eventuallyNoti(1, false, "Internal Weles error while getting Job config : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should fail if SetStatusAndInfo fails", func() {
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "").Return(err)
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       h.Request(j)
+
+                       eventuallyNoti(1, false, "Internal Weles error while changing Job status : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should call NewRequest with empty caps if no device type provided", func() {
+                       config.DeviceType = ""
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(boruta.Capabilities{}, priority, owner, gomock.Any(), gomock.Any()).Return(boruta.ReqID(0), err)
+                       req.EXPECT().ListRequests(nil).AnyTimes()
+
+                       h.Request(j)
+
+                       eventuallyNoti(1, false, "Failed to create request in Boruta : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should call NewRequest with proper priority", func() {
+                       m := map[weles.Priority]boruta.Priority{
+                               weles.LOW:                 boruta.Priority(11),
+                               weles.MEDIUM:              boruta.Priority(7),
+                               weles.HIGH:                boruta.Priority(3),
+                               weles.Priority("unknown"): boruta.Priority(7),
+                       }
+                       for k, v := range m {
+                               config.Priority = k
+                               jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                               jc.EXPECT().GetConfig(j).Return(config, nil)
+                               req.EXPECT().NewRequest(caps, v, owner, gomock.Any(), gomock.Any()).Return(boruta.ReqID(0), err)
+                               req.EXPECT().ListRequests(nil).AnyTimes()
+
+                               h.Request(j)
+
+                               eventuallyNoti(1, false, "Failed to create request in Boruta : test error")
+                               eventuallyEmpty(1)
+                       }
+               })
+       })
+       Describe("With registered request", func() {
+               var listRequestRet chan []boruta.ReqInfo
+               states := []boruta.ReqState{
+                       boruta.WAIT,
+                       boruta.INPROGRESS,
+                       boruta.CANCEL,
+                       boruta.TIMEOUT,
+                       boruta.INVALID,
+                       boruta.DONE,
+                       boruta.FAILED,
+               }
+               ai := boruta.AccessInfo{Addr: &net.IPNet{IP: net.IPv4(1, 2, 3, 4), Mask: net.IPv4Mask(5, 6, 7, 8)}}
+               BeforeEach(func() {
+                       var va, dl time.Time
+                       jc.EXPECT().SetStatusAndInfo(j, weles.JOB_WAITING, "")
+                       jc.EXPECT().GetConfig(j).Return(config, nil)
+                       req.EXPECT().NewRequest(caps, priority, owner, gomock.Any(), gomock.Any()).Return(rid, nil).Do(
+                               func(c boruta.Capabilities, p boruta.Priority, ui boruta.UserInfo, validAfter time.Time, deadline time.Time) {
+                                       va = validAfter
+                                       dl = deadline
+                               })
+                       listRequestRet = make(chan []boruta.ReqInfo)
+                       req.EXPECT().ListRequests(nil).AnyTimes().DoAndReturn(func(boruta.ListFilter) ([]boruta.ReqInfo, error) {
+                               return <-listRequestRet, nil
+                       })
+
+                       before := time.Now()
+                       h.Request(j)
+                       after := time.Now()
+
+                       Expect(va).To(BeTemporally(">=", before))
+                       Expect(va).To(BeTemporally("<=", after))
+                       Expect(dl).To(BeTemporally(">=", before.Add(jobTimeout)))
+                       Expect(dl).To(BeTemporally("<=", after.Add(jobTimeout)))
+
+                       expectRegistered(1)
+               })
+               It("should ignore ID of not registered request", func() {
+                       for _, s := range states {
+                               rinfo := boruta.ReqInfo{ID: boruta.ReqID(0x0BCA), State: s}
+                               listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                               expectRegistered(1)
+                       }
+               })
+               for _, s := range states { // Every state is a separate It, because objects must be reinitialized
+                       It("should ignore if request's state is unchanged : "+string(s), func() {
+                               h.(*BoruterImpl).mutex.Lock()
+
+                               Expect(len(h.(*BoruterImpl).info)).To(Equal(1))
+                               info, ok := h.(*BoruterImpl).info[j]
+                               Expect(ok).To(BeTrue())
+                               info.status = s
+
+                               h.(*BoruterImpl).mutex.Unlock()
+
+                               rinfo := boruta.ReqInfo{ID: rid, State: s}
+                               listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                               expectRegistered(1)
+                       })
+               }
+               It("should acquire Dryad if state changes to INPROGRESS", func() {
+                       req.EXPECT().AcquireWorker(rid).Return(ai, nil)
+                       jc.EXPECT().SetDryad(j, weles.Dryad{Addr: ai.Addr, Key: ai.Key, Username: "boruta-user"})
+
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.INPROGRESS, Job: &boruta.JobInfo{Timeout: time.Now().AddDate(0, 0, 1)}}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, true, "")
+               })
+               It("should fail during acquire if SetDryad fails", func() {
+                       req.EXPECT().AcquireWorker(rid).Return(ai, nil)
+                       jc.EXPECT().SetDryad(j, weles.Dryad{Addr: ai.Addr, Key: ai.Key, Username: "boruta-user"}).Return(err)
+
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.INPROGRESS, Job: &boruta.JobInfo{Timeout: time.Now().AddDate(0, 0, 1)}}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, false, "Internal Weles error while setting Dryad : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should fail during acquire if AcquireWorker fails", func() {
+                       req.EXPECT().AcquireWorker(rid).Return(boruta.AccessInfo{}, err)
+
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.INPROGRESS, Job: &boruta.JobInfo{Timeout: time.Now().AddDate(0, 0, 1)}}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, false, "Cannot acquire worker from Boruta : test error")
+                       eventuallyEmpty(1)
+               })
+               It("should remove request if state changes to CANCEL", func() {
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.CANCEL}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyEmpty(1)
+               })
+               It("should remove request if state changes to DONE", func() {
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.DONE}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyEmpty(1)
+               })
+               It("should fail and remove request if state changes to TIMEOUT", func() {
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.TIMEOUT}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, false, "Timeout in Boruta.")
+                       eventuallyEmpty(1)
+               })
+               It("should fail and remove request if state changes to INVALID", func() {
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.INVALID}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, false, "No suitable device in Boruta to run test.")
+                       eventuallyEmpty(1)
+               })
+               It("should fail and remove request if state changes to FAILED", func() {
+                       rinfo := boruta.ReqInfo{ID: rid, State: boruta.FAILED}
+                       listRequestRet <- []boruta.ReqInfo{rinfo}
+
+                       eventuallyNoti(1, false, "Boruta failed during request execution.")
+                       eventuallyEmpty(1)
+               })
+               Describe("Release", func() {
+                       It("should remove existing request and close it in Boruta", func() {
+                               req.EXPECT().CloseRequest(rid)
+
+                               h.Release(j)
+
+                               eventuallyEmpty(1)
+                       })
+                       It("should ignore not existing request", func() {
+                               h.Release(weles.JobID(0x0BCA))
+                               expectRegistered(1)
+                       })
+               })
+       })
+})