package api
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"craftdesign.group/quarry/internal/children"
"craftdesign.group/quarry/internal/obs"
)
// Each live process says what started it and how long it has run
// against the usual for its kind of call; one past three times its
// usual is marked, and one too young to matter is not.
func TestRunningNowMarksACallPastItsUsual(t *testing.T) {
now := time.Date(2026, 10, 24, 12, 0, 0, 0, time.UTC)
d := Deps{Running: func() []children.Child {
return []children.Child{
{Repo: "fossil", Verb: "timeline", Cause: "GET /r/fossil/timeline", Start: now.Add(-9 * time.Second)},
{Repo: "tcl", Verb: "timeline", Cause: "ingest", Start: now.Add(-1500 * time.Millisecond)},
{Repo: "quarry", Verb: "ls", Cause: "warm", Start: now.Add(-400 * time.Millisecond)},
{Repo: "pikchr", Verb: "zip", Start: now.Add(-30 * time.Second)},
}
}}
var finished []obs.Subprocess
for _, ms := range []int{900, 1000, 1100, 1200} {
finished = append(finished, obs.Subprocess{Verb: "timeline", Duration: time.Duration(ms) * time.Millisecond})
}
for _, ms := range []int{50, 60, 70} {
finished = append(finished, obs.Subprocess{Verb: "ls", Duration: time.Duration(ms) * time.Millisecond})
}
got := d.opsRunningNow(now, finished)
if len(got) != 4 {
t.Fatalf("FAIL: %d rows, want 4: %+v", len(got), got)
}
slow := map[string]bool{}
for _, r := range got {
slow[r.Repo] = r.Slow
}
// 9 s against a 1.1 s usual: slow. 1.5 s against 1.1 s: not.
// 400 ms against 60 ms: past three times, but under a second.
// zip has no usual at all, so nothing to be slow against.
if !slow["fossil"] || slow["tcl"] || slow["quarry"] || slow["pikchr"] {
t.Errorf("FAIL: slow marks %v", slow)
}
if got[0].Cause != "GET /r/fossil/timeline" || got[0].UsualMs != 1100 || got[0].AgeMs != 9000 {
t.Errorf("FAIL: the first row reads %+v", got[0])
}
if got[3].UsualMs != 0 {
t.Errorf("FAIL: a verb with too few calls claims a usual: %+v", got[3])
}
if rows := (Deps{Running: func() []children.Child { return nil }}).opsRunningNow(now, nil); rows == nil || len(rows) != 0 {
t.Errorf("FAIL: nothing running should be an empty list, got %#v", rows)
}
}
// A process's memory comes from /proc: this process's own zit children,
// by verb and start time, each answering for one child.
func TestChildProcsReadTheirOwnChildrenFromProc(t *testing.T) {
root := t.TempDir()
write := func(rel, body string) {
p := filepath.Join(root, rel)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("stat", "cpu 1 2 3\nbtime 1761300000\nprocesses 9\n")
// stat fields after the command: state, ppid, then 17 more, then start ticks.
stat := func(pid, ppid int, comm string, ticks int) string {
f := "S " + fmt.Sprint(ppid)
for i := 0; i < 17; i++ {
f += " 0"
}
return fmt.Sprintf("%d (%s) %s %d 0 0\n", pid, comm, f, ticks)
}
write("101/stat", stat(101, 7, "zit", 500))
write("101/cmdline", "zit\x00timeline\x00-R\x00/srv/fossil\x00")
write("101/statm", "5000 300 10 0 0 0 0\n")
// A zit that is not ours, and a child of ours that is not zit.
write("102/stat", stat(102, 8, "zit", 500))
write("102/cmdline", "zit\x00ls\x00")
write("102/statm", "1 1\n")
write("103/stat", stat(103, 7, "sh (x)", 500))
write("103/cmdline", "/bin/sh\x00-c\x00true\x00")
write("103/statm", "1 1\n")
got := childProcsIn(root, 7)
if len(got) != 1 {
t.Fatalf("FAIL: %d processes, want only pid 101: %+v", len(got), got)
}
p := got[0]
if p.PID != 101 || p.Verb != "timeline" || p.RSS != 300*uint64(os.Getpagesize()) {
t.Errorf("FAIL: process reads %+v", p)
}
if want := time.Unix(1761300000, 0).Add(5 * time.Second); !p.Start.Equal(want) {
t.Errorf("FAIL: started %s, want %s", p.Start, want)
}
}