Mining/pkg/mining/xmrig_test.go

278 lines
7.7 KiB
Go

package mining
import (
"context"
"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_Good(t *testing.T) {
miner := NewXMRigMiner()
if miner == nil {
t.Fatal("NewXMRigMiner returned nil")
}
if miner.Name != "miner" {
t.Errorf("Expected miner name to be 'miner', 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_Good(t *testing.T) {
miner := NewXMRigMiner()
if name := miner.GetName(); name != "miner" {
t.Errorf("Expected GetName() to return 'miner', got '%s'", name)
}
}
func TestXMRigMiner_GetLatestVersion_Good(t *testing.T) {
originalClient := getHTTPClient()
setHTTPClient(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 setHTTPClient(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_GetLatestVersion_Bad(t *testing.T) {
originalClient := getHTTPClient()
setHTTPClient(newTestClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(strings.NewReader("Not Found")),
Header: make(http.Header),
}
}))
defer setHTTPClient(originalClient)
miner := NewXMRigMiner()
_, err := miner.GetLatestVersion()
if err == nil {
t.Fatalf("GetLatestVersion() did not return an error")
}
}
func TestXMRigMiner_Start_Stop_Good(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",
HTTPPort: 9999, // Required for API port assignment
}
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_Start_Stop_Bad(t *testing.T) {
miner := NewXMRigMiner()
miner.MinerBinary = "nonexistent"
config := &Config{
Pool: "test:1234",
Wallet: "testwallet",
}
err := miner.Start(config)
if err == nil {
t.Fatalf("Start() did not return an error")
}
}
func TestXMRigMiner_GetStats_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
summary := XMRigSummary{
Hashrate: struct {
Total []float64 `json:"total"`
Highest float64 `json:"highest"`
}{Total: []float64{123.45}, Highest: 130.0},
Results: struct {
DiffCurrent int `json:"diff_current"`
SharesGood int `json:"shares_good"`
SharesTotal int `json:"shares_total"`
AvgTime int `json:"avg_time"`
AvgTimeMS int `json:"avg_time_ms"`
HashesTotal int `json:"hashes_total"`
Best []int `json:"best"`
}{SharesGood: 10, SharesTotal: 12},
Uptime: 600,
Algo: "rx/0",
}
json.NewEncoder(w).Encode(summary)
}))
defer server.Close()
originalHTTPClient := getHTTPClient()
setHTTPClient(server.Client())
defer setHTTPClient(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(context.Background())
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_GetStats_Bad(t *testing.T) {
// Don't start a server, so the API call will fail
miner := NewXMRigMiner()
miner.Running = true // Mock running state
miner.API.ListenHost = "127.0.0.1"
miner.API.ListenPort = 9999 // A port that is unlikely to be in use
_, err := miner.GetStats(context.Background())
if err == nil {
t.Fatalf("GetStats() did not return an error")
}
}
func TestXMRigMiner_HashrateHistory_Good(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")
}
}