Files
tyto/backend/internal/collectors/gpu/amd_test.go
vikingowl a0a947094d feat: add multi-GPU support and operational modes
Multi-GPU Collection System:
- Add modular GPU collector architecture in collectors/gpu/
- Support AMD (amdgpu), NVIDIA (nvidia-smi), and Intel (i915/xe) GPUs
- GPU Manager auto-detects and aggregates all vendor collectors
- Backward-compatible JSON output for existing frontend

Operational Modes:
- Standalone mode (default): single-host monitoring, no database
- Server mode: multi-device with database, auth, agents (WIP)
- Agent mode: lightweight reporter to central server (WIP)
- Mode selection via TYTO_MODE env var or config.yaml

Configuration Updates:
- Add server config (gRPC port, mTLS settings, registration)
- Add agent config (ID, server URL, TLS certificates)
- Add database config (SQLite/PostgreSQL support)
- Support TYTO_* prefixed environment variables

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 07:21:50 +01:00

194 lines
4.7 KiB
Go

package gpu
import (
"os"
"path/filepath"
"testing"
"tyto/internal/models"
)
func TestAMDCollector(t *testing.T) {
tmpDir := t.TempDir()
sysPath := filepath.Join(tmpDir, "sys")
// Create mock AMD GPU sysfs structure
gpuPath := filepath.Join(sysPath, "class/drm/card0/device")
if err := os.MkdirAll(gpuPath, 0755); err != nil {
t.Fatal(err)
}
// Create driver symlink (required for AMD GPU detection)
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
if err := os.MkdirAll(driverTarget, 0755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(driverTarget, filepath.Join(gpuPath, "driver")); err != nil {
t.Fatal(err)
}
// GPU utilization
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("75\n"), 0644); err != nil {
t.Fatal(err)
}
// VRAM
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_used"), []byte("4294967296\n"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_total"), []byte("17179869184\n"), 0644); err != nil {
t.Fatal(err)
}
// Clock frequencies
sclk := "0: 500Mhz\n1: 800Mhz\n2: 1200Mhz *\n"
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_sclk"), []byte(sclk), 0644); err != nil {
t.Fatal(err)
}
mclk := "0: 400Mhz\n1: 875Mhz *\n"
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_mclk"), []byte(mclk), 0644); err != nil {
t.Fatal(err)
}
// Create hwmon for temperature, power, fan
hwmonPath := filepath.Join(gpuPath, "hwmon/hwmon5")
if err := os.MkdirAll(hwmonPath, 0755); err != nil {
t.Fatal(err)
}
// Temperature (65°C in millidegrees)
if err := os.WriteFile(filepath.Join(hwmonPath, "temp1_input"), []byte("65000\n"), 0644); err != nil {
t.Fatal(err)
}
// Power (150W in microwatts)
if err := os.WriteFile(filepath.Join(hwmonPath, "power1_average"), []byte("150000000\n"), 0644); err != nil {
t.Fatal(err)
}
// Fan RPM
if err := os.WriteFile(filepath.Join(hwmonPath, "fan1_input"), []byte("1500\n"), 0644); err != nil {
t.Fatal(err)
}
collector := NewAMDCollector(sysPath)
count := collector.Detect()
if count != 1 {
t.Errorf("Expected 1 GPU, got %d", count)
}
gpus, err := collector.Collect()
if err != nil {
t.Fatalf("Collect failed: %v", err)
}
if len(gpus) != 1 {
t.Fatalf("Expected 1 GPU result, got %d", len(gpus))
}
gpu := gpus[0]
if gpu.Vendor != models.GPUVendorAMD {
t.Errorf("Expected vendor AMD, got %s", gpu.Vendor)
}
if gpu.Utilization != 75 {
t.Errorf("Expected utilization 75, got %d", gpu.Utilization)
}
if gpu.MemoryUsed != 4294967296 {
t.Errorf("Expected VRAM used 4294967296, got %d", gpu.MemoryUsed)
}
if gpu.MemoryTotal != 17179869184 {
t.Errorf("Expected VRAM total 17179869184, got %d", gpu.MemoryTotal)
}
if gpu.ClockCore != 1200 {
t.Errorf("Expected GPU clock 1200, got %d", gpu.ClockCore)
}
if gpu.ClockMemory != 875 {
t.Errorf("Expected memory clock 875, got %d", gpu.ClockMemory)
}
if gpu.Temperature != 65.0 {
t.Errorf("Expected temperature 65.0, got %f", gpu.Temperature)
}
if gpu.PowerWatts != 150.0 {
t.Errorf("Expected power 150.0W, got %f", gpu.PowerWatts)
}
if gpu.FanRPM != 1500 {
t.Errorf("Expected fan 1500 RPM, got %d", gpu.FanRPM)
}
}
func TestAMDCollector_NoGPU(t *testing.T) {
tmpDir := t.TempDir()
collector := NewAMDCollector(tmpDir)
count := collector.Detect()
if count != 0 {
t.Errorf("Expected 0 GPUs, got %d", count)
}
gpus, err := collector.Collect()
if err != nil {
t.Fatalf("Collect failed: %v", err)
}
if len(gpus) != 0 {
t.Errorf("Expected 0 GPU results, got %d", len(gpus))
}
}
func TestAMDCollector_MultipleGPUs(t *testing.T) {
tmpDir := t.TempDir()
sysPath := filepath.Join(tmpDir, "sys")
// Create two AMD GPUs
for i := 0; i < 2; i++ {
gpuPath := filepath.Join(sysPath, "class/drm", "card"+string(rune('0'+i)), "device")
if err := os.MkdirAll(gpuPath, 0755); err != nil {
t.Fatal(err)
}
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
if err := os.MkdirAll(driverTarget, 0755); err != nil {
// Already exists, ignore
}
// Create symlink only if it doesn't exist
driverLink := filepath.Join(gpuPath, "driver")
if _, err := os.Lstat(driverLink); os.IsNotExist(err) {
if err := os.Symlink(driverTarget, driverLink); err != nil {
t.Fatal(err)
}
}
// Minimal GPU data
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("50\n"), 0644); err != nil {
t.Fatal(err)
}
}
collector := NewAMDCollector(sysPath)
count := collector.Detect()
if count != 2 {
t.Errorf("Expected 2 GPUs, got %d", count)
}
gpus, err := collector.Collect()
if err != nil {
t.Fatalf("Collect failed: %v", err)
}
if len(gpus) != 2 {
t.Errorf("Expected 2 GPU results, got %d", len(gpus))
}
}