//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//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)) } }