1package statefile
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "reflect"
9 "testing"
10 "time"
11
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/zerowrap"
14)
15
16var sample = State{
17 Output: "DP-2",
18 Outputs: []Output{{Name: "HDMI-A-1", Active: 1, Count: 1}, {Name: "DP-2", Active: 2, Count: 3, Workspace: "web"}},
19 Windows: []Window{{ID: 1, AppID: "foot", PID: 100, Output: "HDMI-A-1", Workspace: 1, Visible: true}, {ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 2, Visible: true}},
20}
21
22// The file holds the latest state while the session runs; a stale file from
23// a crashed session goes at start, and the file is removed at exit.
24func TestRunWritesAndRemoves(t *testing.T) {
25 path, err := Path(t.TempDir(), "wayland-9")
26 if err != nil {
27 t.Fatal(err)
28 }
29 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
30 t.Fatal(err)
31 }
32 if err := os.WriteFile(path, []byte(`{"output":"stale"}`), 0o600); err != nil {
33 t.Fatal(err)
34 }
35 states := make(chan ports.State, 1)
36 ctx, cancel := context.WithCancel(context.Background())
37 done := make(chan error, 1)
38 go func() { done <- Run(ctx, path, states, zerowrap.Default()) }()
39 focused := ports.WindowState{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}
40 states <- ports.State{Output: "DP-2", Outputs: []ports.OutputState{{Name: "DP-2", Active: 1, Count: 1}}, Window: &focused, Windows: []ports.WindowState{focused}}
41 want := State{Output: "DP-2", Outputs: []Output{{Name: "DP-2", Active: 1, Count: 1}}, Window: &Window{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}, Windows: []Window{{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}}}
42 deadline := time.Now().Add(2 * time.Second)
43 for {
44 if got, err := Read(path); err == nil && got.Output != "stale" {
45 if !reflect.DeepEqual(got, want) {
46 t.Fatalf("%+v", got)
47 }
48 break
49 }
50 if time.Now().After(deadline) {
51 t.Fatal("no state file")
52 }
53 time.Sleep(5 * time.Millisecond)
54 }
55 cancel()
56 if err := <-done; err != nil {
57 t.Fatal(err)
58 }
59 if _, err := os.Stat(path); !os.IsNotExist(err) {
60 t.Fatal("state file left behind", err)
61 }
62}
63
64func TestPath(t *testing.T) {
65 if p, err := Path("/run/user/1", "/run/user/1/wayland-1"); err != nil || p != "/run/user/1/neferwl/wayland-1.json" {
66 t.Fatal(p, err)
67 }
68 for _, c := range [][2]string{{"", "wayland-1"}, {"/run/user/1", ""}} {
69 if _, err := Path(c[0], c[1]); err == nil {
70 t.Fatal(c)
71 }
72 }
73}
74
75// A shell finds its window through its ancestors.
76func TestOutputOfWalksParents(t *testing.T) {
77 tree := newMockprocTree(t)
78 // shell 300 → login 250 → terminal 200 (window on DP-2)
79 tree.EXPECT().Parent(300).Return(250, true)
80 tree.EXPECT().Parent(250).Return(200, true)
81 o, err := outputOf(sample, 300, tree)
82 if err != nil || o.Name != "DP-2" || o.Active != 2 || o.Count != 3 {
83 t.Fatal(o, err)
84 }
85 // The window's own process needs no walk.
86 if o, err := outputOf(sample, 100, newMockprocTree(t)); err != nil || o.Name != "HDMI-A-1" {
87 t.Fatal(o, err)
88 }
89 // No window up to init.
90 orphan := newMockprocTree(t)
91 orphan.EXPECT().Parent(400).Return(1, true)
92 orphan.EXPECT().Parent(1).Return(0, false)
93 if _, err := outputOf(sample, 400, orphan); !errors.Is(err, ErrNoWindow) {
94 t.Fatal(err)
95 }
96}
97
98// One process with windows on two outputs (a terminal server): the focused
99// window wins, else a visible one.
100func TestOutputOfPrefersFocusedWindow(t *testing.T) {
101 st := sample
102 st.Windows = []Window{
103 {ID: 1, PID: 100, Output: "HDMI-A-1", Visible: true},
104 {ID: 2, PID: 100, Output: "DP-2", Visible: false},
105 {ID: 3, PID: 100, Output: "DP-2", Visible: true},
106 }
107 if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "HDMI-A-1" {
108 t.Fatal(o)
109 }
110 st.Window = &st.Windows[2]
111 if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "DP-2" {
112 t.Fatal(o)
113 }
114}
115
116func TestLinuxProcTreeParent(t *testing.T) {
117 if ppid, ok := (linuxProcTree{}).Parent(os.Getpid()); !ok || ppid != os.Getppid() {
118 t.Fatal(ppid, ok)
119 }
120}