main ece7682

copy_scroll_remote_test.go RawHistoryFolder

1package daemon
2
3import (
4	"fmt"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/protocol"
11)
12
13// TestCopyScrollOverDelayedAcks replays a wheel scroll on a link whose
14// acknowledgements arrive rtt late, for several output windows. One tick is one
15// animation frame (copyScrollFrame). The log shows, per tick, the rows the
16// viewport moved: "-" is a stall, a value above one frame's worth is a catch-up
17// jump. The test asserts that no frame jumps further than copyScrollMaxStep,
18// that every requested row is eventually shown, and that the window the daemon
19// is allowed to fill bounds the stalls.
20func TestCopyScrollOverDelayedAcks(t *testing.T) {
21	// flick: 12 notches at once. sustained: one notch per tick for 30 ticks.
22	scenarios := []struct {
23		name             string
24		upfront, perTick int
25	}{{"flick", 12, 0}, {"sustained", 1, 1}}
26	rtts := []time.Duration{0, 50 * time.Millisecond, 100 * time.Millisecond, 200 * time.Millisecond, 400 * time.Millisecond}
27	for _, scenario := range scenarios {
28		for _, window := range []uint8{1, protocol.MaxOutputWindow} {
29			for _, rtt := range rtts {
30				t.Run(fmt.Sprintf("%s/window%d/%v", scenario.name, window, rtt), func(t *testing.T) {
31					res := replayCopyScroll(t, scenario.upfront, scenario.perTick, window, rtt)
32					t.Logf("rows per tick (- = stall): %s", res.log)
33					if res.maxJump > copyScrollMaxStep {
34						t.Errorf("one frame jumped %d rows, want at most %d", res.maxJump, copyScrollMaxStep)
35					}
36					if res.scrolled != res.requested {
37						t.Fatalf("scrolled %d rows, want %d", res.scrolled, res.requested)
38					}
39				})
40			}
41		}
42	}
43}
44
45// TestCopyScrollWindowBoundsStalls pins the reason the output window matters:
46// on a 100 ms link a one-frame window leaves the viewport still most ticks,
47// while the default window keeps it moving.
48func TestCopyScrollWindowBoundsStalls(t *testing.T) {
49	rtt := 100 * time.Millisecond
50	narrow := replayCopyScroll(t, 1, 1, 1, rtt)
51	wide := replayCopyScroll(t, 1, 1, protocol.MaxOutputWindow, rtt)
52	if wide.stalls >= narrow.stalls {
53		t.Fatalf("window %d stalled %d ticks, window 1 stalled %d: a wider window must stall less", protocol.MaxOutputWindow, wide.stalls, narrow.stalls)
54	}
55}
56
57type copyScrollReplay struct {
58	log                 string
59	maxJump, stalls     int
60	scrolled, requested int
61}
62
63// replayCopyScroll feeds upfront notches at once, then perTick notches per tick
64// for 30 ticks, acknowledging each emitted frame rtt later. A fake clock drives
65// the animation and every step waits for its exact expected effect (a frame
66// emitted, or the frame timer re-armed while the window is full), so the
67// timeouts below only guard against hangs and never decide a result.
68func replayCopyScroll(t *testing.T, upfront, perTick int, window uint8, rtt time.Duration) copyScrollReplay {
69	t.Helper()
70	f := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 80, Rows: 24}, panes: 1, historyRows: 400})
71	f.ac.output.setWindow(window)
72	f.d.enterCopyMode(f.sess, f.ac)
73	f.d.copyWheel(f.sess, f.ac, -150)
74	f.ac.ackOutputState(f.ac.output.currentEpoch(), f.ac.output.next)
75	clock := newCoordinatorMockClock(t, 256)
76	f.d.clock = clock.clock
77	rt := f.ac.overlays
78
79	type ack struct {
80		at          int
81		epoch, seen uint64
82	}
83	type emitted struct{ epoch, state uint64 }
84	var (
85		acks       []ack
86		tick       int
87		frameTimer *coordinatorMockTimer
88		// emit runs under sendMu inside the render, so the epoch and state
89		// it reports are exactly those of the frame sent.
90		sent = make(chan emitted, 256)
91	)
92	f.ac.renderStages.emit = func() { sent <- emitted{f.ac.output.currentEpoch(), f.ac.output.next} }
93	ticksPerRTT := int((rtt + copyScrollFrame - 1) / copyScrollFrame)
94
95	viewport := func() int {
96		rt.copyMu.Lock()
97		defer rt.copyMu.Unlock()
98		return rt.copyMode.ViewportTop
99	}
100	pending := func() int {
101		rt.copyMu.Lock()
102		defer rt.copyMu.Unlock()
103		return rt.copyScroll.remaining
104	}
105	// collectTimer keeps the newest armed animation frame timer, if any.
106	collectTimer := func() {
107		for {
108			select {
109			case tm := <-clock.timers:
110				if tm.duration == copyScrollFrame {
111					frameTimer = tm
112				}
113			default:
114				return
115			}
116		}
117	}
118	frames := 0
119	awaitFrame := func() {
120		select {
121		case e := <-sent:
122			acks = append(acks, ack{at: tick + ticksPerRTT, epoch: e.epoch, seen: e.state})
123			frames++
124		case <-time.After(5 * time.Second):
125			t.Fatal("expected a scroll frame, none was emitted")
126		}
127	}
128	awaitTimer := func() {
129		deadline := time.After(5 * time.Second)
130		for frameTimer == nil {
131			select {
132			case tm := <-clock.timers:
133				if tm.duration == copyScrollFrame {
134					frameTimer = tm
135				}
136			case <-deadline:
137				t.Fatal("held animation did not re-arm its frame timer")
138			}
139		}
140	}
141	fed := 0
142	feed := func(n int) {
143		for range n {
144			idle, full := frameTimer == nil, f.ac.output.atCapacity()
145			f.d.smoothCopyWheel(f.sess, f.ac, -3)
146			fed++
147			if idle && !full {
148				awaitFrame()
149			}
150			collectTimer()
151		}
152	}
153	fire := func() {
154		if frameTimer == nil {
155			return
156		}
157		full := f.ac.output.atCapacity()
158		timer := frameTimer
159		frameTimer = nil
160		timer.ch <- time.Time{}
161		if full {
162			awaitTimer()
163			return
164		}
165		awaitFrame()
166		collectTimer()
167	}
168
169	var res copyScrollReplay
170	var log []string
171	start := viewport()
172	last := start
173	record := func() {
174		if frames == 0 {
175			log = append(log, "-")
176			res.stalls++
177			return
178		}
179		top := viewport()
180		log = append(log, fmt.Sprint(last-top))
181		res.maxJump = max(res.maxJump, last-top)
182		last = top
183		frames = 0
184	}
185
186	feed(upfront)
187	record()
188	for ; tick < 1200 && (pending() != 0 || len(acks) != 0 || tick < 30 && perTick > 0); tick++ {
189		if tick < 30 {
190			feed(perTick)
191		}
192		for len(acks) > 0 && acks[0].at <= tick {
193			f.ac.ackOutputState(acks[0].epoch, acks[0].seen)
194			if rc := f.sess.renderCoordinator(); rc != nil {
195				rc.notifyAck()
196			}
197			acks = acks[1:]
198		}
199		fire()
200		record()
201	}
202	res.log = strings.Join(log, " ")
203	res.scrolled = start - viewport()
204	res.requested = 3 * fed
205	return res
206}

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 12

Tags 11

↑↓ moveenter openesc close