Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
package hostcollect
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/itworx/pulse/internal/process"
)
func processByPID(t *testing.T, snapshot process.RawSnapshot, pid int) process.RawProcess {
t.Helper()
for _, item := range snapshot.Processes {
if item.PID == pid {
return item
}
}
t.Fatalf("pid %d not found in %+v", pid, snapshot.Processes)
return process.RawProcess{}
}
func TestParseProcessStatHandlesParenthesesInTheProgramName(t *testing.T) {
stat, err := parseProcessStat(readFixture(t, "proc-healthy", "1234", "stat"), 1234)
if err != nil {
t.Fatalf("parseProcessStat returned error: %v", err)
}
if stat.comm != "my app) (2)" {
t.Fatalf("comm = %q; the parser must cut at the last closing parenthesis", stat.comm)
}
if stat.ticks != 100000+20000 {
t.Fatalf("ticks = %d", stat.ticks)
}
if stat.startTime != 5000 || stat.rssPages != 250000 {
t.Fatalf("unexpected stat: %+v", stat)
}
if stat.state != "sleeping" {
t.Fatalf("state = %q", stat.state)
}
}
func TestParseProcessStatRejectsMalformedInput(t *testing.T) {
cases := map[string][]byte{
"truncated": readFixture(t, "proc-healthy", "9999", "stat"),
"no comm": []byte("1 systemd S 0 1\n"),
"empty": {},
"bad number": []byte("1 (x) S 0 0 0 0 0 0 0 0 0 0 nan nan 0 0 0 0 0 0 0 0 0 0\n"),
}
for name, data := range cases {
t.Run(name, func(t *testing.T) {
if _, err := parseProcessStat(data, 1); err == nil {
t.Fatal("expected an error")
}
})
}
}
func TestProcessesCollectsInventoryAndSkipsUnreadableTasks(t *testing.T) {
collector, clock := newTestCollector(t, Options{PageSize: 4096})
snapshot, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("Processes returned error: %v", err)
}
if len(snapshot.Processes) != 5 {
t.Fatalf("processes = %d, want 5 (1, 2, 1234, 4567, 5555): %+v", len(snapshot.Processes), snapshot.Processes)
}
// 9999 has a truncated stat file; 3131 exists in the directory listing and even has
// a cmdline, but its stat is already gone — the task exited between readdir and
// open, which must cost one row and not the whole collection.
for _, pid := range []int{9999, 3131} {
for _, item := range snapshot.Processes {
if item.PID == pid {
t.Fatalf("pid %d must be skipped: malformed stat or a task that vanished mid-scan", pid)
}
}
}
init := processByPID(t, snapshot, 1)
if init.Name != "init" {
t.Fatalf("name = %q, want the command line's program name", init.Name)
}
if init.MemoryBytes != 12844*1024 {
t.Fatalf("memory = %d, want VmRSS from status", init.MemoryBytes)
}
if init.RuntimeSeconds < 351281 || init.RuntimeSeconds > 351283 {
t.Fatalf("runtime = %v", init.RuntimeSeconds)
}
kernelThread := processByPID(t, snapshot, 2)
if kernelThread.Name != "kthreadd" {
t.Fatalf("a kernel thread with an empty cmdline must fall back to comm, got %q", kernelThread.Name)
}
if kernelThread.MemoryBytes != 0 {
t.Fatalf("memory = %d, want 0 for a kernel thread", kernelThread.MemoryBytes)
}
// 5555 has a stat file but no status or cmdline: it exited between the two reads.
vanished := processByPID(t, snapshot, 5555)
if vanished.Name != "short-lived" || vanished.MemoryBytes != 500*4096 {
t.Fatalf("unexpected fallback for a task that vanished between reads: %+v", vanished)
}
if vanished.State != "zombie" {
t.Fatalf("state = %q", vanished.State)
}
if _, err := process.Normalize(snapshot, clock.now(), process.Limits{}); err != nil {
t.Fatalf("process.Normalize rejected the collected snapshot: %v", err)
}
}
func TestProcessesNeverCopyCommandLineArguments(t *testing.T) {
collector, _ := newTestCollector(t, Options{})
snapshot, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("Processes returned error: %v", err)
}
payload, err := json.Marshal(snapshot)
if err != nil {
t.Fatalf("marshal: %v", err)
}
for _, secret := range []string{"SUPER-SECRET-VALUE", "hunter2", "--token", "--password", "virtio-net-pci"} {
if strings.Contains(string(payload), secret) {
t.Fatalf("snapshot leaked command line argument %q", secret)
}
}
weird := processByPID(t, snapshot, 1234)
if weird.Name != "weird app (2)" {
t.Fatalf("name = %q, want only the program name", weird.Name)
}
// The 29 KiB command line of pid 4567 is read bounded and reduced to its base name.
huge := processByPID(t, snapshot, 4567)
if huge.Name != "qemu-system-x86_64" {
t.Fatalf("name = %q", huge.Name)
}
if len(huge.Name) > maxNameBytes {
t.Fatalf("name length = %d", len(huge.Name))
}
}
func TestProcessesEnforceTheRowLimitByCost(t *testing.T) {
collector, _ := newTestCollector(t, Options{ProcessLimits: process.Limits{MaxRows: 2, MaxPageSize: 10}})
snapshot, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("Processes returned error: %v", err)
}
if len(snapshot.Processes) != 2 {
t.Fatalf("processes = %d, want the limit of 2", len(snapshot.Processes))
}
// Without a previous sample every CPU percentage is zero, so the tie breaks on
// resident pages: the two largest tasks survive.
if snapshot.Processes[0].PID != 1234 || snapshot.Processes[1].PID != 4567 {
t.Fatalf("unexpected survivors: %+v", snapshot.Processes)
}
}
func TestProcessCPUPercentIsADeltaAndSurvivesPIDReuse(t *testing.T) {
root := copyTree(t, filepath.Join("testdata", "proc-healthy"))
collector, clock := newTestCollector(t, Options{ProcRoot: root})
first, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("first scan: %v", err)
}
if processByPID(t, first, 1234).CPUPercent != 0 {
t.Fatal("a single sample cannot yield CPU utilisation")
}
// 500 extra ticks over 10 seconds at 100 USER_HZ is exactly 50% of one core.
writeFile(t, filepath.Join(root, "1234", "stat"),
"1234 (my app) (2)) S 1 1234 1234 0 -1 4194304 55555 0 12 0 100400 20100 0 0 20 0 8 0 5000 987654321 250000 0 1 1 0 0 0 0 0 0 0 0 0 0 17 5 0 0 0 0 0\n")
// pid 1 is replaced by a new task that reuses the PID: a different start time.
writeFile(t, filepath.Join(root, "1", "stat"),
"1 (systemd) S 0 1 1 0 -1 4194560 12345 678910 12 34 90456 789 1000 2000 20 0 1 0 99 172032000 3210 0 1 1 0 0 0 0 0 0 0 0 0 0 17 3 0 0 0 0 0\n")
clock.advance(10 * time.Second)
second, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("second scan: %v", err)
}
if got := processByPID(t, second, 1234).CPUPercent; got < 49.9 || got > 50.1 {
t.Fatalf("cpu = %v, want 50", got)
}
if got := processByPID(t, second, 1).CPUPercent; got != 0 {
t.Fatalf("cpu = %v; a reused PID must not inherit the previous task's ticks", got)
}
if _, err := process.Normalize(second, clock.now(), process.Limits{}); err != nil {
t.Fatalf("process.Normalize rejected the snapshot: %v", err)
}
}
func TestProcessCPUPercentIgnoresBackwardsCounters(t *testing.T) {
root := copyTree(t, filepath.Join("testdata", "proc-healthy"))
collector, clock := newTestCollector(t, Options{ProcRoot: root})
if _, err := collector.Processes(context.Background()); err != nil {
t.Fatalf("first scan: %v", err)
}
writeFile(t, filepath.Join(root, "1234", "stat"),
"1234 (my app) (2)) S 1 1234 1234 0 -1 4194304 55555 0 12 0 1 1 0 0 20 0 8 0 5000 987654321 250000 0 1 1 0 0 0 0 0 0 0 0 0 0 17 5 0 0 0 0 0\n")
clock.advance(10 * time.Second)
second, err := collector.Processes(context.Background())
if err != nil {
t.Fatalf("second scan: %v", err)
}
if got := processByPID(t, second, 1234).CPUPercent; got != 0 {
t.Fatalf("cpu = %v, want 0 for a counter that went backwards", got)
}
}
func TestProcessesRetainedStateStaysBounded(t *testing.T) {
root := copyTree(t, filepath.Join("testdata", "proc-healthy"))
collector, clock := newTestCollector(t, Options{ProcRoot: root})
if _, err := collector.Processes(context.Background()); err != nil {
t.Fatalf("first scan: %v", err)
}
if err := os.RemoveAll(filepath.Join(root, "1234")); err != nil {
t.Fatalf("remove: %v", err)
}
clock.advance(10 * time.Second)
if _, err := collector.Processes(context.Background()); err != nil {
t.Fatalf("second scan: %v", err)
}
collector.mu.Lock()
retained := len(collector.processes)
collector.mu.Unlock()
if retained != 4 {
t.Fatalf("retained samples = %d, want only the live tasks", retained)
}
}
func TestProcessesFailWhenTheProcfsRootIsUnusable(t *testing.T) {
collector, _ := newTestCollector(t, Options{ProcRoot: filepath.Join("testdata", "does-not-exist")})
if _, err := collector.Processes(context.Background()); err == nil {
t.Fatal("expected an error when the procfs root cannot be read")
}
}
func TestProcessesHonourContextCancellation(t *testing.T) {
collector, _ := newTestCollector(t, Options{})
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := collector.Processes(ctx); !errors.Is(err, context.Canceled) {
t.Fatalf("error = %v, want context.Canceled", err)
}
}
func TestSanitizeNameDropsControlCharacters(t *testing.T) {
if got := sanitizeName("bad\x00name\x1b[31m"); got != "badname[31m" {
t.Fatalf("sanitizeName = %q", got)
}
if got := commandName([]byte("/usr/bin/env\x00FOO=bar\x00")); got != "env" {
t.Fatalf("commandName = %q", got)
}
if got := commandName(nil); got != "" {
t.Fatalf("commandName = %q", got)
}
}