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