diff --git a/pkg/mining/manager_test.go b/pkg/mining/manager_test.go index 716898c..972bb4b 100644 --- a/pkg/mining/manager_test.go +++ b/pkg/mining/manager_test.go @@ -1,47 +1,156 @@ package mining import ( + "os" + "path/filepath" + "runtime" "testing" ) -// TestManager_StartStopMultipleMiners tests starting and stopping multiple miners. -func TestManager_StartStopMultipleMiners(t *testing.T) { - manager := NewManager() - defer manager.Stop() +// setupTestManager creates a new Manager and a dummy executable for tests. +// It also temporarily modifies the PATH to include the dummy executable's directory. +func setupTestManager(t *testing.T) *Manager { + dummyDir := t.TempDir() + executableName := "xmrig" + if runtime.GOOS == "windows" { + executableName += ".exe" + } + dummyPath := filepath.Join(dummyDir, executableName) - configs := []*Config{ - {Pool: "pool1", Wallet: "wallet1"}, + // Create a script that does nothing but exit, to simulate the miner executable + var script []byte + if runtime.GOOS == "windows" { + script = []byte("@echo off\r\nexit 0") + } else { + script = []byte("#!/bin/sh\nexit 0") } - 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") - } + if err := os.WriteFile(dummyPath, script, 0755); err != nil { + t.Fatalf("Failed to create dummy miner executable: %v", err) } + + // Prepend the dummy directory to the PATH + originalPath := os.Getenv("PATH") + t.Cleanup(func() { + os.Setenv("PATH", originalPath) + }) + os.Setenv("PATH", dummyDir+string(os.PathListSeparator)+originalPath) + + return NewManager() } -// TestManager_collectMinerStats tests the stat collection logic. -func TestManager_collectMinerStats(t *testing.T) { - manager := NewManager() - defer manager.Stop() +// TestStartMiner tests the StartMiner function +func TestStartMiner(t *testing.T) { + m := setupTestManager(t) + defer m.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() -} + config := &Config{ + HTTPPort: 9001, // Use a different port to avoid conflict + Pool: "test:1234", + Wallet: "testwallet", + } -// TestManager_GetMinerHashrateHistory tests getting hashrate history. -func TestManager_GetMinerHashrateHistory(t *testing.T) { - manager := NewManager() - defer manager.Stop() + // Case 1: Successfully start a supported miner + miner, err := m.StartMiner("xmrig", config) + if err != nil { + t.Fatalf("Expected to start miner, but got error: %v", err) + } + if miner == nil { + t.Fatal("Expected miner to be non-nil, but it was") + } + if _, exists := m.miners[miner.GetName()]; !exists { + t.Errorf("Miner %s was not added to the manager's list", miner.GetName()) + } - _, err := manager.GetMinerHashrateHistory("non-existent") + // Case 2: Attempt to start an unsupported miner + _, err = m.StartMiner("unsupported", config) if err == nil { - t.Error("Expected error for getting hashrate history for non-existent miner") + t.Error("Expected an error when starting an unsupported miner, but got nil") + } + + // Case 3: Attempt to start a duplicate miner + _, err = m.StartMiner("xmrig", config) + if err == nil { + t.Error("Expected an error when starting a duplicate miner, but got nil") + } +} + +// TestStopMiner tests the StopMiner function +func TestStopMiner(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + config := &Config{ + HTTPPort: 9002, + Pool: "test:1234", + Wallet: "testwallet", + } + + // Case 1: Stop a running miner + miner, _ := m.StartMiner("xmrig", config) + err := m.StopMiner(miner.GetName()) + if err != nil { + t.Fatalf("Expected to stop miner, but got error: %v", err) + } + if _, exists := m.miners[miner.GetName()]; exists { + t.Errorf("Miner %s was not removed from the manager's list", miner.GetName()) + } + + // Case 2: Attempt to stop a non-existent miner + err = m.StopMiner("nonexistent") + if err == nil { + t.Error("Expected an error when stopping a non-existent miner, but got nil") + } +} + +// TestGetMiner tests the GetMiner function +func TestGetMiner(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + config := &Config{ + HTTPPort: 9003, + Pool: "test:1234", + Wallet: "testwallet", + } + + // Case 1: Get an existing miner + startedMiner, _ := m.StartMiner("xmrig", config) + retrievedMiner, err := m.GetMiner(startedMiner.GetName()) + if err != nil { + t.Fatalf("Expected to get miner, but got error: %v", err) + } + if retrievedMiner.GetName() != startedMiner.GetName() { + t.Errorf("Expected to get miner %s, but got %s", startedMiner.GetName(), retrievedMiner.GetName()) + } + + // Case 2: Attempt to get a non-existent miner + _, err = m.GetMiner("nonexistent") + if err == nil { + t.Error("Expected an error when getting a non-existent miner, but got nil") + } +} + +// TestListMiners tests the ListMiners function +func TestListMiners(t *testing.T) { + m := setupTestManager(t) + defer m.Stop() + + // Case 1: List miners when empty + miners := m.ListMiners() + if len(miners) != 0 { + t.Errorf("Expected 0 miners, but got %d", len(miners)) + } + + // Case 2: List miners when not empty + config := &Config{ + HTTPPort: 9004, + Pool: "test:1234", + Wallet: "testwallet", + } + _, _ = m.StartMiner("xmrig", config) + miners = m.ListMiners() + if len(miners) != 1 { + t.Errorf("Expected 1 miner, but got %d", len(miners)) } } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index c7ec1c7..9690ef6 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -110,7 +110,7 @@ func (s *Service) setupRoutes() { // New route to serve the custom HTML element bundle // This path now points to the output of the Angular project within the 'ui' directory - s.Router.StaticFile("/component/mining-dashboard.js", "./ui/dist/ui/main.js") + s.Router.StaticFile("/component/mining-dashboard.js", "./ui/dist/ui/mbe-mining-dashboard.js") // Register Swagger UI route under a distinct sub-path to avoid conflicts swaggerURL := ginSwagger.URL(fmt.Sprintf("http://%s%s/doc.json", s.DisplayAddr, s.SwaggerUIPath)) diff --git a/pkg/mining/service_test.go b/pkg/mining/service_test.go index 34488cf..d4fc74b 100644 --- a/pkg/mining/service_test.go +++ b/pkg/mining/service_test.go @@ -92,6 +92,35 @@ func TestHandleListMiners(t *testing.T) { } } +func TestHandleGetInfo(t *testing.T) { + router, _ := setupTestRouter() + + // Case 1: Successful response + req, _ := http.NewRequest("GET", "/info", 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 TestHandleDoctor(t *testing.T) { + router, mockManager := setupTestRouter() + mockManager.ListAvailableMinersFunc = func() []AvailableMiner { + return []AvailableMiner{{Name: "xmrig"}} + } + + // Case 1: Successful response + req, _ := http.NewRequest("POST", "/doctor", 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) { diff --git a/pkg/mining/xmrig.go b/pkg/mining/xmrig.go index 8ad85f0..9a524c6 100644 --- a/pkg/mining/xmrig.go +++ b/pkg/mining/xmrig.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "os" "os/exec" @@ -161,76 +162,76 @@ func (m *XMRigMiner) Uninstall() error { return os.RemoveAll(m.GetPath()) } -// CheckInstallation checks if the miner is installed and returns its details -func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) { +// findMinerBinary searches for the miner executable, first in the standard installation path, +// then falls back to the system's PATH. +func (m *XMRigMiner) findMinerBinary() (string, error) { + executableName := "xmrig" + if runtime.GOOS == "windows" { + executableName += ".exe" + } + + // 1. Check the standard installation directory first baseInstallPath := m.GetPath() - details := &InstallationDetails{ - Path: baseInstallPath, // Initialize with base path, will be updated to versioned path - } - - if _, err := os.Stat(baseInstallPath); os.IsNotExist(err) { - details.IsInstalled = false - return details, nil - } - - // The directory exists, now check for the executable by finding the versioned sub-folder - files, err := os.ReadDir(baseInstallPath) - if err != nil { - return nil, fmt.Errorf("could not read installation directory: %w", err) - } - - var versionedDir string - for _, f := range files { - if f.IsDir() && strings.HasPrefix(f.Name(), "xmrig-") { - versionedDir = f.Name() - break + if _, err := os.Stat(baseInstallPath); err == nil { + files, err := os.ReadDir(baseInstallPath) + if err == nil { + for _, f := range files { + if f.IsDir() && strings.HasPrefix(f.Name(), "xmrig-") { + versionedPath := filepath.Join(baseInstallPath, f.Name()) + fullPath := filepath.Join(versionedPath, executableName) + if _, err := os.Stat(fullPath); err == nil { + log.Printf("Found miner binary at standard path: %s", fullPath) + return fullPath, nil + } + } + } } } - if versionedDir == "" { - details.IsInstalled = false // Directory exists but is empty or malformed - return details, nil + // 2. Fallback to searching the system PATH + path, err := exec.LookPath(executableName) + if err == nil { + log.Printf("Found miner binary in system PATH: %s", path) + return path, nil } - // Update the Path to be the versioned directory - details.Path = filepath.Join(baseInstallPath, versionedDir) + return "", errors.New("miner executable not found in standard directory or system PATH") +} - var executableName string - if runtime.GOOS == "windows" { - executableName = "xmrig.exe" - } else { - executableName = "xmrig" - } +// CheckInstallation checks if the miner is installed and returns its details +func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) { + details := &InstallationDetails{} - executablePath := filepath.Join(details.Path, executableName) - if _, err := os.Stat(executablePath); os.IsNotExist(err) { - details.IsInstalled = false // Versioned folder exists, but no executable - return details, nil + binaryPath, err := m.findMinerBinary() + if err != nil { + details.IsInstalled = false + return details, nil // Return not-installed, but no error } details.IsInstalled = true - details.MinerBinary = executablePath // Set the full path to the miner binary + details.MinerBinary = binaryPath + details.Path = filepath.Dir(binaryPath) // The directory containing the executable // Try to get the version from the executable - cmd := exec.Command(executablePath, "--version") + cmd := exec.Command(binaryPath, "--version") var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { details.Version = "Unknown (could not run executable)" - return details, nil - } - - // XMRig version output is typically "XMRig 6.18.0" - fields := strings.Fields(out.String()) - if len(fields) >= 2 { - details.Version = fields[1] } else { - details.Version = "Unknown (could not parse version)" + // XMRig version output is typically "XMRig 6.18.0" + fields := strings.Fields(out.String()) + if len(fields) >= 2 { + details.Version = fields[1] + } else { + details.Version = "Unknown (could not parse version)" + } } - // Update the XMRigMiner struct's Path and MinerBinary fields + // Update the XMRigMiner struct's fields m.Path = details.Path m.MinerBinary = details.MinerBinary + m.Version = details.Version // Keep the miner's version in sync return details, nil } diff --git a/server.log b/server.log new file mode 100644 index 0000000..2855339 --- /dev/null +++ b/server.log @@ -0,0 +1,15 @@ +Tidying dependencies... +go mod tidy +go: downloading github.com/inconshreveable/mousetrap v1.1.0 +go: downloading github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 +go: downloading github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 +go: downloading github.com/tklauser/go-sysconf v0.3.15 +go: downloading github.com/stretchr/testify v1.11.1 +go: downloading github.com/gin-contrib/gzip v0.0.6 +go: downloading github.com/google/go-cmp v0.7.0 +go: downloading github.com/ebitengine/purego v0.9.0 +go: downloading github.com/yusufpapurcu/wmi v1.2.4 +go: downloading github.com/json-iterator/go v1.1.12 +go: downloading github.com/modern-go/reflect2 v1.0.2 +go: downloading github.com/bytedance/sonic v1.14.0 +go: downloading github.com/goccy/go-json v0.10.5 diff --git a/server2.log b/server2.log new file mode 100644 index 0000000..1370250 --- /dev/null +++ b/server2.log @@ -0,0 +1,6 @@ +Tidying dependencies... +go mod tidy +Generating Swagger documentation... +swag init -g ./cmd/mining/main.go +make: swag: No such file or directory +make: *** [Makefile:93: docs] Error 127 diff --git a/server3.log b/server3.log new file mode 100644 index 0000000..1370250 --- /dev/null +++ b/server3.log @@ -0,0 +1,6 @@ +Tidying dependencies... +go mod tidy +Generating Swagger documentation... +swag init -g ./cmd/mining/main.go +make: swag: No such file or directory +make: *** [Makefile:93: docs] Error 127 diff --git a/server4.log b/server4.log new file mode 100644 index 0000000..e69de29 diff --git a/server5.log b/server5.log new file mode 100644 index 0000000..e69de29 diff --git a/server6.log b/server6.log new file mode 100644 index 0000000..e69de29 diff --git a/ui/package.json b/ui/package.json index a6e3bd8..2bdcd47 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build --output-path=dist && cat dist/{runtime,polyfills,main}.js > dist/mbe-mining-dashboard.js", + "build": "ng build --output-path=dist/ui && cat dist/ui/runtime.js dist/ui/polyfills.js dist/ui/main.js > dist/ui/mbe-mining-dashboard.js", "watch": "ng build --watch --configuration development", "test": "ng test" },