refactor: Add MinerFactory to centralize miner instantiation

- Create pkg/mining/miner_factory.go with factory pattern
- Support for miner type aliases (e.g., "ttminer" -> "tt-miner")
- Add global convenience functions: CreateMiner, IsMinerSupported, etc.
- Replace 5 duplicate switch statements in manager.go and service.go
- Makes adding new miner types simpler (single registration point)
- Full test coverage in miner_factory_test.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
snider 2025-12-31 11:12:33 +00:00
parent 2a30744a08
commit 50cde8bdf3
4 changed files with 298 additions and 39 deletions

View file

@ -239,14 +239,9 @@ func (m *Manager) StartMiner(ctx context.Context, minerType string, config *Conf
config = &Config{}
}
var miner Miner
switch strings.ToLower(minerType) {
case "xmrig":
miner = NewXMRigMiner()
case "tt-miner", "ttminer":
miner = NewTTMiner()
default:
return nil, fmt.Errorf("unsupported miner type: %s", minerType)
miner, err := CreateMiner(minerType)
if err != nil {
return nil, err
}
instanceName := miner.GetName()
@ -358,14 +353,9 @@ func (m *Manager) UninstallMiner(ctx context.Context, minerType string) error {
}
}
var miner Miner
switch strings.ToLower(minerType) {
case "xmrig":
miner = NewXMRigMiner()
case "tt-miner", "ttminer":
miner = NewTTMiner()
default:
return fmt.Errorf("unsupported miner type: %s", minerType)
miner, err := CreateMiner(minerType)
if err != nil {
return err
}
if err := miner.Uninstall(); err != nil {

129
pkg/mining/miner_factory.go Normal file
View file

@ -0,0 +1,129 @@
package mining
import (
"fmt"
"strings"
"sync"
)
// MinerConstructor is a function that creates a new miner instance
type MinerConstructor func() Miner
// MinerFactory handles miner instantiation and registration
type MinerFactory struct {
mu sync.RWMutex
constructors map[string]MinerConstructor
aliases map[string]string // maps aliases to canonical names
}
// globalFactory is the default factory instance
var globalFactory = NewMinerFactory()
// NewMinerFactory creates a new MinerFactory with default miners registered
func NewMinerFactory() *MinerFactory {
f := &MinerFactory{
constructors: make(map[string]MinerConstructor),
aliases: make(map[string]string),
}
f.registerDefaults()
return f
}
// registerDefaults registers all built-in miners
func (f *MinerFactory) registerDefaults() {
// XMRig miner (CPU/GPU RandomX, Cryptonight, etc.)
f.Register("xmrig", func() Miner { return NewXMRigMiner() })
// TT-Miner (GPU Kawpow, etc.)
f.Register("tt-miner", func() Miner { return NewTTMiner() })
f.RegisterAlias("ttminer", "tt-miner")
}
// Register adds a miner constructor to the factory
func (f *MinerFactory) Register(name string, constructor MinerConstructor) {
f.mu.Lock()
defer f.mu.Unlock()
f.constructors[strings.ToLower(name)] = constructor
}
// RegisterAlias adds an alias for an existing miner type
func (f *MinerFactory) RegisterAlias(alias, canonicalName string) {
f.mu.Lock()
defer f.mu.Unlock()
f.aliases[strings.ToLower(alias)] = strings.ToLower(canonicalName)
}
// Create instantiates a miner of the specified type
func (f *MinerFactory) Create(minerType string) (Miner, error) {
f.mu.RLock()
defer f.mu.RUnlock()
name := strings.ToLower(minerType)
// Check for alias first
if canonical, ok := f.aliases[name]; ok {
name = canonical
}
constructor, ok := f.constructors[name]
if !ok {
return nil, fmt.Errorf("unsupported miner type: %s", minerType)
}
return constructor(), nil
}
// IsSupported checks if a miner type is registered
func (f *MinerFactory) IsSupported(minerType string) bool {
f.mu.RLock()
defer f.mu.RUnlock()
name := strings.ToLower(minerType)
// Check alias
if canonical, ok := f.aliases[name]; ok {
name = canonical
}
_, ok := f.constructors[name]
return ok
}
// ListTypes returns all registered miner type names (excluding aliases)
func (f *MinerFactory) ListTypes() []string {
f.mu.RLock()
defer f.mu.RUnlock()
types := make([]string, 0, len(f.constructors))
for name := range f.constructors {
types = append(types, name)
}
return types
}
// --- Global factory functions for convenience ---
// CreateMiner creates a miner using the global factory
func CreateMiner(minerType string) (Miner, error) {
return globalFactory.Create(minerType)
}
// IsMinerSupported checks if a miner type is supported using the global factory
func IsMinerSupported(minerType string) bool {
return globalFactory.IsSupported(minerType)
}
// ListMinerTypes returns all registered miner types from the global factory
func ListMinerTypes() []string {
return globalFactory.ListTypes()
}
// RegisterMinerType adds a miner constructor to the global factory
func RegisterMinerType(name string, constructor MinerConstructor) {
globalFactory.Register(name, constructor)
}
// RegisterMinerAlias adds an alias to the global factory
func RegisterMinerAlias(alias, canonicalName string) {
globalFactory.RegisterAlias(alias, canonicalName)
}

View file

@ -0,0 +1,155 @@
package mining
import (
"testing"
)
func TestMinerFactory_Create(t *testing.T) {
factory := NewMinerFactory()
tests := []struct {
name string
minerType string
wantErr bool
}{
{"xmrig lowercase", "xmrig", false},
{"xmrig uppercase", "XMRIG", false},
{"xmrig mixed case", "XmRig", false},
{"tt-miner", "tt-miner", false},
{"ttminer alias", "ttminer", false},
{"unknown type", "unknown", true},
{"empty type", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
miner, err := factory.Create(tt.minerType)
if tt.wantErr {
if err == nil {
t.Errorf("Create(%q) expected error, got nil", tt.minerType)
}
} else {
if err != nil {
t.Errorf("Create(%q) unexpected error: %v", tt.minerType, err)
}
if miner == nil {
t.Errorf("Create(%q) returned nil miner", tt.minerType)
}
}
})
}
}
func TestMinerFactory_IsSupported(t *testing.T) {
factory := NewMinerFactory()
tests := []struct {
minerType string
want bool
}{
{"xmrig", true},
{"tt-miner", true},
{"ttminer", true}, // alias
{"unknown", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.minerType, func(t *testing.T) {
if got := factory.IsSupported(tt.minerType); got != tt.want {
t.Errorf("IsSupported(%q) = %v, want %v", tt.minerType, got, tt.want)
}
})
}
}
func TestMinerFactory_ListTypes(t *testing.T) {
factory := NewMinerFactory()
types := factory.ListTypes()
if len(types) < 2 {
t.Errorf("ListTypes() returned %d types, expected at least 2", len(types))
}
// Check that expected types are present
typeMap := make(map[string]bool)
for _, typ := range types {
typeMap[typ] = true
}
expectedTypes := []string{"xmrig", "tt-miner"}
for _, expected := range expectedTypes {
if !typeMap[expected] {
t.Errorf("ListTypes() missing expected type %q", expected)
}
}
}
func TestMinerFactory_Register(t *testing.T) {
factory := NewMinerFactory()
// Register a custom miner type
called := false
factory.Register("custom-miner", func() Miner {
called = true
return NewXMRigMiner() // Return something valid for testing
})
if !factory.IsSupported("custom-miner") {
t.Error("custom-miner should be supported after registration")
}
_, err := factory.Create("custom-miner")
if err != nil {
t.Errorf("Create custom-miner failed: %v", err)
}
if !called {
t.Error("custom constructor was not called")
}
}
func TestMinerFactory_RegisterAlias(t *testing.T) {
factory := NewMinerFactory()
// Register an alias for xmrig
factory.RegisterAlias("x", "xmrig")
if !factory.IsSupported("x") {
t.Error("alias 'x' should be supported")
}
miner, err := factory.Create("x")
if err != nil {
t.Errorf("Create with alias failed: %v", err)
}
if miner == nil {
t.Error("Create with alias returned nil miner")
}
}
func TestGlobalFactory_CreateMiner(t *testing.T) {
// Test global convenience functions
miner, err := CreateMiner("xmrig")
if err != nil {
t.Errorf("CreateMiner failed: %v", err)
}
if miner == nil {
t.Error("CreateMiner returned nil")
}
}
func TestGlobalFactory_IsMinerSupported(t *testing.T) {
if !IsMinerSupported("xmrig") {
t.Error("xmrig should be supported")
}
if IsMinerSupported("nosuchminer") {
t.Error("nosuchminer should not be supported")
}
}
func TestGlobalFactory_ListMinerTypes(t *testing.T) {
types := ListMinerTypes()
if len(types) < 2 {
t.Errorf("ListMinerTypes() returned %d types, expected at least 2", len(types))
}
}

View file

@ -420,14 +420,9 @@ func (s *Service) updateInstallationCache() (*SystemInfo, error) {
}
for _, availableMiner := range s.Manager.ListAvailableMiners() {
var miner Miner
switch availableMiner.Name {
case "xmrig":
miner = NewXMRigMiner()
case "tt-miner":
miner = NewTTMiner()
default:
continue
miner, err := CreateMiner(availableMiner.Name)
if err != nil {
continue // Skip unsupported miner types
}
details, err := miner.CheckInstallation()
if err != nil {
@ -483,14 +478,9 @@ func (s *Service) handleDoctor(c *gin.Context) {
func (s *Service) handleUpdateCheck(c *gin.Context) {
updates := make(map[string]string)
for _, availableMiner := range s.Manager.ListAvailableMiners() {
var miner Miner
switch availableMiner.Name {
case "xmrig":
miner = NewXMRigMiner()
case "tt-miner":
miner = NewTTMiner()
default:
continue
miner, err := CreateMiner(availableMiner.Name)
if err != nil {
continue // Skip unsupported miner types
}
details, err := miner.CheckInstallation()
@ -580,13 +570,8 @@ func (s *Service) handleListAvailableMiners(c *gin.Context) {
// @Router /miners/{miner_type}/install [post]
func (s *Service) handleInstallMiner(c *gin.Context) {
minerType := c.Param("miner_name")
var miner Miner
switch minerType {
case "xmrig":
miner = NewXMRigMiner()
case "tt-miner":
miner = NewTTMiner()
default:
miner, err := CreateMiner(minerType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown miner type"})
return
}