mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-08-07 07:14:50 +00:00
22c14cb438
CI / lint (push) Has been cancelled
CI / checkproto (push) Has been cancelled
CI / test (agent) (push) Has been cancelled
CI / upload-coverage (push) Has been cancelled
CI / test (cli) (push) Has been cancelled
CI / test (cmd) (push) Has been cancelled
CI / test (internal) (push) Has been cancelled
CI / test (manager, true) (push) Has been cancelled
CI / test (pkg) (push) Has been cancelled
* Update protobuf generated files and improve algorithm handling in runner service - Bump protoc version from v7.35.0 to v7.35.1 in generated protobuf files. - Refactor RunRequest message in runner.proto to use string paths for algorithm and requirements instead of byte arrays. - Update runner service to handle algorithm and requirements paths, removing the need for temporary file creation. - Enhance error handling for missing algorithm paths in the runner service. - Modify tests to align with the new RunRequest structure and ensure proper file handling. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * chore: update protoc version to 35.1 in CI workflow Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: update systemd service configurations and dependencies for improved service management Signed-off-by: Sammy Oina <sammyoina@gmail.com> * fix: adjust AlgoWorkingDir handling in tests for Docker algorithm Signed-off-by: Sammy Oina <sammyoina@gmail.com> --------- Signed-off-by: Sammy Oina <sammyoina@gmail.com>
360 lines
10 KiB
Go
360 lines
10 KiB
Go
// Copyright (c) Ultraviolet
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/ultravioletrs/cocos/agent/algorithm"
|
|
pb "github.com/ultravioletrs/cocos/agent/runner"
|
|
)
|
|
|
|
// MockEventService is a mock implementation of events.Service.
|
|
type MockEventService struct {
|
|
events []interface{}
|
|
}
|
|
|
|
func (m *MockEventService) SendEvent(cmpID, event, status string, details json.RawMessage) {
|
|
m.events = append(m.events, map[string]interface{}{
|
|
"cmpID": cmpID,
|
|
"event": event,
|
|
"status": status,
|
|
"details": details,
|
|
})
|
|
}
|
|
|
|
func writeRunnerTestFile(t *testing.T, dir, name string, data []byte, mode os.FileMode) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(dir, name)
|
|
require.NoError(t, os.WriteFile(path, data, mode))
|
|
|
|
return path
|
|
}
|
|
|
|
// TestNewRunnerService tests the creation of a new runner service.
|
|
func TestNewRunnerService(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
|
|
rs := New(logger, eventSvc)
|
|
require.NotNil(t, rs)
|
|
assert.NotNil(t, rs.logger)
|
|
assert.NotNil(t, rs.eventSvc)
|
|
assert.Nil(t, rs.currentAlgo)
|
|
}
|
|
|
|
// TestRunWithBinaryAlgorithm tests running a binary algorithm.
|
|
func TestRunWithBinaryAlgorithm(t *testing.T) {
|
|
origDir, _ := os.Getwd()
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.Chdir(tmpDir))
|
|
defer func() { require.NoError(t, os.Chdir(origDir)) }()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo", []byte("#!/bin/bash\necho 'test'"), 0o700)
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-1",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{"arg1", "arg2"},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Empty(t, resp.Error)
|
|
assert.Equal(t, "test-1", resp.ComputationId)
|
|
}
|
|
|
|
// TestRunWithPythonAlgorithm tests running a Python algorithm.
|
|
func TestRunWithPythonAlgorithm(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo.py", []byte("print('hello')"), 0o600)
|
|
requirementsPath := writeRunnerTestFile(t, tmpDir, "requirements.txt", []byte("numpy==2.2.0"), 0o600)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-python",
|
|
AlgoType: "python",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
RequirementsPath: requirementsPath,
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Empty(t, resp.Error)
|
|
assert.Equal(t, "test-python", resp.ComputationId)
|
|
}
|
|
|
|
// TestRunWithPythonAlgorithmNoRequirements tests running Python without requirements.
|
|
func TestRunWithPythonAlgorithmNoRequirements(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo.py", []byte("print('hello')"), 0o600)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-python-noreq",
|
|
AlgoType: "python",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Empty(t, resp.Error)
|
|
assert.Equal(t, "test-python-noreq", resp.ComputationId)
|
|
}
|
|
|
|
// TestRunWithWasmAlgorithm tests running a WASM algorithm.
|
|
func TestRunWithWasmAlgorithm(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo.wasm", []byte{0x00, 0x61, 0x73, 0x6d}, 0o600)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-wasm",
|
|
AlgoType: "wasm",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
if resp.Error != "" {
|
|
assert.Contains(t, resp.Error, "wasmedge")
|
|
t.Skip("wasmedge not found, skipping test")
|
|
}
|
|
assert.Equal(t, "test-wasm", resp.ComputationId)
|
|
}
|
|
|
|
// TestRunWithDockerAlgorithm tests running a Docker algorithm.
|
|
func TestRunWithDockerAlgorithm(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "Dockerfile", []byte("FROM ubuntu:latest\nRUN echo 'test'"), 0o600)
|
|
|
|
origWorkingDir := algorithm.AlgoWorkingDir
|
|
algorithm.AlgoWorkingDir = tmpDir
|
|
t.Cleanup(func() { algorithm.AlgoWorkingDir = origWorkingDir })
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-docker",
|
|
AlgoType: "docker",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
if resp.Error != "" {
|
|
assert.Contains(t, resp.Error, "Docker")
|
|
t.Skip("Docker issue, skipping test")
|
|
}
|
|
assert.Equal(t, "test-docker", resp.ComputationId)
|
|
}
|
|
|
|
// TestRunWithUnsupportedAlgorithmType tests running with unsupported algorithm type.
|
|
func TestRunWithUnsupportedAlgorithmType(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-unsupported",
|
|
AlgoType: "unsupported",
|
|
AlgorithmPath: "/tmp/test",
|
|
Args: []string{},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.Error(t, err)
|
|
require.Nil(t, resp)
|
|
}
|
|
|
|
// TestRunAlreadyRunning tests running computation when one is already running.
|
|
func TestRunAlreadyRunning(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo", []byte("#!/bin/bash\nsleep 30"), 0o700)
|
|
|
|
// Use a long-running bash script
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-running",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
// Start first computation (will run for 30 seconds)
|
|
go func() {
|
|
_, _ = rs.Run(context.Background(), req)
|
|
}()
|
|
|
|
// Give it time to start
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Try to run another immediately - should fail
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, "computation already running", resp.Error)
|
|
}
|
|
|
|
// TestStopWhenRunning tests stopping a running computation.
|
|
func TestStopWhenRunning(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo", []byte("#!/bin/bash\nsleep 10"), 0o700)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-stop",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
go func() {
|
|
_, _ = rs.Run(context.Background(), req)
|
|
}()
|
|
|
|
// Give it time to start
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
stopReq := &pb.StopRequest{
|
|
ComputationId: "test-stop",
|
|
}
|
|
|
|
stopResp, err := rs.Stop(context.Background(), stopReq)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stopResp)
|
|
}
|
|
|
|
// TestRunErrors tests error paths in Run.
|
|
func TestRunErrors(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
|
|
t.Run("create algo file failure", func(t *testing.T) {
|
|
var err error
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-err",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: "",
|
|
}
|
|
_, err = rs.Run(context.Background(), req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "algorithm path is required")
|
|
})
|
|
|
|
t.Run("requirements file creation failure", func(t *testing.T) {
|
|
// Requirements are now staged by the agent, so the runner no longer creates temp files.
|
|
})
|
|
|
|
t.Run("chmod failure", func(t *testing.T) {
|
|
// Permission management is now the agent's responsibility during staging.
|
|
})
|
|
|
|
t.Run("write algorithm failure", func(t *testing.T) {
|
|
// Write failures are now handled by the agent before invoking the runner.
|
|
})
|
|
}
|
|
|
|
// TestConcurrentRun tests that concurrent runs are properly serialized.
|
|
func TestConcurrentRun(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo", []byte("#!/bin/bash\nsleep 15"), 0o700)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-concurrent",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{},
|
|
}
|
|
|
|
// Start first run in goroutine (will run for 15 seconds)
|
|
go func() {
|
|
_, _ = rs.Run(context.Background(), req)
|
|
}()
|
|
|
|
// Give it time to actually start
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Concurrent attempt should fail
|
|
resp2, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "computation already running", resp2.Error)
|
|
}
|
|
|
|
// TestRunWithMultipleArgs tests running with multiple arguments.
|
|
func TestRunWithMultipleArgs(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
tmpDir := t.TempDir()
|
|
algoPath := writeRunnerTestFile(t, tmpDir, "algo", []byte("#!/bin/bash\necho $@"), 0o700)
|
|
|
|
req := &pb.RunRequest{
|
|
ComputationId: "test-multi-args",
|
|
AlgoType: "bin",
|
|
AlgorithmPath: algoPath,
|
|
Args: []string{"arg1", "arg2", "arg3", "arg4"},
|
|
}
|
|
|
|
resp, err := rs.Run(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Empty(t, resp.Error)
|
|
assert.Equal(t, "test-multi-args", resp.ComputationId)
|
|
}
|
|
|
|
func TestStopFailure(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
eventSvc := &MockEventService{}
|
|
rs := New(logger, eventSvc)
|
|
|
|
// Mock an algorithm that fails on Stop
|
|
rs.currentAlgo = &MockAlgorithmStopFail{}
|
|
|
|
_, err := rs.Stop(context.Background(), &pb.StopRequest{})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
type MockAlgorithmStopFail struct{}
|
|
|
|
func (m *MockAlgorithmStopFail) Run() error { return nil }
|
|
func (m *MockAlgorithmStopFail) Stop() error { return fmt.Errorf("stop failed") }
|