1package atspi
2
3import (
4 "bufio"
5 "os"
6 "os/exec"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/bnema/zerobus"
12 "github.com/stretchr/testify/require"
13)
14
15// startBus starts a private dbus-daemon, so the test never touches the
16// desktop's buses. It skips when the daemon is not installed.
17func startBus(t *testing.T) string {
18 t.Helper()
19 bin, err := exec.LookPath("dbus-daemon")
20 if err != nil {
21 if os.Getenv("CI") != "" {
22 t.Fatal("dbus-daemon not installed")
23 }
24 t.Skip("dbus-daemon not installed")
25 }
26 cmd := exec.Command(bin, "--session", "--nofork", "--nopidfile", "--print-address=1")
27 out, err := cmd.StdoutPipe()
28 require.NoError(t, err)
29 require.NoError(t, cmd.Start())
30 t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
31 line, err := bufio.NewReader(out).ReadString('\n')
32 require.NoError(t, err)
33 return strings.TrimSpace(line)
34}
35
36// TestDoDecodesMatchesPipelined runs Do against an app on a real bus: the
37// GetMatches reply is decoded, and replies to several calls sent before any
38// is read are matched by serial.
39func TestDoDecodesMatchesPipelined(t *testing.T) {
40 addr := startBus(t)
41 app, err := zerobus.Dial(addr)
42 require.NoError(t, err)
43 t.Cleanup(func() { _ = app.Close() })
44 _, err = app.RequestName("org.test.App", 0)
45 require.NoError(t, err)
46
47 done := make(chan struct{})
48 go func() {
49 defer close(done)
50 for {
51 m, err := app.ReadMessage()
52 if err != nil {
53 return
54 }
55 if m.Type != zerobus.TypeMethodCall {
56 continue
57 }
58 req := &zerobus.Message{Sender: strings.Clone(m.Sender), Serial: m.Serial}
59 switch m.Member {
60 case "GetMatches":
61 // Check the rule: struct, then sort, count, traverse.
62 r := m.Body()
63 r.Struct()
64 end := r.Array('i')
65 var states []int32
66 for r.More(end) {
67 states = append(states, r.Int32())
68 }
69 _ = r.Int32()
70 ad := r.Array('{')
71 _ = ad
72 _ = r.Int32()
73 end = r.Array('i')
74 var roles []int32
75 for r.More(end) {
76 roles = append(roles, r.Int32())
77 }
78 ok := r.Err() == nil && len(states) == 2 && states[0] == 1<<25|1<<30 && len(roles) == 5 && uint32(roles[2]) == 1<<24|1<<18|1<<31
79 if !ok {
80 app.NewError(req, "org.test.Error.BadRule", "s").Str("bad rule")
81 _, _ = app.Send()
82 continue
83 }
84 e := app.NewReply(req, "a(so)")
85 a := e.BeginArray('(')
86 for _, p := range []string{"/one", "/two"} {
87 e.Struct()
88 e.Str(":1.99")
89 e.ObjectPath(p)
90 }
91 e.EndArray(a)
92 case "GetExtents":
93 e := app.NewReply(req, "(iiii)")
94 e.Struct()
95 e.Int32(1)
96 e.Int32(2)
97 e.Int32(30)
98 e.Int32(40)
99 case "GetRole":
100 app.NewReply(req, "u").Uint32(roleLink)
101 case "GetState":
102 e := app.NewReply(req, "au")
103 a := e.BeginArray('u')
104 e.Uint32(stShowing)
105 e.Uint32(0)
106 e.EndArray(a)
107 default:
108 app.NewError(req, "org.freedesktop.DBus.Error.UnknownMethod", "s").Str("no")
109 }
110 _, _ = app.Send()
111 }
112 }()
113
114 cc, err := zerobus.Dial(addr)
115 require.NoError(t, err)
116 b, err := newConn(cc)
117 require.NoError(t, err)
118 t.Cleanup(func() { _ = b.Close() })
119
120 calls := []call{
121 {op: opMatches, name: "org.test.App", path: "/frame", rule: matchRule{linksOnly: true}},
122 {op: opExtents, name: "org.test.App", path: "/one", arg: coordWindow},
123 {op: opRole, name: "org.test.App", path: "/one"},
124 {op: opState, name: "org.test.App", path: "/one"},
125 {op: opChildren, name: "org.test.App", path: "/one"},
126 }
127 rep := b.Do(time.Now().Add(2*time.Second), time.Second, calls)
128 require.Len(t, rep, 5)
129 require.NoError(t, rep[0].err)
130 require.Equal(t, []ref{{":1.99", "/one"}, {":1.99", "/two"}}, rep[0].refs)
131 require.NoError(t, rep[1].err)
132 require.Equal(t, [4]int32{1, 2, 30, 40}, rep[1].rect)
133 require.Equal(t, uint32(roleLink), rep[2].u)
134 require.Equal(t, uint64(stShowing), rep[3].state)
135 var be *busError
136 require.ErrorAs(t, rep[4].err, &be)
137 require.Equal(t, "org.freedesktop.DBus.Error.UnknownMethod", be.name)
138
139 // A call to a name nobody owns fails fast, others still succeed.
140 rep = b.Do(time.Now().Add(2*time.Second), time.Second, []call{
141 {op: opRole, name: "org.test.Nobody", path: "/x"},
142 {op: opRole, name: "org.test.App", path: "/x"},
143 })
144 require.Error(t, rep[0].err)
145 require.NoError(t, rep[1].err)
146}
147
148func TestDoTimesOutOnSilentApp(t *testing.T) {
149 addr := startBus(t)
150 app, err := zerobus.Dial(addr)
151 require.NoError(t, err)
152 t.Cleanup(func() { _ = app.Close() })
153 _, err = app.RequestName("org.test.Mute", 0)
154 require.NoError(t, err)
155
156 cc, err := zerobus.Dial(addr)
157 require.NoError(t, err)
158 b, err := newConn(cc)
159 require.NoError(t, err)
160 t.Cleanup(func() { _ = b.Close() })
161
162 t0 := time.Now()
163 rep := b.Do(time.Now().Add(time.Second), 50*time.Millisecond, []call{{op: opRole, name: "org.test.Mute", path: "/x"}})
164 require.ErrorIs(t, rep[0].err, ErrTimeout)
165 require.Less(t, time.Since(t0), 500*time.Millisecond)
166}
167
168// A call left unanswered for idle gets ErrTimeout without breaking the
169// connection; a batch that hits its deadline breaks it, and Do returns near
170// the deadline even though the idle timeout is longer.
171func TestDoDeadlineBreaksConnection(t *testing.T) {
172 addr := startBus(t)
173 app, err := zerobus.Dial(addr)
174 require.NoError(t, err)
175 t.Cleanup(func() { _ = app.Close() })
176 _, err = app.RequestName("org.test.Mute", 0)
177 require.NoError(t, err)
178
179 cc, err := zerobus.Dial(addr)
180 require.NoError(t, err)
181 b, err := newConn(cc)
182 require.NoError(t, err)
183 t.Cleanup(func() { _ = b.Close() })
184
185 mute := []call{{op: opRole, name: "org.test.Mute", path: "/x"}}
186 rep := b.Do(time.Now().Add(time.Second), 40*time.Millisecond, mute)
187 require.ErrorIs(t, rep[0].err, ErrTimeout)
188 require.NotErrorIs(t, rep[0].err, ErrBroken)
189
190 t0 := time.Now()
191 rep = b.Do(time.Now().Add(60*time.Millisecond), time.Second, mute)
192 require.ErrorIs(t, rep[0].err, ErrBroken)
193 require.Less(t, time.Since(t0), 300*time.Millisecond)
194 // The socket is closed: the next batch fails at once.
195 rep = b.Do(time.Now().Add(time.Second), time.Second, mute)
196 require.ErrorIs(t, rep[0].err, ErrBroken)
197}