v0.2.0 07ab4d2

conn_test.go RawHistoryFolder

1package zerobus
2
3import (
4	"bufio"
5	"errors"
6	"os/exec"
7	"strings"
8	"testing"
9	"time"
10)
11
12// privateBus starts a dbus-daemon for the test and returns its address. The
13// test is skipped when dbus-daemon is not installed.
14func privateBus(t *testing.T) string {
15	t.Helper()
16	path, err := exec.LookPath("dbus-daemon")
17	if err != nil {
18		t.Skip("dbus-daemon not installed")
19	}
20	cmd := exec.Command(path, "--session", "--nofork", "--print-address")
21	out, err := cmd.StdoutPipe()
22	if err != nil {
23		t.Fatal(err)
24	}
25	if err := cmd.Start(); err != nil {
26		t.Fatal(err)
27	}
28	t.Cleanup(func() {
29		_ = cmd.Process.Kill()
30		_ = cmd.Wait()
31	})
32	line := make(chan string, 1)
33	go func() {
34		s, _ := bufio.NewReader(out).ReadString('\n')
35		line <- strings.TrimSpace(s)
36	}()
37	select {
38	case addr := <-line:
39		if addr == "" {
40			t.Fatal("dbus-daemon printed no address")
41		}
42		return addr
43	case <-time.After(5 * time.Second):
44		t.Fatal("dbus-daemon did not start")
45	}
46	return ""
47}
48
49func dial(t *testing.T, addr string) *Conn {
50	t.Helper()
51	c, err := Dial(addr)
52	if err != nil {
53		t.Fatal(err)
54	}
55	t.Cleanup(func() { _ = c.Close() })
56	return c
57}
58
59func TestHelloAndListNames(t *testing.T) {
60	c := dial(t, privateBus(t))
61	if !strings.HasPrefix(c.UniqueName(), ":") {
62		t.Fatalf("unique name %q", c.UniqueName())
63	}
64	c.NewCall(busName, busPath, busName, "ListNames", "")
65	m, err := c.Call()
66	if err != nil {
67		t.Fatal(err)
68	}
69	r := m.Body()
70	found := false
71	end := r.Array('s')
72	for r.More(end) {
73		if r.Str() == c.UniqueName() {
74			found = true
75		}
76	}
77	if !r.Done() || !found {
78		t.Fatalf("own name not listed: %v", r.Err())
79	}
80}
81
82func TestErrorReply(t *testing.T) {
83	c := dial(t, privateBus(t))
84	c.NewCall("org.example.Missing", "/", "org.example.I", "M", "").SetFlags(FlagNoAutoStart)
85	_, err := c.Call()
86	var de *Error
87	// ServiceUnknown or NameHasNoOwner, depending on the bus.
88	if !errors.As(err, &de) || !strings.HasPrefix(de.Name, "org.freedesktop.DBus.Error.") || de.Message == "" {
89		t.Fatalf("err = %v", err)
90	}
91	// The connection stays usable after an error reply.
92	if _, err := c.RequestName("org.example.Zerobus", NameFlagDoNotQueue); err != nil {
93		t.Fatal(err)
94	}
95}
96
97func TestSignalAndMethodBetweenPeers(t *testing.T) {
98	addr := privateBus(t)
99	server := dial(t, addr)
100	client := dial(t, addr)
101
102	if code, err := server.RequestName("org.example.Echo", NameFlagDoNotQueue); err != nil || code != NamePrimaryOwner {
103		t.Fatalf("RequestName = %d, %v", code, err)
104	}
105	if err := client.AddMatch("type='signal',interface='org.example.Echo'"); err != nil {
106		t.Fatal(err)
107	}
108
109	// The server answers one Echo call, then emits a signal.
110	done := make(chan error, 1)
111	go func() {
112		for {
113			m, err := server.ReadMessage()
114			if err != nil {
115				done <- err
116				return
117			}
118			if m.Type != TypeMethodCall || m.Member != "Echo" {
119				continue // NameAcquired and similar
120			}
121			s := m.Body().Str()
122			server.NewReply(m, "s").Str(strings.ToUpper(s))
123			if _, err := server.Send(); err != nil {
124				done <- err
125				return
126			}
127			server.NewSignal("/org/example/Echo", "org.example.Echo", "Echoed", "s").Str(s)
128			_, err = server.Send()
129			done <- err
130			return
131		}
132	}()
133
134	client.NewCall("org.example.Echo", "/org/example/Echo", "org.example.Echo", "Echo", "s").Str("hi")
135	m, err := client.Call()
136	if err != nil {
137		t.Fatal(err)
138	}
139	if got := m.Body().Str(); got != "HI" {
140		t.Fatalf("reply %q", got)
141	}
142	if err := <-done; err != nil {
143		t.Fatal(err)
144	}
145	for {
146		m, err := client.ReadMessage()
147		if err != nil {
148			t.Fatal(err)
149		}
150		if m.Type == TypeSignal && m.Member == "Echoed" {
151			if s := m.Body().Str(); s != "hi" || m.Sender != server.UniqueName() {
152				t.Fatalf("signal %q from %q", s, m.Sender)
153			}
154			return
155		}
156	}
157}
158
159func TestHandleDuringCall(t *testing.T) {
160	addr := privateBus(t)
161	c := dial(t, addr)
162	peer := dial(t, addr)
163	if err := c.AddMatch("type='signal',interface='org.example.Ping'"); err != nil {
164		t.Fatal(err)
165	}
166	peer.NewSignal("/a", "org.example.Ping", "Ping", "")
167	if _, err := peer.Send(); err != nil {
168		t.Fatal(err)
169	}
170	// A round trip from the peer proves the bus has routed the signal.
171	peer.NewCall(busName, busPath, busName, "GetId", "")
172	if _, err := peer.Call(); err != nil {
173		t.Fatal(err)
174	}
175
176	var got string
177	var nested error
178	c.Handle = func(m *Message) {
179		if m.Member == "Ping" {
180			got = strings.Clone(m.Member)
181			_, nested = c.ReadMessage()
182			// Sending from Handle is allowed.
183			c.NewSignal("/a", "org.example.Pong", "Pong", "")
184			if _, err := c.Send(); err != nil {
185				t.Error(err)
186			}
187		}
188	}
189	// The signal is queued before the reply, so Call passes it to Handle.
190	c.NewCall(busName, busPath, busName, "GetId", "")
191	if _, err := c.Call(); err != nil {
192		t.Fatal(err)
193	}
194	if got != "Ping" || !errors.Is(nested, ErrNested) {
195		t.Fatalf("handled %q, nested read: %v", got, nested)
196	}
197}
198
199func TestCallRejectsNoReply(t *testing.T) {
200	c := dial(t, privateBus(t))
201	c.NewCall(busName, busPath, busName, "GetId", "").SetFlags(FlagNoReplyExpected)
202	if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
203		t.Fatalf("err = %v, want ErrInvalid", err)
204	}
205	c.NewSignal("/a", "a.b", "C", "")
206	if _, err := c.Call(); !errors.Is(err, ErrInvalid) {
207		t.Fatalf("Call on a signal: %v, want ErrInvalid", err)
208	}
209	// The connection still works.
210	c.NewCall(busName, busPath, busName, "GetId", "")
211	if _, err := c.Call(); err != nil {
212		t.Fatal(err)
213	}
214}
215
216func TestCloseUnblocksRead(t *testing.T) {
217	c := dial(t, privateBus(t))
218	errc := make(chan error, 1)
219	go func() {
220		for {
221			if _, err := c.ReadMessage(); err != nil {
222				errc <- err
223				return
224			}
225		}
226	}()
227	time.Sleep(50 * time.Millisecond)
228	if err := c.Close(); err != nil {
229		t.Fatal(err)
230	}
231	select {
232	case err := <-errc:
233		if !errors.Is(err, ErrClosed) {
234			t.Fatalf("err = %v, want ErrClosed", err)
235		}
236	case <-time.After(5 * time.Second):
237		t.Fatal("ReadMessage did not return after Close")
238	}
239}
240
241func TestCallNoAllocs(t *testing.T) {
242	c := dial(t, privateBus(t))
243	call := func() {
244		c.NewCall(busName, busPath, busName, "GetNameOwner", "s").Str(busName)
245		m, err := c.Call()
246		if err != nil {
247			t.Fatal(err)
248		}
249		if m.Body().Str() == "" {
250			t.Fatal("empty owner")
251		}
252	}
253	call() // grow the buffers once
254	if allocs := testing.AllocsPerRun(200, call); allocs != 0 {
255		t.Fatalf("%v allocations per call, want 0", allocs)
256	}
257}
258
259func TestLargeMessageGrowsBuffer(t *testing.T) {
260	addr := privateBus(t)
261	a := dial(t, addr)
262	b := dial(t, addr)
263	if err := b.AddMatch("type='signal',interface='org.example.Big'"); err != nil {
264		t.Fatal(err)
265	}
266	payload := make([]byte, 3*defaultBufSz)
267	for i := range payload {
268		payload[i] = byte(i)
269	}
270	a.NewSignal("/a", "org.example.Big", "Blob", "ay").ByteArray(payload)
271	if _, err := a.Send(); err != nil {
272		t.Fatal(err)
273	}
274	for {
275		m, err := b.ReadMessage()
276		if err != nil {
277			t.Fatal(err)
278		}
279		if m.Member == "Blob" {
280			got := m.Body().ByteArray()
281			if len(got) != len(payload) || got[len(got)-1] != payload[len(payload)-1] {
282				t.Fatalf("payload of %d bytes", len(got))
283			}
284			return
285		}
286	}
287}
288
289func TestDialErrors(t *testing.T) {
290	for _, addr := range []string{"", "tcp:host=localhost,port=1", "unix:guid=abc", "unix:path=/nonexistent/zerobus"} {
291		if c, err := Dial(addr); err == nil {
292			_ = c.Close()
293			t.Errorf("Dial(%q) succeeded", addr)
294		}
295	}
296}
297
298func TestUnescape(t *testing.T) {
299	got, err := unescape("/tmp/a%20b%2c")
300	if err != nil || got != "/tmp/a b," {
301		t.Fatalf("got %q, %v", got, err)
302	}
303	if _, err := unescape("%zz"); err == nil {
304		t.Fatal("bad escape accepted")
305	}
306}

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 1

Tags 3

↑↓ moveenter openesc close