Add visual VM resource bars to the TUI
The TUI should show VM capacity pressure at a glance instead of making users read raw numbers or drill into per-VM details. Add a compact colored status row under the header that renders CPU, RAM, and disk usage as progress bars. CPU and RAM reflect reserved resources for running VMs, while disk reflects actual allocated overlay and work-disk bytes across all VMs against the filesystem backing banger state. Add host resource and filesystem helpers in the system package and cover the new aggregation and rendering behavior with TUI and system tests. Verified with GOCACHE=/tmp/banger-gocache go test ./... and GOCACHE=/tmp/banger-gocache make build.
This commit is contained in:
parent
38d7eac430
commit
9e98445fa2
4 changed files with 387 additions and 21 deletions
|
|
@ -2,6 +2,8 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -233,3 +235,125 @@ func TestTUIStatusIncludesStageDurationsAfterInitialLoad(t *testing.T) {
|
|||
t.Fatalf("statusText = %q, want stage timings", m.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateRunningVMResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
running, vcpus, memoryBytes := aggregateRunningVMResources([]model.VMRecord{
|
||||
{
|
||||
State: model.VMStateRunning,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 2,
|
||||
MemoryMiB: 1024,
|
||||
},
|
||||
},
|
||||
{
|
||||
State: model.VMStateStopped,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 8,
|
||||
MemoryMiB: 8192,
|
||||
},
|
||||
},
|
||||
{
|
||||
State: model.VMStateRunning,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 4,
|
||||
MemoryMiB: 2048,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if running != 2 || vcpus != 6 || memoryBytes != 3*1024*1024*1024 {
|
||||
t.Fatalf("aggregateRunningVMResources = (%d, %d, %d), want (2, 6, %d)", running, vcpus, memoryBytes, int64(3*1024*1024*1024))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTUIViewShowsResourceBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
|
||||
m.hostCPUCount = 32
|
||||
m.hostMemoryBytes = 125 * 1024 * 1024 * 1024
|
||||
m.hostDiskBytes = 200 * 1024 * 1024 * 1024
|
||||
m.daemonPending = false
|
||||
m.loading = false
|
||||
stateDir := t.TempDir()
|
||||
overlayPath := filepath.Join(stateDir, "system.cow")
|
||||
workDiskPath := filepath.Join(stateDir, "root.ext4")
|
||||
if err := os.WriteFile(overlayPath, make([]byte, 1024), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile overlay: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(workDiskPath, make([]byte, 2048), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile work disk: %v", err)
|
||||
}
|
||||
m.vms = []model.VMRecord{
|
||||
{
|
||||
ID: "vm-1",
|
||||
Name: "devbox",
|
||||
State: model.VMStateRunning,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 2,
|
||||
MemoryMiB: 1024,
|
||||
WorkDiskSizeBytes: 16 * 1024 * 1024 * 1024,
|
||||
},
|
||||
Runtime: model.VMRuntime{
|
||||
SystemOverlay: overlayPath,
|
||||
WorkDiskPath: workDiskPath,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "vm-2",
|
||||
Name: "db",
|
||||
State: model.VMStateStopped,
|
||||
Spec: model.VMSpec{
|
||||
VCPUCount: 4,
|
||||
MemoryMiB: 4096,
|
||||
WorkDiskSizeBytes: 32 * 1024 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
}
|
||||
m.selectedID = "vm-1"
|
||||
m.rebuildTable()
|
||||
m.refreshDetail()
|
||||
|
||||
view := m.View()
|
||||
if !strings.Contains(view, "VMs") || !strings.Contains(view, "1/2") {
|
||||
t.Fatalf("view = %q, want running VM count", view)
|
||||
}
|
||||
if !strings.Contains(view, "CPU") || !strings.Contains(view, "2/32") {
|
||||
t.Fatalf("view = %q, want vcpu aggregate", view)
|
||||
}
|
||||
if !strings.Contains(view, "RAM") || !strings.Contains(view, "1.0G/125.0G") {
|
||||
t.Fatalf("view = %q, want memory aggregate", view)
|
||||
}
|
||||
if !strings.Contains(view, "Disk") {
|
||||
t.Fatalf("view = %q, want disk aggregate", view)
|
||||
}
|
||||
if !strings.Contains(view, "█") || !strings.Contains(view, "░") {
|
||||
t.Fatalf("view = %q, want visual progress bars", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateVMDiskUsage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
overlayPath := filepath.Join(dir, "system.cow")
|
||||
workDiskPath := filepath.Join(dir, "root.ext4")
|
||||
if err := os.WriteFile(overlayPath, make([]byte, 4096), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile overlay: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(workDiskPath, make([]byte, 8192), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile work disk: %v", err)
|
||||
}
|
||||
|
||||
total := aggregateVMDiskUsage([]model.VMRecord{{
|
||||
Runtime: model.VMRuntime{
|
||||
SystemOverlay: overlayPath,
|
||||
WorkDiskPath: workDiskPath,
|
||||
},
|
||||
}})
|
||||
if total <= 0 {
|
||||
t.Fatalf("aggregateVMDiskUsage = %d, want positive allocated bytes", total)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue