refactor/kvconf 3462c76

powerkey_test.go RawHistoryFolder

1package powerkey
2
3import (
4	"bufio"
5	"bytes"
6	"context"
7	"io"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"strings"
12	"sync"
13	"testing"
14	"time"
15
16	"github.com/bnema/zerowrap"
17	"github.com/godbus/dbus/v5"
18)
19
20// startBus starts a dbus-daemon of its own, so tests never touch the
21// system bus. CI must run these tests: there, a missing daemon fails.
22func startBus(t *testing.T) string {
23	t.Helper()
24	bin, err := exec.LookPath("dbus-daemon")
25	if err != nil {
26		if os.Getenv("CI") != "" {
27			t.Fatal("dbus-daemon not installed")
28		}
29		t.Skip("dbus-daemon not installed")
30	}
31	socket := filepath.Join(t.TempDir(), "bus")
32	cmd := exec.Command(bin, "--session", "--nofork", "--nopidfile", "--print-address=1", "--address=unix:path="+socket)
33	out, err := cmd.StdoutPipe()
34	if err != nil {
35		t.Fatal(err)
36	}
37	if err := cmd.Start(); err != nil {
38		t.Fatal(err)
39	}
40	t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
41	line, err := bufio.NewReader(out).ReadString('\n')
42	if err != nil {
43		t.Fatal(err)
44	}
45	return strings.TrimSpace(line)
46}
47
48// grant is one Inhibit call logind answered.
49type grant struct {
50	what, mode string
51	hold       *os.File // our end of the lock: the client has its own copy
52	released   *os.File // reads EOF once every copy of the lock is closed
53}
54
55// logindPeer plays logind on the private bus: it owns the name and answers
56// Inhibit with a pipe, whose other end shows when the client lets go.
57type logindPeer struct {
58	conn   *dbus.Conn
59	grants chan grant
60}
61
62func newPeer(t *testing.T, address string) *logindPeer {
63	t.Helper()
64	conn, err := dbus.Connect(address)
65	if err != nil {
66		t.Fatal(err)
67	}
68	t.Cleanup(func() { _ = conn.Close() })
69	p := &logindPeer{conn: conn, grants: make(chan grant, 8)}
70	table := map[string]any{"Inhibit": func(what, who, why, mode string) (dbus.UnixFD, *dbus.Error) {
71		r, w, err := os.Pipe()
72		if err != nil {
73			return 0, dbus.MakeFailedError(err)
74		}
75		t.Cleanup(func() { _ = r.Close(); _ = w.Close() })
76		fd := dbus.UnixFD(w.Fd()) // before the send: the test closes w after it
77		p.grants <- grant{what: what, mode: mode, hold: w, released: r}
78		return fd, nil
79	}}
80	if err := conn.ExportMethodTable(table, logindPath, manager); err != nil {
81		t.Fatal(err)
82	}
83	p.own(t)
84	return p
85}
86
87func (p *logindPeer) own(t *testing.T) {
88	t.Helper()
89	if r, err := p.conn.RequestName(logind, dbus.NameFlagDoNotQueue); err != nil || r != dbus.RequestNameReplyPrimaryOwner {
90		t.Fatal(r, err)
91	}
92}
93
94func (p *logindPeer) next(t *testing.T) grant {
95	t.Helper()
96	select {
97	case g := <-p.grants:
98		return g
99	case <-time.After(5 * time.Second):
100		t.Fatal("no Inhibit call")
101		return grant{}
102	}
103}
104
105// logBuf is a log sink the test reads while Serve writes.
106type logBuf struct {
107	mu  sync.Mutex
108	buf bytes.Buffer
109}
110
111func (l *logBuf) Write(p []byte) (int, error) {
112	l.mu.Lock()
113	defer l.mu.Unlock()
114	return l.buf.Write(p)
115}
116
117func (l *logBuf) count(s string) int {
118	l.mu.Lock()
119	defer l.mu.Unlock()
120	return strings.Count(l.buf.String(), s)
121}
122
123// wait blocks until the log has n lines containing msg.
124func (l *logBuf) wait(t *testing.T, msg string, n int) {
125	t.Helper()
126	for deadline := time.Now().Add(5 * time.Second); l.count(msg) < n; time.Sleep(5 * time.Millisecond) {
127		if time.Now().After(deadline) {
128			t.Fatalf("log never had %d x %q", n, msg)
129		}
130	}
131}
132
133// holding waits until Serve has received n lock answers: only then does it
134// own the descriptor.
135func (l *logBuf) holding(t *testing.T, n int) {
136	t.Helper()
137	l.wait(t, "holding logind key locks", n)
138}
139
140func run(t *testing.T, address string) (stop func(), log *logBuf) {
141	t.Helper()
142	ctx, cancel := context.WithCancel(context.Background())
143	done := make(chan struct{})
144	log = &logBuf{}
145	logger := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: log}).WithField("component", "powerkey")
146	go func() {
147		defer close(done)
148		Serve(ctx, address, 10*time.Millisecond, logger)
149	}()
150	var once bool
151	stop = func() {
152		if once {
153			return
154		}
155		once = true
156		cancel()
157		select {
158		case <-done:
159		case <-time.After(5 * time.Second):
160			t.Error("Serve outlived its context")
161		}
162	}
163	t.Cleanup(stop)
164	return stop, log
165}
166
167// released reports whether every copy of the lock is closed, once the
168// peer's own copy is.
169func released(t *testing.T, g grant) bool {
170	t.Helper()
171	_ = g.hold.Close()
172	_ = g.released.SetReadDeadline(time.Now().Add(2 * time.Second))
173	_, err := io.ReadAll(g.released)
174	return err == nil
175}
176
177// Serve blocks the four key handlers and lets go of them when it stops.
178func TestServeHoldsAndReleasesTheLock(t *testing.T) {
179	address := startBus(t)
180	peer := newPeer(t, address)
181	stop, log := run(t, address)
182	g := peer.next(t)
183	log.holding(t, 1)
184	if g.what != what || g.mode != "block" {
185		t.Fatalf("Inhibit(%q, %q)", g.what, g.mode)
186	}
187	stop()
188	if !released(t, g) {
189		t.Fatal("the lock outlived Serve")
190	}
191}
192
193// When logind restarts, the locks are taken again and the old ones closed.
194func TestServeTakesTheLockAgainAfterLogindRestarts(t *testing.T) {
195	address := startBus(t)
196	peer := newPeer(t, address)
197	_, log := run(t, address)
198	first := peer.next(t)
199	log.holding(t, 1)
200	if _, err := peer.conn.ReleaseName(logind); err != nil {
201		t.Fatal(err)
202	}
203	log.wait(t, "ignored until the next attempt", 1)
204	// The gap: the old lock is still ours, logind keeps it through a restart.
205	peer.own(t)
206	peer.next(t)
207	log.holding(t, 2)
208	if !released(t, first) {
209		t.Fatal("the first lock was not closed once the second was taken")
210	}
211}
212
213// Without logind on the bus, Serve keeps trying until it appears.
214func TestServeWaitsForLogind(t *testing.T) {
215	address := startBus(t)
216	peer := newPeer(t, address)
217	if _, err := peer.conn.ReleaseName(logind); err != nil {
218		t.Fatal(err)
219	}
220	_, log := run(t, address)
221	log.wait(t, "ignored until the next attempt", 1)
222	peer.own(t)
223	peer.next(t)
224	log.holding(t, 1)
225}
226
227func TestLostTrustsOnlyTheBus(t *testing.T) {
228	body := []any{logind, "", ""}
229	for name, tc := range map[string]struct {
230		sig  *dbus.Signal
231		want bool
232	}{
233		"bus, logind gone":  {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameOwnerChanged", Body: body}, true},
234		"bus, logind moved": {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameOwnerChanged", Body: []any{logind, "", ":1.5"}}, false},
235		"bus, other name":   {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameOwnerChanged", Body: []any{"x.y", "", ""}}, false},
236		"client forging it": {&dbus.Signal{Sender: ":1.9", Name: busDaemon + ".NameOwnerChanged", Body: body}, false},
237		"other signal":      {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameAcquired", Body: body}, false},
238		"malformed body":    {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameOwnerChanged", Body: []any{1, 2, 3}}, false},
239		"short body":        {&dbus.Signal{Sender: busDaemon, Name: busDaemon + ".NameOwnerChanged", Body: []any{logind}}, false},
240	} {
241		if got := lost(tc.sig); got != tc.want {
242			t.Errorf("%s: lost = %v, want %v", name, got, tc.want)
243		}
244	}
245}

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