Files
mfd-monitor-agent/internal/collector/collector_linux_test.go
codex 877e4cf41d docs(agent): 统一中文注释并补充部署文档
将代码与测试注释改为中文以符合仓库规范;新增中文部署指南、
示例配置,并完善构建脚本对 VERSION 注入的说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 02:30:49 +00:00

170 lines
4.8 KiB
Go

//go:build linux
package collector
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"mfd-monitor-agent/internal/status"
)
// TestParseAddressIPv4 验证 Linux IPv4 套接字地址解码。
func TestParseAddressIPv4(t *testing.T) {
ip, port, err := parseAddress("0100007F:1F90", "ipv4")
if err != nil {
t.Fatal(err)
}
if ip != "127.0.0.1" || port != 8080 {
t.Fatalf("address = %s:%d", ip, port)
}
}
// TestParseAddressIPv6 验证 Linux IPv6 套接字地址解码。
func TestParseAddressIPv6(t *testing.T) {
ip, port, err := parseAddress("00000000000000000000000001000000:1F90", "ipv6")
if err != nil {
t.Fatal(err)
}
if ip != "::1" || port != 8080 {
t.Fatalf("address = %s:%d", ip, port)
}
}
// TestParseProcessStatStartTime 验证从 /proc/<pid>/stat 解析第 22 字段。
func TestParseProcessStatStartTime(t *testing.T) {
raw := "1234 (agent worker) S 1 2 3 4 5 0 0 0 0 0 11 22 0 0 20 0 1 0 987654 0 0"
startTime, err := parseProcessStatStartTime(raw)
if err != nil {
t.Fatal(err)
}
if startTime != 987654 {
t.Fatalf("start time = %d", startTime)
}
}
// TestCPUUsageFromProcStatSamples 验证由两次 /proc/stat 采样计算 CPU 使用率。
func TestCPUUsageFromProcStatSamples(t *testing.T) {
prevIdle, prevTotal, err := parseCPUTimes("cpu 100 0 100 800 0 0 0 0 0 0\n")
if err != nil {
t.Fatal(err)
}
nextIdle, nextTotal, err := parseCPUTimes("cpu 150 0 150 900 0 0 0 0 0 0\n")
if err != nil {
t.Fatal(err)
}
got := cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal)
if got != 50 {
t.Fatalf("usage percent = %f", got)
}
}
// TestReadProcessStatReturnsStartTime 验证进程启动时间被暴露。
func TestReadProcessStatReturnsStartTime(t *testing.T) {
path := filepath.Join(t.TempDir(), "stat")
raw := "1234 (agent worker) S 1 2 3 4 5 0 0 0 0 0 11 22 0 0 20 0 1 0 987654 0 0"
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
t.Fatal(err)
}
got, err := readProcessStat(path)
if err != nil {
t.Fatal(err)
}
if got.StartTime != 987654 {
t.Fatalf("start time = %d", got.StartTime)
}
}
// TestReadMemoryReturnsBytesWithUnit 验证内存容量字段以字节暴露。
func TestReadMemoryReturnsBytesWithUnit(t *testing.T) {
meminfo, err := os.ReadFile("/proc/meminfo")
if err != nil {
t.Fatal(err)
}
var totalKiB uint64
for _, line := range strings.Split(string(meminfo), "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "MemTotal:" {
totalKiB, err = strconv.ParseUint(fields[1], 10, 64)
if err != nil {
t.Fatal(err)
}
break
}
}
if totalKiB == 0 {
t.Fatal("MemTotal not found")
}
got := readMemory()
if got.Total != totalKiB*1024 {
t.Fatalf("total memory = %d bytes, want %d bytes", got.Total, totalKiB*1024)
}
if got.Unit != "byte" {
t.Fatalf("unit = %q, want byte", got.Unit)
}
}
// TestShouldIncludeDiskMount 保留物理盘与网络盘并排除伪挂载。
func TestShouldIncludeDiskMount(t *testing.T) {
tests := []struct {
name string
device string
fsType string
total uint64
want bool
}{
{name: "physical block device", device: "/dev/mapper/pve-vm--103--disk--1", fsType: "ext4", total: 1024, want: true},
{name: "nfs mount", device: "server:/export/data", fsType: "nfs4", total: 1024, want: true},
{name: "lxcfs pseudo mount", device: "lxcfs", fsType: "fuse.lxcfs", total: 0, want: false},
{name: "proc pseudo mount", device: "proc", fsType: "proc", total: 0, want: false},
{name: "tmpfs memory mount", device: "tmpfs", fsType: "tmpfs", total: 1024, want: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := shouldIncludeDiskMount(test.device, test.fsType, test.total)
if got != test.want {
t.Fatalf("include = %t, want %t", got, test.want)
}
})
}
}
// TestCollectSocketsMarksPartialWhenProcessFDsUnreadable 验证进程 fd 不可读时套接字映射降级。
func TestCollectSocketsMarksPartialWhenProcessFDsUnreadable(t *testing.T) {
oldSocketInodes := socketInodesFunc
oldReadTCPListeners := readTCPListenersFunc
t.Cleanup(func() {
socketInodesFunc = oldSocketInodes
readTCPListenersFunc = oldReadTCPListeners
})
socketInodesFunc = func() (map[string]status.ProcessInfo, error) {
return map[string]status.ProcessInfo{}, fmt.Errorf("1 process fd directory could not be read")
}
readTCPListenersFunc = func(path string, family string, inodes map[string]status.ProcessInfo) ([]status.TCPListenerInfo, error) {
return []status.TCPListenerInfo{{IP: "127.0.0.1", Port: 8080, Family: family}}, nil
}
var got status.HostStatus
err := collectSockets(context.Background(), &got)
if err == nil {
t.Fatal("expected partial mapping error")
}
if got.TCPListeners.Status != "partial" {
t.Fatalf("status = %q", got.TCPListeners.Status)
}
if len(got.TCPListeners.Items) != 2 {
t.Fatalf("listener count = %d", len(got.TCPListeners.Items))
}
}