Files
poseidon/tests/e2e/websocket_test.go
sirkrypt0 8de489929e Remove stderr fifo after interactive execution with stderr finished
Previously the stderr fifo would not be removed, leaving unwanted
artifacts from the execution behind. We now remove the stderr fifo
after the command finished.
2021-06-14 15:04:09 +02:00

225 lines
8.1 KiB
Go

package e2e
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/suite"
"gitlab.hpi.de/codeocean/codemoon/poseidon/api"
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
"gitlab.hpi.de/codeocean/codemoon/poseidon/nomad"
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests"
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests/helpers"
"net/http"
"strings"
"time"
)
func (s *E2ETestSuite) TestExecuteCommandRoute() {
runnerId, err := ProvideRunner(&dto.RunnerRequest{
ExecutionEnvironmentId: tests.DefaultEnvironmentIDAsInteger,
})
s.Require().NoError(err)
webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerId, &dto.ExecutionRequest{Command: "true"})
s.Require().NoError(err)
s.NotEqual("", webSocketURL)
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)
})
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)
_, err = helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
_, _, _ = connection.ReadMessage()
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)
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, messages[0])
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketOutputStdout, Data: "Hello World\r\n"}, messages[1])
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, messages[2])
}
func (s *E2ETestSuite) TestOutputToStderr() {
connection, err := ProvideWebSocketConnection(&s.Suite, &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)
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)
}
func (s *E2ETestSuite) TestCommandHead() {
hello := "Hello World!"
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "head -n 1"})
s.Require().NoError(err)
startMessage, err := helpers.ReceiveNextWebSocketMessage(connection)
s.Require().NoError(err)
s.Equal(dto.WebSocketMetaStart, startMessage.Type)
err = connection.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("%s\n", hello)))
s.Require().NoError(err)
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err)
s.Equal(err, &websocket.CloseError{Code: websocket.CloseNormalClosure})
stdout, _, _ := helpers.WebSocketOutputMessages(messages)
s.Contains(stdout, fmt.Sprintf("%s\r\n%s\r\n", hello, hello))
}
func (s *E2ETestSuite) TestCommandReturnsAfterTimeout() {
connection, err := ProvideWebSocketConnection(&s.Suite, &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) {
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)
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)
}
func (s *E2ETestSuite) TestStderrFifoIsRemoved() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{
ExecutionEnvironmentId: tests.DefaultEnvironmentIDAsInteger,
})
s.Require().NoError(err)
webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "ls -a /tmp/"})
s.Require().NoError(err)
connection, err := ConnectToWebSocket(webSocketURL)
s.Require().NoError(err)
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
stdout, _, _ := helpers.WebSocketOutputMessages(messages)
s.Contains(stdout, ".fifo", "there should be a .fifo file during the execution")
s.NotContains(s.ListTempDirectory(runnerID), ".fifo", "/tmp/ should not contain any .fifo files after the execution")
}
func (s *E2ETestSuite) ListTempDirectory(runnerID string) string {
alloc, _, err := nomadClient.Allocations().Info(runnerID, nil)
s.Require().NoError(err)
var stdout, stderr bytes.Buffer
exit, err := nomadClient.Allocations().Exec(context.Background(), alloc, nomad.TaskName,
false, []string{"ls", "-a", "/tmp/"}, strings.NewReader(""), &stdout, &stderr, nil, nil)
s.Require().NoError(err)
s.Require().Equal(0, exit)
s.Require().Empty(stderr)
return stdout.String()
}
// ProvideWebSocketConnection establishes a client WebSocket connection to run the passed ExecutionRequest.
// It requires a running Poseidon instance.
func ProvideWebSocketConnection(suite *suite.Suite, request *dto.ExecutionRequest) (connection *websocket.Conn, err error) {
runnerId, err := ProvideRunner(&dto.RunnerRequest{
ExecutionEnvironmentId: tests.DefaultEnvironmentIDAsInteger,
})
if err != nil {
return
}
webSocketURL, err := ProvideWebSocketURL(suite, runnerId, request)
if err != nil {
return
}
connection, err = ConnectToWebSocket(webSocketURL)
return
}
// ProvideWebSocketURL creates a WebSocket endpoint from the ExecutionRequest via an external api request.
// It requires a running Poseidon instance.
func ProvideWebSocketURL(suite *suite.Suite, runnerId string, request *dto.ExecutionRequest) (string, error) {
url := helpers.BuildURL(api.BasePath, api.RunnersPath, runnerId, api.ExecutePath)
executionRequestByteString, _ := json.Marshal(request)
reader := strings.NewReader(string(executionRequestByteString))
resp, err := http.Post(url, "application/json", reader)
suite.Require().NoError(err)
suite.Require().Equal(http.StatusOK, resp.StatusCode)
executionResponse := new(dto.ExecutionResponse)
err = json.NewDecoder(resp.Body).Decode(executionResponse)
suite.Require().NoError(err)
return executionResponse.WebSocketUrl, nil
}
// ConnectToWebSocket establish an external WebSocket connection to the provided url.
// It requires a running Poseidon instance.
func ConnectToWebSocket(url string) (conn *websocket.Conn, err error) {
conn, _, err = websocket.DefaultDialer.Dial(url, nil)
return
}