Add unit tests for separate stdout and stderr on execution

This commit is contained in:
sirkrypt0
2021-06-10 18:16:32 +02:00
committed by Tobias Kantusch
parent f122dd9376
commit d3300e839e
8 changed files with 166 additions and 25 deletions

View File

@ -136,8 +136,8 @@ For example, for an interface called `ExecutorApi` in the package `nomad`, you m
```bash ```bash
mockery \ mockery \
--name=ExecutorApi \ --name=ExecutorApi \
--structname=ExecutorApiMock \ --structname=ExecutorAPIMock \
--filename=ExecutorApiMock.go \ --filename=ExecutorAPIMock.go \
--inpackage --inpackage
``` ```

View File

@ -34,7 +34,7 @@ type WebSocketTestSuite struct {
router *mux.Router router *mux.Router
executionId runner.ExecutionId executionId runner.ExecutionId
runner runner.Runner runner runner.Runner
apiMock *nomad.ExecutorApiMock apiMock *nomad.ExecutorAPIMock
server *httptest.Server server *httptest.Server
} }
@ -307,8 +307,8 @@ func TestRawToCodeOceanWriter(t *testing.T) {
// --- Test suite specific test helpers --- // --- Test suite specific test helpers ---
func newNomadAllocationWithMockedApiClient(runnerId string) (r runner.Runner, mock *nomad.ExecutorApiMock) { func newNomadAllocationWithMockedApiClient(runnerId string) (r runner.Runner, mock *nomad.ExecutorAPIMock) {
mock = &nomad.ExecutorApiMock{} mock = &nomad.ExecutorAPIMock{}
r = runner.NewNomadAllocation(runnerId, mock) r = runner.NewNomadAllocation(runnerId, mock)
return return
} }
@ -335,7 +335,7 @@ func (suite *WebSocketTestSuite) webSocketUrl(scheme, runnerId string, execution
var executionRequestLs = dto.ExecutionRequest{Command: "ls"} var executionRequestLs = dto.ExecutionRequest{Command: "ls"}
// mockApiExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if 'ls existing-file non-existing-file' was executed. // mockApiExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if 'ls existing-file non-existing-file' was executed.
func mockApiExecuteLs(api *nomad.ExecutorApiMock) { func mockApiExecuteLs(api *nomad.ExecutorAPIMock) {
helpers.MockApiExecute(api, &executionRequestLs, helpers.MockApiExecute(api, &executionRequestLs,
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, stdout, stderr io.Writer) (int, error) { func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, stdout, stderr io.Writer) (int, error) {
_, _ = stdout.Write([]byte("existing-file\n")) _, _ = stdout.Write([]byte("existing-file\n"))
@ -347,7 +347,7 @@ func mockApiExecuteLs(api *nomad.ExecutorApiMock) {
var executionRequestHead = dto.ExecutionRequest{Command: "head -n 1"} var executionRequestHead = dto.ExecutionRequest{Command: "head -n 1"}
// mockApiExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed. // mockApiExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed.
func mockApiExecuteHead(api *nomad.ExecutorApiMock) { func mockApiExecuteHead(api *nomad.ExecutorAPIMock) {
helpers.MockApiExecute(api, &executionRequestHead, helpers.MockApiExecute(api, &executionRequestHead,
func(_ string, _ context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) { func(_ string, _ context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
scanner := bufio.NewScanner(stdin) scanner := bufio.NewScanner(stdin)
@ -362,7 +362,7 @@ func mockApiExecuteHead(api *nomad.ExecutorApiMock) {
var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"} var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"}
// mockApiExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep until the execution is canceled. // mockApiExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep until the execution is canceled.
func mockApiExecuteSleep(api *nomad.ExecutorApiMock) <-chan bool { func mockApiExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
canceled := make(chan bool, 1) canceled := make(chan bool, 1)
helpers.MockApiExecute(api, &executionRequestSleep, helpers.MockApiExecute(api, &executionRequestSleep,
func(_ string, ctx context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) { func(_ string, ctx context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
@ -376,7 +376,7 @@ func mockApiExecuteSleep(api *nomad.ExecutorApiMock) <-chan bool {
var executionRequestError = dto.ExecutionRequest{Command: "error"} var executionRequestError = dto.ExecutionRequest{Command: "error"}
// mockApiExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error. // mockApiExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error.
func mockApiExecuteError(api *nomad.ExecutorApiMock) { func mockApiExecuteError(api *nomad.ExecutorAPIMock) {
helpers.MockApiExecute(api, &executionRequestError, helpers.MockApiExecute(api, &executionRequestError,
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) { func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
return 0, errors.New("intended error") return 0, errors.New("intended error")
@ -386,7 +386,7 @@ func mockApiExecuteError(api *nomad.ExecutorApiMock) {
var executionRequestExitNonZero = dto.ExecutionRequest{Command: "exit 42"} var executionRequestExitNonZero = dto.ExecutionRequest{Command: "exit 42"}
// mockApiExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42. // mockApiExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42.
func mockApiExecuteExitNonZero(api *nomad.ExecutorApiMock) { func mockApiExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
helpers.MockApiExecute(api, &executionRequestExitNonZero, helpers.MockApiExecute(api, &executionRequestExitNonZero,
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) { func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
return 42, nil return 42, nil

View File

@ -261,7 +261,7 @@ func TestCreateJobSetsAllGivenArguments(t *testing.T) {
} }
func TestRegisterJobWhenNomadJobRegistrationFails(t *testing.T) { func TestRegisterJobWhenNomadJobRegistrationFails(t *testing.T) {
apiMock := nomad.ExecutorApiMock{} apiMock := nomad.ExecutorAPIMock{}
expectedErr := errors.New("test error") expectedErr := errors.New("test error")
apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr) apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr)
@ -278,7 +278,7 @@ func TestRegisterJobWhenNomadJobRegistrationFails(t *testing.T) {
} }
func TestRegisterJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) { func TestRegisterJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) {
apiMock := nomad.ExecutorApiMock{} apiMock := nomad.ExecutorAPIMock{}
evaluationID := "id" evaluationID := "id"
apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil) apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
@ -295,7 +295,7 @@ func TestRegisterJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) {
} }
func TestRegisterJobReturnsErrorWhenMonitoringEvaluationFails(t *testing.T) { func TestRegisterJobReturnsErrorWhenMonitoringEvaluationFails(t *testing.T) {
apiMock := nomad.ExecutorApiMock{} apiMock := nomad.ExecutorAPIMock{}
evaluationID := "id" evaluationID := "id"
expectedErr := errors.New("test error") expectedErr := errors.New("test error")

View File

@ -16,7 +16,7 @@ import (
type CreateOrUpdateTestSuite struct { type CreateOrUpdateTestSuite struct {
suite.Suite suite.Suite
runnerManagerMock runner.ManagerMock runnerManagerMock runner.ManagerMock
apiMock nomad.ExecutorApiMock apiMock nomad.ExecutorAPIMock
registerNomadJobMockCall *mock.Call registerNomadJobMockCall *mock.Call
request dto.ExecutionEnvironmentRequest request dto.ExecutionEnvironmentRequest
manager *NomadEnvironmentManager manager *NomadEnvironmentManager
@ -28,7 +28,7 @@ func TestCreateOrUpdateTestSuite(t *testing.T) {
func (s *CreateOrUpdateTestSuite) SetupTest() { func (s *CreateOrUpdateTestSuite) SetupTest() {
s.runnerManagerMock = runner.ManagerMock{} s.runnerManagerMock = runner.ManagerMock{}
s.apiMock = nomad.ExecutorApiMock{} s.apiMock = nomad.ExecutorAPIMock{}
s.request = dto.ExecutionEnvironmentRequest{ s.request = dto.ExecutionEnvironmentRequest{
PrewarmingPoolSize: 10, PrewarmingPoolSize: 10,
CPULimit: 20, CPULimit: 20,

View File

@ -1,6 +1,7 @@
package nomad package nomad
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@ -11,8 +12,12 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gitlab.hpi.de/codeocean/codemoon/poseidon/config"
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests" "gitlab.hpi.de/codeocean/codemoon/poseidon/tests"
"io"
"net/url" "net/url"
"regexp"
"strings"
"testing" "testing"
"time" "time"
) )
@ -617,3 +622,139 @@ func createAllocation(modifyTime int64, clientStatus, desiredStatus string) *nom
func createRecentAllocation(clientStatus, desiredStatus string) *nomadApi.Allocation { func createRecentAllocation(clientStatus, desiredStatus string) *nomadApi.Allocation {
return createAllocation(time.Now().Add(time.Minute).UnixNano(), clientStatus, desiredStatus) return createAllocation(time.Now().Add(time.Minute).UnixNano(), clientStatus, desiredStatus)
} }
func TestExecuteCommandTestSuite(t *testing.T) {
suite.Run(t, new(ExecuteCommandTestSuite))
}
type ExecuteCommandTestSuite struct {
suite.Suite
allocationID string
ctx context.Context
testCommand string
testCommandArray []string
expectedStdout string
expectedStderr string
apiMock *apiQuerierMock
nomadAPIClient APIClient
}
func (s *ExecuteCommandTestSuite) SetupTest() {
s.allocationID = "test-allocation-id"
s.ctx = context.Background()
s.testCommand = "echo 'do nothing'"
s.testCommandArray = []string{"sh", "-c", s.testCommand}
s.expectedStdout = "stdout"
s.expectedStderr = "stderr"
s.apiMock = &apiQuerierMock{}
s.nomadAPIClient = APIClient{apiQuerier: s.apiMock}
}
const withTTY = true
func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
config.Config.Server.InteractiveStderr = true
commandExitCode := 42
stderrExitCode := 1
var stdout, stderr bytes.Buffer
var calledStdoutCommand, calledStderrCommand []string
// mock regular call
s.mockExecute(s.testCommandArray, commandExitCode, nil, func(args mock.Arguments) {
var ok bool
calledStdoutCommand, ok = args.Get(2).([]string)
s.Require().True(ok)
writer, ok := args.Get(5).(io.Writer)
s.Require().True(ok)
_, err := writer.Write([]byte(s.expectedStdout))
s.Require().NoError(err)
})
// mock stderr call
s.mockExecute(mock.AnythingOfType("[]string"), stderrExitCode, nil, func(args mock.Arguments) {
var ok bool
calledStderrCommand, ok = args.Get(2).([]string)
s.Require().True(ok)
writer, ok := args.Get(5).(io.Writer)
s.Require().True(ok)
_, err := writer.Write([]byte(s.expectedStderr))
s.Require().NoError(err)
})
exitCode, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullReader{}, &stdout, &stderr)
s.Require().NoError(err)
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 2)
s.Equal(commandExitCode, exitCode)
s.Run("should wrap command in stderr wrapper", func() {
s.Require().NotNil(calledStdoutCommand)
stdoutFifoRegexp := strings.ReplaceAll(regexp.QuoteMeta(stderrWrapperCommandFormat), "%d", "\\d*")
stdoutFifoRegexp = fmt.Sprintf(stdoutFifoRegexp, s.testCommand)
s.Regexp(stdoutFifoRegexp, calledStdoutCommand[len(calledStdoutCommand)-1])
})
s.Run("should call correct stderr command", func() {
s.Require().NotNil(calledStderrCommand)
stderrFifoRegexp := strings.ReplaceAll(regexp.QuoteMeta(stderrFifoCommandFormat), "%d", "\\d*")
s.Regexp(stderrFifoRegexp, calledStderrCommand[len(calledStderrCommand)-1])
})
s.Run("should return correct output", func() {
s.Equal(s.expectedStdout, stdout.String())
s.Equal(s.expectedStderr, stderr.String())
})
}
func (s *ExecuteCommandTestSuite) TestWithSeparateStderrReturnsCommandError() {
config.Config.Server.InteractiveStderr = true
s.mockExecute(s.testCommandArray, 1, tests.ErrDefault, func(args mock.Arguments) {})
s.mockExecute(mock.AnythingOfType("[]string"), 1, nil, func(args mock.Arguments) {})
_, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullReader{}, io.Discard, io.Discard)
s.Equal(tests.ErrDefault, err)
}
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
config.Config.Server.InteractiveStderr = false
var stdout, stderr bytes.Buffer
commandExitCode := 42
// mock regular call
s.mockExecute(s.testCommandArray, commandExitCode, nil, func(args mock.Arguments) {
stdout, ok := args.Get(5).(io.Writer)
s.Require().True(ok)
_, err := stdout.Write([]byte(s.expectedStdout))
s.Require().NoError(err)
stderr, ok := args.Get(6).(io.Writer)
s.Require().True(ok)
_, err = stderr.Write([]byte(s.expectedStderr))
s.Require().NoError(err)
})
exitCode, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullReader{}, &stdout, &stderr)
s.Require().NoError(err)
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 1)
s.Equal(commandExitCode, exitCode)
s.Equal(s.expectedStdout, stdout.String())
s.Equal(s.expectedStderr, stderr.String())
}
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderrReturnsCommandError() {
config.Config.Server.InteractiveStderr = false
s.mockExecute(s.testCommandArray, 1, tests.ErrDefault, func(args mock.Arguments) {})
_, err := s.nomadAPIClient.
ExecuteCommand(s.allocationID, s.ctx, s.testCommandArray, withTTY, nullReader{}, io.Discard, io.Discard)
s.Equal(tests.ErrDefault, err)
}
func (s *ExecuteCommandTestSuite) mockExecute(command interface{}, exitCode int,
err error, runFunc func(arguments mock.Arguments)) {
s.apiMock.On("Execute", s.allocationID, s.ctx, command, withTTY,
mock.Anything, mock.Anything, mock.Anything).
Run(runFunc).
Return(exitCode, err)
}

View File

@ -25,13 +25,13 @@ func TestGetNextRunnerTestSuite(t *testing.T) {
type ManagerTestSuite struct { type ManagerTestSuite struct {
suite.Suite suite.Suite
apiMock *nomad.ExecutorApiMock apiMock *nomad.ExecutorAPIMock
nomadRunnerManager *NomadRunnerManager nomadRunnerManager *NomadRunnerManager
exerciseRunner Runner exerciseRunner Runner
} }
func (s *ManagerTestSuite) SetupTest() { func (s *ManagerTestSuite) SetupTest() {
s.apiMock = &nomad.ExecutorApiMock{} s.apiMock = &nomad.ExecutorAPIMock{}
// Instantly closed context to manually start the update process in some cases // Instantly closed context to manually start the update process in some cases
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
@ -42,7 +42,7 @@ func (s *ManagerTestSuite) SetupTest() {
s.registerDefaultEnvironment() s.registerDefaultEnvironment()
} }
func mockRunnerQueries(apiMock *nomad.ExecutorApiMock, returnedRunnerIds []string) { func mockRunnerQueries(apiMock *nomad.ExecutorAPIMock, returnedRunnerIds []string) {
// reset expected calls to allow new mocked return values // reset expected calls to allow new mocked return values
apiMock.ExpectedCalls = []*mock.Call{} apiMock.ExpectedCalls = []*mock.Call{}
call := apiMock.On("WatchAllocations", mock.Anything, mock.Anything, mock.Anything) call := apiMock.On("WatchAllocations", mock.Anything, mock.Anything, mock.Anything)
@ -273,7 +273,7 @@ func (s *ManagerTestSuite) TestUpdateRunnersRemovesIdleAndUsedRunner() {
s.False(ok) s.False(ok)
} }
func modifyMockedCall(apiMock *nomad.ExecutorApiMock, method string, modifier func(call *mock.Call)) { func modifyMockedCall(apiMock *nomad.ExecutorAPIMock, method string, modifier func(call *mock.Call)) {
for _, c := range apiMock.ExpectedCalls { for _, c := range apiMock.ExpectedCalls {
if c.Method == method { if c.Method == method {
modifier(c) modifier(c)

View File

@ -73,7 +73,7 @@ func TestFromContextReturnsIsNotOkWhenContextHasNoRunner(t *testing.T) {
} }
func TestExecuteCallsAPI(t *testing.T) { func TestExecuteCallsAPI(t *testing.T) {
apiMock := &nomad.ExecutorApiMock{} apiMock := &nomad.ExecutorAPIMock{}
apiMock.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, true, mock.Anything, mock.Anything, mock.Anything).Return(0, nil) apiMock.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, true, mock.Anything, mock.Anything, mock.Anything).Return(0, nil)
runner := NewNomadAllocation(tests.DefaultRunnerID, apiMock) runner := NewNomadAllocation(tests.DefaultRunnerID, apiMock)
@ -106,8 +106,8 @@ func TestExecuteReturnsAfterTimeout(t *testing.T) {
} }
} }
func newApiMockWithTimeLimitHandling() (apiMock *nomad.ExecutorApiMock) { func newApiMockWithTimeLimitHandling() (apiMock *nomad.ExecutorAPIMock) {
apiMock = &nomad.ExecutorApiMock{} apiMock = &nomad.ExecutorAPIMock{}
apiMock. apiMock.
On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, true, mock.Anything, mock.Anything, mock.Anything). On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, true, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) { Run(func(args mock.Arguments) {
@ -125,14 +125,14 @@ func TestUpdateFileSystemTestSuite(t *testing.T) {
type UpdateFileSystemTestSuite struct { type UpdateFileSystemTestSuite struct {
suite.Suite suite.Suite
runner *NomadAllocation runner *NomadAllocation
apiMock *nomad.ExecutorApiMock apiMock *nomad.ExecutorAPIMock
mockedExecuteCommandCall *mock.Call mockedExecuteCommandCall *mock.Call
command []string command []string
stdin *bytes.Buffer stdin *bytes.Buffer
} }
func (s *UpdateFileSystemTestSuite) SetupTest() { func (s *UpdateFileSystemTestSuite) SetupTest() {
s.apiMock = &nomad.ExecutorApiMock{} s.apiMock = &nomad.ExecutorAPIMock{}
s.runner = NewNomadAllocation(tests.DefaultRunnerID, s.apiMock) s.runner = NewNomadAllocation(tests.DefaultRunnerID, s.apiMock)
s.mockedExecuteCommandCall = s.apiMock.On("ExecuteCommand", tests.DefaultRunnerID, mock.Anything, mock.Anything, false, mock.Anything, mock.Anything, mock.Anything). s.mockedExecuteCommandCall = s.apiMock.On("ExecuteCommand", tests.DefaultRunnerID, mock.Anything, mock.Anything, false, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) { Run(func(args mock.Arguments) {

View File

@ -114,7 +114,7 @@ func StartTLSServer(t *testing.T, router *mux.Router) (server *httptest.Server,
// MockApiExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command // MockApiExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command
// corresponding to the given ExecutionRequest is called. // corresponding to the given ExecutionRequest is called.
func MockApiExecute(api *nomad.ExecutorApiMock, request *dto.ExecutionRequest, func MockApiExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
run func(runnerId string, ctx context.Context, command []string, tty bool, stdin io.Reader, stdout, stderr io.Writer) (int, error)) { run func(runnerId string, ctx context.Context, command []string, tty bool, stdin io.Reader, stdout, stderr io.Writer) (int, error)) {
call := api.On("ExecuteCommand", call := api.On("ExecuteCommand",
mock.AnythingOfType("string"), mock.AnythingOfType("string"),