feat: Add context propagation, state sync, and tests
- Add context.Context to ManagerInterface methods (StartMiner, StopMiner, UninstallMiner) - Add WebSocket state sync on client connect (sends current miner states) - Add EventStateSync event type and SetStateProvider method - Add manager lifecycle tests (idempotent stop, context cancellation, shutdown timeout) - Add database tests (initialization, hashrate storage, stats) - Add EventHub tests (creation, broadcast, client count, state provider) - Update all test files for new context-aware API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0c8b2d999b
commit
b454bbd6d6
10 changed files with 633 additions and 52 deletions
150
pkg/database/database_test.go
Normal file
150
pkg/database/database_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) func() {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "test.db")
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
Enabled: true,
|
||||||
|
Path: dbPath,
|
||||||
|
RetentionDays: 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Initialize(cfg); err != nil {
|
||||||
|
t.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
Close()
|
||||||
|
os.Remove(dbPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Database should be initialized
|
||||||
|
dbMu.RLock()
|
||||||
|
initialized := db != nil
|
||||||
|
dbMu.RUnlock()
|
||||||
|
|
||||||
|
if !initialized {
|
||||||
|
t.Error("Database should be initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitialize_Disabled(t *testing.T) {
|
||||||
|
cfg := Config{
|
||||||
|
Enabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Initialize(cfg); err != nil {
|
||||||
|
t.Errorf("Initialize with disabled should not error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose(t *testing.T) {
|
||||||
|
cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Close should not error
|
||||||
|
if err := Close(); err != nil {
|
||||||
|
t.Errorf("Close failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashrateStorage(t *testing.T) {
|
||||||
|
cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Store some hashrate data
|
||||||
|
minerName := "test-miner"
|
||||||
|
minerType := "xmrig"
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
points := []HashratePoint{
|
||||||
|
{Timestamp: now.Add(-5 * time.Minute), Hashrate: 1000},
|
||||||
|
{Timestamp: now.Add(-4 * time.Minute), Hashrate: 1100},
|
||||||
|
{Timestamp: now.Add(-3 * time.Minute), Hashrate: 1200},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range points {
|
||||||
|
if err := InsertHashratePoint(minerName, minerType, p, ResolutionHigh); err != nil {
|
||||||
|
t.Fatalf("Failed to store hashrate point: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the data
|
||||||
|
retrieved, err := GetHashrateHistory(minerName, ResolutionHigh, now.Add(-10*time.Minute), now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get hashrate history: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(retrieved) != 3 {
|
||||||
|
t.Errorf("Expected 3 points, got %d", len(retrieved))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHashrateStats(t *testing.T) {
|
||||||
|
cleanup := setupTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
minerName := "stats-test-miner"
|
||||||
|
minerType := "xmrig"
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Store some test data
|
||||||
|
points := []HashratePoint{
|
||||||
|
{Timestamp: now.Add(-2 * time.Minute), Hashrate: 500},
|
||||||
|
{Timestamp: now.Add(-1 * time.Minute), Hashrate: 1000},
|
||||||
|
{Timestamp: now, Hashrate: 1500},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range points {
|
||||||
|
if err := InsertHashratePoint(minerName, minerType, p, ResolutionHigh); err != nil {
|
||||||
|
t.Fatalf("Failed to store point: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := GetHashrateStats(minerName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.TotalPoints != 3 {
|
||||||
|
t.Errorf("Expected 3 total points, got %d", stats.TotalPoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average should be (500+1000+1500)/3 = 1000
|
||||||
|
if stats.AverageRate != 1000 {
|
||||||
|
t.Errorf("Expected average rate 1000, got %d", stats.AverageRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.MaxRate != 1500 {
|
||||||
|
t.Errorf("Expected max rate 1500, got %d", stats.MaxRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.MinRate != 500 {
|
||||||
|
t.Errorf("Expected min rate 500, got %d", stats.MinRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultConfig(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
|
||||||
|
if !cfg.Enabled {
|
||||||
|
t.Error("Default config should have Enabled=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.RetentionDays != 30 {
|
||||||
|
t.Errorf("Expected default retention 30, got %d", cfg.RetentionDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ func TestDualMiningCPUAndGPU(t *testing.T) {
|
||||||
Devices: "0", // Device 0 only - user must pick
|
Devices: "0", // Device 0 only - user must pick
|
||||||
}
|
}
|
||||||
|
|
||||||
minerInstance, err := manager.StartMiner("xmrig", config)
|
minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start dual miner: %v", err)
|
t.Fatalf("Failed to start dual miner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ func TestDualMiningCPUAndGPU(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
manager.StopMiner(minerInstance.GetName())
|
manager.StopMiner(context.Background(), minerInstance.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGPUDeviceSelection tests that GPU mining requires explicit device selection
|
// TestGPUDeviceSelection tests that GPU mining requires explicit device selection
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ const (
|
||||||
EventMinerConnected EventType = "miner.connected"
|
EventMinerConnected EventType = "miner.connected"
|
||||||
|
|
||||||
// System events
|
// System events
|
||||||
EventPong EventType = "pong"
|
EventPong EventType = "pong"
|
||||||
|
EventStateSync EventType = "state.sync" // Initial state on connect/reconnect
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents a mining event that can be broadcast to clients
|
// Event represents a mining event that can be broadcast to clients
|
||||||
|
|
@ -62,6 +63,9 @@ type wsClient struct {
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateProvider is a function that returns the current state for sync
|
||||||
|
type StateProvider func() interface{}
|
||||||
|
|
||||||
// EventHub manages WebSocket connections and event broadcasting
|
// EventHub manages WebSocket connections and event broadcasting
|
||||||
type EventHub struct {
|
type EventHub struct {
|
||||||
// Registered clients
|
// Registered clients
|
||||||
|
|
@ -84,6 +88,9 @@ type EventHub struct {
|
||||||
|
|
||||||
// Connection limits
|
// Connection limits
|
||||||
maxConnections int
|
maxConnections int
|
||||||
|
|
||||||
|
// State provider for sync on connect
|
||||||
|
stateProvider StateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultMaxConnections is the default maximum WebSocket connections
|
// DefaultMaxConnections is the default maximum WebSocket connections
|
||||||
|
|
@ -126,9 +133,34 @@ func (h *EventHub) Run() {
|
||||||
case client := <-h.register:
|
case client := <-h.register:
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.clients[client] = true
|
h.clients[client] = true
|
||||||
|
stateProvider := h.stateProvider
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
log.Printf("[EventHub] Client connected (total: %d)", len(h.clients))
|
log.Printf("[EventHub] Client connected (total: %d)", len(h.clients))
|
||||||
|
|
||||||
|
// Send initial state sync if provider is set
|
||||||
|
if stateProvider != nil {
|
||||||
|
go func(c *wsClient) {
|
||||||
|
state := stateProvider()
|
||||||
|
if state != nil {
|
||||||
|
event := Event{
|
||||||
|
Type: EventStateSync,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Data: state,
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[EventHub] Failed to marshal state sync: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case c.send <- data:
|
||||||
|
default:
|
||||||
|
// Client buffer full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(client)
|
||||||
|
}
|
||||||
|
|
||||||
case client := <-h.unregister:
|
case client := <-h.unregister:
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
if _, ok := h.clients[client]; ok {
|
if _, ok := h.clients[client]; ok {
|
||||||
|
|
@ -208,6 +240,13 @@ func (h *EventHub) Stop() {
|
||||||
close(h.stop)
|
close(h.stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStateProvider sets the function that provides current state for new clients
|
||||||
|
func (h *EventHub) SetStateProvider(provider StateProvider) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
h.stateProvider = provider
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast sends an event to all subscribed clients
|
// Broadcast sends an event to all subscribed clients
|
||||||
func (h *EventHub) Broadcast(event Event) {
|
func (h *EventHub) Broadcast(event Event) {
|
||||||
if event.Timestamp.IsZero() {
|
if event.Timestamp.IsZero() {
|
||||||
|
|
|
||||||
201
pkg/mining/events_test.go
Normal file
201
pkg/mining/events_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
package mining
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEventHub(t *testing.T) {
|
||||||
|
hub := NewEventHub()
|
||||||
|
if hub == nil {
|
||||||
|
t.Fatal("NewEventHub returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hub.clients == nil {
|
||||||
|
t.Error("clients map should be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hub.maxConnections != DefaultMaxConnections {
|
||||||
|
t.Errorf("Expected maxConnections %d, got %d", DefaultMaxConnections, hub.maxConnections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEventHubWithOptions(t *testing.T) {
|
||||||
|
hub := NewEventHubWithOptions(50)
|
||||||
|
if hub.maxConnections != 50 {
|
||||||
|
t.Errorf("Expected maxConnections 50, got %d", hub.maxConnections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with invalid value
|
||||||
|
hub2 := NewEventHubWithOptions(0)
|
||||||
|
if hub2.maxConnections != DefaultMaxConnections {
|
||||||
|
t.Errorf("Expected default maxConnections for 0, got %d", hub2.maxConnections)
|
||||||
|
}
|
||||||
|
|
||||||
|
hub3 := NewEventHubWithOptions(-1)
|
||||||
|
if hub3.maxConnections != DefaultMaxConnections {
|
||||||
|
t.Errorf("Expected default maxConnections for -1, got %d", hub3.maxConnections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventHubBroadcast(t *testing.T) {
|
||||||
|
hub := NewEventHub()
|
||||||
|
go hub.Run()
|
||||||
|
defer hub.Stop()
|
||||||
|
|
||||||
|
// Create an event
|
||||||
|
event := Event{
|
||||||
|
Type: EventMinerStarted,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Data: MinerEventData{Name: "test-miner"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast should not block even with no clients
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
hub.Broadcast(event)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Success
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Broadcast blocked unexpectedly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventHubClientCount(t *testing.T) {
|
||||||
|
hub := NewEventHub()
|
||||||
|
go hub.Run()
|
||||||
|
defer hub.Stop()
|
||||||
|
|
||||||
|
// Initial count should be 0
|
||||||
|
if count := hub.ClientCount(); count != 0 {
|
||||||
|
t.Errorf("Expected 0 clients, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventHubStop(t *testing.T) {
|
||||||
|
hub := NewEventHub()
|
||||||
|
go hub.Run()
|
||||||
|
|
||||||
|
// Stop should not panic
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Stop panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
hub.Stop()
|
||||||
|
|
||||||
|
// Give time for cleanup
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEvent(t *testing.T) {
|
||||||
|
data := MinerEventData{Name: "test-miner"}
|
||||||
|
event := NewEvent(EventMinerStarted, data)
|
||||||
|
|
||||||
|
if event.Type != EventMinerStarted {
|
||||||
|
t.Errorf("Expected type %s, got %s", EventMinerStarted, event.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Timestamp.IsZero() {
|
||||||
|
t.Error("Timestamp should not be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
eventData, ok := event.Data.(MinerEventData)
|
||||||
|
if !ok {
|
||||||
|
t.Error("Data should be MinerEventData")
|
||||||
|
}
|
||||||
|
if eventData.Name != "test-miner" {
|
||||||
|
t.Errorf("Expected miner name 'test-miner', got '%s'", eventData.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventJSON(t *testing.T) {
|
||||||
|
event := Event{
|
||||||
|
Type: EventMinerStats,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Data: MinerStatsData{
|
||||||
|
Name: "test-miner",
|
||||||
|
Hashrate: 1000,
|
||||||
|
Shares: 10,
|
||||||
|
Rejected: 1,
|
||||||
|
Uptime: 3600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded Event
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.Type != EventMinerStats {
|
||||||
|
t.Errorf("Expected type %s, got %s", EventMinerStats, decoded.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStateProvider(t *testing.T) {
|
||||||
|
hub := NewEventHub()
|
||||||
|
go hub.Run()
|
||||||
|
defer hub.Stop()
|
||||||
|
|
||||||
|
called := false
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
provider := func() interface{} {
|
||||||
|
mu.Lock()
|
||||||
|
called = true
|
||||||
|
mu.Unlock()
|
||||||
|
return map[string]string{"status": "ok"}
|
||||||
|
}
|
||||||
|
|
||||||
|
hub.SetStateProvider(provider)
|
||||||
|
|
||||||
|
// The provider should be set but not called until a client connects
|
||||||
|
mu.Lock()
|
||||||
|
wasCalled := called
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if wasCalled {
|
||||||
|
t.Error("Provider should not be called until client connects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockWebSocketConn provides a minimal mock for testing
|
||||||
|
type MockWebSocketConn struct {
|
||||||
|
websocket.Conn
|
||||||
|
written [][]byte
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventTypes(t *testing.T) {
|
||||||
|
types := []EventType{
|
||||||
|
EventMinerStarting,
|
||||||
|
EventMinerStarted,
|
||||||
|
EventMinerStopping,
|
||||||
|
EventMinerStopped,
|
||||||
|
EventMinerStats,
|
||||||
|
EventMinerError,
|
||||||
|
EventMinerConnected,
|
||||||
|
EventPong,
|
||||||
|
EventStateSync,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, et := range types {
|
||||||
|
if et == "" {
|
||||||
|
t.Error("Event type should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,13 +19,13 @@ var instanceNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_/-]`)
|
||||||
|
|
||||||
// ManagerInterface defines the contract for a miner manager.
|
// ManagerInterface defines the contract for a miner manager.
|
||||||
type ManagerInterface interface {
|
type ManagerInterface interface {
|
||||||
StartMiner(minerType string, config *Config) (Miner, error)
|
StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error)
|
||||||
StopMiner(name string) error
|
StopMiner(ctx context.Context, name string) error
|
||||||
GetMiner(name string) (Miner, error)
|
GetMiner(name string) (Miner, error)
|
||||||
ListMiners() []Miner
|
ListMiners() []Miner
|
||||||
ListAvailableMiners() []AvailableMiner
|
ListAvailableMiners() []AvailableMiner
|
||||||
GetMinerHashrateHistory(name string) ([]HashratePoint, error)
|
GetMinerHashrateHistory(name string) ([]HashratePoint, error)
|
||||||
UninstallMiner(minerType string) error
|
UninstallMiner(ctx context.Context, minerType string) error
|
||||||
Stop()
|
Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ func (m *Manager) autostartMiners() {
|
||||||
for _, minerCfg := range cfg.Miners {
|
for _, minerCfg := range cfg.Miners {
|
||||||
if minerCfg.Autostart && minerCfg.Config != nil {
|
if minerCfg.Autostart && minerCfg.Config != nil {
|
||||||
log.Printf("Autostarting miner: %s", minerCfg.MinerType)
|
log.Printf("Autostarting miner: %s", minerCfg.MinerType)
|
||||||
if _, err := m.StartMiner(minerCfg.MinerType, minerCfg.Config); err != nil {
|
if _, err := m.StartMiner(context.Background(), minerCfg.MinerType, minerCfg.Config); err != nil {
|
||||||
log.Printf("Failed to autostart miner %s: %v", minerCfg.MinerType, err)
|
log.Printf("Failed to autostart miner %s: %v", minerCfg.MinerType, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +223,15 @@ func findAvailablePort() (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartMiner starts a new miner and saves its configuration.
|
// StartMiner starts a new miner and saves its configuration.
|
||||||
func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
// The context can be used to cancel the operation.
|
||||||
|
func (m *Manager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) {
|
||||||
|
// Check for cancellation before acquiring lock
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -314,7 +322,15 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UninstallMiner stops, uninstalls, and removes a miner's configuration.
|
// UninstallMiner stops, uninstalls, and removes a miner's configuration.
|
||||||
func (m *Manager) UninstallMiner(minerType string) error {
|
// The context can be used to cancel the operation.
|
||||||
|
func (m *Manager) UninstallMiner(ctx context.Context, minerType string) error {
|
||||||
|
// Check for cancellation before acquiring lock
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
// Collect miners to stop and delete (can't modify map during iteration)
|
// Collect miners to stop and delete (can't modify map during iteration)
|
||||||
minersToDelete := make([]string, 0)
|
minersToDelete := make([]string, 0)
|
||||||
|
|
@ -394,7 +410,15 @@ func (m *Manager) updateMinerConfig(minerType string, autostart bool, config *Co
|
||||||
|
|
||||||
// StopMiner stops a running miner and removes it from the manager.
|
// StopMiner stops a running miner and removes it from the manager.
|
||||||
// If the miner is already stopped, it will still be removed from the manager.
|
// If the miner is already stopped, it will still be removed from the manager.
|
||||||
func (m *Manager) StopMiner(name string) error {
|
// The context can be used to cancel the operation.
|
||||||
|
func (m *Manager) StopMiner(ctx context.Context, name string) error {
|
||||||
|
// Check for cancellation before acquiring lock
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package mining
|
package mining
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupTestManager creates a new Manager and a dummy executable for tests.
|
// setupTestManager creates a new Manager and a dummy executable for tests.
|
||||||
|
|
@ -51,7 +53,7 @@ func TestStartMiner_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 1: Successfully start a supported miner
|
// Case 1: Successfully start a supported miner
|
||||||
miner, err := m.StartMiner("xmrig", config)
|
miner, err := m.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected to start miner, but got error: %v", err)
|
t.Fatalf("Expected to start miner, but got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +76,7 @@ func TestStartMiner_Bad(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: Attempt to start an unsupported miner
|
// Case 2: Attempt to start an unsupported miner
|
||||||
_, err := m.StartMiner("unsupported", config)
|
_, err := m.StartMiner(context.Background(), "unsupported", config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected an error when starting an unsupported miner, but got nil")
|
t.Error("Expected an error when starting an unsupported miner, but got nil")
|
||||||
}
|
}
|
||||||
|
|
@ -90,12 +92,12 @@ func TestStartMiner_Ugly(t *testing.T) {
|
||||||
Wallet: "testwallet",
|
Wallet: "testwallet",
|
||||||
}
|
}
|
||||||
// Case 1: Successfully start a supported miner
|
// Case 1: Successfully start a supported miner
|
||||||
_, err := m.StartMiner("xmrig", config)
|
_, err := m.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected to start miner, but got error: %v", err)
|
t.Fatalf("Expected to start miner, but got error: %v", err)
|
||||||
}
|
}
|
||||||
// Case 3: Attempt to start a duplicate miner
|
// Case 3: Attempt to start a duplicate miner
|
||||||
_, err = m.StartMiner("xmrig", config)
|
_, err = m.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected an error when starting a duplicate miner, but got nil")
|
t.Error("Expected an error when starting a duplicate miner, but got nil")
|
||||||
}
|
}
|
||||||
|
|
@ -113,8 +115,8 @@ func TestStopMiner_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 1: Stop a running miner
|
// Case 1: Stop a running miner
|
||||||
miner, _ := m.StartMiner("xmrig", config)
|
miner, _ := m.StartMiner(context.Background(), "xmrig", config)
|
||||||
err := m.StopMiner(miner.GetName())
|
err := m.StopMiner(context.Background(), miner.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected to stop miner, but got error: %v", err)
|
t.Fatalf("Expected to stop miner, but got error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +130,7 @@ func TestStopMiner_Bad(t *testing.T) {
|
||||||
defer m.Stop()
|
defer m.Stop()
|
||||||
|
|
||||||
// Case 2: Attempt to stop a non-existent miner
|
// Case 2: Attempt to stop a non-existent miner
|
||||||
err := m.StopMiner("nonexistent")
|
err := m.StopMiner(context.Background(), "nonexistent")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected an error when stopping a non-existent miner, but got nil")
|
t.Error("Expected an error when stopping a non-existent miner, but got nil")
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +148,7 @@ func TestGetMiner_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 1: Get an existing miner
|
// Case 1: Get an existing miner
|
||||||
startedMiner, _ := m.StartMiner("xmrig", config)
|
startedMiner, _ := m.StartMiner(context.Background(), "xmrig", config)
|
||||||
retrievedMiner, err := m.GetMiner(startedMiner.GetName())
|
retrievedMiner, err := m.GetMiner(startedMiner.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected to get miner, but got error: %v", err)
|
t.Fatalf("Expected to get miner, but got error: %v", err)
|
||||||
|
|
@ -184,9 +186,138 @@ func TestListMiners_Good(t *testing.T) {
|
||||||
Pool: "test:1234",
|
Pool: "test:1234",
|
||||||
Wallet: "testwallet",
|
Wallet: "testwallet",
|
||||||
}
|
}
|
||||||
_, _ = m.StartMiner("xmrig", config)
|
_, _ = m.StartMiner(context.Background(), "xmrig", config)
|
||||||
miners = m.ListMiners()
|
miners = m.ListMiners()
|
||||||
if len(miners) != 1 {
|
if len(miners) != 1 {
|
||||||
t.Errorf("Expected 1 miner, but got %d", len(miners))
|
t.Errorf("Expected 1 miner, but got %d", len(miners))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestManagerStop_Idempotent tests that Stop() can be called multiple times safely
|
||||||
|
func TestManagerStop_Idempotent(t *testing.T) {
|
||||||
|
m := setupTestManager(t)
|
||||||
|
|
||||||
|
// Start a miner
|
||||||
|
config := &Config{
|
||||||
|
HTTPPort: 9010,
|
||||||
|
Pool: "test:1234",
|
||||||
|
Wallet: "testwallet",
|
||||||
|
}
|
||||||
|
_, _ = m.StartMiner(context.Background(), "xmrig", config)
|
||||||
|
|
||||||
|
// Call Stop() multiple times - should not panic
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("Stop() panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
m.Stop()
|
||||||
|
m.Stop()
|
||||||
|
m.Stop()
|
||||||
|
|
||||||
|
// If we got here without panicking, the test passes
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStartMiner_CancelledContext tests that StartMiner respects context cancellation
|
||||||
|
func TestStartMiner_CancelledContext(t *testing.T) {
|
||||||
|
m := setupTestManager(t)
|
||||||
|
defer m.Stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // Cancel immediately
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
HTTPPort: 9011,
|
||||||
|
Pool: "test:1234",
|
||||||
|
Wallet: "testwallet",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := m.StartMiner(ctx, "xmrig", config)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when starting miner with cancelled context")
|
||||||
|
}
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Errorf("Expected context.Canceled error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStopMiner_CancelledContext tests that StopMiner respects context cancellation
|
||||||
|
func TestStopMiner_CancelledContext(t *testing.T) {
|
||||||
|
m := setupTestManager(t)
|
||||||
|
defer m.Stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // Cancel immediately
|
||||||
|
|
||||||
|
err := m.StopMiner(ctx, "nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when stopping miner with cancelled context")
|
||||||
|
}
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Errorf("Expected context.Canceled error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerEventHub tests that SetEventHub works correctly
|
||||||
|
func TestManagerEventHub(t *testing.T) {
|
||||||
|
m := setupTestManager(t)
|
||||||
|
defer m.Stop()
|
||||||
|
|
||||||
|
eventHub := NewEventHub()
|
||||||
|
go eventHub.Run()
|
||||||
|
defer eventHub.Stop()
|
||||||
|
|
||||||
|
m.SetEventHub(eventHub)
|
||||||
|
|
||||||
|
// Get initial miner count (may have autostarted miners)
|
||||||
|
initialCount := len(m.ListMiners())
|
||||||
|
|
||||||
|
// Start a miner - should emit events
|
||||||
|
config := &Config{
|
||||||
|
HTTPPort: 9012,
|
||||||
|
Pool: "test:1234",
|
||||||
|
Wallet: "testwallet",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := m.StartMiner(context.Background(), "xmrig", config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give time for events to be processed
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify miner count increased by 1
|
||||||
|
miners := m.ListMiners()
|
||||||
|
if len(miners) != initialCount+1 {
|
||||||
|
t.Errorf("Expected %d miners, got %d", initialCount+1, len(miners))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagerShutdownTimeout tests the graceful shutdown timeout
|
||||||
|
func TestManagerShutdownTimeout(t *testing.T) {
|
||||||
|
m := setupTestManager(t)
|
||||||
|
|
||||||
|
// Start a miner
|
||||||
|
config := &Config{
|
||||||
|
HTTPPort: 9013,
|
||||||
|
Pool: "test:1234",
|
||||||
|
Wallet: "testwallet",
|
||||||
|
}
|
||||||
|
_, _ = m.StartMiner(context.Background(), "xmrig", config)
|
||||||
|
|
||||||
|
// Stop should complete within a reasonable time
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
m.Stop()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Success - stopped in time
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
t.Error("Manager.Stop() took too long - possible shutdown issue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package mining
|
package mining
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ func TestStartAndStopMiner(t *testing.T) {
|
||||||
// but we can test the manager's behavior.
|
// but we can test the manager's behavior.
|
||||||
// This will fail because the miner executable is not present,
|
// This will fail because the miner executable is not present,
|
||||||
// which is expected in a test environment.
|
// which is expected in a test environment.
|
||||||
_, err := manager.StartMiner("xmrig", config)
|
_, err := manager.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Log("StartMiner did not fail as expected in test environment")
|
t.Log("StartMiner did not fail as expected in test environment")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,33 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string,
|
||||||
mgr.SetEventHub(eventHub)
|
mgr.SetEventHub(eventHub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up state provider for WebSocket state sync on reconnect
|
||||||
|
eventHub.SetStateProvider(func() interface{} {
|
||||||
|
miners := manager.ListMiners()
|
||||||
|
if len(miners) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Return current state of all miners
|
||||||
|
state := make([]map[string]interface{}, 0, len(miners))
|
||||||
|
for _, miner := range miners {
|
||||||
|
stats, _ := miner.GetStats(context.Background())
|
||||||
|
minerState := map[string]interface{}{
|
||||||
|
"name": miner.GetName(),
|
||||||
|
"status": "running",
|
||||||
|
}
|
||||||
|
if stats != nil {
|
||||||
|
minerState["hashrate"] = stats.Hashrate
|
||||||
|
minerState["shares"] = stats.Shares
|
||||||
|
minerState["rejected"] = stats.Rejected
|
||||||
|
minerState["uptime"] = stats.Uptime
|
||||||
|
}
|
||||||
|
state = append(state, minerState)
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"miners": state,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
ProfileManager: profileManager,
|
ProfileManager: profileManager,
|
||||||
|
|
@ -567,7 +594,7 @@ func (s *Service) handleUpdateCheck(c *gin.Context) {
|
||||||
// @Router /miners/{miner_type}/uninstall [delete]
|
// @Router /miners/{miner_type}/uninstall [delete]
|
||||||
func (s *Service) handleUninstallMiner(c *gin.Context) {
|
func (s *Service) handleUninstallMiner(c *gin.Context) {
|
||||||
minerType := c.Param("miner_name")
|
minerType := c.Param("miner_name")
|
||||||
if err := s.Manager.UninstallMiner(minerType); err != nil {
|
if err := s.Manager.UninstallMiner(c.Request.Context(), minerType); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -662,7 +689,7 @@ func (s *Service) handleStartMinerWithProfile(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
miner, err := s.Manager.StartMiner(profile.MinerType, &config)
|
miner, err := s.Manager.StartMiner(c.Request.Context(), profile.MinerType, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
|
|
@ -680,7 +707,7 @@ func (s *Service) handleStartMinerWithProfile(c *gin.Context) {
|
||||||
// @Router /miners/{miner_name} [delete]
|
// @Router /miners/{miner_name} [delete]
|
||||||
func (s *Service) handleStopMiner(c *gin.Context) {
|
func (s *Service) handleStopMiner(c *gin.Context) {
|
||||||
minerName := c.Param("miner_name")
|
minerName := c.Param("miner_name")
|
||||||
if err := s.Manager.StopMiner(minerName); err != nil {
|
if err := s.Manager.StopMiner(c.Request.Context(), minerName); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,28 +53,32 @@ func (m *MockMiner) WriteStdin(input string) error { return m.WriteStdinF
|
||||||
type MockManager struct {
|
type MockManager struct {
|
||||||
ListMinersFunc func() []Miner
|
ListMinersFunc func() []Miner
|
||||||
ListAvailableMinersFunc func() []AvailableMiner
|
ListAvailableMinersFunc func() []AvailableMiner
|
||||||
StartMinerFunc func(minerType string, config *Config) (Miner, error)
|
StartMinerFunc func(ctx context.Context, minerType string, config *Config) (Miner, error)
|
||||||
StopMinerFunc func(minerName string) error
|
StopMinerFunc func(ctx context.Context, minerName string) error
|
||||||
GetMinerFunc func(minerName string) (Miner, error)
|
GetMinerFunc func(minerName string) (Miner, error)
|
||||||
GetMinerHashrateHistoryFunc func(minerName string) ([]HashratePoint, error)
|
GetMinerHashrateHistoryFunc func(minerName string) ([]HashratePoint, error)
|
||||||
UninstallMinerFunc func(minerType string) error
|
UninstallMinerFunc func(ctx context.Context, minerType string) error
|
||||||
StopFunc func()
|
StopFunc func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockManager) ListMiners() []Miner { return m.ListMinersFunc() }
|
func (m *MockManager) ListMiners() []Miner { return m.ListMinersFunc() }
|
||||||
func (m *MockManager) ListAvailableMiners() []AvailableMiner { return m.ListAvailableMinersFunc() }
|
func (m *MockManager) ListAvailableMiners() []AvailableMiner { return m.ListAvailableMinersFunc() }
|
||||||
func (m *MockManager) StartMiner(minerType string, config *Config) (Miner, error) {
|
func (m *MockManager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) {
|
||||||
return m.StartMinerFunc(minerType, config)
|
return m.StartMinerFunc(ctx, minerType, config)
|
||||||
|
}
|
||||||
|
func (m *MockManager) StopMiner(ctx context.Context, minerName string) error {
|
||||||
|
return m.StopMinerFunc(ctx, minerName)
|
||||||
}
|
}
|
||||||
func (m *MockManager) StopMiner(minerName string) error { return m.StopMinerFunc(minerName) }
|
|
||||||
func (m *MockManager) GetMiner(minerName string) (Miner, error) {
|
func (m *MockManager) GetMiner(minerName string) (Miner, error) {
|
||||||
return m.GetMinerFunc(minerName)
|
return m.GetMinerFunc(minerName)
|
||||||
}
|
}
|
||||||
func (m *MockManager) GetMinerHashrateHistory(minerName string) ([]HashratePoint, error) {
|
func (m *MockManager) GetMinerHashrateHistory(minerName string) ([]HashratePoint, error) {
|
||||||
return m.GetMinerHashrateHistoryFunc(minerName)
|
return m.GetMinerHashrateHistoryFunc(minerName)
|
||||||
}
|
}
|
||||||
func (m *MockManager) UninstallMiner(minerType string) error { return m.UninstallMinerFunc(minerType) }
|
func (m *MockManager) UninstallMiner(ctx context.Context, minerType string) error {
|
||||||
func (m *MockManager) Stop() { m.StopFunc() }
|
return m.UninstallMinerFunc(ctx, minerType)
|
||||||
|
}
|
||||||
|
func (m *MockManager) Stop() { m.StopFunc() }
|
||||||
|
|
||||||
var _ ManagerInterface = (*MockManager)(nil)
|
var _ ManagerInterface = (*MockManager)(nil)
|
||||||
|
|
||||||
|
|
@ -82,14 +86,18 @@ func setupTestRouter() (*gin.Engine, *MockManager) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
mockManager := &MockManager{
|
mockManager := &MockManager{
|
||||||
ListMinersFunc: func() []Miner { return []Miner{} },
|
ListMinersFunc: func() []Miner { return []Miner{} },
|
||||||
ListAvailableMinersFunc: func() []AvailableMiner { return []AvailableMiner{} },
|
ListAvailableMinersFunc: func() []AvailableMiner { return []AvailableMiner{} },
|
||||||
StartMinerFunc: func(minerType string, config *Config) (Miner, error) { return nil, nil },
|
StartMinerFunc: func(ctx context.Context, minerType string, config *Config) (Miner, error) {
|
||||||
StopMinerFunc: func(minerName string) error { return nil },
|
return nil, nil
|
||||||
GetMinerFunc: func(minerName string) (Miner, error) { return nil, nil },
|
},
|
||||||
GetMinerHashrateHistoryFunc: func(minerName string) ([]HashratePoint, error) { return nil, nil },
|
StopMinerFunc: func(ctx context.Context, minerName string) error { return nil },
|
||||||
UninstallMinerFunc: func(minerType string) error { return nil },
|
GetMinerFunc: func(minerName string) (Miner, error) { return nil, nil },
|
||||||
StopFunc: func() {},
|
GetMinerHashrateHistoryFunc: func(minerName string) ([]HashratePoint, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
UninstallMinerFunc: func(ctx context.Context, minerType string) error { return nil },
|
||||||
|
StopFunc: func() {},
|
||||||
}
|
}
|
||||||
service := &Service{
|
service := &Service{
|
||||||
Manager: mockManager,
|
Manager: mockManager,
|
||||||
|
|
@ -162,7 +170,7 @@ func TestHandleInstallMiner(t *testing.T) {
|
||||||
|
|
||||||
func TestHandleStopMiner(t *testing.T) {
|
func TestHandleStopMiner(t *testing.T) {
|
||||||
router, mockManager := setupTestRouter()
|
router, mockManager := setupTestRouter()
|
||||||
mockManager.StopMinerFunc = func(minerName string) error {
|
mockManager.StopMinerFunc = func(ctx context.Context, minerName string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func TestCPUThrottleSingleMiner(t *testing.T) {
|
||||||
Algo: "rx/0",
|
Algo: "rx/0",
|
||||||
}
|
}
|
||||||
|
|
||||||
minerInstance, err := manager.StartMiner("xmrig", config)
|
minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start miner: %v", err)
|
t.Fatalf("Failed to start miner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ func TestCPUThrottleSingleMiner(t *testing.T) {
|
||||||
t.Errorf("CPU usage %.1f%% exceeds expected ~10%% (with tolerance)", avgCPU)
|
t.Errorf("CPU usage %.1f%% exceeds expected ~10%% (with tolerance)", avgCPU)
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.StopMiner(minerInstance.GetName())
|
manager.StopMiner(context.Background(), minerInstance.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCPUThrottleDualMiners tests that two miners together respect combined CPU limits
|
// TestCPUThrottleDualMiners tests that two miners together respect combined CPU limits
|
||||||
|
|
@ -79,7 +79,7 @@ func TestCPUThrottleDualMiners(t *testing.T) {
|
||||||
Algo: "rx/0",
|
Algo: "rx/0",
|
||||||
}
|
}
|
||||||
|
|
||||||
miner1Instance, err := manager.StartMiner("xmrig", config1)
|
miner1Instance, err := manager.StartMiner(context.Background(), "xmrig", config1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start first miner: %v", err)
|
t.Fatalf("Failed to start first miner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +93,7 @@ func TestCPUThrottleDualMiners(t *testing.T) {
|
||||||
Algo: "gr", // GhostRider algo
|
Algo: "gr", // GhostRider algo
|
||||||
}
|
}
|
||||||
|
|
||||||
miner2Instance, err := manager.StartMiner("xmrig", config2)
|
miner2Instance, err := manager.StartMiner(context.Background(), "xmrig", config2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start second miner: %v", err)
|
t.Fatalf("Failed to start second miner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -119,8 +119,8 @@ func TestCPUThrottleDualMiners(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
manager.StopMiner(miner1Instance.GetName())
|
manager.StopMiner(context.Background(), miner1Instance.GetName())
|
||||||
manager.StopMiner(miner2Instance.GetName())
|
manager.StopMiner(context.Background(), miner2Instance.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCPUThrottleThreadCount tests thread-based CPU limiting
|
// TestCPUThrottleThreadCount tests thread-based CPU limiting
|
||||||
|
|
@ -150,12 +150,12 @@ func TestCPUThrottleThreadCount(t *testing.T) {
|
||||||
Algo: "rx/0",
|
Algo: "rx/0",
|
||||||
}
|
}
|
||||||
|
|
||||||
minerInstance, err := manager.StartMiner("xmrig", config)
|
minerInstance, err := manager.StartMiner(context.Background(), "xmrig", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start miner: %v", err)
|
t.Fatalf("Failed to start miner: %v", err)
|
||||||
}
|
}
|
||||||
t.Logf("Started miner: %s", minerInstance.GetName())
|
t.Logf("Started miner: %s", minerInstance.GetName())
|
||||||
defer manager.StopMiner(minerInstance.GetName())
|
defer manager.StopMiner(context.Background(), minerInstance.GetName())
|
||||||
|
|
||||||
// Let miner warm up
|
// Let miner warm up
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
|
|
@ -194,7 +194,7 @@ func TestMinerResourceIsolation(t *testing.T) {
|
||||||
Algo: "rx/0",
|
Algo: "rx/0",
|
||||||
}
|
}
|
||||||
|
|
||||||
miner1, err := manager.StartMiner("xmrig", config1)
|
miner1, err := manager.StartMiner(context.Background(), "xmrig", config1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start miner 1: %v", err)
|
t.Fatalf("Failed to start miner 1: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +219,7 @@ func TestMinerResourceIsolation(t *testing.T) {
|
||||||
Algo: "gr",
|
Algo: "gr",
|
||||||
}
|
}
|
||||||
|
|
||||||
miner2, err := manager.StartMiner("xmrig", config2)
|
miner2, err := manager.StartMiner(context.Background(), "xmrig", config2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to start miner 2: %v", err)
|
t.Fatalf("Failed to start miner 2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -248,8 +248,8 @@ func TestMinerResourceIsolation(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
manager.StopMiner(miner1.GetName())
|
manager.StopMiner(context.Background(), miner1.GetName())
|
||||||
manager.StopMiner(miner2.GetName())
|
manager.StopMiner(context.Background(), miner2.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// measureCPUUsage measures average CPU usage over a duration
|
// measureCPUUsage measures average CPU usage over a duration
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue