package sysinfo_test import ( "os" "path/filepath" "testing" "time" "glchat/internal/sysinfo" ) // Тесты работают на временных файлах: значения cgroup и /proc подменяются, // поэтому проверки не зависят от окружения и работают в контейнере CI. func writeFile(t *testing.T, dir, name, content string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("запись %s: %v", name, err) } return path } func TestCPUPercentUsesCgroupQuota(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "cpu.max", "200000 100000\n") // 2 ядра writeFile(t, dir, "cpu.stat", "usage_usec 1000000\nuser_usec 900000\nsystem_usec 100000\n") now := time.Unix(1_700_000_000, 0) paths := sysinfo.Paths{ ProcStat: filepath.Join(dir, "stat"), MemInfo: filepath.Join(dir, "meminfo"), CgroupDir: dir, WallClock: func() time.Time { return now }, } sampler := sysinfo.NewSampler(paths) // Первый вызов только запоминает снимок. if percent, _, source, err := sampler.CPUPercent(); err != nil || percent != 0 || source != "cgroup" { t.Fatalf("первый замер = %v, %s, %v", percent, source, err) } // За секунду израсходована половина квоты (1 с из 2 ядро-секунд) → 50 %. now = now.Add(time.Second) writeFile(t, dir, "cpu.stat", "usage_usec 2000000\n") percent, cores, source, err := sampler.CPUPercent() if err != nil { t.Fatalf("второй замер: %v", err) } if cores != 2 || source != "cgroup" { t.Fatalf("ядер=%v источник=%s", cores, source) } if percent < 49 || percent > 51 { t.Fatalf("загрузка = %.2f %%, ожидалось ~50 %%", percent) } } func TestCPUPercentFallsBackToProcStat(t *testing.T) { dir := t.TempDir() statPath := writeFile(t, dir, "stat", "cpu 100 0 100 800 0 0 0 0 0 0\n") writeFile(t, dir, "meminfo", "MemTotal: 1000 kB\nMemAvailable: 400 kB\n") now := time.Unix(1_700_000_000, 0) sampler := sysinfo.NewSampler(sysinfo.Paths{ ProcStat: statPath, MemInfo: filepath.Join(dir, "meminfo"), CgroupDir: filepath.Join(dir, "нет-cgroup"), WallClock: func() time.Time { return now }, }) if _, _, source, err := sampler.CPUPercent(); err != nil || source != "host" { t.Fatalf("источник = %s, %v", source, err) } // За интервал прибавилось 150 тиков занятости и 150 простоя → 50 %. now = now.Add(time.Second) writeFile(t, dir, "stat", "cpu 175 0 175 950 0 0 0 0 0 0\n") percent, cores, source, err := sampler.CPUPercent() if err != nil { t.Fatalf("второй замер: %v", err) } if source != "host" || cores <= 0 { t.Fatalf("источник=%s ядер=%v", source, cores) } if percent < 49 || percent > 51 { t.Fatalf("загрузка = %.2f %%, ожидалось ~50 %%", percent) } } func TestReadMemoryPrefersCgroupLimit(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "memory.current", "15728640\n") writeFile(t, dir, "memory.max", "805306368\n") writeFile(t, dir, "meminfo", "MemTotal: 3479592 kB\nMemAvailable: 1000000 kB\n") memory, err := sysinfo.ReadMemory(sysinfo.Paths{ MemInfo: filepath.Join(dir, "meminfo"), CgroupDir: dir, ProcStat: filepath.Join(dir, "stat"), }) if err != nil { t.Fatalf("память: %v", err) } if memory.Source != "cgroup" || memory.TotalBytes != 805306368 || memory.UsedBytes != 15728640 { t.Fatalf("память = %+v", memory) } if percent := memory.Percent(); percent < 1.9 || percent > 2.0 { t.Fatalf("заполнение = %.2f %%", percent) } } func TestReadMemoryFallsBackToMemInfo(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "meminfo", "MemTotal: 1000 kB\nMemAvailable: 250 kB\n") memory, err := sysinfo.ReadMemory(sysinfo.Paths{ MemInfo: filepath.Join(dir, "meminfo"), CgroupDir: filepath.Join(dir, "нет-cgroup"), ProcStat: filepath.Join(dir, "stat"), }) if err != nil { t.Fatalf("память: %v", err) } if memory.Source != "host" || memory.TotalBytes != 1024_000 || memory.UsedBytes != 768_000 { t.Fatalf("память = %+v", memory) } } func TestReadMemoryUnlimitedCgroupMax(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "memory.current", "1000\n") writeFile(t, dir, "memory.max", "max\n") writeFile(t, dir, "meminfo", "MemTotal: 2000 kB\nMemAvailable: 500 kB\n") memory, err := sysinfo.ReadMemory(sysinfo.Paths{ MemInfo: filepath.Join(dir, "meminfo"), CgroupDir: dir, ProcStat: filepath.Join(dir, "stat"), }) if err != nil { t.Fatalf("память: %v", err) } if memory.Source != "host" { t.Fatalf("при memory.max=max ожидался хостовый режим, получено %+v", memory) } } func TestReadDiskAndFileBytes(t *testing.T) { dir := t.TempDir() disk, err := sysinfo.ReadDisk(dir) if err != nil { t.Fatalf("диск: %v", err) } if disk.TotalBytes <= 0 || disk.UsedBytes <= 0 || disk.FreeBytes <= 0 { t.Fatalf("диск = %+v", disk) } if disk.UsedBytes+disk.FreeBytes > disk.TotalBytes { t.Fatalf("занято+свободно больше общего: %+v", disk) } writeFile(t, dir, "glchat.db", "0123456789") writeFile(t, dir, "glchat.db-wal", "01234") if got := sysinfo.FileBytes(filepath.Join(dir, "glchat.db"), filepath.Join(dir, "glchat.db-wal"), filepath.Join(dir, "нет.db")); got != 15 { t.Fatalf("размер файлов = %d, ожидалось 15", got) } } func TestCPUPercentWithoutProcStat(t *testing.T) { sampler := sysinfo.NewSampler(sysinfo.Paths{ ProcStat: filepath.Join(t.TempDir(), "нет"), MemInfo: filepath.Join(t.TempDir(), "нет"), CgroupDir: filepath.Join(t.TempDir(), "нет"), }) if _, _, _, err := sampler.CPUPercent(); err == nil { t.Fatal("без источников ожидалась ошибка") } }