
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.
225 lines
8.1 KiB
Go
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
|
|
}
|