* 实现周期采集、快照缓存、本地 HTTP API 和主动上报 * 补齐 Linux 和 Windows 主机、网络、资源、进程和 TCP 采集 * 添加服务生命周期管理、配置加载、构建脚本和测试覆盖 BREAKING CHANGE: network.ips 拆分为 network.ipv4s 和 network.ipv6s。
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
//go:build linux
|
|
|
|
package collector
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"mfd-monitor-agent/internal/status"
|
|
)
|
|
|
|
// TestParseAddressIPv4 verifies Linux IPv4 socket addresses are decoded.
|
|
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 verifies Linux IPv6 socket addresses are decoded.
|
|
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 verifies field 22 parsing from /proc/<pid>/stat.
|
|
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 verifies CPU usage from two /proc/stat samples.
|
|
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 verifies process start time is exposed.
|
|
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 verifies memory capacity fields are exposed in bytes.
|
|
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 keeps physical and network disks while dropping pseudo mounts.
|
|
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 verifies socket mapping degradation.
|
|
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))
|
|
}
|
|
}
|