power_test.go RawHistoryFolder

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

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 23

Tags 10

↑↓ moveenter openesc close