diff --git a/api/api.go b/api/api.go index 7f4ad59..4ee0d6e 100644 --- a/api/api.go +++ b/api/api.go @@ -12,9 +12,10 @@ import ( var log = logging.GetLogger("api") const ( - BasePath = "/api/v1" - HealthPath = "/health" - RunnersPath = "/runners" + BasePath = "/api/v1" + HealthPath = "/health" + RunnersPath = "/runners" + EnvironmentsPath = "/execution-environments" ) // NewRouter returns a *mux.Router which can be @@ -37,6 +38,12 @@ func configureV1Router(router *mux.Router, runnerManager runner.Manager, environ v1.HandleFunc(HealthPath, Health).Methods(http.MethodGet) runnerController := &RunnerController{manager: runnerManager} + environmentController := &EnvironmentController{manager: environmentManager} + + configureRoutes := func(router *mux.Router) { + runnerController.ConfigureRoutes(router) + environmentController.ConfigureRoutes(router) + } if auth.InitializeAuthentication() { // Create new authenticated subrouter. @@ -45,6 +52,6 @@ func configureV1Router(router *mux.Router, runnerManager runner.Manager, environ authenticatedV1Router.Use(auth.HTTPAuthenticationMiddleware) runnerController.ConfigureRoutes(authenticatedV1Router) } else { - runnerController.ConfigureRoutes(v1) + configureRoutes(v1) } } diff --git a/api/dto/dto.go b/api/dto/dto.go index 43d41ad..7e0f6ed 100644 --- a/api/dto/dto.go +++ b/api/dto/dto.go @@ -32,14 +32,13 @@ func (er *ExecutionRequest) FullCommand() []string { } // ExecutionEnvironmentRequest is the expected json structure of the request body for the create execution environment function. -// nolint:unused,structcheck type ExecutionEnvironmentRequest struct { - prewarmingPoolSize uint - cpuLimit uint - memoryLimit uint - image string - networkAccess bool - exposedPorts []uint16 + PrewarmingPoolSize uint `json:"prewarmingPoolSize"` + CPULimit uint `json:"cpuLimit"` + MemoryLimit uint `json:"memoryLimit"` + Image string `json:"image"` + NetworkAccess bool `json:"networkAccess"` + ExposedPorts []uint16 `json:"exposedPorts"` } // RunnerResponse is the expected response when providing a runner. diff --git a/api/environments.go b/api/environments.go index 527ae0b..77746b3 100644 --- a/api/environments.go +++ b/api/environments.go @@ -1,17 +1,53 @@ package api import ( + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto" "gitlab.hpi.de/codeocean/codemoon/poseidon/environment" "net/http" ) +const ( + executionEnvironmentIDKey = "executionEnvironmentId" + createOrUpdateRouteName = "createOrUpdate" +) + type EnvironmentController struct { - manager environment.Manager // nolint:unused,structcheck + manager environment.Manager } -// create creates a new execution environment on the executor. -func (e *EnvironmentController) create(writer http.ResponseWriter, request *http.Request) { // nolint:unused +func (e *EnvironmentController) ConfigureRoutes(router *mux.Router) { + environmentRouter := router.PathPrefix(EnvironmentsPath).Subrouter() + specificEnvironmentRouter := environmentRouter.Path(fmt.Sprintf("/{%s:[0-9]+}", executionEnvironmentIDKey)).Subrouter() + specificEnvironmentRouter.HandleFunc("", e.createOrUpdate).Methods(http.MethodPut).Name(createOrUpdateRouteName) +} +// createOrUpdate creates/updates an execution environment on the executor. +func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, request *http.Request) { + req := new(dto.ExecutionEnvironmentRequest) + if err := json.NewDecoder(request.Body).Decode(req); err != nil { + writeBadRequest(writer, err) + return + } + + id, ok := mux.Vars(request)[executionEnvironmentIDKey] + if !ok { + writeBadRequest(writer, fmt.Errorf("could not find %s", executionEnvironmentIDKey)) + return + } + + created, err := e.manager.CreateOrUpdate(id, *req) + if err != nil { + writeInternalServerError(writer, err, dto.ErrorUnknown) + } + + if created { + writer.WriteHeader(http.StatusCreated) + } else { + writer.WriteHeader(http.StatusNoContent) + } } // delete removes an execution environment from the executor diff --git a/api/environments_test.go b/api/environments_test.go new file mode 100644 index 0000000..3ccde52 --- /dev/null +++ b/api/environments_test.go @@ -0,0 +1,100 @@ +package api + +import ( + "bytes" + "encoding/json" + "github.com/gorilla/mux" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto" + "gitlab.hpi.de/codeocean/codemoon/poseidon/environment" + "gitlab.hpi.de/codeocean/codemoon/poseidon/tests" + "net/http" + "net/http/httptest" + "testing" +) + +type EnvironmentControllerTestSuite struct { + suite.Suite + manager *environment.ManagerMock + router *mux.Router +} + +func TestEnvironmentControllerTestSuite(t *testing.T) { + suite.Run(t, new(EnvironmentControllerTestSuite)) +} + +func (s *EnvironmentControllerTestSuite) SetupTest() { + s.manager = &environment.ManagerMock{} + s.router = NewRouter(nil, s.manager) +} + +type CreateOrUpdateEnvironmentTestSuite struct { + EnvironmentControllerTestSuite + path string + id string + body []byte +} + +func TestCreateOrUpdateEnvironmentTestSuite(t *testing.T) { + suite.Run(t, new(CreateOrUpdateEnvironmentTestSuite)) +} + +func (s *CreateOrUpdateEnvironmentTestSuite) SetupTest() { + s.EnvironmentControllerTestSuite.SetupTest() + s.id = tests.DefaultEnvironmentIdAsString + testURL, err := s.router.Get(createOrUpdateRouteName).URL(executionEnvironmentIDKey, s.id) + if err != nil { + s.T().Fatal(err) + } + s.path = testURL.String() + s.body, err = json.Marshal(dto.ExecutionEnvironmentRequest{}) + if err != nil { + s.T().Fatal(err) + } +} + +func (s *CreateOrUpdateEnvironmentTestSuite) recordRequest() *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + request, err := http.NewRequest(http.MethodPut, s.path, bytes.NewReader(s.body)) + if err != nil { + s.T().Fatal(err) + } + s.router.ServeHTTP(recorder, request) + return recorder +} + +func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsBadRequestWhenBadBody() { + s.body = []byte{} + recorder := s.recordRequest() + s.Equal(http.StatusBadRequest, recorder.Code) +} + +func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsInternalServerErrorWhenManagerReturnsError() { + testError := tests.DefaultError + s.manager. + On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest")). + Return(false, testError) + + recorder := s.recordRequest() + s.Equal(http.StatusInternalServerError, recorder.Code) + s.Contains(recorder.Body.String(), testError.Error()) +} + +func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsCreatedIfNewEnvironment() { + s.manager. + On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest")). + Return(true, nil) + + recorder := s.recordRequest() + s.Equal(http.StatusCreated, recorder.Code) +} + +func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsNoContentIfNotNewEnvironment() { + s.manager. + On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest")). + Return(false, nil) + + recorder := s.recordRequest() + s.Equal(http.StatusNoContent, recorder.Code) +} diff --git a/environment/manager.go b/environment/manager.go index fecaafa..79fd1d7 100644 --- a/environment/manager.go +++ b/environment/manager.go @@ -6,6 +6,7 @@ import ( "gitlab.hpi.de/codeocean/codemoon/poseidon/logging" "gitlab.hpi.de/codeocean/codemoon/poseidon/nomad" "gitlab.hpi.de/codeocean/codemoon/poseidon/runner" + "strconv" ) var log = logging.GetLogger("environment") @@ -16,11 +17,12 @@ type Manager interface { // It should be called during the startup process (e.g. on creation of the Manager). Load() - // Create creates a new execution environment on the executor. - Create( + // CreateOrUpdate creates/updates an execution environment on the executor. + // Iff the job was created, the returned boolean is true and the returned error is nil. + CreateOrUpdate( id string, request dto.ExecutionEnvironmentRequest, - ) + ) (bool, error) // Delete removes the execution environment with the given id from the executor. Delete(id string) @@ -38,11 +40,28 @@ type NomadEnvironmentManager struct { defaultJob nomadApi.Job } -func (m *NomadEnvironmentManager) Create( +func (m *NomadEnvironmentManager) CreateOrUpdate( id string, request dto.ExecutionEnvironmentRequest, -) { +) (bool, error) { + idInt, err := strconv.Atoi(id) + if err != nil { + return false, err + } + exists := m.runnerManager.EnvironmentExists(runner.EnvironmentId(idInt)) + err = m.registerJob(id, + request.PrewarmingPoolSize, request.CPULimit, request.MemoryLimit, + request.Image, request.NetworkAccess, request.ExposedPorts) + + if err == nil { + if !exists { + m.runnerManager.RegisterEnvironment( + runner.EnvironmentId(idInt), runner.NomadJobId(id), int(request.PrewarmingPoolSize)) + } + return !exists, nil + } + return false, err } func (m *NomadEnvironmentManager) Delete(id string) { diff --git a/environment/manager_mock.go b/environment/manager_mock.go index 0e17c28..96db957 100644 --- a/environment/manager_mock.go +++ b/environment/manager_mock.go @@ -2,16 +2,35 @@ package environment -import mock "github.com/stretchr/testify/mock" +import ( + mock "github.com/stretchr/testify/mock" + dto "gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto" +) // ManagerMock is an autogenerated mock type for the Manager type type ManagerMock struct { mock.Mock } -// Create provides a mock function with given fields: id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts -func (_m *ManagerMock) Create(id string, prewarmingPoolSize uint, cpuLimit uint, memoryLimit uint, image string, networkAccess bool, exposedPorts []uint16) { - _m.Called(id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts) +// CreateOrUpdate provides a mock function with given fields: id, request +func (_m *ManagerMock) CreateOrUpdate(id string, request dto.ExecutionEnvironmentRequest) (bool, error) { + ret := _m.Called(id, request) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, dto.ExecutionEnvironmentRequest) bool); ok { + r0 = rf(id, request) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, dto.ExecutionEnvironmentRequest) error); ok { + r1 = rf(id, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // Delete provides a mock function with given fields: id diff --git a/environment/manager_test.go b/environment/manager_test.go new file mode 100644 index 0000000..995b132 --- /dev/null +++ b/environment/manager_test.go @@ -0,0 +1,136 @@ +package environment + +import ( + nomadApi "github.com/hashicorp/nomad/api" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto" + "gitlab.hpi.de/codeocean/codemoon/poseidon/nomad" + "gitlab.hpi.de/codeocean/codemoon/poseidon/runner" + "gitlab.hpi.de/codeocean/codemoon/poseidon/tests" + "math" + "strconv" + "testing" +) + +type CreateOrUpdateTestSuite struct { + suite.Suite + runnerManagerMock runner.ManagerMock + apiMock nomad.ExecutorApiMock + registerNomadJobMockCall *mock.Call + request dto.ExecutionEnvironmentRequest + manager *NomadEnvironmentManager +} + +func TestCreateOrUpdateTestSuite(t *testing.T) { + suite.Run(t, new(CreateOrUpdateTestSuite)) +} + +func (s *CreateOrUpdateTestSuite) SetupTest() { + s.runnerManagerMock = runner.ManagerMock{} + s.apiMock = nomad.ExecutorApiMock{} + s.request = dto.ExecutionEnvironmentRequest{ + PrewarmingPoolSize: 10, + CPULimit: 20, + MemoryLimit: 30, + Image: "my-image", + NetworkAccess: false, + ExposedPorts: nil, + } + + s.registerNomadJobMockCall = s.apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("eval-id", nil) + s.apiMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.AnythingOfType("*context.emptyCtx")).Return(nil) + + s.manager = &NomadEnvironmentManager{ + runnerManager: &s.runnerManagerMock, + api: &s.apiMock, + } +} + +func (s *CreateOrUpdateTestSuite) mockEnvironmentExists(exists bool) { + s.runnerManagerMock.On("EnvironmentExists", mock.AnythingOfType("EnvironmentId")).Return(exists) +} + +func (s *CreateOrUpdateTestSuite) mockRegisterEnvironment() *mock.Call { + return s.runnerManagerMock.On("RegisterEnvironment", + mock.AnythingOfType("EnvironmentId"), mock.AnythingOfType("NomadJobId"), mock.AnythingOfType("int")). + Return() +} + +func (s *CreateOrUpdateTestSuite) createJobForRequest() *nomadApi.Job { + return createJob(s.manager.defaultJob, tests.DefaultEnvironmentIdAsString, + s.request.PrewarmingPoolSize, s.request.CPULimit, s.request.MemoryLimit, + s.request.Image, s.request.NetworkAccess, s.request.ExposedPorts) +} + +func (s *CreateOrUpdateTestSuite) TestFailsOnInvalidID() { + _, err := s.manager.CreateOrUpdate("invalid-id", s.request) + s.Error(err) +} + +func (s *CreateOrUpdateTestSuite) TestFailsOnTooLargeID() { + tooLargeIntStr := strconv.Itoa(math.MaxInt64) + "0" + _, err := s.manager.CreateOrUpdate(tooLargeIntStr, s.request) + s.Error(err) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentExistsRegistersCorrectJob() { + s.mockEnvironmentExists(true) + expectedJob := s.createJobForRequest() + + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.NoError(err) + s.False(created) + s.apiMock.AssertCalled(s.T(), "RegisterNomadJob", expectedJob) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentExistsOccurredErrorIsPassed() { + s.mockEnvironmentExists(true) + + s.registerNomadJobMockCall.Return("", tests.DefaultError) + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.False(created) + s.Equal(tests.DefaultError, err) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentExistsReturnsFalse() { + s.mockEnvironmentExists(true) + + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.NoError(err) + s.False(created) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentDoesNotExistRegistersCorrectJob() { + s.mockEnvironmentExists(false) + s.mockRegisterEnvironment() + + expectedJob := s.createJobForRequest() + + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.NoError(err) + s.True(created) + s.apiMock.AssertCalled(s.T(), "RegisterNomadJob", expectedJob) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentDoesNotExistRegistersCorrectEnvironment() { + s.mockEnvironmentExists(false) + s.mockRegisterEnvironment() + + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.True(created) + s.NoError(err) + s.runnerManagerMock.AssertCalled(s.T(), "RegisterEnvironment", + runner.EnvironmentId(tests.DefaultEnvironmentIdAsInteger), runner.NomadJobId(tests.DefaultEnvironmentIdAsString), s.request.PrewarmingPoolSize) +} + +func (s *CreateOrUpdateTestSuite) TestWhenEnvironmentDoesNotExistOccurredErrorIsPassedAndNoEnvironmentRegistered() { + s.mockEnvironmentExists(false) + s.mockRegisterEnvironment() + + s.registerNomadJobMockCall.Return("", tests.DefaultError) + created, err := s.manager.CreateOrUpdate(tests.DefaultEnvironmentIdAsString, s.request) + s.False(created) + s.Equal(tests.DefaultError, err) + s.runnerManagerMock.AssertNotCalled(s.T(), "RegisterEnvironment") +} diff --git a/runner/manager.go b/runner/manager.go index 3cda11f..3a0f4aa 100644 --- a/runner/manager.go +++ b/runner/manager.go @@ -27,6 +27,9 @@ type Manager interface { // RegisterEnvironment adds a new environment that should be managed. RegisterEnvironment(environmentId EnvironmentId, nomadJobId NomadJobId, desiredIdleRunnersCount int) + // EnvironmentExists returns whether the environment with the given id exists. + EnvironmentExists(id EnvironmentId) bool + // Claim returns a new runner. // It makes sure that the runner is not in use yet and returns an error if no runner could be provided. Claim(id EnvironmentId) (Runner, error) @@ -75,6 +78,11 @@ func (m *NomadRunnerManager) RegisterEnvironment(environmentId EnvironmentId, no go m.refreshEnvironment(environmentId) } +func (m *NomadRunnerManager) EnvironmentExists(id EnvironmentId) (ok bool) { + _, ok = m.jobs.Get(id) + return +} + func (m *NomadRunnerManager) Claim(environmentId EnvironmentId) (Runner, error) { job, ok := m.jobs.Get(environmentId) if !ok { diff --git a/runner/manager_mock.go b/runner/manager_mock.go index 8ad7e50..58af5f7 100644 --- a/runner/manager_mock.go +++ b/runner/manager_mock.go @@ -32,6 +32,20 @@ func (_m *ManagerMock) Claim(id EnvironmentId) (Runner, error) { return r0, r1 } +// EnvironmentExists provides a mock function with given fields: id +func (_m *ManagerMock) EnvironmentExists(id EnvironmentId) bool { + ret := _m.Called(id) + + var r0 bool + if rf, ok := ret.Get(0).(func(EnvironmentId) bool); ok { + r0 = rf(id) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // Get provides a mock function with given fields: runnerId func (_m *ManagerMock) Get(runnerId string) (Runner, error) { ret := _m.Called(runnerId) diff --git a/runner/manager_test.go b/runner/manager_test.go index a501554..8de2245 100644 --- a/runner/manager_test.go +++ b/runner/manager_test.go @@ -3,6 +3,7 @@ package runner import ( "errors" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "gitlab.hpi.de/codeocean/codemoon/poseidon/nomad" "gitlab.hpi.de/codeocean/codemoon/poseidon/tests" @@ -25,156 +26,172 @@ type ManagerTestSuite struct { exerciseRunner Runner } -func (suite *ManagerTestSuite) SetupTest() { - suite.apiMock = &nomad.ExecutorApiMock{} - suite.nomadRunnerManager = NewNomadRunnerManager(suite.apiMock) - suite.exerciseRunner = NewRunner(tests.DefaultRunnerId) - suite.mockRunnerQueries([]string{}) - suite.registerDefaultEnvironment() +func (s *ManagerTestSuite) SetupTest() { + s.apiMock = &nomad.ExecutorApiMock{} + s.nomadRunnerManager = NewNomadRunnerManager(s.apiMock) + s.exerciseRunner = NewRunner(tests.DefaultRunnerId) + s.mockRunnerQueries([]string{}) + s.registerDefaultEnvironment() } -func (suite *ManagerTestSuite) mockRunnerQueries(returnedRunnerIds []string) { +func (s *ManagerTestSuite) mockRunnerQueries(returnedRunnerIds []string) { // reset expected calls to allow new mocked return values - suite.apiMock.ExpectedCalls = []*mock.Call{} - suite.apiMock.On("LoadRunners", tests.DefaultJobId).Return(returnedRunnerIds, nil) - suite.apiMock.On("JobScale", tests.DefaultJobId).Return(len(returnedRunnerIds), nil) - suite.apiMock.On("SetJobScale", tests.DefaultJobId, mock.AnythingOfType("int"), "Runner Requested").Return(nil) + s.apiMock.ExpectedCalls = []*mock.Call{} + s.apiMock.On("LoadRunners", tests.DefaultJobId).Return(returnedRunnerIds, nil) + s.apiMock.On("JobScale", tests.DefaultJobId).Return(len(returnedRunnerIds), nil) + s.apiMock.On("SetJobScale", tests.DefaultJobId, mock.AnythingOfType("int"), "Runner Requested").Return(nil) } -func (suite *ManagerTestSuite) registerDefaultEnvironment() { - suite.nomadRunnerManager.RegisterEnvironment(defaultEnvironmentId, tests.DefaultJobId, defaultDesiredRunnersCount) +func (s *ManagerTestSuite) registerDefaultEnvironment() { + s.nomadRunnerManager.RegisterEnvironment(defaultEnvironmentId, tests.DefaultJobId, defaultDesiredRunnersCount) } -func (suite *ManagerTestSuite) AddIdleRunnerForDefaultEnvironment(r Runner) { - job, _ := suite.nomadRunnerManager.jobs.Get(defaultEnvironmentId) +func (s *ManagerTestSuite) AddIdleRunnerForDefaultEnvironment(r Runner) { + job, _ := s.nomadRunnerManager.jobs.Get(defaultEnvironmentId) job.idleRunners.Add(r) } -func (suite *ManagerTestSuite) waitForRunnerRefresh() { +func (s *ManagerTestSuite) waitForRunnerRefresh() { time.Sleep(100 * time.Millisecond) } -func (suite *ManagerTestSuite) TestRegisterEnvironmentAddsNewJob() { - suite.nomadRunnerManager.RegisterEnvironment(anotherEnvironmentId, tests.DefaultJobId, defaultDesiredRunnersCount) - job, ok := suite.nomadRunnerManager.jobs.Get(defaultEnvironmentId) - suite.True(ok) - suite.NotNil(job) +func (s *ManagerTestSuite) TestRegisterEnvironmentAddsNewJob() { + s.nomadRunnerManager.RegisterEnvironment(anotherEnvironmentId, tests.DefaultJobId, defaultDesiredRunnersCount) + job, ok := s.nomadRunnerManager.jobs.Get(defaultEnvironmentId) + s.True(ok) + s.NotNil(job) } -func (suite *ManagerTestSuite) TestClaimReturnsNotFoundErrorIfEnvironmentNotFound() { - runner, err := suite.nomadRunnerManager.Claim(EnvironmentId(42)) - suite.Nil(runner) - suite.Equal(ErrUnknownExecutionEnvironment, err) +func (s *ManagerTestSuite) TestClaimReturnsNotFoundErrorIfEnvironmentNotFound() { + runner, err := s.nomadRunnerManager.Claim(EnvironmentId(42)) + s.Nil(runner) + s.Equal(ErrUnknownExecutionEnvironment, err) } -func (suite *ManagerTestSuite) TestClaimReturnsRunnerIfAvailable() { - suite.AddIdleRunnerForDefaultEnvironment(suite.exerciseRunner) - receivedRunner, err := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - suite.NoError(err) - suite.Equal(suite.exerciseRunner, receivedRunner) +func (s *ManagerTestSuite) TestClaimReturnsRunnerIfAvailable() { + s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner) + receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentId) + s.NoError(err) + s.Equal(s.exerciseRunner, receivedRunner) } -func (suite *ManagerTestSuite) TestClaimReturnsErrorIfNoRunnerAvailable() { - suite.waitForRunnerRefresh() - runner, err := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - suite.Nil(runner) - suite.Equal(ErrNoRunnersAvailable, err) +func (s *ManagerTestSuite) TestClaimReturnsErrorIfNoRunnerAvailable() { + s.waitForRunnerRefresh() + runner, err := s.nomadRunnerManager.Claim(defaultEnvironmentId) + s.Nil(runner) + s.Equal(ErrNoRunnersAvailable, err) } -func (suite *ManagerTestSuite) TestClaimReturnsNoRunnerOfDifferentEnvironment() { - suite.AddIdleRunnerForDefaultEnvironment(suite.exerciseRunner) - receivedRunner, err := suite.nomadRunnerManager.Claim(anotherEnvironmentId) - suite.Nil(receivedRunner) - suite.Error(err) +func (s *ManagerTestSuite) TestClaimReturnsNoRunnerOfDifferentEnvironment() { + s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner) + receivedRunner, err := s.nomadRunnerManager.Claim(anotherEnvironmentId) + s.Nil(receivedRunner) + s.Error(err) } -func (suite *ManagerTestSuite) TestClaimDoesNotReturnTheSameRunnerTwice() { - suite.AddIdleRunnerForDefaultEnvironment(suite.exerciseRunner) - suite.AddIdleRunnerForDefaultEnvironment(NewRunner(tests.AnotherRunnerId)) +func (s *ManagerTestSuite) TestClaimDoesNotReturnTheSameRunnerTwice() { + s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner) + s.AddIdleRunnerForDefaultEnvironment(NewRunner(tests.AnotherRunnerId)) - firstReceivedRunner, _ := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - secondReceivedRunner, _ := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - suite.NotEqual(firstReceivedRunner, secondReceivedRunner) + firstReceivedRunner, _ := s.nomadRunnerManager.Claim(defaultEnvironmentId) + secondReceivedRunner, _ := s.nomadRunnerManager.Claim(defaultEnvironmentId) + s.NotEqual(firstReceivedRunner, secondReceivedRunner) } -func (suite *ManagerTestSuite) TestClaimThrowsAnErrorIfNoRunnersAvailable() { - receivedRunner, err := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - suite.Nil(receivedRunner) - suite.Error(err) +func (s *ManagerTestSuite) TestClaimThrowsAnErrorIfNoRunnersAvailable() { + receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentId) + s.Nil(receivedRunner) + s.Error(err) } -func (suite *ManagerTestSuite) TestClaimAddsRunnerToUsedRunners() { - suite.mockRunnerQueries([]string{tests.DefaultRunnerId}) - suite.waitForRunnerRefresh() - receivedRunner, _ := suite.nomadRunnerManager.Claim(defaultEnvironmentId) - savedRunner, ok := suite.nomadRunnerManager.usedRunners.Get(receivedRunner.Id()) - suite.True(ok) - suite.Equal(savedRunner, receivedRunner) +func (s *ManagerTestSuite) TestClaimAddsRunnerToUsedRunners() { + s.mockRunnerQueries([]string{tests.DefaultRunnerId}) + s.waitForRunnerRefresh() + receivedRunner, _ := s.nomadRunnerManager.Claim(defaultEnvironmentId) + savedRunner, ok := s.nomadRunnerManager.usedRunners.Get(receivedRunner.Id()) + s.True(ok) + s.Equal(savedRunner, receivedRunner) } -func (suite *ManagerTestSuite) TestGetReturnsRunnerIfRunnerIsUsed() { - suite.nomadRunnerManager.usedRunners.Add(suite.exerciseRunner) - savedRunner, err := suite.nomadRunnerManager.Get(suite.exerciseRunner.Id()) - suite.NoError(err) - suite.Equal(savedRunner, suite.exerciseRunner) +func (s *ManagerTestSuite) TestGetReturnsRunnerIfRunnerIsUsed() { + s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner) + savedRunner, err := s.nomadRunnerManager.Get(s.exerciseRunner.Id()) + s.NoError(err) + s.Equal(savedRunner, s.exerciseRunner) } -func (suite *ManagerTestSuite) TestGetReturnsErrorIfRunnerNotFound() { - savedRunner, err := suite.nomadRunnerManager.Get(tests.DefaultRunnerId) - suite.Nil(savedRunner) - suite.Error(err) +func (s *ManagerTestSuite) TestGetReturnsErrorIfRunnerNotFound() { + savedRunner, err := s.nomadRunnerManager.Get(tests.DefaultRunnerId) + s.Nil(savedRunner) + s.Error(err) } -func (suite *ManagerTestSuite) TestReturnRemovesRunnerFromUsedRunners() { - suite.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil) - suite.nomadRunnerManager.usedRunners.Add(suite.exerciseRunner) - err := suite.nomadRunnerManager.Return(suite.exerciseRunner) - suite.Nil(err) - _, ok := suite.nomadRunnerManager.usedRunners.Get(suite.exerciseRunner.Id()) - suite.False(ok) +func (s *ManagerTestSuite) TestReturnRemovesRunnerFromUsedRunners() { + s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil) + s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner) + err := s.nomadRunnerManager.Return(s.exerciseRunner) + s.Nil(err) + _, ok := s.nomadRunnerManager.usedRunners.Get(s.exerciseRunner.Id()) + s.False(ok) } -func (suite *ManagerTestSuite) TestReturnCallsDeleteRunnerApiMethod() { - suite.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil) - err := suite.nomadRunnerManager.Return(suite.exerciseRunner) - suite.Nil(err) - suite.apiMock.AssertCalled(suite.T(), "DeleteRunner", suite.exerciseRunner.Id()) +func (s *ManagerTestSuite) TestReturnCallsDeleteRunnerApiMethod() { + s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil) + err := s.nomadRunnerManager.Return(s.exerciseRunner) + s.Nil(err) + s.apiMock.AssertCalled(s.T(), "DeleteRunner", s.exerciseRunner.Id()) } -func (suite *ManagerTestSuite) TestReturnReturnsErrorWhenApiCallFailed() { - suite.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(errors.New("return failed")) - err := suite.nomadRunnerManager.Return(suite.exerciseRunner) - suite.Error(err) +func (s *ManagerTestSuite) TestReturnReturnsErrorWhenApiCallFailed() { + s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(errors.New("return failed")) + err := s.nomadRunnerManager.Return(s.exerciseRunner) + s.Error(err) } -func (suite *ManagerTestSuite) TestRefreshFetchesRunners() { - suite.mockRunnerQueries([]string{tests.DefaultRunnerId}) - suite.waitForRunnerRefresh() - suite.apiMock.AssertCalled(suite.T(), "LoadRunners", tests.DefaultJobId) +func (s *ManagerTestSuite) TestRefreshFetchesRunners() { + s.mockRunnerQueries([]string{tests.DefaultRunnerId}) + s.waitForRunnerRefresh() + s.apiMock.AssertCalled(s.T(), "LoadRunners", tests.DefaultJobId) } -func (suite *ManagerTestSuite) TestNewRunnersFoundInRefreshAreAddedToIdleRunners() { - suite.mockRunnerQueries([]string{tests.DefaultRunnerId}) - suite.waitForRunnerRefresh() - job, _ := suite.nomadRunnerManager.jobs.Get(defaultEnvironmentId) +func (s *ManagerTestSuite) TestNewRunnersFoundInRefreshAreAddedToIdleRunners() { + s.mockRunnerQueries([]string{tests.DefaultRunnerId}) + s.waitForRunnerRefresh() + job, _ := s.nomadRunnerManager.jobs.Get(defaultEnvironmentId) _, ok := job.idleRunners.Get(tests.DefaultRunnerId) - suite.True(ok) + s.True(ok) } -func (suite *ManagerTestSuite) TestRefreshScalesJob() { - suite.mockRunnerQueries([]string{tests.DefaultRunnerId}) - suite.waitForRunnerRefresh() +func (s *ManagerTestSuite) TestRefreshScalesJob() { + s.mockRunnerQueries([]string{tests.DefaultRunnerId}) + s.waitForRunnerRefresh() // use one runner to necessitate rescaling - _, _ = suite.nomadRunnerManager.Claim(defaultEnvironmentId) - suite.waitForRunnerRefresh() - suite.apiMock.AssertCalled(suite.T(), "SetJobScale", tests.DefaultJobId, defaultDesiredRunnersCount+1, "Runner Requested") + _, _ = s.nomadRunnerManager.Claim(defaultEnvironmentId) + s.waitForRunnerRefresh() + s.apiMock.AssertCalled(s.T(), "SetJobScale", tests.DefaultJobId, defaultDesiredRunnersCount+1, "Runner Requested") } -func (suite *ManagerTestSuite) TestRefreshAddsRunnerToPool() { - suite.mockRunnerQueries([]string{tests.DefaultRunnerId}) - suite.waitForRunnerRefresh() - job, _ := suite.nomadRunnerManager.jobs.Get(defaultEnvironmentId) +func (s *ManagerTestSuite) TestRefreshAddsRunnerToPool() { + s.mockRunnerQueries([]string{tests.DefaultRunnerId}) + s.waitForRunnerRefresh() + job, _ := s.nomadRunnerManager.jobs.Get(defaultEnvironmentId) poolRunner, ok := job.idleRunners.Get(tests.DefaultRunnerId) - suite.True(ok) - suite.Equal(tests.DefaultRunnerId, poolRunner.Id()) + s.True(ok) + s.Equal(tests.DefaultRunnerId, poolRunner.Id()) +} + +func (s *ManagerTestSuite) TestWhenEnvironmentDoesNotExistEnvironmentExistsReturnsFalse() { + id := anotherEnvironmentId + _, ok := s.nomadRunnerManager.jobs.Get(id) + require.False(s.T(), ok) + + s.False(s.nomadRunnerManager.EnvironmentExists(id)) +} + +func (s *ManagerTestSuite) TestWhenEnvironmentExistsEnvironmentExistsReturnsTrue() { + id := anotherEnvironmentId + s.nomadRunnerManager.jobs.Add(&NomadJob{environmentId: id}) + + exists := s.nomadRunnerManager.EnvironmentExists(id) + s.True(exists) } diff --git a/tests/constants.go b/tests/constants.go index f1d5e7d..9cfb01f 100644 --- a/tests/constants.go +++ b/tests/constants.go @@ -9,6 +9,7 @@ const ( DefaultDirectoryName = "test/" FileNameWithAbsolutePath = "/test.txt" DefaultEnvironmentIdAsInteger = 0 + DefaultEnvironmentIdAsString = "0" AnotherEnvironmentIdAsInteger = 42 DefaultJobId = "s0m3-j0b-1d" AnotherJobId = "4n0th3r-j0b-1d" @@ -19,5 +20,5 @@ const ( ) var ( - DefaultError = errors.New("an error occured") + DefaultError = errors.New("an error occurred") )