From 4ffbb712edbacf302cef5a82acee519ed17b5abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Pa=C3=9F?= <22845248+mpass99@users.noreply.github.com> Date: Thu, 3 Feb 2022 14:32:05 +0100 Subject: [PATCH] Parametrize e2e tests to also check AWS environments. - Fix destroy runner after timeout. - Add file deletion --- .github/workflows/ci.yml | 4 + internal/environment/aws_environment.go | 13 +- internal/environment/aws_manager.go | 2 +- internal/environment/aws_manager_test.go | 6 +- internal/runner/aws_runner.go | 63 +++- internal/runner/inactivity_timer.go | 4 +- internal/runner/nomad_runner.go | 4 +- internal/runner/nomad_runner_test.go | 2 +- internal/runner/runner.go | 2 +- tests/e2e/e2e_test.go | 53 +++- tests/e2e/environments_test.go | 107 ++++--- tests/e2e/runners_test.go | 386 +++++++++++++---------- tests/e2e/websocket_test.go | 201 ++++++------ 13 files changed, 505 insertions(+), 342 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c665d7..180ff0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,10 @@ jobs: e2e-test: runs-on: ubuntu-latest needs: [ compile, dep-scan, test ] + env: + POSEIDON_AWS_ENABLED: true + POSEIDON_AWS_ENDPOINT: ${{ secrets.POSEIDON_AWS_ENDPOINT }} + POSEIDON_AWS_FUNCTIONS: ${{ secrets.POSEIDON_AWS_FUNCTIONS }} steps: - name: Checkout repository uses: actions/checkout@v2 diff --git a/internal/environment/aws_environment.go b/internal/environment/aws_environment.go index a927b92..63d7424 100644 --- a/internal/environment/aws_environment.go +++ b/internal/environment/aws_environment.go @@ -8,12 +8,13 @@ import ( ) type AWSEnvironment struct { - id dto.EnvironmentID - awsEndpoint string + id dto.EnvironmentID + awsEndpoint string + onDestroyRunner runner.DestroyRunnerHandler } -func NewAWSEnvironment() *AWSEnvironment { - return &AWSEnvironment{} +func NewAWSEnvironment(onDestroyRunner runner.DestroyRunnerHandler) *AWSEnvironment { + return &AWSEnvironment{onDestroyRunner: onDestroyRunner} } func (a *AWSEnvironment) MarshalJSON() ([]byte, error) { @@ -86,11 +87,11 @@ func (a *AWSEnvironment) Register() error { } func (a *AWSEnvironment) Delete() error { - panic("implement me") + return nil } func (a *AWSEnvironment) Sample() (r runner.Runner, ok bool) { - workload, err := runner.NewAWSFunctionWorkload(a, nil) + workload, err := runner.NewAWSFunctionWorkload(a, a.onDestroyRunner) if err != nil { return nil, false } diff --git a/internal/environment/aws_manager.go b/internal/environment/aws_manager.go index 1ec4138..0df373b 100644 --- a/internal/environment/aws_manager.go +++ b/internal/environment/aws_manager.go @@ -53,7 +53,7 @@ func (a *AWSEnvironmentManager) CreateOrUpdate( } _, ok := a.runnerManager.GetEnvironment(id) - e := NewAWSEnvironment() + e := NewAWSEnvironment(a.runnerManager.Return) e.SetID(id) e.SetImage(request.Image) a.runnerManager.StoreEnvironment(e) diff --git a/internal/environment/aws_manager_test.go b/internal/environment/aws_manager_test.go index 43c50b5..6dd9c40 100644 --- a/internal/environment/aws_manager_test.go +++ b/internal/environment/aws_manager_test.go @@ -66,7 +66,7 @@ func TestAWSEnvironmentManager_Get(t *testing.T) { }) t.Run("Returns environment when it was added before", func(t *testing.T) { - expectedEnvironment := NewAWSEnvironment() + expectedEnvironment := NewAWSEnvironment(nil) expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) runnerManager.StoreEnvironment(expectedEnvironment) @@ -82,7 +82,7 @@ func TestAWSEnvironmentManager_List(t *testing.T) { t.Run("returs also environments of the rest of the manager chain", func(t *testing.T) { nextHandler := &ManagerHandlerMock{} - existingEnvironment := NewAWSEnvironment() + existingEnvironment := NewAWSEnvironment(nil) nextHandler.On("List", mock.AnythingOfType("bool")). Return([]runner.ExecutionEnvironment{existingEnvironment}, nil) m.SetNextHandler(nextHandler) @@ -95,7 +95,7 @@ func TestAWSEnvironmentManager_List(t *testing.T) { m.SetNextHandler(nil) t.Run("Returns added environment", func(t *testing.T) { - localEnvironment := NewAWSEnvironment() + localEnvironment := NewAWSEnvironment(nil) localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) runnerManager.StoreEnvironment(localEnvironment) diff --git a/internal/runner/aws_runner.go b/internal/runner/aws_runner.go index 833dd47..b244377 100644 --- a/internal/runner/aws_runner.go +++ b/internal/runner/aws_runner.go @@ -24,29 +24,33 @@ type awsFunctionRequest struct { // AWSFunctionWorkload is an abstraction to build a request to an AWS Lambda Function. type AWSFunctionWorkload struct { InactivityTimer - id string - fs map[dto.FilePath][]byte - executions execution.Storer - onDestroy destroyRunnerHandler - environment ExecutionEnvironment + id string + fs map[dto.FilePath][]byte + executions execution.Storer + runningExecutions map[execution.ID]context.CancelFunc + onDestroy DestroyRunnerHandler + environment ExecutionEnvironment } // NewAWSFunctionWorkload creates a new AWSFunctionWorkload with the provided id. func NewAWSFunctionWorkload( - environment ExecutionEnvironment, onDestroy destroyRunnerHandler) (*AWSFunctionWorkload, error) { + environment ExecutionEnvironment, onDestroy DestroyRunnerHandler) (*AWSFunctionWorkload, error) { newUUID, err := uuid.NewUUID() if err != nil { return nil, fmt.Errorf("failed generating runner id: %w", err) } workload := &AWSFunctionWorkload{ - id: newUUID.String(), - fs: make(map[dto.FilePath][]byte), - executions: execution.NewLocalStorage(), - onDestroy: onDestroy, - environment: environment, + id: newUUID.String(), + fs: make(map[dto.FilePath][]byte), + executions: execution.NewLocalStorage(), + runningExecutions: make(map[execution.ID]context.CancelFunc), + onDestroy: onDestroy, + environment: environment, } - workload.InactivityTimer = NewInactivityTimer(workload, onDestroy) + workload.InactivityTimer = NewInactivityTimer(workload, func(_ Runner) error { + return workload.Destroy() + }) return workload, nil } @@ -73,16 +77,21 @@ func (w *AWSFunctionWorkload) ExecuteInteractively(id string, _ io.ReadWriter, s if !ok { return nil, nil, ErrorUnknownExecution } - command, ctx, cancel := prepareExecution(request) + exitInternal := make(chan ExitInfo) exit := make(chan ExitInfo, 1) - go w.executeCommand(ctx, command, stdout, stderr, exit) + + go w.executeCommand(ctx, command, stdout, stderr, exitInternal) + go w.handleRunnerTimeout(ctx, exitInternal, exit, execution.ID(id)) + return exit, cancel, nil } // UpdateFileSystem copies Files into the executor. -// ToDo: Currently, file deletion is not supported (but it could be). func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequest) error { + for _, path := range request.Delete { + delete(w.fs, path) + } for _, file := range request.Copy { w.fs[file.Path] = file.Content } @@ -90,6 +99,9 @@ func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequ } func (w *AWSFunctionWorkload) Destroy() error { + for _, cancel := range w.runningExecutions { + cancel() + } if err := w.onDestroy(w); err != nil { return fmt.Errorf("error while destroying aws runner: %w", err) } @@ -99,6 +111,7 @@ func (w *AWSFunctionWorkload) Destroy() error { func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []string, stdout, stderr io.Writer, exit chan<- ExitInfo, ) { + defer close(exit) data := &awsFunctionRequest{ Action: w.environment.Image(), Cmd: command, @@ -128,7 +141,6 @@ func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []stri err = ErrorRunnerInactivityTimeout } exit <- ExitInfo{exitCode, err} - close(exit) } func (w *AWSFunctionWorkload) receiveOutput( @@ -157,7 +169,7 @@ func (w *AWSFunctionWorkload) receiveOutput( case dto.WebSocketOutputStdout: // We do not check the written bytes as the rawToCodeOceanWriter receives everything or nothing. _, err = stdout.Write([]byte(wsMessage.Data)) - case dto.WebSocketOutputStderr: + case dto.WebSocketOutputStderr, dto.WebSocketOutputError: _, err = stderr.Write([]byte(wsMessage.Data)) } if err != nil { @@ -166,3 +178,20 @@ func (w *AWSFunctionWorkload) receiveOutput( } return 1, fmt.Errorf("receiveOutput stpped by context: %w", ctx.Err()) } + +// handleRunnerTimeout listens for a runner timeout and aborts the execution in that case. +// It listens via a context in runningExecutions that is canceled on the timeout event. +func (w *AWSFunctionWorkload) handleRunnerTimeout(ctx context.Context, + exitInternal <-chan ExitInfo, exit chan<- ExitInfo, id execution.ID) { + executionCtx, cancelExecution := context.WithCancel(ctx) + w.runningExecutions[id] = cancelExecution + defer delete(w.runningExecutions, id) + defer close(exit) + + select { + case exitInfo := <-exitInternal: + exit <- exitInfo + case <-executionCtx.Done(): + exit <- ExitInfo{255, ErrorRunnerInactivityTimeout} + } +} diff --git a/internal/runner/inactivity_timer.go b/internal/runner/inactivity_timer.go index 3bda980..689b53a 100644 --- a/internal/runner/inactivity_timer.go +++ b/internal/runner/inactivity_timer.go @@ -37,11 +37,11 @@ type InactivityTimerImplementation struct { duration time.Duration state TimerState runner Runner - onDestroy destroyRunnerHandler + onDestroy DestroyRunnerHandler mu sync.Mutex } -func NewInactivityTimer(runner Runner, onDestroy destroyRunnerHandler) InactivityTimer { +func NewInactivityTimer(runner Runner, onDestroy DestroyRunnerHandler) InactivityTimer { return &InactivityTimerImplementation{ state: TimerInactive, runner: runner, diff --git a/internal/runner/nomad_runner.go b/internal/runner/nomad_runner.go index 320fc28..f9fc758 100644 --- a/internal/runner/nomad_runner.go +++ b/internal/runner/nomad_runner.go @@ -41,12 +41,12 @@ type NomadJob struct { id string portMappings []nomadApi.PortMapping api nomad.ExecutorAPI - onDestroy destroyRunnerHandler + onDestroy DestroyRunnerHandler } // NewNomadJob creates a new NomadJob with the provided id. func NewNomadJob(id string, portMappings []nomadApi.PortMapping, - apiClient nomad.ExecutorAPI, onDestroy destroyRunnerHandler, + apiClient nomad.ExecutorAPI, onDestroy DestroyRunnerHandler, ) *NomadJob { job := &NomadJob{ id: id, diff --git a/internal/runner/nomad_runner_test.go b/internal/runner/nomad_runner_test.go index a97ebc7..d2d020b 100644 --- a/internal/runner/nomad_runner_test.go +++ b/internal/runner/nomad_runner_test.go @@ -391,7 +391,7 @@ func (s *UpdateFileSystemTestSuite) readFilesFromTarArchive(tarArchive io.Reader // NewRunner creates a new runner with the provided id and manager. func NewRunner(id string, manager Accessor) Runner { - var handler destroyRunnerHandler + var handler DestroyRunnerHandler if manager != nil { handler = manager.Return } else { diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 9973a36..cd09de8 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -11,7 +11,7 @@ type ExitInfo struct { Err error } -type destroyRunnerHandler = func(r Runner) error +type DestroyRunnerHandler = func(r Runner) error type Runner interface { InactivityTimer diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 37adde2..ee3dc5e 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/suite" "net/http" "os" + "strings" "testing" "time" ) @@ -27,6 +28,7 @@ var ( testDockerImage = flag.String("dockerImage", "", "Docker image to use in E2E tests") nomadClient *nomadApi.Client nomadNamespace string + environmentIDs []dto.EnvironmentID ) type E2ETestSuite struct { @@ -45,11 +47,42 @@ func TestE2ETestSuite(t *testing.T) { // Overwrite TestMain for custom setup. func TestMain(m *testing.M) { log.Info("Test Setup") - err := config.InitConfig() - if err != nil { + if err := config.InitConfig(); err != nil { log.WithError(err).Fatal("Could not initialize configuration") } + initNomad() + initAWS() + + // wait for environment to become ready + <-time.After(10 * time.Second) + log.Info("Test Run") + code := m.Run() + + deleteE2EEnvironments() + cleanupJobsForEnvironment(&testing.T{}, tests.DefaultEnvironmentIDAsString) + os.Exit(code) +} + +func initAWS() { + for i, function := range strings.Fields(config.Config.AWS.Functions) { + id := dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger + i + 1) + path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, id.ToString()) + request := dto.ExecutionEnvironmentRequest{Image: function} + resp, err := helpers.HTTPPutJSON(path, request) + if err != nil || resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + log.WithField("function", function).WithError(err).Fatal("Couldn't create default environment for e2e tests") + } + environmentIDs = append(environmentIDs, id) + err = resp.Body.Close() + if err != nil { + log.Fatal("Failed closing body") + } + } +} + +func initNomad() { nomadNamespace = config.Config.Nomad.Namespace + var err error nomadClient, err = nomadApi.NewClient(&nomadApi.Config{ Address: config.Config.Nomad.URL().String(), TLSConfig: &nomadApi.TLSConfig{}, @@ -57,16 +90,9 @@ func TestMain(m *testing.M) { }) if err != nil { log.WithError(err).Fatal("Could not create Nomad client") + return } - log.Info("Test Run") createDefaultEnvironment() - - // wait for environment to become ready - <-time.After(10 * time.Second) - - code := m.Run() - cleanupJobsForEnvironment(&testing.T{}, tests.DefaultEnvironmentIDAsString) - os.Exit(code) } func createDefaultEnvironment() { @@ -89,8 +115,15 @@ func createDefaultEnvironment() { if err != nil || resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { log.WithError(err).Fatal("Couldn't create default environment for e2e tests") } + environmentIDs = append(environmentIDs, tests.DefaultEnvironmentIDAsInteger) err = resp.Body.Close() if err != nil { log.Fatal("Failed closing body") } } + +func deleteE2EEnvironments() { + for _, id := range environmentIDs { + deleteEnvironment(&testing.T{}, id.ToString()) + } +} diff --git a/tests/e2e/environments_test.go b/tests/e2e/environments_test.go index 79b816f..fe921b2 100644 --- a/tests/e2e/environments_test.go +++ b/tests/e2e/environments_test.go @@ -2,8 +2,10 @@ package e2e import ( "encoding/json" + "fmt" nomadApi "github.com/hashicorp/nomad/api" "github.com/openHPI/poseidon/internal/api" + "github.com/openHPI/poseidon/internal/config" "github.com/openHPI/poseidon/internal/nomad" "github.com/openHPI/poseidon/pkg/dto" "github.com/openHPI/poseidon/tests" @@ -17,6 +19,8 @@ import ( "time" ) +var isAWSEnvironment = []bool{false, true} + func TestCreateOrUpdateEnvironment(t *testing.T) { path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) @@ -73,13 +77,13 @@ func TestCreateOrUpdateEnvironment(t *testing.T) { func TestListEnvironments(t *testing.T) { path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath) - t.Run("returns list with one element", func(t *testing.T) { + t.Run("returns list with all static and the e2e environment", func(t *testing.T) { response, err := http.Get(path) //nolint:gosec // because we build this path right above require.NoError(t, err) assert.Equal(t, http.StatusOK, response.StatusCode) environmentsArray := assertEnvironmentArrayInResponse(t, response) - assert.Equal(t, 1, len(environmentsArray)) + assert.Equal(t, len(environmentIDs), len(environmentsArray)) }) t.Run("returns list including the default environment", func(t *testing.T) { @@ -88,24 +92,28 @@ func TestListEnvironments(t *testing.T) { require.Equal(t, http.StatusOK, response.StatusCode) environmentsArray := assertEnvironmentArrayInResponse(t, response) - require.Equal(t, 1, len(environmentsArray)) - - assertEnvironment(t, environmentsArray[0], tests.DefaultEnvironmentIDAsInteger) - }) - - t.Run("Added environments can be retrieved without fetch", func(t *testing.T) { - createEnvironment(t, tests.AnotherEnvironmentIDAsString) - - response, err := http.Get(path) //nolint:gosec // because we build this path right above - require.NoError(t, err) - require.Equal(t, http.StatusOK, response.StatusCode) - - environmentsArray := assertEnvironmentArrayInResponse(t, response) - require.Equal(t, 2, len(environmentsArray)) + require.Equal(t, len(environmentIDs), len(environmentsArray)) foundIDs := parseIDsFromEnvironments(t, environmentsArray) - assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)) + assert.Contains(t, foundIDs, dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) }) - deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) + + for _, useAWS := range isAWSEnvironment { + t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) { + t.Run("Added environments can be retrieved without fetch", func(t *testing.T) { + createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS) + + response, err := http.Get(path) //nolint:gosec // because we build this path right above + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + + environmentsArray := assertEnvironmentArrayInResponse(t, response) + require.Equal(t, len(environmentIDs)+1, len(environmentsArray)) + foundIDs := parseIDsFromEnvironments(t, environmentsArray) + assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)) + }) + deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) + }) + } t.Run("Added environments can be retrieved with fetch", func(t *testing.T) { // Add environment without Poseidon @@ -122,16 +130,17 @@ func TestListEnvironments(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, response.StatusCode) environmentsArray := assertEnvironmentArrayInResponse(t, response) - require.Equal(t, 1, len(environmentsArray)) - assertEnvironment(t, environmentsArray[0], tests.DefaultEnvironmentIDAsInteger) + require.Equal(t, len(environmentIDs), len(environmentsArray)) + foundIDs := parseIDsFromEnvironments(t, environmentsArray) + assert.Contains(t, foundIDs, dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) // List with fetch should include the added environment response, err = http.Get(path + "?fetch=true") //nolint:gosec // because we build this path right above require.NoError(t, err) require.Equal(t, http.StatusOK, response.StatusCode) environmentsArray = assertEnvironmentArrayInResponse(t, response) - require.Equal(t, 2, len(environmentsArray)) - foundIDs := parseIDsFromEnvironments(t, environmentsArray) + require.Equal(t, len(environmentIDs)+1, len(environmentsArray)) + foundIDs = parseIDsFromEnvironments(t, environmentsArray) assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)) }) deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) @@ -148,18 +157,22 @@ func TestGetEnvironment(t *testing.T) { assertEnvironment(t, environment, tests.DefaultEnvironmentIDAsInteger) }) - t.Run("Added environments can be retrieved without fetch", func(t *testing.T) { - createEnvironment(t, tests.AnotherEnvironmentIDAsString) + for _, useAWS := range isAWSEnvironment { + t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) { + t.Run("Added environments can be retrieved without fetch", func(t *testing.T) { + createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS) - path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) - response, err := http.Get(path) //nolint:gosec // because we build this path right above - require.NoError(t, err) - require.Equal(t, http.StatusOK, response.StatusCode) + path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) + response, err := http.Get(path) //nolint:gosec // because we build this path right above + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) - environment := getEnvironmentFromResponse(t, response) - assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger) - }) - deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) + environment := getEnvironmentFromResponse(t, response) + assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger) + }) + deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) + }) + } t.Run("Added environments can be retrieved with fetch", func(t *testing.T) { // Add environment without Poseidon @@ -188,17 +201,21 @@ func TestGetEnvironment(t *testing.T) { } func TestDeleteEnvironment(t *testing.T) { - t.Run("Removes added environment", func(t *testing.T) { - createEnvironment(t, tests.AnotherEnvironmentIDAsString) + for _, useAWS := range isAWSEnvironment { + t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) { + t.Run("Removes added environment", func(t *testing.T) { + createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS) - path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) - response, err := helpers.HTTPDelete(path, nil) - assert.NoError(t, err) - assert.Equal(t, http.StatusNoContent, response.StatusCode) - }) + path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) + response, err := helpers.HTTPDelete(path, nil) + assert.NoError(t, err) + assert.Equal(t, http.StatusNoContent, response.StatusCode) + }) + }) + } t.Run("Removes Nomad Job", func(t *testing.T) { - createEnvironment(t, tests.AnotherEnvironmentIDAsString) + createEnvironment(t, tests.AnotherEnvironmentIDAsString, false) // Expect created Nomad job jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger) @@ -295,17 +312,23 @@ func cleanupJobsForEnvironment(t *testing.T, environmentID string) { } //nolint:unparam // Because its more clear if the environment id is written in the real test -func createEnvironment(t *testing.T, environmentID string) { +func createEnvironment(t *testing.T, environmentID string, aws bool) { t.Helper() path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, environmentID) request := dto.ExecutionEnvironmentRequest{ PrewarmingPoolSize: 1, CPULimit: 100, MemoryLimit: 100, - Image: *testDockerImage, NetworkAccess: false, ExposedPorts: nil, } + if aws { + functions := strings.Fields(config.Config.AWS.Functions) + require.NotZero(t, len(functions)) + request.Image = functions[0] + } else { + request.Image = *testDockerImage + } assertPutReturnsStatusAndZeroContent(t, path, request, http.StatusCreated) } diff --git a/tests/e2e/runners_test.go b/tests/e2e/runners_test.go index 1719214..ca6d554 100644 --- a/tests/e2e/runners_test.go +++ b/tests/e2e/runners_test.go @@ -16,39 +16,41 @@ import ( ) func (s *E2ETestSuite) TestProvideRunnerRoute() { - runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - }) - s.Require().NoError(err) - reader := bytes.NewReader(runnerRequestByteString) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) + s.Require().NoError(err) + reader := bytes.NewReader(runnerRequestByteString) - s.Run("valid request returns a runner", func() { - resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) - s.Require().NoError(err) - s.Equal(http.StatusOK, resp.StatusCode) + s.Run("valid request returns a runner", func() { + resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) + s.Require().NoError(err) + s.Equal(http.StatusOK, resp.StatusCode) - runnerResponse := new(dto.RunnerResponse) - err = json.NewDecoder(resp.Body).Decode(runnerResponse) - s.Require().NoError(err) - s.NotEmpty(runnerResponse.ID) - }) + runnerResponse := new(dto.RunnerResponse) + err = json.NewDecoder(resp.Body).Decode(runnerResponse) + s.Require().NoError(err) + s.NotEmpty(runnerResponse.ID) + }) - s.Run("invalid request returns bad request", func() { - resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", strings.NewReader("")) - s.Require().NoError(err) - s.Equal(http.StatusBadRequest, resp.StatusCode) - }) + s.Run("invalid request returns bad request", func() { + resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", strings.NewReader("")) + s.Require().NoError(err) + s.Equal(http.StatusBadRequest, resp.StatusCode) + }) - s.Run("requesting runner of unknown execution environment returns not found", func() { - runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ - ExecutionEnvironmentID: tests.NonExistingIntegerID, + s.Run("requesting runner of unknown execution environment returns not found", func() { + runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ + ExecutionEnvironmentID: tests.NonExistingIntegerID, + }) + s.Require().NoError(err) + reader := bytes.NewReader(runnerRequestByteString) + resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) + s.Require().NoError(err) + s.Equal(http.StatusNotFound, resp.StatusCode) + }) }) - s.Require().NoError(err) - reader := bytes.NewReader(runnerRequestByteString) - resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) - s.Require().NoError(err) - s.Equal(http.StatusNotFound, resp.StatusCode) - }) + } } // ProvideRunner creates a runner with the given RunnerRequest via an external request. @@ -77,117 +79,143 @@ func ProvideRunner(request *dto.RunnerRequest) (string, error) { } func (s *E2ETestSuite) TestDeleteRunnerRoute() { - runnerID, err := ProvideRunner(&dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - }) - s.NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) + s.NoError(err) - s.Run("Deleting the runner returns NoContent", func() { - resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) - s.NoError(err) - s.Equal(http.StatusNoContent, resp.StatusCode) - }) + s.Run("Deleting the runner returns NoContent", func() { + resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + }) - s.Run("Deleting it again returns NotFound", func() { - resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) - s.NoError(err) - s.Equal(http.StatusNotFound, resp.StatusCode) - }) + s.Run("Deleting it again returns NotFound", func() { + resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) + s.NoError(err) + s.Equal(http.StatusNotFound, resp.StatusCode) + }) - s.Run("Deleting non-existing runner returns NotFound", func() { - resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID), nil) - s.NoError(err) - s.Equal(http.StatusNotFound, resp.StatusCode) - }) + s.Run("Deleting non-existing runner returns NotFound", func() { + resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID), nil) + s.NoError(err) + s.Equal(http.StatusNotFound, resp.StatusCode) + }) + }) + } } //nolint:funlen // there are a lot of tests for the files route, this function can be a little longer than 100 lines ;) func (s *E2ETestSuite) TestCopyFilesRoute() { - runnerID, err := ProvideRunner(&dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - }) - s.NoError(err) - copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ - Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, - }) - s.Require().NoError(err) - sendCopyRequest := func(reader io.Reader) (*http.Response, error) { - return helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), - "application/json", reader) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) + s.NoError(err) + copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ + Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, + }) + s.Require().NoError(err) + sendCopyRequest := func(reader io.Reader) (*http.Response, error) { + return helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), + "application/json", reader) + } + + s.Run("File copy with valid payload succeeds", func() { + resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + + s.Run("File content can be printed on runner", func() { + s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) + }) + }) + + s.Run("Files are put in correct location", func() { + relativeFilePath := "relative/file/path.txt" + relativeFileContent := "Relative file content" + absoluteFilePath := "/tmp/absolute/file/path.txt" + absoluteFileContent := "Absolute file content" + testFilePathsCopyRequestString, err := json.Marshal(&dto.UpdateFileSystemRequest{ + Copy: []dto.File{ + {Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)}, + {Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)}, + }, + }) + s.Require().NoError(err) + + resp, err := sendCopyRequest(bytes.NewReader(testFilePathsCopyRequestString)) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + + s.Run("File content of file with relative path can be printed on runner", func() { + // the print command is executed in the context of the default working directory of the container + s.assertFileContent(runnerID, relativeFilePath, relativeFileContent) + }) + + s.Run("File content of file with absolute path can be printed on runner", func() { + s.assertFileContent(runnerID, absoluteFilePath, absoluteFileContent) + }) + }) + + s.Run("File deletion request deletes file on runner", func() { + copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ + Delete: []dto.FilePath{tests.DefaultFileName}, + }) + s.Require().NoError(err) + + resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + + s.Run("File content can no longer be printed", func() { + stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName) + s.Equal("", stdout) + s.Contains(stderr, "No such file or directory") + }) + }) + + s.Run("File copy happens after file deletion", func() { + copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ + Delete: []dto.FilePath{tests.DefaultFileName}, + Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, + }) + s.Require().NoError(err) + + resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + _ = resp.Body.Close() + + s.Run("File content can be printed on runner", func() { + s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) + }) + }) + + s.Run("File copy with invalid payload returns bad request", func() { + resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), + "text/html", strings.NewReader("")) + s.NoError(err) + s.Equal(http.StatusBadRequest, resp.StatusCode) + }) + + s.Run("Copying to non-existing runner returns NotFound", func() { + resp, err := helpers.HTTPPatch( + helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID, api.UpdateFileSystemPath), + "application/json", bytes.NewReader(copyFilesRequestByteString)) + s.NoError(err) + s.Equal(http.StatusNotFound, resp.StatusCode) + }) + }) } +} - s.Run("File copy with valid payload succeeds", func() { - resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) +func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() { + s.Run("Nomad/If one file produces permission denied error, others are still copied", func() { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ + ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, + }) s.NoError(err) - s.Equal(http.StatusNoContent, resp.StatusCode) - s.Run("File content can be printed on runner", func() { - s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) - }) - }) - - s.Run("Files are put in correct location", func() { - relativeFilePath := "relative/file/path.txt" - relativeFileContent := "Relative file content" - absoluteFilePath := "/tmp/absolute/file/path.txt" - absoluteFileContent := "Absolute file content" - testFilePathsCopyRequestString, err := json.Marshal(&dto.UpdateFileSystemRequest{ - Copy: []dto.File{ - {Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)}, - {Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)}, - }, - }) - s.Require().NoError(err) - - resp, err := sendCopyRequest(bytes.NewReader(testFilePathsCopyRequestString)) - s.NoError(err) - s.Equal(http.StatusNoContent, resp.StatusCode) - - s.Run("File content of file with relative path can be printed on runner", func() { - // the print command is executed in the context of the default working directory of the container - s.assertFileContent(runnerID, relativeFilePath, relativeFileContent) - }) - - s.Run("File content of file with absolute path can be printed on runner", func() { - s.assertFileContent(runnerID, absoluteFilePath, absoluteFileContent) - }) - }) - - s.Run("File deletion request deletes file on runner", func() { - copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ - Delete: []dto.FilePath{tests.DefaultFileName}, - }) - s.Require().NoError(err) - - resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) - s.NoError(err) - s.Equal(http.StatusNoContent, resp.StatusCode) - - s.Run("File content can no longer be printed", func() { - stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName) - s.Equal("", stdout) - s.Contains(stderr, "No such file or directory") - }) - }) - - s.Run("File copy happens after file deletion", func() { - copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ - Delete: []dto.FilePath{tests.DefaultFileName}, - Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, - }) - s.Require().NoError(err) - - resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) - s.NoError(err) - s.Equal(http.StatusNoContent, resp.StatusCode) - _ = resp.Body.Close() - - s.Run("File content can be printed on runner", func() { - s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) - }) - }) - - s.Run("If one file produces permission denied error, others are still copied", func() { newFileContent := []byte("New content") copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ Copy: []dto.File{ @@ -197,7 +225,8 @@ func (s *E2ETestSuite) TestCopyFilesRoute() { }) s.Require().NoError(err) - resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) + resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), + "application/json", bytes.NewReader(copyFilesRequestByteString)) s.NoError(err) s.Equal(http.StatusInternalServerError, resp.StatusCode) internalServerError := new(dto.InternalServerError) @@ -211,49 +240,70 @@ func (s *E2ETestSuite) TestCopyFilesRoute() { }) }) - s.Run("File copy with invalid payload returns bad request", func() { - resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), - "text/html", strings.NewReader("")) - s.NoError(err) - s.Equal(http.StatusBadRequest, resp.StatusCode) - }) + s.Run("AWS/If one file produces permission denied error, others are still copied", func() { + for _, environmentID := range environmentIDs { + if environmentID == tests.DefaultEnvironmentIDAsInteger { + continue + } + s.Run(environmentID.ToString(), func() { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) + s.NoError(err) - s.Run("Copying to non-existing runner returns NotFound", func() { - resp, err := helpers.HTTPPatch( - helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID, api.UpdateFileSystemPath), - "application/json", bytes.NewReader(copyFilesRequestByteString)) - s.NoError(err) - s.Equal(http.StatusNotFound, resp.StatusCode) + newFileContent := []byte("New content") + copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ + Copy: []dto.File{ + {Path: "/dev/sda", Content: []byte(tests.DefaultFileContent)}, + {Path: tests.DefaultFileName, Content: newFileContent}, + }, + }) + s.Require().NoError(err) + + resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), + "application/json", bytes.NewReader(copyFilesRequestByteString)) + s.NoError(err) + s.Equal(http.StatusNoContent, resp.StatusCode) + _ = resp.Body.Close() + + stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName) + s.Equal(string(newFileContent), stdout) + s.Contains(stderr, "Permission denied") + }) + } }) } func (s *E2ETestSuite) TestRunnerGetsDestroyedAfterInactivityTimeout() { - inactivityTimeout := 5 // seconds - runnerID, err := ProvideRunner(&dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - InactivityTimeout: inactivityTimeout, - }) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + inactivityTimeout := 2 // seconds + runnerID, err := ProvideRunner(&dto.RunnerRequest{ + ExecutionEnvironmentID: int(environmentID), + InactivityTimeout: inactivityTimeout, + }) + s.Require().NoError(err) - executionTerminated := make(chan bool) - var lastMessage *dto.WebSocketMessage - go func() { - webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "sleep infinity"}) - s.Require().NoError(err) - connection, err := ConnectToWebSocket(webSocketURL) - s.Require().NoError(err) + executionTerminated := make(chan bool) + var lastMessage *dto.WebSocketMessage + go func() { + webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "sleep infinity"}) + s.Require().NoError(err) + connection, err := ConnectToWebSocket(webSocketURL) + s.Require().NoError(err) - messages, err := helpers.ReceiveAllWebSocketMessages(connection) - if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { - s.Fail("websocket abnormal closure") - } - controlMessages := helpers.WebSocketControlMessages(messages) - s.Require().NotEmpty(controlMessages) - lastMessage = controlMessages[len(controlMessages)-1] - executionTerminated <- true - }() - s.Require().True(tests.ChannelReceivesSomething(executionTerminated, time.Duration(inactivityTimeout+5)*time.Second)) - s.Equal(dto.WebSocketMetaTimeout, lastMessage.Type) + messages, err := helpers.ReceiveAllWebSocketMessages(connection) + if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { + s.Fail("websocket abnormal closure") + } + controlMessages := helpers.WebSocketControlMessages(messages) + s.Require().NotEmpty(controlMessages) + lastMessage = controlMessages[len(controlMessages)-1] + log.Warn("") + executionTerminated <- true + }() + s.Require().True(tests.ChannelReceivesSomething(executionTerminated, time.Duration(inactivityTimeout+5)*time.Second)) + s.Equal(dto.WebSocketMetaTimeout, lastMessage.Type) + }) + } } func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent string) { @@ -263,8 +313,10 @@ func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent str } func (s *E2ETestSuite) PrintContentOfFileOnRunner(runnerID, filename string) (stdout, stderr string) { - webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, - &dto.ExecutionRequest{Command: fmt.Sprintf("cat %s", filename)}) + webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{ + Command: fmt.Sprintf("cat %s", filename), + TimeLimit: int(tests.DefaultTestTimeout.Seconds()), + }) s.Require().NoError(err) connection, err := ConnectToWebSocket(webSocketURL) s.Require().NoError(err) diff --git a/tests/e2e/websocket_test.go b/tests/e2e/websocket_test.go index ebd1735..abb0085 100644 --- a/tests/e2e/websocket_test.go +++ b/tests/e2e/websocket_test.go @@ -18,81 +18,95 @@ import ( ) func (s *E2ETestSuite) TestExecuteCommandRoute() { - runnerID, err := ProvideRunner(&dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - }) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) + s.Require().NoError(err) - webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "true"}) - s.Require().NoError(err) - s.NotEqual("", webSocketURL) + webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "true"}) + s.Require().NoError(err) + s.NotEqual("", webSocketURL) - var connection *websocket.Conn - var connectionClosed bool + var connection *websocket.Conn + var connectionClosed bool - connection, err = ConnectToWebSocket(webSocketURL) - s.Require().NoError(err, "websocket connects") - closeHandler := connection.CloseHandler() - connection.SetCloseHandler(func(code int, text string) error { - connectionClosed = true - return closeHandler(code, text) - }) + connection, err = ConnectToWebSocket(webSocketURL) + s.Require().NoError(err, "websocket connects") + closeHandler := connection.CloseHandler() + connection.SetCloseHandler(func(code int, text string) error { + connectionClosed = true + return closeHandler(code, text) + }) - startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) - s.Require().NoError(err) - s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, startMessage) + startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) + s.Require().NoError(err) + s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, startMessage) - exitMessage, err := helpers.ReceiveNextWebSocketMessage(connection) - s.Require().NoError(err) - s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, exitMessage) + exitMessage, err := helpers.ReceiveNextWebSocketMessage(connection) + s.Require().NoError(err) + s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, exitMessage) - _, err = helpers.ReceiveAllWebSocketMessages(connection) - s.Require().Error(err) - s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) + _, err = helpers.ReceiveAllWebSocketMessages(connection) + s.Require().Error(err) + s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) - _, _, err = connection.ReadMessage() - s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure)) - s.True(connectionClosed, "connection should be closed") + _, _, err = connection.ReadMessage() + s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure)) + s.True(connectionClosed, "connection should be closed") + }) + } } func (s *E2ETestSuite) TestOutputToStdout() { - connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "echo Hello World"}) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + connection, err := ProvideWebSocketConnection(&s.Suite, environmentID, + &dto.ExecutionRequest{Command: "echo -n Hello World"}) + s.Require().NoError(err) - messages, err := helpers.ReceiveAllWebSocketMessages(connection) - s.Require().Error(err) - s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) + messages, err := helpers.ReceiveAllWebSocketMessages(connection) + s.Require().Error(err) + s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) - controlMessages := helpers.WebSocketControlMessages(messages) - s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) - s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, controlMessages[1]) + controlMessages := helpers.WebSocketControlMessages(messages) + s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) + s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, controlMessages[1]) - stdout, _, _ := helpers.WebSocketOutputMessages(messages) - s.Require().Equal("Hello World\r\n", stdout) + stdout, _, _ := helpers.WebSocketOutputMessages(messages) + s.Require().Equal("Hello World", stdout) + }) + } } func (s *E2ETestSuite) TestOutputToStderr() { - connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "cat -invalid"}) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + connection, err := ProvideWebSocketConnection(&s.Suite, environmentID, + &dto.ExecutionRequest{Command: "cat -invalid"}) + s.Require().NoError(err) - messages, err := helpers.ReceiveAllWebSocketMessages(connection) - s.Require().Error(err) - s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) + messages, err := helpers.ReceiveAllWebSocketMessages(connection) + s.Require().Error(err) + s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) - controlMessages := helpers.WebSocketControlMessages(messages) - s.Require().Equal(2, len(controlMessages)) - s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) - s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 1}, controlMessages[1]) + controlMessages := helpers.WebSocketControlMessages(messages) + s.Require().Equal(2, len(controlMessages)) + s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) + s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 1}, controlMessages[1]) - stdout, stderr, errors := helpers.WebSocketOutputMessages(messages) - s.NotContains(stdout, "cat: invalid option", "Stdout should not contain the error") - s.Contains(stderr, "cat: invalid option", "Stderr should contain the error") - s.Empty(errors) + stdout, stderr, errors := helpers.WebSocketOutputMessages(messages) + s.NotContains(stdout, "cat: invalid option", "Stdout should not contain the error") + s.Contains(stderr, "cat: invalid option", "Stderr should contain the error") + s.Empty(errors) + }) + } } +// AWS environments do not support stdin at this moment therefore they cannot take this test. func (s *E2ETestSuite) TestCommandHead() { hello := "Hello World!" - connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "head -n 1"}) + connection, err := ProvideWebSocketConnection(&s.Suite, tests.DefaultEnvironmentIDAsInteger, + &dto.ExecutionRequest{Command: "head -n 1"}) s.Require().NoError(err) startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) @@ -110,49 +124,58 @@ func (s *E2ETestSuite) TestCommandHead() { } func (s *E2ETestSuite) TestCommandReturnsAfterTimeout() { - connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "sleep 4", TimeLimit: 1}) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + connection, err := ProvideWebSocketConnection(&s.Suite, environmentID, + &dto.ExecutionRequest{Command: "sleep 4", TimeLimit: 1}) + s.Require().NoError(err) - c := make(chan bool) - var messages []*dto.WebSocketMessage - go func() { - messages, err = helpers.ReceiveAllWebSocketMessages(connection) - if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { + c := make(chan bool) + var messages []*dto.WebSocketMessage + go func() { + messages, err = helpers.ReceiveAllWebSocketMessages(connection) + if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { + s.T().Fail() + } + close(c) + }() + + select { + case <-time.After(2 * time.Second): + s.T().Fatal("The execution should have returned by now") + case <-c: + if s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}, messages[len(messages)-1]) { + return + } + } s.T().Fail() - } - close(c) - }() - - select { - case <-time.After(2 * time.Second): - s.T().Fatal("The execution should have returned by now") - case <-c: - if s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}, messages[len(messages)-1]) { - return - } + }) } - s.T().Fail() } func (s *E2ETestSuite) TestEchoEnvironment() { - connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{ - Command: "echo $hello", - Environment: map[string]string{"hello": "world"}, - }) - s.Require().NoError(err) + for _, environmentID := range environmentIDs { + s.Run(environmentID.ToString(), func() { + connection, err := ProvideWebSocketConnection(&s.Suite, environmentID, &dto.ExecutionRequest{ + Command: "echo -n $hello", + Environment: map[string]string{"hello": "world"}, + }) + s.Require().NoError(err) - startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) - s.Require().NoError(err) - s.Equal(dto.WebSocketMetaStart, startMessage.Type) + startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) + s.Require().NoError(err) + s.Equal(dto.WebSocketMetaStart, startMessage.Type) - messages, err := helpers.ReceiveAllWebSocketMessages(connection) - s.Require().Error(err) - s.Equal(err, &websocket.CloseError{Code: websocket.CloseNormalClosure}) - stdout, _, _ := helpers.WebSocketOutputMessages(messages) - s.Equal("world\r\n", stdout) + messages, err := helpers.ReceiveAllWebSocketMessages(connection) + s.Require().Error(err) + s.Equal(err, &websocket.CloseError{Code: websocket.CloseNormalClosure}) + stdout, _, _ := helpers.WebSocketOutputMessages(messages) + s.Equal("world", stdout) + }) + } } -func (s *E2ETestSuite) TestStderrFifoIsRemoved() { +func (s *E2ETestSuite) TestNomadStderrFifoIsRemoved() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, }) @@ -191,11 +214,9 @@ func (s *E2ETestSuite) ListTempDirectory(runnerID string) string { } // ProvideWebSocketConnection establishes a client WebSocket connection to run the passed ExecutionRequest. -// It requires a running Poseidon instance. -func ProvideWebSocketConnection(s *suite.Suite, request *dto.ExecutionRequest) (*websocket.Conn, error) { - runnerID, err := ProvideRunner(&dto.RunnerRequest{ - ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, - }) +func ProvideWebSocketConnection( + s *suite.Suite, environmentID dto.EnvironmentID, request *dto.ExecutionRequest) (*websocket.Conn, error) { + runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) if err != nil { return nil, fmt.Errorf("error providing runner: %w", err) }