diff --git a/task/task.go b/task/task.go index 0abc65d..df3f8e1 100644 --- a/task/task.go +++ b/task/task.go @@ -7,6 +7,15 @@ import ( "github.com/google/uuid" ) +type TaskStatus int + +const ( + Running TaskStatus = iota + Succeeded + Stopped + Failed +) + var ( ErrAlreadyExists = errors.New("task already exists") ErrNotExists = errors.New("task does not exist") @@ -18,5 +27,7 @@ type Task struct { Executable string Args []string - Cmd *exec.Cmd + Status TaskStatus + + Cmd *exec.Cmd `json:"-"` } diff --git a/tests/helper/task.go b/tests/helper/task.go index 9c270f5..ac7d64f 100644 --- a/tests/helper/task.go +++ b/tests/helper/task.go @@ -76,6 +76,12 @@ func PrintFileTask(name, path string) task.Task { } } +func JsonDecodeTask(r io.Reader) task.Task { + var t task.Task + json.NewDecoder(r).Decode(&t) + return t +} + func JsonEncodeTask(t task.Task) io.Reader { w := &bytes.Buffer{} json.NewEncoder(w).Encode(t) diff --git a/tests/worker/get_task_test.go b/tests/worker/get_task_test.go new file mode 100644 index 0000000..8bd0daa --- /dev/null +++ b/tests/worker/get_task_test.go @@ -0,0 +1,144 @@ +package worker + +import ( + "dirigeant/task" + "dirigeant/tests/helper" + "dirigeant/worker" + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestGetTask__ShouldReturnAnErrorIfNotFound(t *testing.T) { + api := &worker.Api{ + Worker: &worker.Worker{}, + } + id := uuid.New() + request := helper.NewTaskGetRequest(id) + responseRecorder := httptest.NewRecorder() + + api.HandleGetTask(responseRecorder, request) + + assert.Equal(t, http.StatusNotFound, responseRecorder.Code, "Response status code should be 404 Not Found") + assert.Equal(t, fmt.Sprintf("A task with %s ID not found", id), responseRecorder.Body.String(), "Response body should contain error message") +} + +func TestGetTask__ShouldReturnFinishedTask(t *testing.T) { + tcs := []struct { + name string + path string + responseStatus int + responseBody string + taskStatus task.TaskStatus + }{ + { + name: "print-hosts-file", + path: helper.HostsFilePath, + responseStatus: http.StatusCreated, + responseBody: "", + taskStatus: task.Succeeded, + }, + { + name: "print-non-existing-file", + path: "non-existing-file.txt", + responseStatus: http.StatusInternalServerError, + responseBody: "Error when executing the task: exit status 1", + taskStatus: task.Failed, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + api := &worker.Api{ + Worker: worker.NewWorker(), + } + testTask := helper.PrintFileTask(tc.name, tc.path) + + // 1 - create a task + createRequest := helper.NewTaskPostRequest(testTask) + createResponseRecorder := httptest.NewRecorder() + + api.HandleCreateTask(createResponseRecorder, createRequest) + + assert.Equal(t, tc.responseStatus, createResponseRecorder.Code) + assert.Equal(t, tc.responseBody, createResponseRecorder.Body.String()) + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, tc.taskStatus, persistedTask.Status) + + // 2 - get a task + getRequest := helper.NewTaskGetRequest(testTask.ID) + getResponseRecorder := httptest.NewRecorder() + + api.HandleGetTask(getResponseRecorder, getRequest) + + assert.Equal(t, http.StatusOK, getResponseRecorder.Code, "Response status code should be 200 OK") + + gottenTask := helper.JsonDecodeTask(getResponseRecorder.Body) + + assert.Equal(t, testTask.ID, gottenTask.ID, "Gotten task ID should match the created one") + assert.Equal(t, tc.taskStatus, gottenTask.Status) + }) + } +} + +func TestGetTask__ShouldReturnRunningTask(t *testing.T) { + api := &worker.Api{ + Worker: worker.NewWorker(), + } + testTask := helper.PingTask("ping-task", "127.0.0.1") + + // 1 - Create a task + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + createRequest := helper.NewTaskPostRequest(testTask) + createResponseRecorder := httptest.NewRecorder() + + api.HandleCreateTask(createResponseRecorder, createRequest) + + assert.Equal(t, http.StatusCreated, createResponseRecorder.Code, "Response status code should be 201 Created") + assert.Empty(t, createResponseRecorder.Body, "Response body should be empty") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Succeeded, persistedTask.Status, "Persisted task status should be Succeeded") + }() + + time.Sleep(1 * time.Second) + + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Running, persistedTask.Status, "Persisted task Status should be Running") + + // 2 - Get a task + getRequest := helper.NewTaskGetRequest(testTask.ID) + getResponseRecorder := httptest.NewRecorder() + + api.HandleGetTask(getResponseRecorder, getRequest) + + assert.Equal(t, http.StatusOK, getResponseRecorder.Code, "Response status code should be 200 OK") + + gottenTask := helper.JsonDecodeTask(getResponseRecorder.Body) + + assert.Equal(t, testTask.ID, gottenTask.ID, "Gotten task ID should match the created one") + assert.Equal(t, task.Running, gottenTask.Status) + + wg.Wait() +} diff --git a/tests/worker/start_task_test.go b/tests/worker/start_task_test.go index 80bcab5..b33e78d 100644 --- a/tests/worker/start_task_test.go +++ b/tests/worker/start_task_test.go @@ -27,8 +27,12 @@ func TestStartTask__ShouldPersistTask(t *testing.T) { assert.Equal(t, http.StatusCreated, responseRecorder.Code, "Response status code should be 201 Created") assert.Empty(t, responseRecorder.Body, "Response body should be empty") - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") - assert.NotNil(t, api.Worker.GetTask(testTask.ID), "Persisted task ID should match the one from request") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Succeeded, persistedTask.Status, "Persisted task Status should be Succeeded") } func TestStartTask__ShouldReturnAnErrorIfCreatingTheSameTaskTwice(t *testing.T) { @@ -45,8 +49,12 @@ func TestStartTask__ShouldReturnAnErrorIfCreatingTheSameTaskTwice(t *testing.T) assert.Equal(t, http.StatusCreated, firstResponseRecorder.Code, "Response status code should be 201 Created") assert.Empty(t, firstResponseRecorder.Body, "Response body should be empty") - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") - assert.NotNil(t, api.Worker.GetTask(testTask.ID), "Persisted task ID should match the one from request") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Succeeded, persistedTask.Status, "Persisted task Status should be Succeeded") // 2 - Create the same task for the second time secondRequest := helper.NewTaskPostRequest(testTask) @@ -56,8 +64,11 @@ func TestStartTask__ShouldReturnAnErrorIfCreatingTheSameTaskTwice(t *testing.T) assert.Equal(t, http.StatusConflict, secondResponseRecorder.Code, "Response status code should be 409 Conflict") assert.Equal(t, fmt.Sprintf("Error when executing the task: %s", task.ErrAlreadyExists), secondResponseRecorder.Body.String(), "Response body should contain error message") - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") - assert.NotNil(t, api.Worker.GetTask(testTask.ID), "Persisted task ID should match the one from request") + + persistedTask = api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Succeeded, persistedTask.Status, "Persisted task Status should be Succeeded") } func TestStartTask__AllButOneRequestsShouldFailIfCreatingTheSameTaskSimultaneously(t *testing.T) { @@ -97,8 +108,12 @@ func TestStartTask__AllButOneRequestsShouldFailIfCreatingTheSameTaskSimultaneous assert.Equal(t, 1, succeededRequests, "There should be only 1 succeeded request") assert.Equal(t, numOfRequests-1, conflictedRequests, "There should be only N-1 conflicted requests") - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") - assert.NotNil(t, api.Worker.GetTask(testTask.ID), "Persisted task ID should match the one from request") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Succeeded, persistedTask.Status, "Persisted task Status should be Succeeded") } func TestStartTask__ShouldHandleClientClosedRequest(t *testing.T) { @@ -124,12 +139,17 @@ func TestStartTask__ShouldHandleClientClosedRequest(t *testing.T) { assert.Equal(t, 499, createResponseRecorder.Code, "Response status code should be 499 Client Closed Request") assert.Equal(t, "Error when executing the task: client closed request", createResponseRecorder.Body.String(), "Response body should contain error message") assert.NotEmpty(t, stdout, "Task logs shouldn't be empty") - assert.Zero(t, api.Worker.LenTasks(), "Tasks map should be empty") + assert.Zero(t, api.Worker.LenTasks(), "Worker should have no tasks") }() time.Sleep(1 * time.Second) - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Running, persistedTask.Status, "Persisted task Status should be Running") // 2 - Cancel a request cancel() diff --git a/tests/worker/stop_task_test.go b/tests/worker/stop_task_test.go index da98d68..546cb70 100644 --- a/tests/worker/stop_task_test.go +++ b/tests/worker/stop_task_test.go @@ -41,7 +41,7 @@ func TestStopTask__ShouldStopCompletedTask(t *testing.T) { assert.Equal(t, http.StatusCreated, responseRecorder.Code, "Response status code should be 201 Created") assert.Empty(t, responseRecorder.Body, "Response body should be empty") - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") // 2 - Delete a task request = helper.NewTaskDeleteRequest(testTask.ID) @@ -51,7 +51,7 @@ func TestStopTask__ShouldStopCompletedTask(t *testing.T) { assert.Equal(t, http.StatusNoContent, responseRecorder.Code, "Response status code should be 204 No Content") assert.Empty(t, responseRecorder.Body, "Response body should be empty") - assert.Zero(t, api.Worker.LenTasks(), "Tasks map should be empty") + assert.Zero(t, api.Worker.LenTasks(), "Worker should have no tasks") } func TestStopTask__ShouldStopRunningTask(t *testing.T) { @@ -73,12 +73,17 @@ func TestStopTask__ShouldStopRunningTask(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, createResponseRecorder.Code, "Response status code should be 500 Internal Server Error") assert.Equal(t, fmt.Sprintf("Error when executing the task: %s", helper.SignalKilledErrMessage), createResponseRecorder.Body.String(), "Response body should contain error message") - assert.Zero(t, api.Worker.LenTasks(), "Tasks map should be empty") + assert.Zero(t, api.Worker.LenTasks(), "Worker should have no tasks") }() time.Sleep(1 * time.Second) - assert.Equal(t, 1, api.Worker.LenTasks(), "Tasks map should contain 1 task") + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, task.Running, persistedTask.Status, "Persisted task Status should be Running") // 2 - Delete a task deleteRequest := helper.NewTaskDeleteRequest(testTask.ID) @@ -88,7 +93,7 @@ func TestStopTask__ShouldStopRunningTask(t *testing.T) { assert.Equal(t, http.StatusNoContent, deleteResponseRecorder.Code, "Response status code should be 204 No Content") assert.Empty(t, deleteResponseRecorder.Body, "Response body should be empty") - assert.Zero(t, api.Worker.LenTasks(), "Tasks map should be empty") + assert.Zero(t, api.Worker.LenTasks(), "Worker should have no tasks") wg.Wait() } diff --git a/tests/worker/task_logs_test.go b/tests/worker/task_logs_test.go index dbf4763..bab7076 100644 --- a/tests/worker/task_logs_test.go +++ b/tests/worker/task_logs_test.go @@ -1,6 +1,7 @@ package worker import ( + "dirigeant/task" "dirigeant/tests/helper" "dirigeant/worker" "fmt" @@ -18,6 +19,7 @@ func TestTaskLogs__PrintFile(t *testing.T) { responseStatus int responseBody string stdoutRegexp string + taskStatus task.TaskStatus }{ { name: "print-hosts-file", @@ -25,6 +27,7 @@ func TestTaskLogs__PrintFile(t *testing.T) { responseStatus: http.StatusCreated, responseBody: "", stdoutRegexp: "localhost", + taskStatus: task.Succeeded, }, { name: "print-non-existing-file", @@ -32,6 +35,7 @@ func TestTaskLogs__PrintFile(t *testing.T) { responseStatus: http.StatusInternalServerError, responseBody: "Error when executing the task: exit status 1", stdoutRegexp: fmt.Sprintf(helper.NoFileErrMessage, "non-existing-file.txt"), + taskStatus: task.Failed, }, } @@ -40,7 +44,8 @@ func TestTaskLogs__PrintFile(t *testing.T) { api := &worker.Api{ Worker: worker.NewWorker(), } - request := helper.NewTaskPostRequest(helper.PrintFileTask(tc.name, tc.path)) + testTask := helper.PrintFileTask(tc.name, tc.path) + request := helper.NewTaskPostRequest(testTask) responseRecorder := httptest.NewRecorder() stdout := helper.CaptureStdout(func() { @@ -49,7 +54,13 @@ func TestTaskLogs__PrintFile(t *testing.T) { assert.Equal(t, tc.responseStatus, responseRecorder.Code) assert.Equal(t, tc.responseBody, responseRecorder.Body.String()) + assert.Equal(t, 1, api.Worker.LenTasks(), "Worker should have 1 task") assert.Regexp(t, tc.stdoutRegexp, stdout) + + persistedTask := api.Worker.GetTask(testTask.ID) + + assert.NotNil(t, persistedTask, "Persisted task ID should match the one from request") + assert.Equal(t, tc.taskStatus, persistedTask.Status) }) } } diff --git a/worker/worker.go b/worker/worker.go index 1438b1d..6c27d32 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -53,6 +53,7 @@ func (w *Worker) StartTask(t task.Task) error { } t.Cmd = exec.Command(t.Executable, t.Args...) + t.Status = task.Running w.tasks[t.ID] = &t w.Unlock() @@ -60,9 +61,12 @@ func (w *Worker) StartTask(t task.Task) error { stdout, err := t.Cmd.CombinedOutput() os.Stdout.Write(stdout) if err != nil { + t.Status = task.Failed return err } + t.Status = task.Succeeded + return nil } @@ -79,6 +83,8 @@ func (w *Worker) StopTask(id uuid.UUID) error { if err := t.Cmd.Process.Kill(); err != nil { return err } + + t.Status = task.Stopped } delete(w.tasks, t.ID)