v0.3.0 cf370a0

power_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"slices"
6	"testing"
7	"time"
8
9	"github.com/bnema/neferwl/internal/adapters/config"
10	"github.com/bnema/neferwl/internal/core"
11	"github.com/bnema/neferwl/internal/ports"
12)
13
14func powerCore(t *testing.T) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) {
15	t.Helper()
16	input := make(chan ports.InputEvent, 4)
17	client := make(chan ports.ClientEvent, 4)
18	output := make(chan ports.OutputEvent, 2)
19	commands := make(chan ports.ClientCommand, 64)
20	scenes := make(chan []ports.Scene, 1)
21	c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)})
22	if err != nil {
23		t.Fatal(err)
24	}
25	ctx, cancel := context.WithCancel(context.Background())
26	done := make(chan error, 1)
27	go func() { done <- c.Run(ctx) }()
28	t.Cleanup(func() { cancel(); receive(t, done) })
29	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
30	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
31	return input, client, output, commands, scenes
32}
33
34// sceneOf waits for a scene set in which pred holds for the named output.
35func sceneOf(t *testing.T, scenes <-chan []ports.Scene, name string, pred func(ports.Scene) bool) {
36	t.Helper()
37	deadline := time.After(2 * time.Second)
38	for {
39		select {
40		case set := <-scenes:
41			for _, s := range set {
42				if s.Output == name && pred(s) {
43					return
44				}
45			}
46		case <-deadline:
47			t.Fatalf("no matching scene for %s", name)
48		}
49	}
50}
51
52// A client turning an output off marks its scene and tells wayland; back
53// on clears both. Other outputs stay on.
54func TestOutputPower(t *testing.T) {
55	_, client, _, commands, scenes := powerCore(t)
56	sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
57	client <- ports.OutputPower{Output: "OUT-2", On: false}
58	sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
59	for {
60		if v := commandOf[ports.SetOutputs](t, commands); slices.Equal(v.Off, []string{"OUT-2"}) {
61			break
62		}
63	}
64	client <- ports.OutputPower{Output: "OUT-2", On: true}
65	sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
66	for {
67		if v := commandOf[ports.SetOutputs](t, commands); len(v.Off) == 0 {
68			break
69		}
70	}
71	// Unknown outputs are ignored.
72	client <- ports.OutputPower{Output: "NOPE", On: false}
73	sceneOf(t, scenes, "OUT-1", func(s ports.Scene) bool { return !s.Off })
74}
75
76// An output unplugged while off (a display in deep sleep) reconnects off;
77// after input it reconnects on. Alone, it is the last output: core keeps a
78// placeholder screen meanwhile.
79func TestOutputReconnectedWhileOff(t *testing.T) {
80	for _, alone := range []bool{false, true} {
81		t.Run(map[bool]string{false: "beside another", true: "alone"}[alone], func(t *testing.T) {
82			input, client, output, commands, scenes := powerCore(t)
83			stop := make(chan struct{})
84			defer close(stop)
85			active := make(chan struct{}, 1)
86			go func() { // core blocks on a full command channel
87				for {
88					select {
89					case c := <-commands:
90						if _, ok := c.(ports.UserActivity); ok {
91							active <- struct{}{}
92						}
93					case <-stop:
94						return
95					}
96				}
97			}()
98			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
99			if alone {
100				output <- ports.OutputRemoved{Name: "OUT-1"}
101				gone(t, scenes, "OUT-1")
102			}
103			client <- ports.OutputPower{Output: "OUT-2", On: false}
104			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
105			output <- ports.OutputRemoved{Name: "OUT-2"}
106			gone(t, scenes, "OUT-2") // no older scene can answer below
107			output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
108			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
109
110			client <- ports.OutputPower{Output: "OUT-2", On: true}
111			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
112			client <- ports.OutputPower{Output: "OUT-2", On: false}
113			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
114			output <- ports.OutputRemoved{Name: "OUT-2"}
115			gone(t, scenes, "OUT-2")
116			input <- ports.PointerMotion{X: 1, Y: 1}
117			select { // input and outputs are separate channels: order them
118			case <-active:
119			case <-time.After(2 * time.Second):
120				t.Fatal("input not handled")
121			}
122			output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
123			sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
124		})
125	}
126}
127
128// gone waits for a scene set without the named output.
129func gone(t *testing.T, scenes <-chan []ports.Scene, name string) {
130	t.Helper()
131	deadline := time.After(2 * time.Second)
132	for {
133		select {
134		case set := <-scenes:
135			if !slices.ContainsFunc(set, func(s ports.Scene) bool { return s.Output == name }) {
136				return
137			}
138		case <-deadline:
139			t.Fatalf("%s still in the scenes", name)
140		}
141	}
142}
143
144// Input reports user activity to wayland, at most once per interval.
145func TestUserActivity(t *testing.T) {
146	input, _, _, commands, _ := powerCore(t)
147	for range 3 {
148		input <- ports.PointerMotion{X: 1, Y: 1}
149	}
150	commandOf[ports.UserActivity](t, commands)
151	deadline := time.After(ports.ActivityInterval / 2)
152	for {
153		select {
154		case c := <-commands:
155			if _, ok := c.(ports.UserActivity); ok {
156				t.Fatal("second UserActivity within the interval")
157			}
158			continue
159		case <-deadline:
160		}
161		break
162	}
163	time.Sleep(ports.ActivityInterval)
164	input <- ports.PointerMotion{X: 2, Y: 2}
165	commandOf[ports.UserActivity](t, commands)
166}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close