feat/hints a471fa4

outbox_run_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"testing"
6	"testing/synctest"
7
8	"github.com/bnema/neferwl/internal/core"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// outboxRig runs a core whose command channel nobody reads until the test
13// does. Run and the sender live in a synctest bubble, so synctest.Wait
14// returns once both are idle.
15type outboxRig struct {
16	input    chan ports.InputEvent
17	client   chan ports.ClientEvent
18	output   chan ports.OutputEvent
19	commands chan ports.ClientCommand
20	scenes   chan []ports.Scene
21}
22
23func startOutboxRig(t *testing.T, commandCap int) *outboxRig {
24	t.Helper()
25	cfg := scrollDefaults()
26	cfg.Border.Width = 0
27	cfg.Outputs = []ports.OutputConfig{{Name: "DP-2", Scale: 2}}
28	cfg.Binds["Cmd+minus"] = "scale-down"
29	r := &outboxRig{
30		input:    make(chan ports.InputEvent),
31		client:   make(chan ports.ClientEvent),
32		output:   make(chan ports.OutputEvent),
33		commands: make(chan ports.ClientCommand, commandCap),
34		scenes:   make(chan []ports.Scene, 1),
35	}
36	c, err := core.New(cfg, core.Channels{Input: r.input, Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes}, core.Options{})
37	if err != nil {
38		t.Fatal(err)
39	}
40	ctx, cancel := context.WithCancel(context.Background())
41	done := make(chan error, 1)
42	go func() { done <- c.Run(ctx) }()
43	t.Cleanup(func() {
44		cancel()
45		// Run waits for its sender, so a sender stuck on the full channel
46		// would keep this from returning.
47		if err := receive(t, done); err != nil {
48			t.Error(err)
49		}
50	})
51	r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "DP-2", Width: 5120, Height: 2160}}
52	r.client <- ports.WindowMapped{ID: 1}
53	synctest.Wait()
54	// Settle: drop the scenes and the commands of the map.
55	r.drain()
56	return r
57}
58
59// drain empties the scene and command channels and lets the sender refill
60// them from its queue until nothing moves.
61func (r *outboxRig) drain() []ports.ClientCommand {
62	var got []ports.ClientCommand
63	for {
64		synctest.Wait()
65		select {
66		case <-r.scenes:
67		default:
68		}
69		select {
70		case v := <-r.commands:
71			got = append(got, v)
72		default:
73			synctest.Wait()
74			if len(r.commands) == 0 {
75				return got
76			}
77		}
78	}
79}
80
81// A display that reads nothing must not stall core: motion keeps coming, and
82// a bind after it still runs and publishes its scene.
83func TestCommandsNeverBlockCore(t *testing.T) {
84	synctest.Test(t, func(t *testing.T) {
85		r := startOutboxRig(t, 32)
86		const n = 10000
87		for i := range n {
88			// Each send returns only once Run is back in its select.
89			r.input <- ports.PointerMotion{X: 500 + float64(i%100), Y: 200, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}
90		}
91		r.input <- ports.KeyEvent{Keysym: "minus", Keycode: 12, Mods: ports.ModSuper, Pressed: true}
92		synctest.Wait()
93		select {
94		case set := <-r.scenes:
95			if set[0].Scale != 5.0/3 {
96				t.Fatalf("scale %v, want 5/3", set[0].Scale)
97			}
98		default:
99			t.Fatal("no scene: core is blocked on commands")
100		}
101		// Once the display reads again, it gets everything in order, with
102		// the motion merged and no movement lost.
103		var dx, dy, ux, uy float64
104		var motions int
105		var scaled bool
106		for _, v := range r.drain() {
107			switch v := v.(type) {
108			case ports.PointerMotionTo:
109				motions++
110				dx, dy, ux, uy = dx+v.DX, dy+v.DY, ux+v.UnaccelDX, uy+v.UnaccelDY
111			case ports.SetOutputs:
112				scaled = v.Outputs[0].Scale == 5.0/3
113			}
114		}
115		if dx != n || dy != 2*n || ux != 3*n || uy != 4*n {
116			t.Fatalf("lost movement: %v %v %v %v", dx, dy, ux, uy)
117		}
118		if motions > 40 {
119			t.Fatalf("%d motions reached the display, want them merged while it was away", motions)
120		}
121		if !scaled {
122			t.Fatal("scale change not sent after the motions")
123		}
124	})
125}
126
127// Commands reach the display in the order core made them. Only adjacent
128// motions of one window merge.
129func TestCommandsKeepOrderAndMergeAdjacentMotion(t *testing.T) {
130	synctest.Test(t, func(t *testing.T) {
131		r := startOutboxRig(t, 0)
132		// No reader: the sender holds the first command, which is in flight
133		// and no longer merges, and the rest queues behind it.
134		motion := func(x, dx float64) {
135			r.input <- ports.PointerMotion{X: x, Y: 200, DX: dx, UnaccelDX: dx}
136			synctest.Wait()
137		}
138		// How the first commands split between the sender's batch and the
139		// queue depends on when it wakes. The first key is a barrier: what
140		// is pushed after the owner handled it never merges with what
141		// came before, so the rest is deterministic.
142		motion(500, 1)
143		r.input <- ports.KeyEvent{Keysym: "y", Keycode: 21, Pressed: true}
144		synctest.Wait()
145		motion(501, 2)
146		motion(502, 4)
147		r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
148		synctest.Wait()
149		motion(503, 8)
150		r.input <- ports.KeyEvent{Keysym: "x", Keycode: 45, Pressed: true}
151		synctest.Wait()
152		motion(504, 16)
153		motion(505, 32)
154		var got []ports.ClientCommand
155		behind := false
156		for _, v := range r.drainUnbuffered() {
157			switch v.(type) {
158			case ports.UserActivity, ports.SetOutputs:
159				continue
160			}
161			if k, ok := v.(ports.ForwardKey); ok && k.Key.Keysym == "y" {
162				behind = true
163				continue
164			}
165			if behind {
166				got = append(got, v)
167			}
168		}
169		want := []ports.ClientCommand{
170			ports.PointerMotionTo{ID: 1, X: 502, Y: 200, DX: 6, UnaccelDX: 6},
171			ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true},
172			ports.PointerMotionTo{ID: 1, X: 503, Y: 200, DX: 8, UnaccelDX: 8},
173			ports.ForwardKey{ID: 1, Key: ports.KeyEvent{Keysym: "x", Keycode: 45, Pressed: true}},
174			ports.PointerMotionTo{ID: 1, X: 505, Y: 200, DX: 48, UnaccelDX: 48},
175		}
176		wantOrder(t, got, want)
177	})
178}
179
180// drainUnbuffered receives what the sender hands over one by one.
181func (r *outboxRig) drainUnbuffered() []ports.ClientCommand {
182	var got []ports.ClientCommand
183	for {
184		synctest.Wait()
185		select {
186		case v := <-r.commands:
187			got = append(got, v)
188		default:
189			return got
190		}
191	}
192}
193
194func wantOrder(t *testing.T, got, want []ports.ClientCommand) {
195	t.Helper()
196	if len(got) != len(want) {
197		t.Fatalf("got %d commands, want %d:\n%#v", len(got), len(want), got)
198	}
199	for i := range want {
200		if got[i] != want[i] {
201			t.Errorf("command %d: got %#v, want %#v", i, got[i], want[i])
202		}
203	}
204}

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