From 149c8b8f345fb6620b0ac14d710ea10ce0e40071 Mon Sep 17 00:00:00 2001 From: Lukasz Wojciechowski Date: Mon, 27 Nov 2017 16:56:25 +0100 Subject: [PATCH] Add Controller with tests Change-Id: I4681c0e085983094f9ce2699617bb9b52d2e086a Signed-off-by: Lukasz Wojciechowski --- controller/controller.go | 159 ++++++++++++++++++++++++++++++ controller/controller_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 controller/controller.go create mode 100644 controller/controller_test.go diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 0000000..200fc1c --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2017 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 provides Controller implementation. Controller binds all +// major components of Weles and provides logic layer for running proper methods +// of these components. +// +// Controller implements also JobManager interface providing API for controlling +// Weles' Jobs. This interface should be used by HTTP API server. +package controller + +import ( + "sync" + + . "git.tizen.org/tools/weles" +) + +// Controller binds all major components of Weles and provides logic layer +// for running proper methods of these components. +// +// It implements JobManager interface providing API for controlling +// Weles' Jobs. This interface should be used by HTTP API server. +// Controller should be created with NewController function. +type Controller struct { + JobManager + + // jobs references module implementing Jobs management. + jobs JobsController + // parser controls parsing yaml file and creation of Job's config. + parser Parser + // downloader controls downloading artifacts required for the Job. + downloader Downloader + // boruter acquires, release and monitors Dryads from Boruta. + boruter Boruter + // dryader delegates Jobs execution to DryadJobManager and monitor progress. + dryader Dryader + // finish is channel for stopping internal goroutine. + finish chan int + // looper waits for internal goroutine running loop to finish. + looper sync.WaitGroup +} + +// NewController creates and initializes a new instance of Controller. +// It is the only valid way of Controller struct creation. +func NewController(js JobsController, pa Parser, do Downloader, bo Boruter, dr Dryader) *Controller { + c := &Controller{ + jobs: js, + parser: pa, + downloader: do, + boruter: bo, + dryader: dr, + finish: make(chan int), + } + c.looper.Add(1) + go c.loop() + return c +} + +// Finish internal goroutine. +func (c *Controller) Finish() { + c.finish <- 1 + c.looper.Wait() +} + +// CreateJob creates a new Job in Weles using recipe passed in YAML format. +// It is a part of JobManager implementation. +func (c *Controller) CreateJob(yaml []byte) (JobID, error) { + j, err := c.jobs.NewJob(yaml) + if err != nil { + return JobID(0), err + } + + go c.parser.Parse(j) + + return j, nil +} + +// CancelJob cancels Job identified by argument. Job execution is stopped. +// It is a part of JobManager implementation. +func (c *Controller) CancelJob(j JobID) error { + err := c.jobs.SetStatusAndInfo(j, JOB_CANCELED, "") + if err != nil { + return err + } + c.dryader.Cancel(j) + c.boruter.Release(j) + return nil +} + +// ListJobs returns information on Jobs. If argument is a nil/empty slice +// information about all Jobs is returned. Otherwise result is filtered +// and contains information about requested Jobs only. +// It is a part of JobManager implementation. +func (c *Controller) ListJobs(filter []JobID) ([]JobInfo, error) { + return nil, ErrNotImplemented +} + +// loop implements main loop of the Controller reacting to different events +// related to processed Jobs. +func (c *Controller) loop() { + defer c.looper.Done() + for { + select { + case <-c.finish: + return + case noti := <-c.parser.Listen(): + if !noti.OK { + c.fail(noti.JobID, noti.Msg) + continue + } + c.downloader.Download(noti.JobID) + case noti := <-c.downloader.Listen(): + if !noti.OK { + c.fail(noti.JobID, noti.Msg) + continue + } + c.boruter.Request(noti.JobID) + case noti := <-c.boruter.Listen(): + if !noti.OK { + c.fail(noti.JobID, noti.Msg) + continue + } + c.dryader.Start(noti.JobID) + case noti := <-c.dryader.Listen(): + if !noti.OK { + c.fail(noti.JobID, noti.Msg) + continue + } + c.succeed(noti.JobID) + } + } +} + +// fail sets Job in FAILED state and if needed stops Job's execution on Dryad +// and releases Dryad to Boruta. +func (c *Controller) fail(j JobID, msg string) { + c.jobs.SetStatusAndInfo(j, JOB_FAILED, msg) + c.dryader.Cancel(j) + c.boruter.Release(j) +} + +// succeed sets Job in COMPLETED state. +func (c *Controller) succeed(j JobID) { + c.jobs.SetStatusAndInfo(j, JOB_COMPLETED, "") + c.boruter.Release(j) +} diff --git a/controller/controller_test.go b/controller/controller_test.go new file mode 100644 index 0000000..efc3b30 --- /dev/null +++ b/controller/controller_test.go @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2017 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" + "sync" + + . "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("Controller", func() { + var jc *cmock.MockJobsController + var par *cmock.MockParser + var dow *cmock.MockDownloader + var bor *cmock.MockBoruter + var dry *cmock.MockDryader + var h *Controller + var ctrl *gomock.Controller + var parChan chan notifier.Notification + var dowChan chan notifier.Notification + var borChan chan notifier.Notification + var dryChan chan notifier.Notification + var done bool + var mutex *sync.Mutex + + j := JobID(0xCAFE) + err := errors.New("test error") + yaml := []byte("test yaml") + testMsg := "test msg" + notiOk := notifier.Notification{JobID: j, OK: true} + notiFail := notifier.Notification{JobID: j, OK: false, Msg: testMsg} + + setDone := func(JobID) { + mutex.Lock() + defer mutex.Unlock() + done = true + } + eventuallyDone := func() { + Eventually(func() bool { + mutex.Lock() + defer mutex.Unlock() + return done + }).Should(BeTrue()) + } + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + + jc = cmock.NewMockJobsController(ctrl) + par = cmock.NewMockParser(ctrl) + dow = cmock.NewMockDownloader(ctrl) + bor = cmock.NewMockBoruter(ctrl) + dry = cmock.NewMockDryader(ctrl) + + parChan = make(chan notifier.Notification) + dowChan = make(chan notifier.Notification) + borChan = make(chan notifier.Notification) + dryChan = make(chan notifier.Notification) + + par.EXPECT().Listen().AnyTimes().Return((<-chan notifier.Notification)(parChan)) + dow.EXPECT().Listen().AnyTimes().Return((<-chan notifier.Notification)(dowChan)) + bor.EXPECT().Listen().AnyTimes().Return((<-chan notifier.Notification)(borChan)) + dry.EXPECT().Listen().AnyTimes().Return((<-chan notifier.Notification)(dryChan)) + + h = NewController(jc, par, dow, bor, dry) + + mutex = &sync.Mutex{} + done = false + }) + AfterEach(func() { + h.Finish() + ctrl.Finish() + }) + + Describe("NewController", func() { + It("should create a new object", func() { + Expect(h).NotTo(BeNil()) + Expect(h.jobs).To(Equal(jc)) + Expect(h.parser).To(Equal(par)) + Expect(h.downloader).To(Equal(dow)) + Expect(h.boruter).To(Equal(bor)) + Expect(h.dryader).To(Equal(dry)) + Expect(h.finish).NotTo(BeNil()) + }) + }) + Describe("CreateJob", func() { + It("should create a new Job and delegate parsing", func() { + jc.EXPECT().NewJob(yaml).Return(j, nil) + par.EXPECT().Parse(j).Do(setDone) + + retJobID, retErr := h.CreateJob(yaml) + + Expect(retErr).To(BeNil()) + Expect(retJobID).To(Equal(j)) + eventuallyDone() + }) + It("should fail if JobsController.NewJob fails", func() { + jc.EXPECT().NewJob(yaml).Return(JobID(0), err) + + retJobID, retErr := h.CreateJob(yaml) + + Expect(retErr).To(Equal(err)) + Expect(retJobID).To(Equal(JobID(0))) + }) + }) + + Describe("CancelJob", func() { + It("should cancel Job, stop execution on Dryad and release Dryad to Boruta", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_CANCELED, "") + dry.EXPECT().Cancel(j) + bor.EXPECT().Release(j) + + retErr := h.CancelJob(j) + + Expect(retErr).To(BeNil()) + }) + It("should cancel Job, stop execution on Dryad and release Dryad to Boruta", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_CANCELED, "").Return(err) + + retErr := h.CancelJob(j) + + Expect(retErr).To(Equal(err)) + }) + }) + Describe("ListJobs", func() { + It("should not be implemented yet", func() { + filter := []JobID{2, 3, 5} + info, err := h.ListJobs(filter) + + Expect(err).To(Equal(ErrNotImplemented)) + Expect(info).To(BeZero()) + }) + }) + Describe("Actions", func() { + Describe("Parser", func() { + It("should start download when parser finished", func() { + dow.EXPECT().Download(j).Do(setDone) + + parChan <- notiOk + + eventuallyDone() + }) + It("should fail when parser failed", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_FAILED, testMsg) + dry.EXPECT().Cancel(j) + bor.EXPECT().Release(j) + + parChan <- notiFail + }) + }) + Describe("Downloader", func() { + It("should request Dryad from Boruta when downloader finished", func() { + bor.EXPECT().Request(j).Do(setDone) + + dowChan <- notiOk + + eventuallyDone() + }) + It("should fail when downloader failed", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_FAILED, testMsg) + dry.EXPECT().Cancel(j) + bor.EXPECT().Release(j) + + dowChan <- notiFail + }) + }) + Describe("Boruter", func() { + It("should start Job execution when Dryad is acquired from Boruta", func() { + dry.EXPECT().Start(j).Do(setDone) + + borChan <- notiOk + + eventuallyDone() + }) + It("should fail when Boruta reports error (fail, timout, ...)", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_FAILED, testMsg) + dry.EXPECT().Cancel(j) + bor.EXPECT().Release(j) + + borChan <- notiFail + }) + }) + Describe("Dryader", func() { + It("should complete Job after Dryad Job is done", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_COMPLETED, "") + bor.EXPECT().Release(j).Do(setDone) + + dryChan <- notiOk + + eventuallyDone() + }) + It("should fail when Dryader fails", func() { + jc.EXPECT().SetStatusAndInfo(j, JOB_FAILED, testMsg) + dry.EXPECT().Cancel(j) + bor.EXPECT().Release(j) + + dryChan <- notiFail + }) + }) + }) +}) -- 2.7.4