From 50cde8bdf35ac4c963350e824af044a2978e57b3 Mon Sep 17 00:00:00 2001 From: snider Date: Wed, 31 Dec 2025 11:12:33 +0000 Subject: [PATCH] refactor: Add MinerFactory to centralize miner instantiation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- pkg/mining/manager.go | 22 ++--- pkg/mining/miner_factory.go | 129 +++++++++++++++++++++++++ pkg/mining/miner_factory_test.go | 155 +++++++++++++++++++++++++++++++ pkg/mining/service.go | 31 ++----- 4 files changed, 298 insertions(+), 39 deletions(-) create mode 100644 pkg/mining/miner_factory.go create mode 100644 pkg/mining/miner_factory_test.go diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index f9d4b98..c766dd8 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -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 { diff --git a/pkg/mining/miner_factory.go b/pkg/mining/miner_factory.go new file mode 100644 index 0000000..715fdbe --- /dev/null +++ b/pkg/mining/miner_factory.go @@ -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) +} diff --git a/pkg/mining/miner_factory_test.go b/pkg/mining/miner_factory_test.go new file mode 100644 index 0000000..c4d7da6 --- /dev/null +++ b/pkg/mining/miner_factory_test.go @@ -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)) + } +} diff --git a/pkg/mining/service.go b/pkg/mining/service.go index 55410d0..505182b 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -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 }