Merge pull request #4 from Snider/feature-increase-test-coverage
feat: Increase test coverage for pkg/mining
This commit is contained in:
commit
01fb04ef02
8 changed files with 467 additions and 13 deletions
|
|
@ -16,6 +16,8 @@ type Manager struct {
|
|||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
var _ ManagerInterface = (*Manager)(nil)
|
||||
|
||||
// NewManager creates a new miner manager
|
||||
func NewManager() *Manager {
|
||||
m := &Manager{
|
||||
|
|
|
|||
12
pkg/mining/manager_interface.go
Normal file
12
pkg/mining/manager_interface.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package mining
|
||||
|
||||
// ManagerInterface defines the interface for a miner manager.
|
||||
type ManagerInterface interface {
|
||||
StartMiner(minerType string, config *Config) (Miner, error)
|
||||
StopMiner(name string) error
|
||||
GetMiner(name string) (Miner, error)
|
||||
ListMiners() []Miner
|
||||
ListAvailableMiners() []AvailableMiner
|
||||
GetMinerHashrateHistory(name string) ([]HashratePoint, error)
|
||||
Stop()
|
||||
}
|
||||
47
pkg/mining/manager_test.go
Normal file
47
pkg/mining/manager_test.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestManager_StartStopMultipleMiners tests starting and stopping multiple miners.
|
||||
func TestManager_StartStopMultipleMiners(t *testing.T) {
|
||||
manager := NewManager()
|
||||
defer manager.Stop()
|
||||
|
||||
configs := []*Config{
|
||||
{Pool: "pool1", Wallet: "wallet1"},
|
||||
}
|
||||
|
||||
minerNames := []string{"xmrig"}
|
||||
|
||||
for i, config := range configs {
|
||||
// Since we can't start a real miner in the test, we'll just check that the manager doesn't crash.
|
||||
// A more complete test would involve a mock miner.
|
||||
_, err := manager.StartMiner(minerNames[i], config)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error when starting miner without executable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_collectMinerStats tests the stat collection logic.
|
||||
func TestManager_collectMinerStats(t *testing.T) {
|
||||
manager := NewManager()
|
||||
defer manager.Stop()
|
||||
|
||||
// Since we can't start a real miner, we can't fully test this.
|
||||
// A more complete test would involve a mock miner that can be added to the manager.
|
||||
manager.collectMinerStats()
|
||||
}
|
||||
|
||||
// TestManager_GetMinerHashrateHistory tests getting hashrate history.
|
||||
func TestManager_GetMinerHashrateHistory(t *testing.T) {
|
||||
manager := NewManager()
|
||||
defer manager.Stop()
|
||||
|
||||
_, err := manager.GetMinerHashrateHistory("non-existent")
|
||||
if err == nil {
|
||||
t.Error("Expected error for getting hashrate history for non-existent miner")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -56,15 +53,6 @@ type SystemInfo struct {
|
|||
InstalledMinersInfo []*InstallationDetails `json:"installed_miners_info"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Manager *Manager
|
||||
Router *gin.Engine
|
||||
Server *http.Server
|
||||
DisplayAddr string // The address to display in messages (e.g., 127.0.0.1:8080)
|
||||
SwaggerInstanceName string
|
||||
APIBasePath string // The base path for all API routes (e.g., /api/v1/mining)
|
||||
SwaggerUIPath string // The path where Swagger UI assets are served (e.g., /api/v1/mining/swagger)
|
||||
}
|
||||
|
||||
// Config represents the config for a miner, including XMRig specific options
|
||||
type Config struct {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,19 @@ import (
|
|||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// Service encapsulates the gin-gonic router and the mining manager.
|
||||
type Service struct {
|
||||
Manager ManagerInterface
|
||||
Router *gin.Engine
|
||||
Server *http.Server
|
||||
DisplayAddr string
|
||||
SwaggerInstanceName string
|
||||
APIBasePath string
|
||||
SwaggerUIPath string
|
||||
}
|
||||
|
||||
// NewService creates a new mining service
|
||||
func NewService(manager *Manager, listenAddr string, displayAddr string, swaggerNamespace string) *Service {
|
||||
func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) *Service {
|
||||
apiBasePath := "/" + strings.Trim(swaggerNamespace, "/")
|
||||
swaggerUIPath := apiBasePath + "/swagger" // Serve Swagger UI under a distinct sub-path
|
||||
|
||||
|
|
|
|||
158
pkg/mining/service_test.go
Normal file
158
pkg/mining/service_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MockMiner is a mock implementation of the Miner interface for testing.
|
||||
type MockMiner struct {
|
||||
InstallFunc func() error
|
||||
UninstallFunc func() error
|
||||
StartFunc func(config *Config) error
|
||||
StopFunc func() error
|
||||
GetStatsFunc func() (*PerformanceMetrics, error)
|
||||
GetNameFunc func() string
|
||||
GetPathFunc func() string
|
||||
GetBinaryPathFunc func() string
|
||||
CheckInstallationFunc func() (*InstallationDetails, error)
|
||||
GetLatestVersionFunc func() (string, error)
|
||||
GetHashrateHistoryFunc func() []HashratePoint
|
||||
AddHashratePointFunc func(point HashratePoint)
|
||||
ReduceHashrateHistoryFunc func(now time.Time)
|
||||
}
|
||||
|
||||
func (m *MockMiner) Install() error { return m.InstallFunc() }
|
||||
func (m *MockMiner) Uninstall() error { return m.UninstallFunc() }
|
||||
func (m *MockMiner) Start(config *Config) error { return m.StartFunc(config) }
|
||||
func (m *MockMiner) Stop() error { return m.StopFunc() }
|
||||
func (m *MockMiner) GetStats() (*PerformanceMetrics, error) { return m.GetStatsFunc() }
|
||||
func (m *MockMiner) GetName() string { return m.GetNameFunc() }
|
||||
func (m *MockMiner) GetPath() string { return m.GetPathFunc() }
|
||||
func (m *MockMiner) GetBinaryPath() string { return m.GetBinaryPathFunc() }
|
||||
func (m *MockMiner) CheckInstallation() (*InstallationDetails, error) { return m.CheckInstallationFunc() }
|
||||
func (m *MockMiner) GetLatestVersion() (string, error) { return m.GetLatestVersionFunc() }
|
||||
func (m *MockMiner) GetHashrateHistory() []HashratePoint { return m.GetHashrateHistoryFunc() }
|
||||
func (m *MockMiner) AddHashratePoint(point HashratePoint) { m.AddHashratePointFunc(point) }
|
||||
func (m *MockMiner) ReduceHashrateHistory(now time.Time) { m.ReduceHashrateHistoryFunc(now) }
|
||||
|
||||
// MockManager is a mock implementation of the Manager for testing.
|
||||
type MockManager struct {
|
||||
ListMinersFunc func() []Miner
|
||||
ListAvailableMinersFunc func() []AvailableMiner
|
||||
StartMinerFunc func(minerType string, config *Config) (Miner, error)
|
||||
StopMinerFunc func(minerName string) error
|
||||
GetMinerFunc func(minerName string) (Miner, error)
|
||||
GetMinerHashrateHistoryFunc func(minerName string) ([]HashratePoint, error)
|
||||
StopFunc func()
|
||||
}
|
||||
|
||||
func (m *MockManager) ListMiners() []Miner { return m.ListMinersFunc() }
|
||||
func (m *MockManager) ListAvailableMiners() []AvailableMiner { return m.ListAvailableMinersFunc() }
|
||||
func (m *MockManager) StartMiner(minerType string, config *Config) (Miner, error) { return m.StartMinerFunc(minerType, config) }
|
||||
func (m *MockManager) StopMiner(minerName string) error { return m.StopMinerFunc(minerName) }
|
||||
func (m *MockManager) GetMiner(minerName string) (Miner, error) { return m.GetMinerFunc(minerName) }
|
||||
func (m *MockManager) GetMinerHashrateHistory(minerName string) ([]HashratePoint, error) { return m.GetMinerHashrateHistoryFunc(minerName) }
|
||||
func (m *MockManager) Stop() { m.StopFunc() }
|
||||
|
||||
var _ ManagerInterface = (*MockManager)(nil)
|
||||
|
||||
func setupTestRouter() (*gin.Engine, *MockManager) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
mockManager := &MockManager{}
|
||||
service := &Service{
|
||||
Manager: mockManager,
|
||||
Router: router,
|
||||
APIBasePath: "/",
|
||||
SwaggerUIPath: "/swagger",
|
||||
}
|
||||
service.setupRoutes()
|
||||
return router, mockManager
|
||||
}
|
||||
|
||||
func TestHandleListMiners(t *testing.T) {
|
||||
router, mockManager := setupTestRouter()
|
||||
mockManager.ListMinersFunc = func() []Miner {
|
||||
return []Miner{&XMRigMiner{Name: "test-miner"}}
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", "/miners", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleStartMiner(t *testing.T) {
|
||||
router, mockManager := setupTestRouter()
|
||||
mockManager.StartMinerFunc = func(minerType string, config *Config) (Miner, error) {
|
||||
return &XMRigMiner{Name: "test-miner"}, nil
|
||||
}
|
||||
|
||||
config := &Config{Pool: "pool", Wallet: "wallet"}
|
||||
body, _ := json.Marshal(config)
|
||||
req, _ := http.NewRequest("POST", "/miners/xmrig", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleStopMiner(t *testing.T) {
|
||||
router, mockManager := setupTestRouter()
|
||||
mockManager.StopMinerFunc = func(minerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/miners/test-miner", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetMinerStats(t *testing.T) {
|
||||
router, mockManager := setupTestRouter()
|
||||
mockManager.GetMinerFunc = func(minerName string) (Miner, error) {
|
||||
return &MockMiner{GetStatsFunc: func() (*PerformanceMetrics, error) {
|
||||
return &PerformanceMetrics{Hashrate: 100}, nil
|
||||
}}, nil
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", "/miners/test-miner/stats", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetMinerHashrateHistory(t *testing.T) {
|
||||
router, mockManager := setupTestRouter()
|
||||
mockManager.GetMinerHashrateHistoryFunc = func(minerName string) ([]HashratePoint, error) {
|
||||
return []HashratePoint{{Timestamp: time.Now(), Hashrate: 100}}, nil
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", "/miners/test-miner/hashrate-history", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
|
@ -574,6 +574,18 @@ func (m *XMRigMiner) AddHashratePoint(point HashratePoint) {
|
|||
|
||||
// ReduceHashrateHistory aggregates older high-resolution data into 1-minute averages
|
||||
// and adds them to the low-resolution history.
|
||||
func (m *XMRigMiner) GetHighResHistoryLength() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.HashrateHistory)
|
||||
}
|
||||
|
||||
func (m *XMRigMiner) GetLowResHistoryLength() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.LowResHashrateHistory)
|
||||
}
|
||||
|
||||
func (m *XMRigMiner) ReduceHashrateHistory(now time.Time) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
224
pkg/mining/xmrig_test.go
Normal file
224
pkg/mining/xmrig_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockRoundTripper is a mock implementation of http.RoundTripper for testing.
|
||||
type MockRoundTripper func(req *http.Request) *http.Response
|
||||
|
||||
// RoundTrip executes a single HTTP transaction, returning a Response for the given Request.
|
||||
func (f MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
// newTestClient returns *http.Client with Transport replaced to avoid making real calls.
|
||||
func newTestClient(fn MockRoundTripper) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to create a temporary directory for testing
|
||||
func tempDir(t *testing.T) string {
|
||||
dir, err := os.MkdirTemp("", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestNewXMRigMiner(t *testing.T) {
|
||||
miner := NewXMRigMiner()
|
||||
if miner == nil {
|
||||
t.Fatal("NewXMRigMiner returned nil")
|
||||
}
|
||||
if miner.Name != "xmrig" {
|
||||
t.Errorf("Expected miner name to be 'xmrig', got '%s'", miner.Name)
|
||||
}
|
||||
if miner.Version != "latest" {
|
||||
t.Errorf("Expected miner version to be 'latest', got '%s'", miner.Version)
|
||||
}
|
||||
if !miner.API.Enabled {
|
||||
t.Error("Expected API to be enabled by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMRigMiner_GetName(t *testing.T) {
|
||||
miner := NewXMRigMiner()
|
||||
if name := miner.GetName(); name != "xmrig" {
|
||||
t.Errorf("Expected GetName() to return 'xmrig', got '%s'", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMRigMiner_GetLatestVersion(t *testing.T) {
|
||||
originalClient := httpClient
|
||||
httpClient = newTestClient(func(req *http.Request) *http.Response {
|
||||
if req.URL.String() != "https://api.github.com/repos/xmrig/xmrig/releases/latest" {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(strings.NewReader("Not Found")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"tag_name": "v6.18.0"}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
defer func() { httpClient = originalClient }()
|
||||
|
||||
miner := NewXMRigMiner()
|
||||
version, err := miner.GetLatestVersion()
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestVersion() returned an error: %v", err)
|
||||
}
|
||||
if version != "v6.18.0" {
|
||||
t.Errorf("Expected version 'v6.18.0', got '%s'", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMRigMiner_Start_Stop(t *testing.T) {
|
||||
// Create a temporary directory for the dummy executable
|
||||
tmpDir := t.TempDir()
|
||||
dummyExePath := filepath.Join(tmpDir, "xmrig")
|
||||
if runtime.GOOS == "windows" {
|
||||
dummyExePath += ".bat"
|
||||
// Create a dummy batch file for Windows
|
||||
if err := os.WriteFile(dummyExePath, []byte("@echo off\n"), 0755); err != nil {
|
||||
t.Fatalf("failed to create dummy executable: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Create a dummy shell script for other OSes
|
||||
if err := os.WriteFile(dummyExePath, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
t.Fatalf("failed to create dummy executable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
miner := NewXMRigMiner()
|
||||
miner.MinerBinary = dummyExePath
|
||||
|
||||
config := &Config{
|
||||
Pool: "test:1234",
|
||||
Wallet: "testwallet",
|
||||
}
|
||||
|
||||
err := miner.Start(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Start() returned an error: %v", err)
|
||||
}
|
||||
if !miner.Running {
|
||||
t.Fatal("Miner is not running after Start()")
|
||||
}
|
||||
|
||||
err = miner.Stop()
|
||||
if err != nil {
|
||||
// On some systems, stopping a process that quickly exits can error. We log but don't fail.
|
||||
t.Logf("Stop() returned an error (often benign in tests): %v", err)
|
||||
}
|
||||
|
||||
// Give a moment for the process to be marked as not running
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
miner.mu.Lock()
|
||||
if miner.Running {
|
||||
miner.mu.Unlock()
|
||||
t.Fatal("Miner is still running after Stop()")
|
||||
}
|
||||
miner.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestXMRigMiner_GetStats(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
summary := XMRigSummary{
|
||||
Hashrate: struct {
|
||||
Total []float64 `json:"total"`
|
||||
}{Total: []float64{123.45}},
|
||||
Results: struct {
|
||||
SharesGood uint64 `json:"shares_good"`
|
||||
SharesTotal uint64 `json:"shares_total"`
|
||||
}{SharesGood: 10, SharesTotal: 12},
|
||||
Uptime: 600,
|
||||
Algorithm: "rx/0",
|
||||
}
|
||||
json.NewEncoder(w).Encode(summary)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
originalHTTPClient := httpClient
|
||||
httpClient = server.Client()
|
||||
defer func() { httpClient = originalHTTPClient }()
|
||||
|
||||
miner := NewXMRigMiner()
|
||||
miner.Running = true // Mock running state
|
||||
miner.API.ListenHost = strings.TrimPrefix(server.URL, "http://")
|
||||
miner.API.ListenHost, miner.API.ListenPort = server.Listener.Addr().String(), 0
|
||||
parts := strings.Split(server.Listener.Addr().String(), ":")
|
||||
miner.API.ListenHost = parts[0]
|
||||
fmt.Sscanf(parts[1], "%d", &miner.API.ListenPort)
|
||||
|
||||
stats, err := miner.GetStats()
|
||||
if err != nil {
|
||||
t.Fatalf("GetStats() returned an error: %v", err)
|
||||
}
|
||||
if stats.Hashrate != 123 {
|
||||
t.Errorf("Expected hashrate 123, got %d", stats.Hashrate)
|
||||
}
|
||||
if stats.Shares != 10 {
|
||||
t.Errorf("Expected 10 shares, got %d", stats.Shares)
|
||||
}
|
||||
if stats.Rejected != 2 {
|
||||
t.Errorf("Expected 2 rejected shares, got %d", stats.Rejected)
|
||||
}
|
||||
if stats.Uptime != 600 {
|
||||
t.Errorf("Expected uptime 600, got %d", stats.Uptime)
|
||||
}
|
||||
if stats.Algorithm != "rx/0" {
|
||||
t.Errorf("Expected algorithm 'rx/0', got '%s'", stats.Algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMRigMiner_HashrateHistory(t *testing.T) {
|
||||
miner := NewXMRigMiner()
|
||||
now := time.Now()
|
||||
|
||||
// Add high-resolution points
|
||||
for i := 0; i < 10; i++ {
|
||||
miner.AddHashratePoint(HashratePoint{Timestamp: now.Add(time.Duration(i) * time.Second), Hashrate: 100 + i})
|
||||
}
|
||||
|
||||
history := miner.GetHashrateHistory()
|
||||
if len(history) != 10 {
|
||||
t.Fatalf("Expected 10 hashrate points, got %d", len(history))
|
||||
}
|
||||
|
||||
// Test ReduceHashrateHistory
|
||||
// Move time forward to make some points eligible for reduction
|
||||
future := now.Add(HighResolutionDuration + 30*time.Second)
|
||||
miner.ReduceHashrateHistory(future)
|
||||
|
||||
// After reduction, high-res history should be smaller
|
||||
if miner.GetHighResHistoryLength() >= 10 {
|
||||
t.Errorf("High-res history not reduced, size: %d", miner.GetHighResHistoryLength())
|
||||
}
|
||||
if miner.GetLowResHistoryLength() == 0 {
|
||||
t.Error("Low-res history not populated")
|
||||
}
|
||||
|
||||
combinedHistory := miner.GetHashrateHistory()
|
||||
if len(combinedHistory) == 0 {
|
||||
t.Error("GetHashrateHistory returned empty slice after reduction")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue