test(02-02): add failing tests for PPID chain walk + workspace resolution

- TestReadPPID, TestReadPPIDMissing
- TestResolveWorkspace, TestResolveWorkspaceNotFound, TestResolveWorkspaceMaxDepth
- TestBuildTerminalWorkspaceMapUnit
- Add go.i3wm.org/i3/v4 dependency
This commit is contained in:
Pierre Martin
2026-03-23 17:42:44 +01:00
parent a49f7d1c57
commit c9a28df3dc
4 changed files with 213 additions and 0 deletions

169
workspace_test.go Normal file
View File

@@ -0,0 +1,169 @@
package main
import (
"fmt"
"os"
"path/filepath"
"testing"
i3 "go.i3wm.org/i3/v4"
)
// --- Helpers ---
func writeFakeStatus(t *testing.T, procDir string, pid, ppid int) {
t.Helper()
pidDir := filepath.Join(procDir, fmt.Sprintf("%d", pid))
if err := os.MkdirAll(pidDir, 0o755); err != nil {
t.Fatal(err)
}
content := fmt.Sprintf("Name:\tprocess\nPPid:\t%d\nUid:\t1000\n", ppid)
if err := os.WriteFile(filepath.Join(pidDir, "status"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
// --- Mock types ---
type mockI3TreeProvider struct {
tree i3.Tree
err error
}
func (m *mockI3TreeProvider) GetTree() (i3.Tree, error) {
return m.tree, m.err
}
type mockX11PIDResolver struct {
pids map[int64]int // windowID -> PID
}
func (m *mockX11PIDResolver) GetPID(windowID int64) (int, error) {
pid, ok := m.pids[windowID]
if !ok {
return 0, fmt.Errorf("no PID for window %d", windowID)
}
return pid, nil
}
// --- ReadPPID tests ---
func TestReadPPID(t *testing.T) {
procDir := t.TempDir()
writeFakeStatus(t, procDir, 100, 50)
ppid, err := ReadPPID(procDir, 100)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ppid != 50 {
t.Errorf("ReadPPID = %d, want 50", ppid)
}
}
func TestReadPPIDMissing(t *testing.T) {
procDir := t.TempDir()
_, err := ReadPPID(procDir, 999)
if err == nil {
t.Fatal("expected error for missing PID, got nil")
}
}
// --- ResolveWorkspace tests ---
func TestResolveWorkspace(t *testing.T) {
procDir := t.TempDir()
// Chain: 100 -> 50 -> 10
writeFakeStatus(t, procDir, 100, 50)
writeFakeStatus(t, procDir, 50, 10)
writeFakeStatus(t, procDir, 10, 1)
terminalMap := map[int]string{10: "workspace 3"}
ws := ResolveWorkspace(procDir, 100, terminalMap)
if ws != "workspace 3" {
t.Errorf("ResolveWorkspace = %q, want %q", ws, "workspace 3")
}
}
func TestResolveWorkspaceNotFound(t *testing.T) {
procDir := t.TempDir()
writeFakeStatus(t, procDir, 100, 50)
writeFakeStatus(t, procDir, 50, 1)
writeFakeStatus(t, procDir, 1, 0)
terminalMap := map[int]string{999: "workspace 5"}
ws := ResolveWorkspace(procDir, 100, terminalMap)
if ws != "" {
t.Errorf("ResolveWorkspace = %q, want empty", ws)
}
}
func TestResolveWorkspaceMaxDepth(t *testing.T) {
procDir := t.TempDir()
// Chain of 25 PIDs: 100 -> 99 -> 98 -> ... -> 76
for pid := 100; pid > 75; pid-- {
writeFakeStatus(t, procDir, pid, pid-1)
}
// PID 75 is in the terminal map but depth > 20
terminalMap := map[int]string{75: "workspace 1"}
ws := ResolveWorkspace(procDir, 100, terminalMap)
if ws != "" {
t.Errorf("ResolveWorkspace should return empty after max depth, got %q", ws)
}
}
// --- BuildTerminalWorkspaceMap tests ---
func TestBuildTerminalWorkspaceMapUnit(t *testing.T) {
// Mock i3 tree with 2 workspaces, each containing a window
tree := i3.Tree{
Root: &i3.Node{
Type: i3.Root,
Nodes: []*i3.Node{
{
Type: i3.OutputNode,
Name: "eDP-1",
Nodes: []*i3.Node{
{
Type: i3.WorkspaceNode,
Name: "1",
Nodes: []*i3.Node{
{Type: i3.Con, Window: 12345},
},
},
{
Type: i3.WorkspaceNode,
Name: "3",
Nodes: []*i3.Node{
{Type: i3.Con, Window: 67890},
},
},
},
},
},
},
}
x11 := &mockX11PIDResolver{
pids: map[int64]int{
12345: 1000,
67890: 2000,
},
}
result, err := BuildTerminalWorkspaceMap(&mockI3TreeProvider{tree: tree}, x11)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result[1000] != "1" {
t.Errorf("PID 1000 workspace = %q, want %q", result[1000], "1")
}
if result[2000] != "3" {
t.Errorf("PID 2000 workspace = %q, want %q", result[2000], "3")
}
}