Dashboard Editor & Layout: - Full-screen visual editor for reorganizing cards - Drag-and-drop cards between sections - Toggle card visibility with persistence to localStorage - Reset to default layout option Alerts System: - Threshold-based alerts for CPU, memory, temperature, disk, GPU - Alert manager with duration requirements - AlertsCard component with settings UI - API endpoints for alerts CRUD New Collectors: - Docker container monitoring with parallel stats fetching - Systemd service status via D-Bus - Historical metrics storage (1 hour at 1s intervals) PWA Support: - Service worker with offline caching - Web app manifest with SVG icons - iOS PWA meta tags Mobile Responsive: - Collapsible hamburger menu on mobile - Adaptive grid layouts for all screen sizes - Touch-friendly hover states - Safe area insets for notched devices UI Enhancements: - Light/dark theme toggle with persistence - Keyboard shortcuts (T=theme, R=refresh, ?=help) - Per-process expandable details in ProcessesCard - Sparkline charts for historical data Performance Fixes: - Buffered SSE channels to prevent blocking - Parallel Docker stats collection with timeout - D-Bus timeout for systemd collector Tests: - Unit tests for CPU, memory, network collectors - Alert manager tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
package collectors
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestCPUCollector(t *testing.T) {
|
|
// Create temp directory with mock /proc files
|
|
tmpDir := t.TempDir()
|
|
procPath := filepath.Join(tmpDir, "proc")
|
|
sysPath := filepath.Join(tmpDir, "sys")
|
|
|
|
// Create necessary directories
|
|
if err := os.MkdirAll(procPath, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(sysPath, "devices/system/cpu/cpufreq/policy0"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create mock /proc/stat
|
|
statContent := `cpu 10132153 290696 3084719 46828483 16683 0 25195 0 0 0
|
|
cpu0 1393280 32966 572056 13343292 6130 0 17875 0 0 0
|
|
cpu1 1264380 32862 535089 13315801 3580 0 7275 0 0 0
|
|
`
|
|
if err := os.WriteFile(filepath.Join(procPath, "stat"), []byte(statContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create mock /proc/loadavg
|
|
loadavgContent := "1.23 2.34 3.45 1/123 12345\n"
|
|
if err := os.WriteFile(filepath.Join(procPath, "loadavg"), []byte(loadavgContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create mock scaling_cur_freq
|
|
if err := os.WriteFile(filepath.Join(sysPath, "devices/system/cpu/cpufreq/policy0/scaling_cur_freq"), []byte("3500000\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
collector := NewCPUCollector(procPath, sysPath)
|
|
|
|
// First collection to initialize
|
|
_, err := collector.Collect()
|
|
if err != nil {
|
|
t.Fatalf("First collect failed: %v", err)
|
|
}
|
|
|
|
// Update stat with new values to calculate usage
|
|
statContent2 := `cpu 10142153 290696 3084719 46838483 16683 0 25195 0 0 0
|
|
cpu0 1403280 32966 572056 13353292 6130 0 17875 0 0 0
|
|
cpu1 1274380 32862 535089 13325801 3580 0 7275 0 0 0
|
|
`
|
|
if err := os.WriteFile(filepath.Join(procPath, "stat"), []byte(statContent2), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Second collection
|
|
stats, err := collector.Collect()
|
|
if err != nil {
|
|
t.Fatalf("Second collect failed: %v", err)
|
|
}
|
|
|
|
// Verify results
|
|
if len(stats.Cores) == 0 {
|
|
t.Error("Expected at least one core")
|
|
}
|
|
|
|
if stats.LoadAverage.Load1 != 1.23 {
|
|
t.Errorf("Expected load1=1.23, got %f", stats.LoadAverage.Load1)
|
|
}
|
|
|
|
if stats.LoadAverage.Load5 != 2.34 {
|
|
t.Errorf("Expected load5=2.34, got %f", stats.LoadAverage.Load5)
|
|
}
|
|
|
|
if stats.LoadAverage.Load15 != 3.45 {
|
|
t.Errorf("Expected load15=3.45, got %f", stats.LoadAverage.Load15)
|
|
}
|
|
}
|
|
|
|
func TestCPUCollector_MissingFiles(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
collector := NewCPUCollector(tmpDir, tmpDir)
|
|
|
|
// Should handle gracefully - either error or empty stats is acceptable
|
|
stats, err := collector.Collect()
|
|
if err != nil {
|
|
// Error is acceptable for missing files
|
|
return
|
|
}
|
|
|
|
// If no error, should have empty/default values
|
|
if len(stats.Cores) != 0 {
|
|
t.Errorf("Expected no cores with missing files, got %d", len(stats.Cores))
|
|
}
|
|
}
|