Merge lp:~chipaca/ubuntu-push/the-push-automatic into lp:ubuntu-push

Proposed by John Lenton
Status: Merged
Approved by: John Lenton
Approved revision: no longer in the source branch.
Merged at revision: 113
Proposed branch: lp:~chipaca/ubuntu-push/the-push-automatic
Merge into: lp:ubuntu-push
Diff against target: 2976 lines (+1434/-486)
26 files modified
bus/emblemcounter/emblemcounter.go (+30/-9)
bus/emblemcounter/emblemcounter_test.go (+27/-7)
bus/haptic/haptic.go (+10/-6)
bus/haptic/haptic_test.go (+9/-2)
bus/notifications/raw.go (+26/-12)
bus/notifications/raw_test.go (+25/-56)
bus/urldispatcher/urldispatcher.go (+22/-3)
bus/urldispatcher/urldispatcher_test.go (+68/-6)
click/click.go (+7/-0)
click/click_test.go (+3/-0)
client/client.go (+9/-17)
client/client_test.go (+6/-28)
client/service/postal.go (+119/-28)
client/service/postal_test.go (+301/-170)
debian/changelog (+21/-0)
launch_helper/helper_output.go (+1/-0)
launch_helper/helper_test.go (+2/-2)
launch_helper/kindpool.go (+75/-8)
launch_helper/kindpool_test.go (+244/-87)
messaging/cmessaging/cmessaging.go (+29/-1)
messaging/cmessaging/cmessaging_c.go (+31/-1)
messaging/messaging.go (+136/-25)
messaging/messaging_test.go (+220/-12)
messaging/reply/reply.go (+3/-0)
sounds/sounds.go (+9/-5)
sounds/sounds_test.go (+1/-1)
To merge this branch: bzr merge lp:~chipaca/ubuntu-push/the-push-automatic
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+228526@code.launchpad.net

Commit message

Merge from automatic.

Description of the change

Merge from automatic.

To post a comment you must log in.
113. By John Lenton

Merge from automatic.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bus/emblemcounter/emblemcounter.go'
2--- bus/emblemcounter/emblemcounter.go 2014-07-09 10:22:09 +0000
3+++ bus/emblemcounter/emblemcounter.go 2014-07-29 16:56:22 +0000
4@@ -49,16 +49,37 @@
5
6 // Look for an EmblemCounter section in a Notification and, if
7 // present, presents it to the user.
8-func (ctr *EmblemCounter) Present(app *click.AppId, nid string, notification *launch_helper.Notification) {
9- if notification == nil || notification.EmblemCounter == nil {
10- ctr.log.Debugf("[%s] no notification or no EmblemCounter in the notification; doing nothing: %#v", nid, notification)
11- return
12+func (ctr *EmblemCounter) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool {
13+ if notification == nil {
14+ panic("please check notification is not nil before calling present")
15 }
16+
17 ec := notification.EmblemCounter
18+
19+ if ec == nil {
20+ ctr.log.Debugf("[%s] notification has no EmblemCounter: %#v", nid, ec)
21+ return false
22+ }
23 ctr.log.Debugf("[%s] setting emblem counter for %s to %d (visible: %t)", nid, app.Base(), ec.Count, ec.Visible)
24-
25- quoted := string(nih.Quote([]byte(app.Base())))
26-
27- ctr.bus.SetProperty("count", "/"+quoted, dbus.Variant{ec.Count})
28- ctr.bus.SetProperty("countVisible", "/"+quoted, dbus.Variant{ec.Visible})
29+ return ctr.SetCounter(app, ec.Count, ec.Visible)
30+}
31+
32+// SetCounter sets an emblem counter on the launcher for app to count (if
33+// visible is true), or clears it (if count is 0 or visible is false).
34+func (ctr *EmblemCounter) SetCounter(app *click.AppId, count int32, visible bool) bool {
35+ base := app.Base()
36+ quoted := string(nih.Quote([]byte(base)))
37+
38+ err := ctr.bus.SetProperty("count", "/"+quoted, dbus.Variant{count})
39+ if err != nil {
40+ ctr.log.Errorf("call to set count failed: %v", err)
41+ return false
42+ }
43+ err = ctr.bus.SetProperty("countVisible", "/"+quoted, dbus.Variant{visible})
44+ if err != nil {
45+ ctr.log.Errorf("call to set countVisible failed: %v", err)
46+ return false
47+ }
48+
49+ return true
50 }
51
52=== modified file 'bus/emblemcounter/emblemcounter_test.go'
53--- bus/emblemcounter/emblemcounter_test.go 2014-07-11 19:42:57 +0000
54+++ bus/emblemcounter/emblemcounter_test.go 2014-07-29 16:56:22 +0000
55@@ -45,6 +45,21 @@
56 ecs.app = clickhelp.MustParseAppId("com.example.test_test-app_0")
57 }
58
59+// checks that SetCounter() actually calls SetProperty on the launcher
60+func (ecs *ecSuite) TestSetCounterSetsTheCounter(c *C) {
61+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
62+ quoted := string(nih.Quote([]byte(ecs.app.Base())))
63+
64+ ec := New(endp, ecs.log)
65+ c.Check(ec.SetCounter(ecs.app, 42, true), Equals, true)
66+ callArgs := testibus.GetCallArgs(endp)
67+ c.Assert(callArgs, HasLen, 2)
68+ c.Check(callArgs[0].Member, Equals, "::SetProperty")
69+ c.Check(callArgs[1].Member, Equals, "::SetProperty")
70+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/" + quoted, dbus.Variant{Value: int32(42)}})
71+ c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/" + quoted, dbus.Variant{Value: true}})
72+}
73+
74 // checks that Present() actually calls SetProperty on the launcher
75 func (ecs *ecSuite) TestPresentPresents(c *C) {
76 endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
77@@ -52,7 +67,7 @@
78
79 ec := New(endp, ecs.log)
80 notif := launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{Count: 42, Visible: true}}
81- ec.Present(ecs.app, "nid", &notif)
82+ c.Check(ec.Present(ecs.app, "nid", &notif), Equals, true)
83 callArgs := testibus.GetCallArgs(endp)
84 c.Assert(callArgs, HasLen, 2)
85 c.Check(callArgs[0].Member, Equals, "::SetProperty")
86@@ -67,16 +82,12 @@
87 endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
88 ec := New(endp, ecs.log)
89
90- // nothing happens if nil Notification
91- ec.Present(ecs.app, "nid", nil)
92- c.Assert(testibus.GetCallArgs(endp), HasLen, 0)
93-
94 // nothing happens if no EmblemCounter in Notification
95- ec.Present(ecs.app, "nid", &launch_helper.Notification{})
96+ c.Check(ec.Present(ecs.app, "nid", &launch_helper.Notification{}), Equals, false)
97 c.Assert(testibus.GetCallArgs(endp), HasLen, 0)
98
99 // but an empty EmblemCounter is acted on
100- ec.Present(ecs.app, "nid", &launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{}})
101+ c.Check(ec.Present(ecs.app, "nid", &launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{}}), Equals, true)
102 callArgs := testibus.GetCallArgs(endp)
103 c.Assert(callArgs, HasLen, 2)
104 c.Check(callArgs[0].Member, Equals, "::SetProperty")
105@@ -84,3 +95,12 @@
106 c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/" + quoted, dbus.Variant{Value: int32(0)}})
107 c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/" + quoted, dbus.Variant{Value: false}})
108 }
109+
110+// check that Present() panics if the notification is nil
111+func (ecs *ecSuite) TestPanicsIfNil(c *C) {
112+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
113+ ec := New(endp, ecs.log)
114+
115+ // nothing happens if no EmblemCounter in Notification
116+ c.Check(func() { ec.Present(ecs.app, "nid", nil) }, Panics, `please check notification is not nil before calling present`)
117+}
118
119=== modified file 'bus/haptic/haptic.go'
120--- bus/haptic/haptic.go 2014-07-15 15:02:33 +0000
121+++ bus/haptic/haptic.go 2014-07-29 16:56:22 +0000
122@@ -44,9 +44,13 @@
123 }
124
125 // Present presents the notification via a vibrate pattern
126-func (haptic *Haptic) Present(_ *click.AppId, _ string, notification *launch_helper.Notification) bool {
127- if notification == nil || notification.Vibrate == nil {
128- haptic.log.Debugf("no notification or no Vibrate in the notification; doing nothing: %#v", notification)
129+func (haptic *Haptic) Present(_ *click.AppId, nid string, notification *launch_helper.Notification) bool {
130+ if notification == nil {
131+ panic("please check notification is not nil before calling present")
132+ }
133+
134+ if notification.Vibrate == nil {
135+ haptic.log.Debugf("[%s] notification has no Vibrate: %#v", nid, notification.Vibrate)
136 return false
137 }
138 pattern := notification.Vibrate.Pattern
139@@ -58,13 +62,13 @@
140 pattern = []uint32{notification.Vibrate.Duration}
141 }
142 if len(pattern) == 0 {
143- haptic.log.Debugf("not enough information in the notification's Vibrate section to create a pattern")
144+ haptic.log.Debugf("[%s] not enough information in the Vibrate to create a pattern", nid)
145 return false
146 }
147- haptic.log.Debugf("vibrating %d times to the tune of %v", repeat, pattern)
148+ haptic.log.Debugf("[%s] vibrating %d times to the tune of %v", nid, repeat, pattern)
149 err := haptic.bus.Call("VibratePattern", bus.Args(pattern, repeat))
150 if err != nil {
151- haptic.log.Errorf("VibratePattern call returned %v", err)
152+ haptic.log.Errorf("[%s] call to VibratePattern returned %v", nid, err)
153 return false
154 }
155 return true
156
157=== modified file 'bus/haptic/haptic_test.go'
158--- bus/haptic/haptic_test.go 2014-07-11 19:42:57 +0000
159+++ bus/haptic/haptic_test.go 2014-07-29 16:56:22 +0000
160@@ -106,10 +106,17 @@
161 endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
162
163 ec := New(endp, hs.log)
164- // no notification at all
165- c.Check(ec.Present(hs.app, "", nil), Equals, false)
166 // no Vibration in the notificaton
167 c.Check(ec.Present(hs.app, "", &launch_helper.Notification{}), Equals, false)
168 // empty Vibration
169 c.Check(ec.Present(hs.app, "", &launch_helper.Notification{Vibrate: &launch_helper.Vibration{}}), Equals, false)
170 }
171+
172+// check that Present() panics if the notification is nil
173+func (hs *hapticSuite) TestPanicsIfNil(c *C) {
174+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
175+
176+ ec := New(endp, hs.log)
177+ // no notification at all
178+ c.Check(func() { ec.Present(hs.app, "", nil) }, Panics, `please check notification is not nil before calling present`)
179+}
180
181=== modified file 'bus/notifications/raw.go'
182--- bus/notifications/raw.go 2014-07-17 22:43:17 +0000
183+++ bus/notifications/raw.go 2014-07-29 16:56:22 +0000
184@@ -131,14 +131,18 @@
185 // If card.Actions has 1 action, it's an interactive notification.
186 // If card.Actions has 2 actions, it will show as a snap decision.
187 // If it has more actions, who knows (good luck).
188-func (raw *RawNotifications) Present(app *click.AppId, nid string, notification *launch_helper.Notification) (uint32, error) {
189- if notification == nil || notification.Card == nil || !notification.Card.Popup || notification.Card.Summary == "" {
190- raw.log.Debugf("[%s] skipping notification: nil, or nil card, or not popup, or no summary: %#v", nid, notification)
191- return 0, nil
192+func (raw *RawNotifications) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool {
193+ if notification == nil {
194+ panic("please check notification is not nil before calling present")
195 }
196
197 card := notification.Card
198
199+ if card == nil || !card.Popup || card.Summary == "" {
200+ raw.log.Debugf("[%s] notification has no popup card: %#v", nid, card)
201+ return false
202+ }
203+
204 hints := make(map[string]*dbus.Variant)
205 hints["x-canonical-secondary-icon"] = &dbus.Variant{app.Icon()}
206
207@@ -152,7 +156,8 @@
208 Action: action,
209 })
210 if err != nil {
211- return 0, err
212+ raw.log.Errorf("[%s] while marshaling %#v to json: %v", nid, action, err)
213+ return false
214 }
215 actions[2*i] = string(act)
216 actions[2*i+1] = action
217@@ -160,13 +165,22 @@
218 switch len(card.Actions) {
219 case 0:
220 // nothing
221+ default:
222+ raw.log.Errorf("[%s] don't know what to do with %d actions; ignoring the rest", nid, len(card.Actions))
223+ actions = actions[:2]
224+ fallthrough
225 case 1:
226 hints["x-canonical-switch-to-application"] = &dbus.Variant{"true"}
227- case 2:
228- hints["x-canonical-snap-decisions"] = &dbus.Variant{"true"}
229- hints["x-canonical-private-button-tint"] = &dbus.Variant{"true"}
230- default:
231- raw.log.Debugf("[%s] don't know what to do with %d actions; no hints set", nid, len(card.Actions))
232- }
233- return raw.Notify(appId, 0, card.Icon, card.Summary, card.Body, actions, hints, 30*1000)
234+ }
235+
236+ raw.log.Debugf("[%s] creating popup (or snap decision) for %s (summary: %s)", nid, app.Base(), card.Summary)
237+
238+ _, err := raw.Notify(appId, 0, card.Icon, card.Summary, card.Body, actions, hints, 30*1000)
239+
240+ if err != nil {
241+ raw.log.Errorf("[%s] call to Notify failed: %v", nid, err)
242+ return false
243+ }
244+
245+ return true
246 }
247
248=== modified file 'bus/notifications/raw_test.go'
249--- bus/notifications/raw_test.go 2014-07-17 22:57:16 +0000
250+++ bus/notifications/raw_test.go 2014-07-29 16:56:22 +0000
251@@ -160,16 +160,15 @@
252 func (s *RawSuite) TestPresentNotifies(c *C) {
253 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
254 raw := Raw(endp, s.log)
255- nid, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true}})
256- c.Check(err, IsNil)
257- c.Check(nid, Equals, uint32(1))
258+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true}})
259+ c.Check(worked, Equals, true)
260 }
261
262 func (s *RawSuite) TestPresentOneAction(c *C) {
263 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
264 raw := Raw(endp, s.log)
265- _, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes"}}})
266- c.Check(err, IsNil)
267+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes"}}})
268+ c.Check(worked, Equals, true)
269 callArgs := testibus.GetCallArgs(endp)
270 c.Assert(callArgs, HasLen, 1)
271 c.Assert(callArgs[0].Member, Equals, "Notify")
272@@ -192,78 +191,48 @@
273 func (s *RawSuite) TestPresentTwoActions(c *C) {
274 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
275 raw := Raw(endp, s.log)
276- _, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes", "No"}}})
277- c.Check(err, IsNil)
278+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes", "No"}}})
279+ c.Check(worked, Equals, true)
280 callArgs := testibus.GetCallArgs(endp)
281 c.Assert(callArgs, HasLen, 1)
282 c.Assert(callArgs[0].Member, Equals, "Notify")
283 c.Assert(len(callArgs[0].Args), Equals, 8)
284 actions, ok := callArgs[0].Args[5].([]string)
285 c.Assert(ok, Equals, true)
286- c.Assert(actions, HasLen, 4)
287+ c.Assert(actions, HasLen, 2)
288 c.Check(actions[0], Equals, `{"app":"com.example.test_test-app_0","act":"Yes","nid":"notifId"}`)
289 c.Check(actions[1], Equals, "Yes")
290- c.Check(actions[2], Equals, `{"app":"com.example.test_test-app_0","act":"No","aid":1,"nid":"notifId"}`)
291- c.Check(actions[3], Equals, "No")
292- hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant)
293- c.Assert(ok, Equals, true)
294- // with two actions, there should be 3 hints set:
295- c.Assert(hints, HasLen, 3)
296- c.Check(hints["x-canonical-switch-to-application"], IsNil)
297- c.Check(hints["x-canonical-secondary-icon"], NotNil)
298- c.Check(hints["x-canonical-snap-decisions"], NotNil)
299- c.Check(hints["x-canonical-private-button-tint"], NotNil)
300- c.Check(hints["x-canonical-non-shaped-icon"], IsNil) // checking just in case
301-}
302-
303-func (s *RawSuite) TestPresentThreeActions(c *C) {
304- endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
305- raw := Raw(endp, s.log)
306- _, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes", "No", "What"}}})
307- c.Check(err, IsNil)
308- callArgs := testibus.GetCallArgs(endp)
309- c.Assert(callArgs, HasLen, 1)
310- c.Assert(callArgs[0].Member, Equals, "Notify")
311- c.Assert(len(callArgs[0].Args), Equals, 8)
312- actions, ok := callArgs[0].Args[5].([]string)
313- c.Assert(ok, Equals, true)
314- c.Assert(actions, HasLen, 6)
315- hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant)
316- c.Assert(ok, Equals, true)
317- // with two actions, there should be 3 hints set:
318- c.Check(hints, HasLen, 1)
319- c.Check(hints["x-canonical-secondary-icon"], NotNil)
320- c.Check(s.log.Captured(), Matches, `(?ms).* no hints set$`)
321-}
322-
323-func (s *RawSuite) TestPresentNoNotificationDoesNotNotify(c *C) {
324- endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
325- raw := Raw(endp, s.log)
326- nid, err := raw.Present(s.app, "notifId", nil)
327- c.Check(err, IsNil)
328- c.Check(nid, Equals, uint32(0))
329+ // note that the rest are ignored.
330+ hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant)
331+ c.Assert(ok, Equals, true)
332+ c.Assert(hints, HasLen, 2)
333+ c.Check(hints["x-canonical-switch-to-application"], NotNil)
334+ c.Check(hints["x-canonical-secondary-icon"], NotNil)
335+}
336+
337+func (s *RawSuite) TestPresentNoNotificationPanics(c *C) {
338+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
339+ raw := Raw(endp, s.log)
340+ c.Check(func() { raw.Present(s.app, "notifId", nil) }, Panics, `please check notification is not nil before calling present`)
341 }
342
343 func (s *RawSuite) TestPresentNoCardDoesNotNotify(c *C) {
344 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
345 raw := Raw(endp, s.log)
346- nid, err := raw.Present(s.app, "notifId", &launch_helper.Notification{})
347- c.Check(err, IsNil)
348- c.Check(nid, Equals, uint32(0))
349+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{})
350+ c.Check(worked, Equals, false)
351 }
352
353 func (s *RawSuite) TestPresentNoSummaryDoesNotNotify(c *C) {
354 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
355 raw := Raw(endp, s.log)
356- nid, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{}})
357- c.Check(err, IsNil)
358- c.Check(nid, Equals, uint32(0))
359+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{}})
360+ c.Check(worked, Equals, false)
361 }
362
363 func (s *RawSuite) TestPresentNoPopupNoNotify(c *C) {
364 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
365 raw := Raw(endp, s.log)
366- nid, err := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary"}})
367- c.Check(err, IsNil)
368- c.Check(nid, Equals, uint32(0))
369+ worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary"}})
370+ c.Check(worked, Equals, false)
371 }
372
373=== modified file 'bus/urldispatcher/urldispatcher.go'
374--- bus/urldispatcher/urldispatcher.go 2014-04-02 08:23:15 +0000
375+++ bus/urldispatcher/urldispatcher.go 2014-07-29 16:56:22 +0000
376@@ -19,6 +19,7 @@
377
378 import (
379 "launchpad.net/ubuntu-push/bus"
380+ "launchpad.net/ubuntu-push/click"
381 "launchpad.net/ubuntu-push/logger"
382 )
383
384@@ -32,7 +33,8 @@
385 // A URLDispatcher is a simple beast, with a single method that does what it
386 // says on the box.
387 type URLDispatcher interface {
388- DispatchURL(string) error
389+ DispatchURL(string, *click.AppId) error
390+ TestURL(*click.AppId, []string) bool
391 }
392
393 type urlDispatcher struct {
394@@ -47,11 +49,28 @@
395
396 var _ URLDispatcher = &urlDispatcher{} // ensures it conforms
397
398-func (ud *urlDispatcher) DispatchURL(url string) error {
399+func (ud *urlDispatcher) DispatchURL(url string, app *click.AppId) error {
400 ud.log.Debugf("Dispatching %s", url)
401- err := ud.endp.Call("DispatchURL", bus.Args(url))
402+ err := ud.endp.Call("DispatchURL", bus.Args(url, app.DispatchPackage()))
403 if err != nil {
404 ud.log.Errorf("Dispatch to %s failed with %s", url, err)
405 }
406 return err
407 }
408+
409+func (ud *urlDispatcher) TestURL(app *click.AppId, urls []string) bool {
410+ ud.log.Debugf("TestURL: %s", urls)
411+ var appIds []string
412+ err := ud.endp.Call("TestURL", bus.Args(urls), &appIds)
413+ if err != nil {
414+ ud.log.Errorf("TestURL for %s failed with %s", urls, err)
415+ return false
416+ }
417+ for _, appId := range appIds {
418+ if appId != app.Versioned() {
419+ ud.log.Debugf("Notification skipped because of different appid for actions: %v - %s != %s", urls, appId, app.Versioned())
420+ return false
421+ }
422+ }
423+ return true
424+}
425
426=== modified file 'bus/urldispatcher/urldispatcher_test.go'
427--- bus/urldispatcher/urldispatcher_test.go 2014-02-05 18:17:26 +0000
428+++ bus/urldispatcher/urldispatcher_test.go 2014-07-29 16:56:22 +0000
429@@ -19,7 +19,7 @@
430 import (
431 . "launchpad.net/gocheck"
432 testibus "launchpad.net/ubuntu-push/bus/testing"
433- "launchpad.net/ubuntu-push/logger"
434+ clickhelp "launchpad.net/ubuntu-push/click/testing"
435 helpers "launchpad.net/ubuntu-push/testing"
436 "launchpad.net/ubuntu-push/testing/condition"
437 "testing"
438@@ -29,7 +29,7 @@
439 func TestUrldispatcher(t *testing.T) { TestingT(t) }
440
441 type UDSuite struct {
442- log logger.Logger
443+ log *helpers.TestLogger
444 }
445
446 var _ = Suite(&UDSuite{})
447@@ -38,16 +38,78 @@
448 s.log = helpers.NewTestLogger(c, "debug")
449 }
450
451-func (s *UDSuite) TestWorks(c *C) {
452+func (s *UDSuite) TestDispatchURLWorks(c *C) {
453 endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{})
454 ud := New(endp, s.log)
455- err := ud.DispatchURL("this")
456+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
457+ err := ud.DispatchURL("this", appId)
458 c.Check(err, IsNil)
459 }
460
461-func (s *UDSuite) TestFailsIfCallFails(c *C) {
462+func (s *UDSuite) TestDispatchURLFailsIfCallFails(c *C) {
463 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
464 ud := New(endp, s.log)
465- err := ud.DispatchURL("this")
466+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
467+ err := ud.DispatchURL("this", appId)
468 c.Check(err, NotNil)
469 }
470+
471+func (s *UDSuite) TestTestURLWorks(c *C) {
472+ endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{[]string{"com.example.test_app_0.99"}})
473+ ud := New(endp, s.log)
474+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
475+ c.Check(ud.TestURL(appId, []string{"this"}), Equals, true)
476+ c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[this\].*`)
477+}
478+
479+func (s *UDSuite) TestTestURLFailsIfCallFails(c *C) {
480+ endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
481+ ud := New(endp, s.log)
482+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
483+ c.Check(ud.TestURL(appId, []string{"this"}), Equals, false)
484+}
485+
486+func (s *UDSuite) TestTestURLMultipleURLs(c *C) {
487+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []string{"com.example.test_app_0.99", "com.example.test_app_0.99"})
488+ ud := New(endp, s.log)
489+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
490+ urls := []string{"potato://test-app", "potato_a://foo"}
491+ c.Check(ud.TestURL(appId, urls), Equals, true)
492+ c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[potato://test-app potato_a://foo\].*`)
493+}
494+
495+func (s *UDSuite) TestTestURLWrongApp(c *C) {
496+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []string{"com.example.test_test-app_0.1"})
497+ ud := New(endp, s.log)
498+ appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
499+ urls := []string{"potato://test-app"}
500+ c.Check(ud.TestURL(appId, urls), Equals, false)
501+ c.Check(s.log.Captured(), Matches, `(?sm).*Notification skipped because of different appid for actions: \[potato://test-app\] - com.example.test_test-app_0.1 != com.example.test_app_0.99`)
502+}
503+
504+func (s *UDSuite) TestTestURLOneWrongApp(c *C) {
505+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []string{"com.example.test_test-app_0", "com.example.test_test-app1"})
506+ ud := New(endp, s.log)
507+ appId := clickhelp.MustParseAppId("com.example.test_test-app_0")
508+ urls := []string{"potato://test-app", "potato_a://foo"}
509+ c.Check(ud.TestURL(appId, urls), Equals, false)
510+ c.Check(s.log.Captured(), Matches, `(?sm).*Notification skipped because of different appid for actions: \[potato://test-app potato_a://foo\] - com.example.test_test-app1 != com.example.test_test-app.*`)
511+}
512+
513+func (s *UDSuite) TestTestURLInvalidURL(c *C) {
514+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false), []string{"com.example.test_test-app_0.1"})
515+ ud := New(endp, s.log)
516+ appId := clickhelp.MustParseAppId("com.example.test_app_0.2")
517+ urls := []string{"notsupported://test-app"}
518+ c.Check(ud.TestURL(appId, urls), Equals, false)
519+ c.Check(s.log.Captured(), Matches, `(?sm).*TestURL for \[notsupported://test-app\] failed with no way.*`)
520+}
521+
522+func (s *UDSuite) TestTestURLLegacyApp(c *C) {
523+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []string{"ubuntu-system-settings"})
524+ ud := New(endp, s.log)
525+ appId := clickhelp.MustParseAppId("_ubuntu-system-settings")
526+ urls := []string{"settings://test-app"}
527+ c.Check(ud.TestURL(appId, urls), Equals, true)
528+ c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[settings://test-app\].*`)
529+}
530
531=== modified file 'click/click.go'
532--- click/click.go 2014-07-18 20:45:21 +0000
533+++ click/click.go 2014-07-29 16:56:22 +0000
534@@ -98,6 +98,13 @@
535 return app.Package == pkgname
536 }
537
538+func (app *AppId) DispatchPackage() string {
539+ if app.Click {
540+ return app.Package
541+ }
542+ return app.Application
543+}
544+
545 func (app *AppId) Original() string {
546 return app.original
547 }
548
549=== modified file 'click/click_test.go'
550--- click/click_test.go 2014-07-18 20:45:21 +0000
551+++ click/click_test.go 2014-07-29 16:56:22 +0000
552@@ -42,6 +42,7 @@
553 c.Check(app.Click, Equals, true)
554 c.Check(app.Original(), Equals, "com.ubuntu.clock_clock")
555 c.Check(fmt.Sprintf("%s", app), Equals, "com.ubuntu.clock_clock")
556+ c.Check(app.DispatchPackage(), Equals, "com.ubuntu.clock")
557
558 app, err = ParseAppId("com.ubuntu.clock_clock_10")
559 c.Assert(err, IsNil)
560@@ -55,6 +56,7 @@
561 c.Check(app.Versioned(), Equals, "com.ubuntu.clock_clock_10")
562 c.Check(app.Base(), Equals, "com.ubuntu.clock_clock")
563 c.Check(app.DesktopId(), Equals, "com.ubuntu.clock_clock_10.desktop")
564+ c.Check(app.DispatchPackage(), Equals, "com.ubuntu.clock")
565
566 for _, s := range []string{"com.ubuntu.clock_clock_10_4", "com.ubuntu.clock", ""} {
567 app, err = ParseAppId(s)
568@@ -81,6 +83,7 @@
569 c.Check(app.Versioned(), Equals, "python3.4")
570 c.Check(app.Base(), Equals, "python3.4")
571 c.Check(app.DesktopId(), Equals, "python3.4.desktop")
572+ c.Check(app.DispatchPackage(), Equals, "python3.4")
573
574 for _, s := range []string{"_.foo", "_foo/", "_/foo"} {
575 app, err = ParseAppId(s)
576
577=== modified file 'client/client.go'
578--- client/client.go 2014-07-21 01:18:16 +0000
579+++ client/client.go 2014-07-29 16:56:22 +0000
580@@ -32,8 +32,6 @@
581 "os/exec"
582 "strings"
583
584- "code.google.com/p/go-uuid/uuid"
585-
586 "launchpad.net/ubuntu-push/bus"
587 "launchpad.net/ubuntu-push/bus/connectivity"
588 "launchpad.net/ubuntu-push/bus/networkmanager"
589@@ -85,7 +83,7 @@
590 // Post converts a push message into a presentable notification
591 // and a postal message, presents the former and stores the
592 // latter in the application's mailbox.
593- Post(app *click.AppId, nid string, payload json.RawMessage) error
594+ Post(app *click.AppId, nid string, payload json.RawMessage)
595 // IsRunning() returns whether the service is running
596 IsRunning() bool
597 // Stop() stops the service
598@@ -398,29 +396,23 @@
599 }
600 // marshal the last decoded msg to json
601 payload, err := json.Marshal(msg.Decoded[len(msg.Decoded)-1])
602- if err == nil {
603- appId, _ := click.ParseAppId("_ubuntu-system-settings")
604- err = client.postalService.Post(appId, uuid.New(), payload)
605- }
606 if err != nil {
607 client.log.Errorf("while posting broadcast notification %d: %v", msg.TopLevel, err)
608- } else {
609- client.log.Debugf("posted broadcast notification %d.", msg.TopLevel)
610+ return err
611 }
612- return err
613+ appId, _ := click.ParseAppId("_ubuntu-system-settings")
614+ client.postalService.Post(appId, "", payload)
615+ client.log.Debugf("posted broadcast notification %d.", msg.TopLevel)
616+ return nil
617 }
618
619 // handleUnicastNotification deals with receiving a unicast notification
620 func (client *PushClient) handleUnicastNotification(anotif session.AddressedNotification) error {
621 app := anotif.To
622 msg := anotif.Notification
623- err := client.postalService.Post(app, msg.MsgId, msg.Payload)
624- if err != nil {
625- client.log.Errorf("while posting unicast notification %s for %s: %v", msg.MsgId, msg.AppId, err)
626- } else {
627- client.log.Debugf("posted unicast notification %s for %s.", msg.MsgId, msg.AppId)
628- }
629- return err
630+ client.postalService.Post(app, msg.MsgId, msg.Payload)
631+ client.log.Debugf("posted unicast notification %s for %s.", msg.MsgId, msg.AppId)
632+ return nil
633 }
634
635 // doLoop connects events with their handlers
636
637=== modified file 'client/client_test.go'
638--- client/client_test.go 2014-07-21 01:18:16 +0000
639+++ client/client_test.go 2014-07-29 16:56:22 +0000
640@@ -107,13 +107,12 @@
641 postArgs []postArgs
642 }
643
644-func (d *dumbPostal) Post(app *click.AppId, nid string, payload json.RawMessage) error {
645+func (d *dumbPostal) Post(app *click.AppId, nid string, payload json.RawMessage) {
646 d.postCount++
647 if app.Application == "ubuntu-system-settings" {
648 d.bcastCount++
649 }
650 d.postArgs = append(d.postArgs, postArgs{app, nid, payload})
651- return d.err
652 }
653
654 var _ PostalService = (*dumbPostal)(nil)
655@@ -786,6 +785,9 @@
656 positiveBroadcastNotification = &session.BroadcastNotification{
657 Decoded: []map[string]interface{}{
658 map[string]interface{}{
659+ "daily/mako": []interface{}{float64(102), "tubular"},
660+ },
661+ map[string]interface{}{
662 "daily/mako": []interface{}{float64(103), "tubular"},
663 },
664 },
665@@ -811,7 +813,8 @@
666 c.Assert(d.postArgs, HasLen, 1)
667 expectedApp, _ := click.ParseAppId("_ubuntu-system-settings")
668 c.Check(d.postArgs[0].app, DeepEquals, expectedApp)
669- expectedData, _ := json.Marshal(positiveBroadcastNotification.Decoded[0])
670+ c.Check(d.postArgs[0].nid, Equals, "")
671+ expectedData, _ := json.Marshal(positiveBroadcastNotification.Decoded[1])
672 c.Check([]byte(d.postArgs[0].payload), DeepEquals, expectedData)
673 }
674
675@@ -826,18 +829,6 @@
676 c.Check(d.bcastCount, Equals, 0)
677 }
678
679-func (cs *clientSuite) TestHandleBroadcastNotificationFail(c *C) {
680- cli := NewPushClient(cs.configPath, cs.leveldbPath)
681- cli.systemImageInfo = siInfoRes
682- cli.log = cs.log
683- d := new(dumbPostal)
684- err := errors.New("potato")
685- d.err = err
686- cli.postalService = d
687- c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), Equals, err)
688- c.Check(d.bcastCount, Equals, 1)
689-}
690-
691 /*****************************************************************
692 handleUnicastNotification tests
693 ******************************************************************/
694@@ -860,19 +851,6 @@
695 c.Check(d.postArgs[0].payload, DeepEquals, notif.Payload)
696 }
697
698-func (cs *clientSuite) TestHandleUcastNotificationError(c *C) {
699- cli := NewPushClient(cs.configPath, cs.leveldbPath)
700- cli.log = cs.log
701- d := new(dumbPostal)
702- cli.postalService = d
703- fail := errors.New("fail")
704- d.err = fail
705-
706- c.Check(cli.handleUnicastNotification(session.AddressedNotification{appHello, notif}), Equals, fail)
707- // check we sent the notification
708- c.Check(d.postCount, Equals, 1)
709-}
710-
711 /*****************************************************************
712 handleUnregister tests
713 ******************************************************************/
714
715=== modified file 'client/service/postal.go'
716--- client/service/postal.go 2014-07-21 01:18:16 +0000
717+++ client/service/postal.go 2014-07-29 16:56:22 +0000
718@@ -39,7 +39,21 @@
719 "launchpad.net/ubuntu-push/util"
720 )
721
722-type messageHandler func(*click.AppId, string, *launch_helper.HelperOutput) error
723+type messageHandler func(*click.AppId, string, *launch_helper.HelperOutput) bool
724+
725+// a Presenter is something that knows how to present a Notification
726+type Presenter interface {
727+ Present(*click.AppId, string, *launch_helper.Notification) bool
728+}
729+
730+type notificationCentre interface {
731+ Presenter
732+ GetCh() chan *reply.MMActionReply
733+ RemoveNotification(string, bool)
734+ StartCleanupLoop()
735+ Tags(*click.AppId) []string
736+ Clear(*click.AppId, ...string) int
737+}
738
739 // PostalService is the dbus api
740 type PostalService struct {
741@@ -48,7 +62,7 @@
742 msgHandler messageHandler
743 launchers map[string]launch_helper.HelperLauncher
744 HelperPool launch_helper.HelperPool
745- messagingMenu *messaging.MessagingMenu
746+ messagingMenu notificationCentre
747 // the endpoints are only exposed for testing from client
748 // XXX: uncouple some more so this isn't necessary
749 EmblemCounterEndp bus.Endpoint
750@@ -57,6 +71,7 @@
751 URLDispatcherEndp bus.Endpoint
752 WindowStackEndp bus.Endpoint
753 // presenters:
754+ Presenters []Presenter
755 emblemCounter *emblemcounter.EmblemCounter
756 haptic *haptic.Haptic
757 notifications *notifications.RawNotifications
758@@ -112,8 +127,11 @@
759 // Start() dials the bus, grab the name, and listens for method calls.
760 func (svc *PostalService) Start() error {
761 err := svc.DBusService.Start(bus.DispatchMap{
762- "PopAll": svc.popAll,
763- "Post": svc.post,
764+ "PopAll": svc.popAll,
765+ "Post": svc.post,
766+ "ListPersistent": svc.listPersistent,
767+ "ClearPersistent": svc.clearPersistent,
768+ "SetCounter": svc.setCounter,
769 }, PostalServiceBusAddress)
770 if err != nil {
771 return err
772@@ -128,6 +146,13 @@
773 svc.haptic = haptic.New(svc.HapticEndp, svc.Log)
774 svc.sound = sounds.New(svc.Log)
775 svc.messagingMenu = messaging.New(svc.Log)
776+ svc.Presenters = []Presenter{
777+ svc.notifications,
778+ svc.emblemCounter,
779+ svc.haptic,
780+ svc.sound,
781+ svc.messagingMenu,
782+ }
783 if useTrivialHelper {
784 svc.HelperPool = launch_helper.NewTrivialHelperPool(svc.Log)
785 } else {
786@@ -136,7 +161,8 @@
787 svc.windowStack = windowstack.New(svc.WindowStackEndp, svc.Log)
788
789 go svc.consumeHelperResults(svc.HelperPool.Start())
790- go svc.handleActions(actionsCh, svc.messagingMenu.Ch)
791+ go svc.handleActions(actionsCh, svc.messagingMenu.GetCh())
792+ svc.messagingMenu.StartCleanupLoop()
793 return nil
794 }
795
796@@ -155,8 +181,10 @@
797 svc.Log.Debugf("handleActions got nil action; ignoring")
798 } else {
799 url := action.Action
800+ // remove the notification from the messaging menu
801+ svc.messagingMenu.RemoveNotification(action.Nid, true)
802 // this ignores the error (it's been logged already)
803- svc.urlDispatcher.DispatchURL(url)
804+ svc.urlDispatcher.DispatchURL(url, action.App)
805 }
806 case mmuAction, ok := <-mmuActionsCh:
807 if !ok {
808@@ -168,9 +196,9 @@
809 svc.Log.Debugf("handleActions (MMU) got: %v", mmuAction)
810 url := mmuAction.Action
811 // remove the notification from the messagingmenu map
812- svc.messagingMenu.RemoveNotification(mmuAction.Notification)
813+ svc.messagingMenu.RemoveNotification(mmuAction.Notification, false)
814 // this ignores the error (it's been logged already)
815- svc.urlDispatcher.DispatchURL(url)
816+ svc.urlDispatcher.DispatchURL(url, mmuAction.App)
817 }
818
819 }
820@@ -209,6 +237,54 @@
821 return notifications.Raw(svc.NotificationsEndp, svc.Log).WatchActions()
822 }
823
824+func (svc *PostalService) listPersistent(path string, args, _ []interface{}) ([]interface{}, error) {
825+ app, err := svc.grabDBusPackageAndAppId(path, args, 0)
826+ if err != nil {
827+ return nil, err
828+ }
829+
830+ tagmap := svc.messagingMenu.Tags(app)
831+ return []interface{}{tagmap}, nil
832+}
833+
834+func (svc *PostalService) clearPersistent(path string, args, _ []interface{}) ([]interface{}, error) {
835+ if len(args) == 0 {
836+ return nil, ErrBadArgCount
837+ }
838+ app, err := svc.grabDBusPackageAndAppId(path, args[:1], 0)
839+ if err != nil {
840+ return nil, err
841+ }
842+ tags := make([]string, len(args)-1)
843+ for i, itag := range args[1:] {
844+ tag, ok := itag.(string)
845+ if !ok {
846+ return nil, ErrBadArgType
847+ }
848+ tags[i] = tag
849+ }
850+ return []interface{}{uint32(svc.messagingMenu.Clear(app, tags...))}, nil
851+}
852+
853+func (svc *PostalService) setCounter(path string, args, _ []interface{}) ([]interface{}, error) {
854+ app, err := svc.grabDBusPackageAndAppId(path, args, 2)
855+ if err != nil {
856+ return nil, err
857+ }
858+
859+ count, ok := args[1].(int32)
860+ if !ok {
861+ return nil, ErrBadArgType
862+ }
863+ visible, ok := args[2].(bool)
864+ if !ok {
865+ return nil, ErrBadArgType
866+ }
867+
868+ svc.emblemCounter.SetCounter(app, count, visible)
869+ return nil, nil
870+}
871+
872 func (svc *PostalService) popAll(path string, args, _ []interface{}) ([]interface{}, error) {
873 app, err := svc.grabDBusPackageAndAppId(path, args, 0)
874 if err != nil {
875@@ -250,14 +326,16 @@
876 return nil, ErrBadJSON
877 }
878
879- nid := newNid()
880-
881- return nil, svc.Post(app, nid, rawJSON)
882+ svc.Post(app, "", rawJSON)
883+ return nil, nil
884 }
885
886 // Post() signals to an application over dbus that a notification
887-// has arrived.
888-func (svc *PostalService) Post(app *click.AppId, nid string, payload json.RawMessage) error {
889+// has arrived. If nid is "" generate one.
890+func (svc *PostalService) Post(app *click.AppId, nid string, payload json.RawMessage) {
891+ if nid == "" {
892+ nid = newNid()
893+ }
894 arg := launch_helper.HelperInput{
895 App: app,
896 NotificationId: nid,
897@@ -270,7 +348,6 @@
898 kind = "legacy"
899 }
900 svc.HelperPool.Run(kind, &arg)
901- return nil
902 }
903
904 func (svc *PostalService) consumeHelperResults(ch chan *launch_helper.HelperResult) {
905@@ -299,27 +376,41 @@
906 box.Append(output.Message, nid)
907
908 if svc.msgHandler != nil {
909- err := svc.msgHandler(app, nid, &output)
910- if err != nil {
911- svc.DBusService.Log.Errorf("msgHandler returned %v", err)
912- return
913+ b := svc.msgHandler(app, nid, &output)
914+ if !b {
915+ svc.Log.Debugf("msgHandler did not present the notification")
916 }
917- svc.DBusService.Log.Debugf("call to msgHandler successful")
918 }
919
920 svc.Bus.Signal("Post", "/"+string(nih.Quote([]byte(app.Package))), []interface{}{appId})
921 }
922
923-func (svc *PostalService) messageHandler(app *click.AppId, nid string, output *launch_helper.HelperOutput) error {
924+func (svc *PostalService) validateActions(app *click.AppId, notif *launch_helper.Notification) bool {
925+ if notif.Card == nil || len(notif.Card.Actions) == 0 {
926+ return true
927+ }
928+ return svc.urlDispatcher.TestURL(app, notif.Card.Actions)
929+}
930+
931+func (svc *PostalService) messageHandler(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool {
932+ if output == nil || output.Notification == nil {
933+ svc.Log.Debugf("skipping notification: nil.")
934+ return false
935+ }
936+ // validate actions
937+ if !svc.validateActions(app, output.Notification) {
938+ // no need to log, (it's been logged already)
939+ return false
940+ }
941 if !svc.windowStack.IsAppFocused(app) {
942- svc.messagingMenu.Present(app, nid, output.Notification)
943- _, err := svc.notifications.Present(app, nid, output.Notification)
944- svc.emblemCounter.Present(app, nid, output.Notification)
945- svc.haptic.Present(app, nid, output.Notification)
946- svc.sound.Present(app, nid, output.Notification)
947- return err
948+ b := false
949+ for _, p := range svc.Presenters {
950+ // we don't want this to shortcut :)
951+ b = p.Present(app, nid, output.Notification) || b
952+ }
953+ return b
954 } else {
955- svc.Log.Debugf("Notification skipped because app is focused.")
956- return nil
957+ svc.Log.Debugf("notification skipped because app is focused.")
958+ return false
959 }
960 }
961
962=== modified file 'client/service/postal_test.go'
963--- client/service/postal_test.go 2014-07-21 10:38:55 +0000
964+++ client/service/postal_test.go 2014-07-29 16:56:22 +0000
965@@ -18,15 +18,14 @@
966
967 import (
968 "encoding/json"
969- "errors"
970+ "fmt"
971 "io/ioutil"
972 "os"
973 "path/filepath"
974 "sort"
975 "time"
976
977- "code.google.com/p/go-uuid/uuid"
978-
979+ "launchpad.net/go-dbus/v1"
980 . "launchpad.net/gocheck"
981
982 "launchpad.net/ubuntu-push/bus"
983@@ -37,6 +36,7 @@
984 clickhelp "launchpad.net/ubuntu-push/click/testing"
985 "launchpad.net/ubuntu-push/launch_helper"
986 "launchpad.net/ubuntu-push/messaging/reply"
987+ "launchpad.net/ubuntu-push/nih"
988 helpers "launchpad.net/ubuntu-push/testing"
989 "launchpad.net/ubuntu-push/testing/condition"
990 )
991@@ -80,16 +80,16 @@
992 }
993 }
994
995-func installTickMessageHandler(svc *PostalService) chan error {
996- ch := make(chan error)
997+func installTickMessageHandler(svc *PostalService) chan bool {
998+ ch := make(chan bool)
999 msgHandler := svc.GetMessageHandler()
1000- svc.SetMessageHandler(func(app *click.AppId, nid string, output *launch_helper.HelperOutput) error {
1001- var err error
1002+ svc.SetMessageHandler(func(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool {
1003+ var b bool
1004 if msgHandler != nil {
1005- err = msgHandler(app, nid, output)
1006+ b = msgHandler(app, nid, output)
1007 }
1008- ch <- err
1009- return err
1010+ ch <- b
1011+ return b
1012 })
1013 return ch
1014 }
1015@@ -270,6 +270,60 @@
1016 }
1017
1018 //
1019+// post() tests
1020+
1021+func (ps *postalSuite) TestPostHappyPath(c *C) {
1022+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1023+ svc.msgHandler = nil
1024+ ch := installTickMessageHandler(svc)
1025+ svc.launchers = map[string]launch_helper.HelperLauncher{
1026+ "click": ps.fakeLauncher,
1027+ }
1028+ c.Assert(svc.Start(), IsNil)
1029+ payload := `{"message": {"world":1}}`
1030+ rvs, err := svc.post(aPackageOnBus, []interface{}{anAppId, payload}, nil)
1031+ c.Assert(err, IsNil)
1032+ c.Check(rvs, IsNil)
1033+
1034+ if ps.fakeLauncher.done != nil {
1035+ // wait for the two posts to "launch"
1036+ inputData := takeNextBytes(ps.fakeLauncher.ch)
1037+ c.Check(string(inputData), Equals, payload)
1038+
1039+ go ps.fakeLauncher.done("0") // OneDone
1040+ }
1041+
1042+ c.Check(takeNextBool(ch), Equals, false) // one,
1043+ // xxx here?
1044+ c.Assert(svc.mbox, HasLen, 1)
1045+ box, ok := svc.mbox[anAppId]
1046+ c.Check(ok, Equals, true)
1047+ msgs := box.AllMessages()
1048+ c.Assert(msgs, HasLen, 1)
1049+ c.Check(msgs[0], Equals, `{"world":1}`)
1050+ c.Check(box.nids[0], Not(Equals), "")
1051+}
1052+
1053+func (ps *postalSuite) TestPostFailsIfBadArgs(c *C) {
1054+ for i, s := range []struct {
1055+ args []interface{}
1056+ errt error
1057+ }{
1058+ {nil, ErrBadArgCount},
1059+ {[]interface{}{}, ErrBadArgCount},
1060+ {[]interface{}{1}, ErrBadArgCount},
1061+ {[]interface{}{anAppId, 1}, ErrBadArgType},
1062+ {[]interface{}{anAppId, "zoom"}, ErrBadJSON},
1063+ {[]interface{}{1, "hello"}, ErrBadArgType},
1064+ {[]interface{}{1, 2, 3}, ErrBadArgCount},
1065+ {[]interface{}{"bar", "hello"}, ErrBadAppId},
1066+ } {
1067+ reg, err := new(PostalService).post(aPackageOnBus, s.args, nil)
1068+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
1069+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
1070+ }
1071+}
1072+
1073 // Post() tests
1074
1075 func (ps *postalSuite) TestPostWorks(c *C) {
1076@@ -282,30 +336,31 @@
1077 "legacy": fakeLauncher2,
1078 }
1079 c.Assert(svc.Start(), IsNil)
1080- rvs, err := svc.post(aPackageOnBus, []interface{}{anAppId, `{"message":{"world":1}}`}, nil)
1081- c.Assert(err, IsNil)
1082- c.Check(rvs, IsNil)
1083- rvs, err = svc.post(aPackageOnBus, []interface{}{anAppId, `{"message":{"moon":1}}`}, nil)
1084- c.Assert(err, IsNil)
1085- c.Check(rvs, IsNil)
1086- rvs, err = svc.post("_", []interface{}{"_classic-app", `{"message":{"mars":42}}`}, nil)
1087- c.Assert(err, IsNil)
1088- c.Check(rvs, IsNil)
1089+
1090+ app := clickhelp.MustParseAppId(anAppId)
1091+ // these two, being for the same app, will be done sequentially.
1092+ svc.Post(app, "m1", json.RawMessage(`{"message":{"world":1}}`))
1093+ svc.Post(app, "m2", json.RawMessage(`{"message":{"moon":1}}`))
1094+ classicApp := clickhelp.MustParseAppId("_classic-app")
1095+ svc.Post(classicApp, "m3", json.RawMessage(`{"message":{"mars":42}}`))
1096
1097 if ps.fakeLauncher.done != nil {
1098 // wait for the two posts to "launch"
1099 takeNextBytes(ps.fakeLauncher.ch)
1100- takeNextBytes(ps.fakeLauncher.ch)
1101 takeNextBytes(fakeLauncher2.ch)
1102-
1103 go ps.fakeLauncher.done("0") // OneDone
1104+ go fakeLauncher2.done("0")
1105+
1106+ inputData := takeNextBytes(ps.fakeLauncher.ch)
1107+
1108+ c.Check(string(inputData), Equals, `{"message":{"moon":1}}`)
1109+
1110 go ps.fakeLauncher.done("1") // OneDone
1111- go fakeLauncher2.done("0")
1112 }
1113
1114- c.Check(takeNextError(ch), IsNil) // one,
1115- c.Check(takeNextError(ch), IsNil) // two,
1116- c.Check(takeNextError(ch), IsNil) // three posts
1117+ c.Check(takeNextBool(ch), Equals, false) // one,
1118+ c.Check(takeNextBool(ch), Equals, false) // two,
1119+ c.Check(takeNextBool(ch), Equals, false) // three posts
1120 c.Assert(svc.mbox, HasLen, 2)
1121 box, ok := svc.mbox[anAppId]
1122 c.Check(ok, Equals, true)
1123@@ -313,6 +368,7 @@
1124 c.Assert(msgs, HasLen, 2)
1125 c.Check(msgs[0], Equals, `{"world":1}`)
1126 c.Check(msgs[1], Equals, `{"moon":1}`)
1127+ c.Check(box.nids, DeepEquals, []string{"m1", "m2"})
1128 box, ok = svc.mbox["_classic-app"]
1129 c.Assert(ok, Equals, true)
1130 msgs = box.AllMessages()
1131@@ -320,7 +376,34 @@
1132 c.Check(msgs[0], Equals, `{"mars":42}`)
1133 }
1134
1135-func (ps *postalSuite) TestPostSignal(c *C) {
1136+func (ps *postalSuite) TestPostCallsMessageHandlerDetails(c *C) {
1137+ ch := make(chan *launch_helper.HelperOutput)
1138+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1139+ svc.launchers = map[string]launch_helper.HelperLauncher{
1140+ "click": ps.fakeLauncher,
1141+ }
1142+ c.Assert(svc.Start(), IsNil)
1143+ // check the message handler gets called
1144+ app := clickhelp.MustParseAppId(anAppId)
1145+ f := func(app *click.AppId, nid string, s *launch_helper.HelperOutput) bool {
1146+ c.Check(app.Base(), Equals, anAppId)
1147+ c.Check(nid, Equals, "m7")
1148+ ch <- s
1149+ return true
1150+ }
1151+ svc.SetMessageHandler(f)
1152+ svc.Post(app, "m7", json.RawMessage("{}"))
1153+
1154+ if ps.fakeLauncher.done != nil {
1155+ takeNextBytes(ps.fakeLauncher.ch)
1156+
1157+ go ps.fakeLauncher.done("0") // OneDone
1158+ }
1159+
1160+ c.Check(takeNextHelperOutput(ch), DeepEquals, &launch_helper.HelperOutput{})
1161+}
1162+
1163+func (ps *postalSuite) TestAfterMessageHandlerSignal(c *C) {
1164 svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1165 svc.msgHandler = nil
1166
1167@@ -341,106 +424,24 @@
1168 c.Check(callArgs[l-1].Args, DeepEquals, []interface{}{"Post", aPackageOnBus, []interface{}{anAppId}})
1169 }
1170
1171-func (ps *postalSuite) TestPostFailsIfPostFails(c *C) {
1172- bus := testibus.NewTestingEndpoint(condition.Work(true),
1173- condition.Work(false))
1174- svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1175- svc.Bus = bus
1176- svc.SetMessageHandler(func(*click.AppId, string, *launch_helper.HelperOutput) error { return errors.New("fail") })
1177- _, err := svc.post("/hello", []interface{}{"xyzzy"}, nil)
1178- c.Check(err, NotNil)
1179-}
1180-
1181-func (ps *postalSuite) TestPostFailsIfBadArgs(c *C) {
1182- for i, s := range []struct {
1183- args []interface{}
1184- errt error
1185- }{
1186- {nil, ErrBadArgCount},
1187- {[]interface{}{}, ErrBadArgCount},
1188- {[]interface{}{1}, ErrBadArgCount},
1189- {[]interface{}{anAppId, 1}, ErrBadArgType},
1190- {[]interface{}{anAppId, "zoom"}, ErrBadJSON},
1191- {[]interface{}{1, "hello"}, ErrBadArgType},
1192- {[]interface{}{1, 2, 3}, ErrBadArgCount},
1193- {[]interface{}{"bar", "hello"}, ErrBadAppId},
1194- } {
1195- reg, err := new(PostalService).post(aPackageOnBus, s.args, nil)
1196- c.Check(reg, IsNil, Commentf("iteration #%d", i))
1197- c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
1198- }
1199-}
1200-
1201-//
1202-// Post (Broadcast) tests
1203-
1204-func (ps *postalSuite) TestPostBroadcast(c *C) {
1205-
1206- bus := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
1207- svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1208-
1209- svc.NotificationsEndp = bus
1210- svc.launchers = map[string]launch_helper.HelperLauncher{
1211- "legacy": ps.fakeLauncher,
1212- }
1213- c.Assert(svc.Start(), IsNil)
1214-
1215- msgId := uuid.New()
1216- svc.SetMessageHandler(func(app *click.AppId, nid string, output *launch_helper.HelperOutput) error {
1217- expectedAppId, _ := click.ParseAppId("_ubuntu-system-settings")
1218- c.Check(app, DeepEquals, expectedAppId)
1219- c.Check(nid, Equals, msgId)
1220- return nil
1221- })
1222- decoded := map[string]interface{}{
1223- "daily/mako": []interface{}{float64(102), "tubular"},
1224- }
1225- // marshal decoded to json
1226- payload, _ := json.Marshal(decoded)
1227- appId, _ := click.ParseAppId("_ubuntu-system-settings")
1228- err := svc.Post(appId, msgId, payload)
1229- c.Assert(err, IsNil)
1230-
1231- if ps.fakeLauncher.done != nil {
1232- inputData := takeNextBytes(ps.fakeLauncher.ch)
1233- expectedData, _ := json.Marshal(decoded)
1234- c.Check(inputData, DeepEquals, expectedData)
1235- go ps.fakeLauncher.done("0") // OneDone
1236- }
1237-}
1238-
1239-func (ps *postalSuite) TestPostBroadcastDoesNotFail(c *C) {
1240- bus := testibus.NewTestingEndpoint(condition.Work(true),
1241- condition.Work(false))
1242- svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1243- svc.launchers = map[string]launch_helper.HelperLauncher{
1244- "legacy": ps.fakeLauncher,
1245- }
1246- c.Assert(svc.Start(), IsNil)
1247- svc.NotificationsEndp = bus
1248- svc.SetMessageHandler(func(*click.AppId, string, *launch_helper.HelperOutput) error {
1249- ps.log.Debugf("about to fail")
1250- return errors.New("fail")
1251- })
1252- ch := installTickMessageHandler(svc)
1253- decoded := map[string]interface{}{
1254- "daily/mako": []interface{}{float64(102), "tubular"},
1255- }
1256- // marshal decoded to json
1257- payload, _ := json.Marshal(decoded)
1258- appId, _ := click.ParseAppId("_ubuntu-system-settings")
1259- msgId := uuid.New()
1260- err := svc.Post(appId, msgId, payload)
1261- c.Assert(err, IsNil)
1262-
1263- if ps.fakeLauncher.done != nil {
1264- takeNextBytes(ps.fakeLauncher.ch)
1265- go ps.fakeLauncher.done("0") // OneDone
1266- }
1267-
1268- c.Check(takeNextError(ch), NotNil) // the messagehandler failed
1269- c.Check(err, IsNil) // but broadcast was oblivious
1270- c.Check(ps.log.Captured(), Matches, `(?sm).*about to fail$`)
1271+func (ps *postalSuite) TestFailingMessageHandlerSurvived(c *C) {
1272+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1273+ svc.SetMessageHandler(func(*click.AppId, string, *launch_helper.HelperOutput) bool {
1274+ return false
1275+ })
1276+
1277+ hInp := &launch_helper.HelperInput{
1278+ App: clickhelp.MustParseAppId(anAppId),
1279+ }
1280+ res := &launch_helper.HelperResult{Input: hInp}
1281+
1282+ svc.handleHelperResult(res)
1283+
1284+ c.Check(ps.log.Captured(), Equals, "DEBUG msgHandler did not present the notification\n")
1285+ // we actually want to send a signal even if we didn't do anything
1286+ callArgs := testibus.GetCallArgs(ps.bus)
1287+ c.Assert(len(callArgs), Equals, 1)
1288+ c.Check(callArgs[0].Member, Equals, "::Signal")
1289 }
1290
1291 //
1292@@ -492,41 +493,15 @@
1293 svc := new(PostalService)
1294 c.Assert(svc.msgHandler, IsNil)
1295 var ext = &launch_helper.HelperOutput{}
1296- e := errors.New("Hello")
1297- f := func(_ *click.AppId, _ string, s *launch_helper.HelperOutput) error { ext = s; return e }
1298+ f := func(_ *click.AppId, _ string, s *launch_helper.HelperOutput) bool { ext = s; return false }
1299 c.Check(svc.GetMessageHandler(), IsNil)
1300 svc.SetMessageHandler(f)
1301 c.Check(svc.GetMessageHandler(), NotNil)
1302 hOutput := &launch_helper.HelperOutput{[]byte("37"), nil}
1303- c.Check(svc.msgHandler(nil, "", hOutput), Equals, e)
1304+ c.Check(svc.msgHandler(nil, "", hOutput), Equals, false)
1305 c.Check(ext, DeepEquals, hOutput)
1306 }
1307
1308-func (ps *postalSuite) TestPostCallsMessageHandler(c *C) {
1309- ch := make(chan *launch_helper.HelperOutput)
1310- svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1311- svc.launchers = map[string]launch_helper.HelperLauncher{
1312- "click": ps.fakeLauncher,
1313- }
1314- c.Assert(svc.Start(), IsNil)
1315- // check the message handler gets called
1316- f := func(_ *click.AppId, _ string, s *launch_helper.HelperOutput) error { ch <- s; return nil }
1317- svc.SetMessageHandler(f)
1318- c.Check(svc.Post(&click.AppId{Click: true}, "thing", json.RawMessage("{}")), IsNil)
1319-
1320- if ps.fakeLauncher.done != nil {
1321- takeNextBytes(ps.fakeLauncher.ch)
1322-
1323- go ps.fakeLauncher.done("0") // OneDone
1324- }
1325-
1326- c.Check(takeNextHelperOutput(ch), DeepEquals, &launch_helper.HelperOutput{})
1327- err := errors.New("ouch")
1328- svc.SetMessageHandler(func(*click.AppId, string, *launch_helper.HelperOutput) error { return err })
1329- // but the error doesn't bubble out
1330- c.Check(svc.Post(&click.AppId{}, "", json.RawMessage("{}")), IsNil)
1331-}
1332-
1333 func (ps *postalSuite) TestMessageHandlerPresents(c *C) {
1334 endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
1335 svc := NewPostalService(nil, ps.log)
1336@@ -544,12 +519,12 @@
1337 vib := &launch_helper.Vibration{Duration: 500}
1338 emb := &launch_helper.EmblemCounter{Count: 2, Visible: true}
1339 output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card, EmblemCounter: emb, Vibrate: vib}}
1340- err := svc.messageHandler(&click.AppId{}, "", output)
1341- c.Assert(err, IsNil)
1342+ b := svc.messageHandler(&click.AppId{}, "", output)
1343+ c.Assert(b, Equals, true)
1344 args := testibus.GetCallArgs(endp)
1345 l := len(args)
1346 if l < 4 {
1347- c.Fatal("not enough elements in resposne from GetCallArgs")
1348+ c.Fatal("not enough elements in response from GetCallArgs")
1349 }
1350 mm := make([]string, 4)
1351 for i, m := range args[l-4:] {
1352@@ -561,7 +536,7 @@
1353 c.Check(mm, DeepEquals, []string{"::SetProperty", "::SetProperty", "Notify", "VibratePattern"})
1354 // For the other ones, check the logs
1355 c.Check(ps.log.Captured(), Matches, `(?sm).* no persistable card:.*`)
1356- c.Check(ps.log.Captured(), Matches, `(?sm).* no Sound in the notification.*`)
1357+ c.Check(ps.log.Captured(), Matches, `(?sm).* notification has no Sound:.*`)
1358 }
1359
1360 func (ps *postalSuite) TestMessageHandlerReportsFailedNotifies(c *C) {
1361@@ -581,18 +556,18 @@
1362 svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1363 svc.WindowStackEndp = endp
1364 c.Assert(svc.Start(), IsNil)
1365- output := &launch_helper.HelperOutput{} // Doesn't matter
1366- err := svc.messageHandler(clickhelp.MustParseAppId("com.example.test_test-app_0"), "", output)
1367- c.Check(err, IsNil)
1368- c.Check(ps.log.Captured(), Matches, `(?sm).* Notification skipped because app is focused.*`)
1369+ output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{}} // Doesn't matter
1370+ b := svc.messageHandler(clickhelp.MustParseAppId("com.example.test_test-app_0"), "", output)
1371+ c.Check(b, Equals, false)
1372+ c.Check(ps.log.Captured(), Matches, `(?sm).* notification skipped because app is focused.*`)
1373 }
1374
1375 func (ps *postalSuite) TestMessageHandlerReportsButIgnoresUnmarshalErrors(c *C) {
1376 svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1377 c.Assert(svc.Start(), IsNil)
1378 output := &launch_helper.HelperOutput{[]byte(`broken`), nil}
1379- err := svc.messageHandler(nil, "", output)
1380- c.Check(err, IsNil)
1381+ b := svc.messageHandler(nil, "", output)
1382+ c.Check(b, Equals, false)
1383 c.Check(ps.log.Captured(), Matches, "(?msi).*skipping notification: nil.*")
1384 }
1385
1386@@ -602,20 +577,35 @@
1387 c.Assert(svc.Start(), IsNil)
1388 svc.NotificationsEndp = endp
1389 output := &launch_helper.HelperOutput{[]byte(`{}`), nil}
1390- err := svc.messageHandler(nil, "", output)
1391- c.Assert(err, IsNil)
1392+ b := svc.messageHandler(nil, "", output)
1393+ c.Assert(b, Equals, false)
1394 c.Check(ps.log.Captured(), Matches, "(?msi).*skipping notification: nil.*")
1395 }
1396
1397+func (ps *postalSuite) TestMessageHandlerInvalidAction(c *C) {
1398+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1399+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false), []string{"com.example.test_test-app"})
1400+ svc.URLDispatcherEndp = endp
1401+ c.Assert(svc.Start(), IsNil)
1402+ card := launch_helper.Card{Actions: []string{"notsupported://test-app"}}
1403+ output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: &card}}
1404+ b := svc.messageHandler(clickhelp.MustParseAppId("com.example.test_test-app_0"), "", output)
1405+ c.Check(b, Equals, false)
1406+ c.Check(ps.log.Captured(), Matches, `(?sm).*TestURL for \[notsupported://test-app\] failed with no way.*`)
1407+}
1408+
1409 func (ps *postalSuite) TestHandleActionsDispatches(c *C) {
1410 svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1411+ fmm := new(fakeMM)
1412+ app, _ := click.ParseAppId("com.example.test_test-app")
1413 c.Assert(svc.Start(), IsNil)
1414+ svc.messagingMenu = fmm
1415 aCh := make(chan *notifications.RawAction)
1416 rCh := make(chan *reply.MMActionReply)
1417 bCh := make(chan bool)
1418 go func() {
1419 aCh <- nil // just in case?
1420- aCh <- &notifications.RawAction{Action: "potato://"}
1421+ aCh <- &notifications.RawAction{App: app, Action: "potato://", Nid: "xyzzy"}
1422 close(aCh)
1423 bCh <- true
1424 }()
1425@@ -624,19 +614,22 @@
1426 args := testibus.GetCallArgs(ps.urlDispBus)
1427 c.Assert(args, HasLen, 1)
1428 c.Check(args[0].Member, Equals, "DispatchURL")
1429- c.Assert(args[0].Args, HasLen, 1)
1430+ c.Assert(args[0].Args, HasLen, 2)
1431 c.Assert(args[0].Args[0], Equals, "potato://")
1432+ c.Assert(args[0].Args[1], Equals, app.DispatchPackage())
1433+ c.Check(fmm.calls, DeepEquals, []string{"remove:xyzzy:true"})
1434 }
1435
1436 func (ps *postalSuite) TestHandleMMUActionsDispatches(c *C) {
1437 svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1438 c.Assert(svc.Start(), IsNil)
1439+ app, _ := click.ParseAppId("com.example.test_test-app")
1440 aCh := make(chan *notifications.RawAction)
1441 rCh := make(chan *reply.MMActionReply)
1442 bCh := make(chan bool)
1443 go func() {
1444 rCh <- nil // just in case?
1445- rCh <- &reply.MMActionReply{Action: "potato://", Notification: "foo.bar"}
1446+ rCh <- &reply.MMActionReply{App: app, Action: "potato://", Notification: "foo.bar"}
1447 close(rCh)
1448 bCh <- true
1449 }()
1450@@ -645,6 +638,144 @@
1451 args := testibus.GetCallArgs(ps.urlDispBus)
1452 c.Assert(args, HasLen, 1)
1453 c.Check(args[0].Member, Equals, "DispatchURL")
1454- c.Assert(args[0].Args, HasLen, 1)
1455+ c.Assert(args[0].Args, HasLen, 2)
1456 c.Assert(args[0].Args[0], Equals, "potato://")
1457+ c.Assert(args[0].Args[1], Equals, app.DispatchPackage())
1458+}
1459+
1460+func (ps *postalSuite) TestValidateActions(c *C) {
1461+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1462+ endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []string{"com.example.test_test-app_0"})
1463+ svc.URLDispatcherEndp = endp
1464+ c.Assert(svc.Start(), IsNil)
1465+ card := launch_helper.Card{Actions: []string{"potato://test-app"}}
1466+ notif := &launch_helper.Notification{Card: &card}
1467+ b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif)
1468+ c.Check(b, Equals, true)
1469+}
1470+
1471+func (ps *postalSuite) TestValidateActionsNoActions(c *C) {
1472+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1473+ card := launch_helper.Card{}
1474+ notif := &launch_helper.Notification{Card: &card}
1475+ b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif)
1476+ c.Check(b, Equals, true)
1477+}
1478+
1479+func (ps *postalSuite) TestValidateActionsNoCard(c *C) {
1480+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1481+ notif := &launch_helper.Notification{}
1482+ b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif)
1483+ c.Check(b, Equals, true)
1484+}
1485+
1486+type fakeMM struct {
1487+ calls []string
1488+}
1489+
1490+func (*fakeMM) Present(*click.AppId, string, *launch_helper.Notification) bool { return false }
1491+func (*fakeMM) GetCh() chan *reply.MMActionReply { return nil }
1492+func (*fakeMM) StartCleanupLoop() {}
1493+func (fmm *fakeMM) RemoveNotification(s string, b bool) {
1494+ fmm.calls = append(fmm.calls, fmt.Sprintf("remove:%s:%t", s, b))
1495+}
1496+func (fmm *fakeMM) Clear(*click.AppId, ...string) int {
1497+ fmm.calls = append(fmm.calls, "clear")
1498+ return 42
1499+}
1500+func (fmm *fakeMM) Tags(*click.AppId) []string {
1501+ fmm.calls = append(fmm.calls, "tags")
1502+ return []string{"hello"}
1503+}
1504+
1505+func (ps *postalSuite) TestListPersistent(c *C) {
1506+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1507+ fmm := new(fakeMM)
1508+ svc.messagingMenu = fmm
1509+
1510+ itags, err := svc.listPersistent(aPackageOnBus, []interface{}{anAppId}, nil)
1511+ c.Assert(err, IsNil)
1512+ c.Assert(itags, HasLen, 1)
1513+ c.Assert(itags[0], FitsTypeOf, []string(nil))
1514+ tags := itags[0].([]string)
1515+ c.Check(tags, DeepEquals, []string{"hello"})
1516+ c.Check(fmm.calls, DeepEquals, []string{"tags"})
1517+}
1518+
1519+func (ps *postalSuite) TestListPersistentErrors(c *C) {
1520+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1521+ _, err := svc.listPersistent(aPackageOnBus, nil, nil)
1522+ c.Check(err, Equals, ErrBadArgCount)
1523+ _, err = svc.listPersistent(aPackageOnBus, []interface{}{42}, nil)
1524+ c.Check(err, Equals, ErrBadArgType)
1525+ _, err = svc.listPersistent(aPackageOnBus, []interface{}{"xyzzy"}, nil)
1526+ c.Check(err, Equals, ErrBadAppId)
1527+}
1528+
1529+func (ps *postalSuite) TestClearPersistent(c *C) {
1530+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1531+ fmm := new(fakeMM)
1532+ svc.messagingMenu = fmm
1533+
1534+ icleared, err := svc.clearPersistent(aPackageOnBus, []interface{}{anAppId, "one", ""}, nil)
1535+ c.Assert(err, IsNil)
1536+ c.Assert(icleared, HasLen, 1)
1537+ c.Check(icleared[0], Equals, uint32(42))
1538+}
1539+
1540+func (ps *postalSuite) TestClearPersistentErrors(c *C) {
1541+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1542+ for i, s := range []struct {
1543+ args []interface{}
1544+ err error
1545+ }{
1546+ {[]interface{}{}, ErrBadArgCount},
1547+ {[]interface{}{42}, ErrBadArgType},
1548+ {[]interface{}{"xyzzy"}, ErrBadAppId},
1549+ {[]interface{}{anAppId, 42}, ErrBadArgType},
1550+ {[]interface{}{anAppId, "", 42}, ErrBadArgType},
1551+ } {
1552+ _, err := svc.clearPersistent(aPackageOnBus, s.args, nil)
1553+ c.Check(err, Equals, s.err, Commentf("iter %d", i))
1554+ }
1555+}
1556+
1557+func (ps *postalSuite) TestSetCounter(c *C) {
1558+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1559+ c.Check(svc.Start(), IsNil)
1560+
1561+ _, err := svc.setCounter(aPackageOnBus, []interface{}{anAppId, int32(42), true}, nil)
1562+ c.Assert(err, IsNil)
1563+
1564+ quoted := "/" + string(nih.Quote([]byte(anAppId)))
1565+
1566+ callArgs := testibus.GetCallArgs(svc.EmblemCounterEndp)
1567+ c.Assert(callArgs, HasLen, 2)
1568+ c.Check(callArgs[0].Member, Equals, "::SetProperty")
1569+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", quoted, dbus.Variant{int32(42)}})
1570+
1571+ c.Check(callArgs[1].Member, Equals, "::SetProperty")
1572+ c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", quoted, dbus.Variant{true}})
1573+}
1574+
1575+func (ps *postalSuite) TestSetCounterErrors(c *C) {
1576+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
1577+ svc.Start()
1578+ for i, s := range []struct {
1579+ args []interface{}
1580+ err error
1581+ }{
1582+ {[]interface{}{anAppId, int32(42), true}, nil}, // for reference
1583+ {[]interface{}{}, ErrBadArgCount},
1584+ {[]interface{}{anAppId}, ErrBadArgCount},
1585+ {[]interface{}{anAppId, int32(42)}, ErrBadArgCount},
1586+ {[]interface{}{anAppId, int32(42), true, "potato"}, ErrBadArgCount},
1587+ {[]interface{}{"xyzzy", int32(42), true}, ErrBadAppId},
1588+ {[]interface{}{1234567, int32(42), true}, ErrBadArgType},
1589+ {[]interface{}{anAppId, "potatoe", true}, ErrBadArgType},
1590+ {[]interface{}{anAppId, int32(42), "ru"}, ErrBadArgType},
1591+ } {
1592+ _, err := svc.setCounter(aPackageOnBus, s.args, nil)
1593+ c.Check(err, Equals, s.err, Commentf("iter %d", i))
1594+ }
1595 }
1596
1597=== modified file 'debian/changelog'
1598--- debian/changelog 2014-07-23 14:09:31 +0000
1599+++ debian/changelog 2014-07-29 16:56:22 +0000
1600@@ -1,3 +1,24 @@
1601+ubuntu-push (0.50) UNRELEASED; urgency=medium
1602+
1603+ [ Samuele Pedroni ]
1604+ * Cleanup and improve pos/Post tests
1605+
1606+ [ Guillermo Gonzalez ]
1607+ * Add a loop to cleanup MessagingMenu.notifications map when the
1608+ notifications are dismissed.
1609+ * Add TestURL to URLDispatcher and update DispatchURL signature.
1610+ * Add validateActions (check with url-dispatcher->TestURL) method to the
1611+ PostalService and wire it in messageHandler.
1612+
1613+ [ John R. Lenton ]
1614+ * Implement ListPersistent/ClearPersistent/SetCounter postal endpoints.
1615+ * Remove snap decisions support.
1616+ * Remove the notification from the messaging menu when the bubble is
1617+ acted on.
1618+ * Don't run more than 5 helpers at a time, and never more than one per app.
1619+
1620+ -- John R. Lenton <john.lenton@canonical.com> Mon, 28 Jul 2014 17:44:35 +0200
1621+
1622 ubuntu-push (0.49.1+14.10.20140723.1-0ubuntu1) utopic; urgency=medium
1623
1624 [ John R. Lenton ]
1625
1626=== modified file 'launch_helper/helper_output.go'
1627--- launch_helper/helper_output.go 2014-07-18 14:23:34 +0000
1628+++ launch_helper/helper_output.go 2014-07-29 16:56:22 +0000
1629@@ -54,6 +54,7 @@
1630 Sound string `json:"sound"` // a sound file. Users can disable this, so don't rely on it exclusively. Defaults to empty (no sound).
1631 Vibrate *Vibration `json:"vibrate"` // users can disable this, blah blah. Defaults to null (no vibration)
1632 EmblemCounter *EmblemCounter `json:"emblem-counter"` // puts a counter on an emblem in the launcher. Defaults to nil (no change to emblem counter).
1633+ Tag string `json:"tag,omitempty"` // tag used for Clear/ListPersistent.
1634 }
1635
1636 // HelperOutput is the expected output of a helper
1637
1638=== modified file 'launch_helper/helper_test.go'
1639--- launch_helper/helper_test.go 2014-07-18 14:53:47 +0000
1640+++ launch_helper/helper_test.go 2014-07-29 16:56:22 +0000
1641@@ -43,11 +43,11 @@
1642 }
1643
1644 func (s *runnerSuite) TestTrivialPoolWorks(c *C) {
1645- notif := &Notification{Sound: "42"}
1646+ notif := &Notification{Sound: "42", Tag: "foo"}
1647
1648 triv := NewTrivialHelperPool(s.testlog)
1649 ch := triv.Start()
1650- in := &HelperInput{App: s.app, Payload: []byte(`{"message": {"m":42}, "notification": {"sound": "42"}}`)}
1651+ in := &HelperInput{App: s.app, Payload: []byte(`{"message": {"m":42}, "notification": {"sound": "42", "tag": "foo"}}`)}
1652 triv.Run("klick", in)
1653 out := <-ch
1654 c.Assert(out, NotNil)
1655
1656=== modified file 'launch_helper/kindpool.go'
1657--- launch_helper/kindpool.go 2014-07-19 00:18:18 +0000
1658+++ launch_helper/kindpool.go 2014-07-29 16:56:22 +0000
1659@@ -60,10 +60,12 @@
1660 log logger.Logger
1661 chOut chan *HelperResult
1662 chIn chan *HelperInput
1663+ chDone chan *click.AppId
1664 launchers map[string]HelperLauncher
1665 lock sync.Mutex
1666 hmap map[string]*HelperArgs
1667 maxRuntime time.Duration
1668+ maxNum int
1669 }
1670
1671 // DefaultLaunchers produces the default map for kind -> HelperLauncher
1672@@ -81,12 +83,14 @@
1673 hmap: make(map[string]*HelperArgs),
1674 launchers: launchers,
1675 maxRuntime: 5 * time.Second,
1676+ maxNum: 5,
1677 }
1678 }
1679
1680 func (pool *kindHelperPool) Start() chan *HelperResult {
1681 pool.chOut = make(chan *HelperResult)
1682 pool.chIn = make(chan *HelperInput, InputBufferSize)
1683+ pool.chDone = make(chan *click.AppId)
1684
1685 for kind, launcher := range pool.launchers {
1686 kind1 := kind
1687@@ -98,18 +102,72 @@
1688 }
1689 }
1690
1691- // xxx make sure at most X helpers are running
1692- go func() {
1693- for i := range pool.chIn {
1694- if pool.handleOne(i) != nil {
1695- pool.failOne(i)
1696- }
1697- }
1698- }()
1699+ go pool.loop()
1700
1701 return pool.chOut
1702 }
1703
1704+func (pool *kindHelperPool) loop() {
1705+ running := make(map[string]bool)
1706+ var backlog []*HelperInput
1707+
1708+ for {
1709+ select {
1710+ case in, ok := <-pool.chIn:
1711+ if !ok {
1712+ return
1713+ }
1714+ if len(running) >= pool.maxNum || running[in.App.Original()] {
1715+ backlog = append(backlog, in)
1716+ pool.log.Debugf("current helper input backlog has grown to %d entries.", len(backlog))
1717+ } else {
1718+ if pool.tryOne(in) {
1719+ running[in.App.Original()] = true
1720+ }
1721+ }
1722+ case app := <-pool.chDone:
1723+ delete(running, app.Original())
1724+ if len(backlog) == 0 {
1725+ continue
1726+ }
1727+ backlogSz := 0
1728+ done := false
1729+ for i, in := range backlog {
1730+ if in != nil {
1731+ if !done && !running[in.App.Original()] {
1732+ backlog[i] = nil
1733+ if pool.tryOne(in) {
1734+ running[in.App.Original()] = true
1735+ done = true
1736+ }
1737+ } else {
1738+ backlogSz++
1739+ }
1740+ }
1741+ }
1742+ backlog = pool.shrinkBacklog(backlog, backlogSz)
1743+ pool.log.Debugf("current helper input backlog has shrunk to %d entries.", backlogSz)
1744+ }
1745+ }
1746+}
1747+
1748+func (pool *kindHelperPool) shrinkBacklog(backlog []*HelperInput, backlogSz int) []*HelperInput {
1749+ if backlogSz == 0 {
1750+ return nil
1751+ }
1752+ if cap(backlog) < 2*backlogSz {
1753+ return backlog
1754+ }
1755+ pool.log.Debugf("copying backlog to avoid wasting too much space (%d/%d used)", backlogSz, cap(backlog))
1756+ clean := make([]*HelperInput, 0, backlogSz)
1757+ for _, bentry := range backlog {
1758+ if bentry != nil {
1759+ clean = append(clean, bentry)
1760+ }
1761+ }
1762+ return clean
1763+}
1764+
1765 func (pool *kindHelperPool) Stop() {
1766 close(pool.chIn)
1767 for kind, launcher := range pool.launchers {
1768@@ -125,6 +183,14 @@
1769 pool.chIn <- input
1770 }
1771
1772+func (pool *kindHelperPool) tryOne(input *HelperInput) bool {
1773+ if pool.handleOne(input) != nil {
1774+ pool.failOne(input)
1775+ return false
1776+ }
1777+ return true
1778+}
1779+
1780 func (pool *kindHelperPool) failOne(input *HelperInput) {
1781 pool.log.Errorf("unable to get helper output; putting payload into message")
1782 pool.chOut <- &HelperResult{HelperOutput: HelperOutput{Message: input.Payload, Notification: nil}, Input: input}
1783@@ -218,6 +284,7 @@
1784 // nothing to do
1785 return
1786 }
1787+ pool.chDone <- args.Input.App
1788 defer func() {
1789 pool.cleanupTempFiles(args.FileIn, args.FileOut)
1790 }()
1791
1792=== modified file 'launch_helper/kindpool_test.go'
1793--- launch_helper/kindpool_test.go 2014-07-19 00:18:18 +0000
1794+++ launch_helper/kindpool_test.go 2014-07-29 16:56:22 +0000
1795@@ -17,6 +17,7 @@
1796 package launch_helper
1797
1798 import (
1799+ "fmt"
1800 "io/ioutil"
1801 "os"
1802 "path/filepath"
1803@@ -32,18 +33,30 @@
1804 )
1805
1806 type poolSuite struct {
1807- log *helpers.TestLogger
1808- pool HelperPool
1809+ log *helpers.TestLogger
1810+ pool HelperPool
1811+ fakeLauncher *fakeHelperLauncher
1812 }
1813
1814 var _ = Suite(&poolSuite{})
1815
1816+func takeNext(ch chan *HelperResult, c *C) *HelperResult {
1817+ select {
1818+ case res := <-ch:
1819+ return res
1820+ case <-time.After(100 * time.Millisecond):
1821+ c.Fatal("timeout waiting for result")
1822+ }
1823+ return nil
1824+}
1825+
1826 type fakeHelperLauncher struct {
1827 done func(string)
1828 obs int
1829 err error
1830 lhex string
1831 argCh chan [5]string
1832+ runid int
1833 }
1834
1835 func (fhl *fakeHelperLauncher) InstallObserver(done func(string)) error {
1836@@ -67,7 +80,9 @@
1837
1838 func (fhl *fakeHelperLauncher) Launch(appId string, exec string, f1 string, f2 string) (string, error) {
1839 fhl.argCh <- [5]string{"Launch", appId, exec, f1, f2}
1840- return "0", fhl.err
1841+ runid := fmt.Sprintf("%d", fhl.runid)
1842+ fhl.runid++
1843+ return runid, fhl.err
1844 }
1845
1846 func (fhl *fakeHelperLauncher) Stop(appId string, iid string) error {
1847@@ -75,12 +90,21 @@
1848 return nil
1849 }
1850
1851-var fakeLauncher *fakeHelperLauncher
1852+func (s *poolSuite) waitForArgs(c *C, method string) [5]string {
1853+ var args [5]string
1854+ select {
1855+ case args = <-s.fakeLauncher.argCh:
1856+ case <-time.After(2 * time.Second):
1857+ c.Fatal("didn't call " + method)
1858+ }
1859+ c.Assert(args[0], Equals, method)
1860+ return args
1861+}
1862
1863 func (s *poolSuite) SetUpTest(c *C) {
1864 s.log = helpers.NewTestLogger(c, "debug")
1865- fakeLauncher = &fakeHelperLauncher{argCh: make(chan [5]string, 10)}
1866- s.pool = NewHelperPool(map[string]HelperLauncher{"fake": fakeLauncher}, s.log)
1867+ s.fakeLauncher = &fakeHelperLauncher{argCh: make(chan [5]string, 10)}
1868+ s.pool = NewHelperPool(map[string]HelperLauncher{"fake": s.fakeLauncher}, s.log)
1869 xdgCacheHome = c.MkDir
1870 }
1871
1872@@ -99,12 +123,12 @@
1873
1874 // check that Stop (tries to) remove the observer
1875 func (s *poolSuite) TestStartStopWork(c *C) {
1876- c.Check(fakeLauncher.obs, Equals, 0)
1877+ c.Check(s.fakeLauncher.obs, Equals, 0)
1878 s.pool.Start()
1879- c.Check(fakeLauncher.done, NotNil)
1880- c.Check(fakeLauncher.obs, Equals, 1)
1881+ c.Check(s.fakeLauncher.done, NotNil)
1882+ c.Check(s.fakeLauncher.obs, Equals, 1)
1883 s.pool.Stop()
1884- c.Check(fakeLauncher.obs, Equals, 0)
1885+ c.Check(s.fakeLauncher.obs, Equals, 0)
1886 }
1887
1888 func (s *poolSuite) TestRunLaunches(c *C) {
1889@@ -119,12 +143,8 @@
1890 Payload: []byte(`"hello"`),
1891 }
1892 s.pool.Run("fake", &input)
1893- select {
1894- case arg := <-fakeLauncher.argCh:
1895- c.Check(arg[:3], DeepEquals, []string{"Launch", helpId, "bar"})
1896- case <-time.After(100 * time.Millisecond):
1897- c.Fatal("didn't call Launch")
1898- }
1899+ launchArgs := s.waitForArgs(c, "Launch")
1900+ c.Check(launchArgs[:3], DeepEquals, []string{"Launch", helpId, "bar"})
1901 args := s.pool.(*kindHelperPool).peekId("fake:0", func(*HelperArgs) {})
1902 c.Assert(args, NotNil)
1903 args.Timer.Stop()
1904@@ -135,7 +155,7 @@
1905 }
1906
1907 func (s *poolSuite) TestRunLaunchesLegacyStyle(c *C) {
1908- fakeLauncher.lhex = "lhex"
1909+ s.fakeLauncher.lhex = "lhex"
1910 s.pool.Start()
1911 defer s.pool.Stop()
1912 appId := "_legacy"
1913@@ -146,12 +166,8 @@
1914 Payload: []byte(`"hello"`),
1915 }
1916 s.pool.Run("fake", &input)
1917- select {
1918- case arg := <-fakeLauncher.argCh:
1919- c.Check(arg[:3], DeepEquals, []string{"Launch", "", "lhex"})
1920- case <-time.After(100 * time.Millisecond):
1921- c.Fatal("didn't call Launch")
1922- }
1923+ launchArgs := s.waitForArgs(c, "Launch")
1924+ c.Check(launchArgs[:3], DeepEquals, []string{"Launch", "", "lhex"})
1925 args := s.pool.(*kindHelperPool).peekId("fake:0", func(*HelperArgs) {})
1926 c.Assert(args, NotNil)
1927 args.Timer.Stop()
1928@@ -170,12 +186,7 @@
1929 Payload: []byte(`"hello"`),
1930 }
1931 s.pool.Run("not-there", &input)
1932- var res *HelperResult
1933- select {
1934- case res = <-ch:
1935- case <-time.After(100 * time.Millisecond):
1936- c.Fatal("timeout")
1937- }
1938+ res := takeNext(ch, c)
1939 c.Check(res.Message, DeepEquals, input.Payload)
1940 c.Check(res.Notification, IsNil)
1941 c.Check(*res.Input, DeepEquals, input)
1942@@ -191,19 +202,14 @@
1943 Payload: []byte(`"hello"`),
1944 }
1945 s.pool.Run("fake", &input)
1946- var res *HelperResult
1947- select {
1948- case res = <-ch:
1949- case <-time.After(100 * time.Millisecond):
1950- c.Fatal("timeout")
1951- }
1952+ res := takeNext(ch, c)
1953 c.Check(res.Message, DeepEquals, input.Payload)
1954 c.Check(res.Notification, IsNil)
1955 c.Check(*res.Input, DeepEquals, input)
1956 }
1957
1958 func (s *poolSuite) TestRunCantLaunch(c *C) {
1959- fakeLauncher.err = cual.ErrCantLaunch
1960+ s.fakeLauncher.err = cual.ErrCantLaunch
1961 ch := s.pool.Start()
1962 defer s.pool.Stop()
1963 appId := "com.example.test_test-app"
1964@@ -215,18 +221,9 @@
1965 Payload: []byte(`"hello"`),
1966 }
1967 s.pool.Run("fake", &input)
1968- select {
1969- case arg := <-fakeLauncher.argCh:
1970- c.Check(arg[:3], DeepEquals, []string{"Launch", helpId, "bar"})
1971- case <-time.After(100 * time.Millisecond):
1972- c.Fatal("didn't call Launch")
1973- }
1974- var res *HelperResult
1975- select {
1976- case res = <-ch:
1977- case <-time.After(100 * time.Millisecond):
1978- c.Fatal("timeout")
1979- }
1980+ launchArgs := s.waitForArgs(c, "Launch")
1981+ c.Check(launchArgs[:3], DeepEquals, []string{"Launch", helpId, "bar"})
1982+ res := takeNext(ch, c)
1983 c.Check(res.Message, DeepEquals, input.Payload)
1984 c.Check(s.log.Captured(), Equals, "DEBUG using helper com.example.test_test-app-helper (exec: bar) for app com.example.test_test-app\n"+"ERROR unable to launch helper com.example.test_test-app-helper: can't launch helper\n"+"ERROR unable to get helper output; putting payload into message\n")
1985 }
1986@@ -244,27 +241,14 @@
1987 Payload: []byte(`"hello"`),
1988 }
1989 s.pool.Run("fake", &input)
1990- select {
1991- case arg := <-fakeLauncher.argCh:
1992- c.Check(arg[0], Equals, "Launch")
1993- case <-time.After(100 * time.Millisecond):
1994- c.Fatal("didn't call Launch")
1995- }
1996- select {
1997- case arg := <-fakeLauncher.argCh:
1998- c.Check(arg[:3], DeepEquals, []string{"Stop", helpId, "0"})
1999- case <-time.After(2 * time.Second):
2000- c.Fatal("didn't call Stop")
2001- }
2002+ launchArgs := s.waitForArgs(c, "Launch")
2003+ c.Check(launchArgs[0], Equals, "Launch")
2004+ stopArgs := s.waitForArgs(c, "Stop")
2005+ c.Check(stopArgs[:3], DeepEquals, []string{"Stop", helpId, "0"})
2006 // this will be invoked
2007- go fakeLauncher.done("0")
2008+ go s.fakeLauncher.done("0")
2009
2010- var res *HelperResult
2011- select {
2012- case res = <-ch:
2013- case <-time.After(100 * time.Millisecond):
2014- c.Fatal("timeout")
2015- }
2016+ res := takeNext(ch, c)
2017 c.Check(res.Message, DeepEquals, input.Payload)
2018 }
2019
2020@@ -294,19 +278,14 @@
2021 f, err := os.Create(args.FileOut)
2022 c.Assert(err, IsNil)
2023 defer f.Close()
2024- _, err = f.Write([]byte(`{"notification": {"sound": "hello"}}`))
2025+ _, err = f.Write([]byte(`{"notification": {"sound": "hello", "tag": "a-tag"}}`))
2026 c.Assert(err, IsNil)
2027
2028 go pool.OneDone("l:1")
2029
2030- var res *HelperResult
2031- select {
2032- case res = <-ch:
2033- case <-time.After(100 * time.Millisecond):
2034- c.Fatal("timeout")
2035- }
2036+ res := takeNext(ch, c)
2037
2038- expected := HelperOutput{Notification: &Notification{Sound: "hello"}}
2039+ expected := HelperOutput{Notification: &Notification{Sound: "hello", Tag: "a-tag"}}
2040 c.Check(res.HelperOutput, DeepEquals, expected)
2041 c.Check(pool.hmap, HasLen, 0)
2042 }
2043@@ -330,12 +309,7 @@
2044
2045 go pool.OneDone("l:1")
2046
2047- var res *HelperResult
2048- select {
2049- case res = <-ch:
2050- case <-time.After(100 * time.Millisecond):
2051- c.Fatal("timeout")
2052- }
2053+ res := takeNext(ch, c)
2054
2055 expected := HelperOutput{Message: args.Input.Payload}
2056 c.Check(res.HelperOutput, DeepEquals, expected)
2057@@ -368,12 +342,7 @@
2058
2059 go pool.OneDone("l:1")
2060
2061- var res *HelperResult
2062- select {
2063- case res = <-ch:
2064- case <-time.After(100 * time.Millisecond):
2065- c.Fatal("timeout")
2066- }
2067+ res := takeNext(ch, c)
2068
2069 expected := HelperOutput{Message: args.Input.Payload}
2070 c.Check(res.HelperOutput, DeepEquals, expected)
2071@@ -438,3 +407,191 @@
2072 c.Check(err, IsNil)
2073 c.Check(dname, Equals, filepath.Join(tmpDir, "pkg.name"))
2074 }
2075+
2076+// checks that the a second helper run of an already-running helper
2077+// (for an app) goes to the backlog
2078+func (s *poolSuite) TestSecondRunSameAppToBacklog(c *C) {
2079+ ch := s.pool.Start()
2080+ defer s.pool.Stop()
2081+
2082+ app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
2083+ input1 := &HelperInput{
2084+ App: app1,
2085+ NotificationId: "foo1",
2086+ Payload: []byte(`"hello1"`),
2087+ }
2088+ app2 := clickhelp.MustParseAppId("com.example.test_test-app-1")
2089+ input2 := &HelperInput{
2090+ App: app2,
2091+ NotificationId: "foo2",
2092+ Payload: []byte(`"hello2"`),
2093+ }
2094+
2095+ c.Assert(app1.Base(), Equals, app2.Base())
2096+
2097+ s.pool.Run("fake", input1)
2098+ s.pool.Run("fake", input2)
2099+
2100+ s.waitForArgs(c, "Launch")
2101+ go s.fakeLauncher.done("0")
2102+ takeNext(ch, c)
2103+
2104+ // this is where we check that:
2105+ c.Check(s.log.Captured(), Matches, `(?ms).* helper input backlog has grown to 1 entries.$`)
2106+}
2107+
2108+// checks that the an Nth helper run goes to the backlog
2109+func (s *poolSuite) TestRunNthAppToBacklog(c *C) {
2110+ s.pool.(*kindHelperPool).maxNum = 2
2111+ ch := s.pool.Start()
2112+ defer s.pool.Stop()
2113+
2114+ app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
2115+ input1 := &HelperInput{
2116+ App: app1,
2117+ NotificationId: "foo1",
2118+ Payload: []byte(`"hello1"`),
2119+ }
2120+ app2 := clickhelp.MustParseAppId("com.example.test_test-app-2")
2121+ input2 := &HelperInput{
2122+ App: app2,
2123+ NotificationId: "foo2",
2124+ Payload: []byte(`"hello2"`),
2125+ }
2126+ app3 := clickhelp.MustParseAppId("com.example.test_test-app-3")
2127+ input3 := &HelperInput{
2128+ App: app3,
2129+ NotificationId: "foo3",
2130+ Payload: []byte(`"hello3"`),
2131+ }
2132+
2133+ s.pool.Run("fake", input1)
2134+ s.waitForArgs(c, "Launch")
2135+
2136+ s.pool.Run("fake", input2)
2137+ s.log.ResetCapture()
2138+ s.waitForArgs(c, "Launch")
2139+
2140+ s.pool.Run("fake", input3)
2141+
2142+ go s.fakeLauncher.done("0")
2143+ s.waitForArgs(c, "Launch")
2144+
2145+ res := takeNext(ch, c)
2146+ c.Assert(res, NotNil)
2147+ c.Assert(res.Input, NotNil)
2148+ c.Assert(res.Input.App, NotNil)
2149+ c.Assert(res.Input.App.Original(), Equals, "com.example.test_test-app-1")
2150+ go s.fakeLauncher.done("1")
2151+ go s.fakeLauncher.done("2")
2152+ takeNext(ch, c)
2153+ takeNext(ch, c)
2154+
2155+ // this is the crux: we're checking that the third Run() went to the backlog.
2156+ c.Check(s.log.Captured(), Matches,
2157+ `(?ms).* helper input backlog has grown to 1 entries\.$.*shrunk to 0 entries\.$`)
2158+}
2159+
2160+func (s *poolSuite) TestRunBacklogFailedContinuesDiffApp(c *C) {
2161+ s.pool.(*kindHelperPool).maxNum = 1
2162+ ch := s.pool.Start()
2163+ defer s.pool.Stop()
2164+
2165+ app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
2166+ input1 := &HelperInput{
2167+ App: app1,
2168+ NotificationId: "foo1",
2169+ Payload: []byte(`"hello1"`),
2170+ }
2171+ app2 := clickhelp.MustParseAppId("com.example.test_test-app-2")
2172+ input2 := &HelperInput{
2173+ App: app2,
2174+ NotificationId: "foo2",
2175+ Payload: []byte(`"hello2"`),
2176+ }
2177+ app3 := clickhelp.MustParseAppId("com.example.test_test-app-3")
2178+ input3 := &HelperInput{
2179+ App: app3,
2180+ NotificationId: "foo3",
2181+ Payload: []byte(`"hello3"`),
2182+ }
2183+ app4 := clickhelp.MustParseAppId("com.example.test_test-app-4")
2184+ input4 := &HelperInput{
2185+ App: app4,
2186+ NotificationId: "foo4",
2187+ Payload: []byte(`"hello4"`),
2188+ }
2189+
2190+ s.pool.Run("fake", input1)
2191+ s.waitForArgs(c, "Launch")
2192+ s.pool.Run("NOT-THERE", input2) // this will fail
2193+ s.pool.Run("fake", input3)
2194+ s.pool.Run("fake", input4)
2195+
2196+ go s.fakeLauncher.done("0")
2197+ // Everything up to here was just set-up.
2198+ //
2199+ // What we're checking for is that, if a helper launch fails, the
2200+ // next one in the backlog is picked up.
2201+ c.Assert(takeNext(ch, c).Input.App, Equals, app1)
2202+ c.Assert(takeNext(ch, c).Input.App, Equals, app2)
2203+ go s.fakeLauncher.done("2")
2204+ s.waitForArgs(c, "Launch")
2205+ c.Check(s.log.Captured(), Matches,
2206+ `(?ms).* helper input backlog has grown to 3 entries\.$.*shrunk to 1 entries\.$`)
2207+}
2208+
2209+func (s *poolSuite) TestBigBacklogShrinks(c *C) {
2210+ s.pool.(*kindHelperPool).maxNum = 1
2211+ ch := s.pool.Start()
2212+ defer s.pool.Stop()
2213+ numBad := 10
2214+
2215+ app := clickhelp.MustParseAppId("com.example.test_test-app")
2216+ s.pool.Run("fake", &HelperInput{App: app, NotificationId: "0", Payload: []byte(`""`)})
2217+
2218+ for i := 0; i < numBad; i++ {
2219+ s.pool.Run("NOT-THERE", &HelperInput{App: app})
2220+ }
2221+ s.pool.Run("fake", &HelperInput{App: app, NotificationId: "1", Payload: []byte(`""`)})
2222+ s.pool.Run("fake", &HelperInput{App: app, NotificationId: "2", Payload: []byte(`""`)})
2223+ s.waitForArgs(c, "Launch")
2224+ go s.fakeLauncher.done("0")
2225+ // now we should get the fake + all the bad ones
2226+ for i := 0; i < numBad+1; i++ {
2227+ takeNext(ch, c)
2228+ }
2229+ s.waitForArgs(c, "Launch")
2230+ go s.fakeLauncher.done("1")
2231+ takeNext(ch, c)
2232+ // so now there's one good one "running", and one more waiting.
2233+ c.Check(s.log.Captured(), Matches, `(?ms).* shrunk to 1 entries\.$`)
2234+ // and the shrinker shrunk
2235+ c.Check(s.log.Captured(), Matches, `(?ms).*copying backlog to avoid wasting too much space .*`)
2236+}
2237+
2238+func (s *poolSuite) TestBacklogShrinkerNilToNil(c *C) {
2239+ pool := s.pool.(*kindHelperPool)
2240+ c.Check(pool.shrinkBacklog(nil, 0), IsNil)
2241+}
2242+
2243+func (s *poolSuite) TestBacklogShrinkerEmptyToNil(c *C) {
2244+ pool := s.pool.(*kindHelperPool)
2245+ empty := []*HelperInput{nil, nil, nil}
2246+ c.Check(pool.shrinkBacklog(empty, 0), IsNil)
2247+}
2248+
2249+func (s *poolSuite) TestBacklogShrinkerFullUntouched(c *C) {
2250+ pool := s.pool.(*kindHelperPool)
2251+ input := &HelperInput{}
2252+ full := []*HelperInput{input, input, input}
2253+ c.Check(pool.shrinkBacklog(full, 3), DeepEquals, full)
2254+}
2255+
2256+func (s *poolSuite) TestBacklogShrinkerSparseShrunk(c *C) {
2257+ pool := s.pool.(*kindHelperPool)
2258+ input := &HelperInput{}
2259+ sparse := []*HelperInput{nil, input, nil, input, nil}
2260+ full := []*HelperInput{input, input}
2261+ c.Check(pool.shrinkBacklog(sparse, 2), DeepEquals, full)
2262+}
2263
2264=== modified file 'messaging/cmessaging/cmessaging.go'
2265--- messaging/cmessaging/cmessaging.go 2014-07-16 16:10:03 +0000
2266+++ messaging/cmessaging/cmessaging.go 2014-07-29 16:56:22 +0000
2267@@ -25,11 +25,16 @@
2268 void add_notification(const gchar* desktop_id, const gchar* notification_id,
2269 const gchar* icon_path, const gchar* summary, const gchar* body,
2270 gint64 timestamp, const gchar** actions, gpointer obj);
2271+
2272+void remove_notification(const gchar* desktop_id, const gchar* notification_id);
2273+
2274+gboolean notification_exists(const gchar* desktop_id, const gchar* notification_id);
2275 */
2276 import "C"
2277 import "unsafe"
2278
2279 import (
2280+ "launchpad.net/ubuntu-push/click"
2281 "launchpad.net/ubuntu-push/launch_helper"
2282 "launchpad.net/ubuntu-push/messaging/reply"
2283 )
2284@@ -37,6 +42,9 @@
2285 type Payload struct {
2286 Ch chan *reply.MMActionReply
2287 Actions []string
2288+ App *click.AppId
2289+ Tag string
2290+ Gone bool
2291 }
2292
2293 func gchar(s string) *C.gchar {
2294@@ -56,7 +64,7 @@
2295 if action == "" && len(payload.Actions) >= 2 {
2296 action = payload.Actions[1]
2297 }
2298- mmar := &reply.MMActionReply{Notification: C.GoString(c_notification), Action: action}
2299+ mmar := &reply.MMActionReply{Notification: C.GoString(c_notification), Action: action, App: payload.App}
2300 payload.Ch <- mmar
2301 }
2302
2303@@ -81,6 +89,26 @@
2304 C.add_notification(desktop_id, notification_id, icon_path, summary, body, timestamp, nil, (C.gpointer)(payload))
2305 }
2306
2307+func RemoveNotification(desktopId string, notificationId string) {
2308+ desktop_id := gchar(desktopId)
2309+ defer gfree(desktop_id)
2310+
2311+ notification_id := gchar(notificationId)
2312+ defer gfree(notification_id)
2313+
2314+ C.remove_notification(desktop_id, notification_id)
2315+}
2316+
2317+func NotificationExists(desktopId string, notificationId string) bool {
2318+ notification_id := gchar(notificationId)
2319+ defer gfree(notification_id)
2320+
2321+ desktop_id := gchar(desktopId)
2322+ defer gfree(desktop_id)
2323+
2324+ return C.notification_exists(desktop_id, notification_id) == C.TRUE
2325+}
2326+
2327 func init() {
2328 go C.g_main_loop_run(C.g_main_loop_new(nil, C.FALSE))
2329 }
2330
2331=== modified file 'messaging/cmessaging/cmessaging_c.go'
2332--- messaging/cmessaging/cmessaging_c.go 2014-07-16 13:59:39 +0000
2333+++ messaging/cmessaging/cmessaging_c.go 2014-07-29 16:56:22 +0000
2334@@ -25,10 +25,11 @@
2335 handleActivate(action, messaging_menu_message_get_id(msg), obj);
2336 }
2337
2338+static GHashTable* map = NULL;
2339+
2340 void add_notification (const gchar* desktop_id, const gchar* notification_id,
2341 const gchar* icon_path, const gchar* summary, const gchar* body,
2342 gint64 timestamp, const gchar** actions, gpointer obj) {
2343- static GHashTable* map = NULL;
2344 if (map == NULL) {
2345 map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
2346 }
2347@@ -54,5 +55,34 @@
2348 g_signal_connect(msg, "activate", G_CALLBACK(activate_cb), obj);
2349 g_object_unref(msg);
2350 }
2351+
2352+void remove_notification (const gchar* desktop_id, const gchar* notification_id) {
2353+ if (map == NULL) {
2354+ return;
2355+ }
2356+ MessagingMenuApp* app = g_hash_table_lookup (map, desktop_id);
2357+ if (app == NULL) {
2358+ // no app in the hash table, bailout
2359+ return;
2360+ }
2361+ messaging_menu_app_remove_message_by_id (app, notification_id);
2362+}
2363+
2364+gboolean notification_exists (const gchar* desktop_id, const gchar* notification_id) {
2365+ if (map == NULL) {
2366+ return FALSE;
2367+ }
2368+ MessagingMenuApp* app = g_hash_table_lookup (map, desktop_id);
2369+ if (app == NULL) {
2370+ // no app in the hash table, bailout
2371+ return FALSE;
2372+ }
2373+ MessagingMenuMessage* msg = messaging_menu_app_get_message(app, notification_id);
2374+ if (msg != NULL) {
2375+ // the notification is still there
2376+ return TRUE;
2377+ }
2378+ return FALSE;
2379+}
2380 */
2381 import "C"
2382
2383=== modified file 'messaging/messaging.go'
2384--- messaging/messaging.go 2014-07-16 17:51:06 +0000
2385+++ messaging/messaging.go 2014-07-29 16:56:22 +0000
2386@@ -21,6 +21,7 @@
2387 import (
2388 "encoding/json"
2389 "sync"
2390+ "time"
2391
2392 "launchpad.net/ubuntu-push/bus/notifications"
2393 "launchpad.net/ubuntu-push/click"
2394@@ -30,56 +31,166 @@
2395 "launchpad.net/ubuntu-push/messaging/reply"
2396 )
2397
2398+var cleanupLoopDuration = 5 * time.Minute
2399+
2400 type MessagingMenu struct {
2401- Log logger.Logger
2402- Ch chan *reply.MMActionReply
2403- notifications map[string]*cmessaging.Payload
2404- lock sync.RWMutex
2405+ Log logger.Logger
2406+ Ch chan *reply.MMActionReply
2407+ notifications map[string]*cmessaging.Payload // keep a ref to the Payload used in the MMU callback
2408+ lock sync.RWMutex
2409+ stopCleanupLoopCh chan bool
2410+ ticker *time.Ticker
2411+ tickerCh <-chan time.Time
2412 }
2413
2414 // New returns a new MessagingMenu
2415 func New(log logger.Logger) *MessagingMenu {
2416- return &MessagingMenu{Log: log, Ch: make(chan *reply.MMActionReply), notifications: make(map[string]*cmessaging.Payload)}
2417+ ticker := time.NewTicker(cleanupLoopDuration)
2418+ stopCh := make(chan bool)
2419+ return &MessagingMenu{Log: log, Ch: make(chan *reply.MMActionReply), notifications: make(map[string]*cmessaging.Payload), ticker: ticker, tickerCh: ticker.C, stopCleanupLoopCh: stopCh}
2420 }
2421
2422 var cAddNotification = cmessaging.AddNotification
2423-
2424-func (mmu *MessagingMenu) addNotification(desktopId string, notificationId string, card *launch_helper.Card, actions []string) {
2425- payload := &cmessaging.Payload{Ch: mmu.Ch, Actions: actions}
2426+var cRemoveNotification = cmessaging.RemoveNotification
2427+var cNotificationExists = cmessaging.NotificationExists
2428+
2429+// GetCh returns the reply channel, exactly like mm.Ch.
2430+func (mmu *MessagingMenu) GetCh() chan *reply.MMActionReply {
2431+ return mmu.Ch
2432+}
2433+
2434+func (mmu *MessagingMenu) addNotification(app *click.AppId, notificationId string, tag string, card *launch_helper.Card, actions []string) {
2435 mmu.lock.Lock()
2436- // XXX: only gets removed if the action is activated.
2437+ defer mmu.lock.Unlock()
2438+ payload := &cmessaging.Payload{Ch: mmu.Ch, Actions: actions, App: app, Tag: tag}
2439 mmu.notifications[notificationId] = payload
2440- mmu.lock.Unlock()
2441- cAddNotification(desktopId, notificationId, card, payload)
2442+ cAddNotification(app.DesktopId(), notificationId, card, payload)
2443 }
2444
2445-// RemoveNotification deletes the notification from internal map
2446-func (mmu *MessagingMenu) RemoveNotification(notificationId string) {
2447+func (mmu *MessagingMenu) RemoveNotification(notificationId string, fromUI bool) {
2448 mmu.lock.Lock()
2449 defer mmu.lock.Unlock()
2450+ payload := mmu.notifications[notificationId]
2451 delete(mmu.notifications, notificationId)
2452-}
2453-
2454-func (mmu *MessagingMenu) Present(app *click.AppId, notificationId string, notification *launch_helper.Notification) {
2455- if notification == nil || notification.Card == nil || !notification.Card.Persist || notification.Card.Summary == "" {
2456- mmu.Log.Debugf("[%s] no notification or notification has no persistable card: %#v", notificationId, notification)
2457- return
2458- }
2459- actions := make([]string, 2*len(notification.Card.Actions))
2460- for i, action := range notification.Card.Actions {
2461+ if payload != nil && payload.App != nil && fromUI {
2462+ cRemoveNotification(payload.App.DesktopId(), notificationId)
2463+ }
2464+}
2465+
2466+// cleanupNotifications remove notifications that were cleared from the messaging menu
2467+func (mmu *MessagingMenu) cleanUpNotifications() {
2468+ mmu.lock.Lock()
2469+ defer mmu.lock.Unlock()
2470+ for nid, payload := range mmu.notifications {
2471+ if payload.Gone {
2472+ // sweep
2473+ delete(mmu.notifications, nid)
2474+ // don't check the mmu for this nid
2475+ continue
2476+ }
2477+ exists := cNotificationExists(payload.App.DesktopId(), nid)
2478+ if !exists {
2479+ // mark
2480+ payload.Gone = true
2481+ }
2482+ }
2483+}
2484+
2485+func (mmu *MessagingMenu) StartCleanupLoop() {
2486+ mmu.doStartCleanupLoop(mmu.cleanUpNotifications)
2487+}
2488+
2489+func (mmu *MessagingMenu) doStartCleanupLoop(cleanupFunc func()) {
2490+ go func() {
2491+ for {
2492+ select {
2493+ case <-mmu.tickerCh:
2494+ cleanupFunc()
2495+ case <-mmu.stopCleanupLoopCh:
2496+ mmu.ticker.Stop()
2497+ mmu.Log.Debugf("CleanupLoop stopped.")
2498+ return
2499+ }
2500+ }
2501+ }()
2502+}
2503+
2504+func (mmu *MessagingMenu) StopCleanupLoop() {
2505+ mmu.stopCleanupLoopCh <- true
2506+}
2507+
2508+func (mmu *MessagingMenu) Tags(app *click.AppId) []string {
2509+ orig := app.Original()
2510+ tags := []string(nil)
2511+ mmu.lock.RLock()
2512+ defer mmu.lock.RUnlock()
2513+ for _, payload := range mmu.notifications {
2514+ if payload.App.Original() == orig {
2515+ tags = append(tags, payload.Tag)
2516+ }
2517+ }
2518+ return tags
2519+}
2520+
2521+func (mmu *MessagingMenu) Clear(app *click.AppId, tags ...string) int {
2522+ orig := app.Original()
2523+ var nids []string
2524+
2525+ mmu.lock.RLock()
2526+ // O(n×m). Should be small n and m though.
2527+ for nid, payload := range mmu.notifications {
2528+ if payload.App.Original() == orig {
2529+ if len(tags) == 0 {
2530+ nids = append(nids, nid)
2531+ } else {
2532+ for _, tag := range tags {
2533+ if payload.Tag == tag {
2534+ nids = append(nids, nid)
2535+ }
2536+ }
2537+ }
2538+ }
2539+ }
2540+ mmu.lock.RUnlock()
2541+
2542+ for _, nid := range nids {
2543+ mmu.RemoveNotification(nid, true)
2544+ }
2545+
2546+ return len(nids)
2547+}
2548+
2549+func (mmu *MessagingMenu) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool {
2550+ if notification == nil {
2551+ panic("please check notification is not nil before calling present")
2552+ }
2553+
2554+ card := notification.Card
2555+
2556+ if card == nil || !card.Persist || card.Summary == "" {
2557+ mmu.Log.Debugf("[%s] notification has no persistable card: %#v", nid, card)
2558+ return false
2559+ }
2560+
2561+ actions := make([]string, 2*len(card.Actions))
2562+ for i, action := range card.Actions {
2563 act, err := json.Marshal(&notifications.RawAction{
2564 App: app,
2565- Nid: notificationId,
2566+ Nid: nid,
2567 ActionId: i,
2568 Action: action,
2569 })
2570 if err != nil {
2571 mmu.Log.Errorf("Failed to build action: %s", action)
2572- return
2573+ return false
2574 }
2575 actions[2*i] = string(act)
2576 actions[2*i+1] = action
2577 }
2578
2579- mmu.addNotification(app.DesktopId(), notificationId, notification.Card, actions)
2580+ mmu.Log.Debugf("[%s] creating notification centre entry for %s (summary: %s)", nid, app.Base(), card.Summary)
2581+
2582+ mmu.addNotification(app, nid, notification.Tag, card, actions)
2583+
2584+ return true
2585 }
2586
2587=== modified file 'messaging/messaging_test.go'
2588--- messaging/messaging_test.go 2014-07-16 17:54:32 +0000
2589+++ messaging/messaging_test.go 2014-07-29 16:56:22 +0000
2590@@ -17,6 +17,8 @@
2591 package messaging
2592
2593 import (
2594+ "time"
2595+
2596 . "launchpad.net/gocheck"
2597 "testing"
2598
2599@@ -24,6 +26,7 @@
2600 clickhelp "launchpad.net/ubuntu-push/click/testing"
2601 "launchpad.net/ubuntu-push/launch_helper"
2602 "launchpad.net/ubuntu-push/messaging/cmessaging"
2603+ "launchpad.net/ubuntu-push/messaging/reply"
2604 helpers "launchpad.net/ubuntu-push/testing"
2605 )
2606
2607@@ -41,6 +44,17 @@
2608 cAddNotification = func(a string, n string, c *launch_helper.Card, payload *cmessaging.Payload) {
2609 ms.log.Debugf("ADD: app: %s, not: %s, card: %v, chan: %v", a, n, c, payload)
2610 }
2611+ cRemoveNotification = func(a, n string) {
2612+ ms.log.Debugf("REMOVE: app: %s, not: %s", a, n)
2613+ }
2614+ // just in case
2615+ cNotificationExists = nil
2616+}
2617+
2618+func (ms *MessagingSuite) TearDownSuite(c *C) {
2619+ cAddNotification = cmessaging.AddNotification
2620+ cRemoveNotification = cmessaging.RemoveNotification
2621+ cNotificationExists = cmessaging.NotificationExists
2622 }
2623
2624 func (ms *MessagingSuite) SetUpTest(c *C) {
2625@@ -53,7 +67,7 @@
2626 card := launch_helper.Card{Summary: "ehlo", Persist: true}
2627 notif := launch_helper.Notification{Card: &card}
2628
2629- mmu.Present(ms.app, "notif-id", &notif)
2630+ c.Check(mmu.Present(ms.app, "notif-id", &notif), Equals, true)
2631
2632 c.Check(ms.log.Captured(), Matches, `(?s).* ADD:.*notif-id.*`)
2633 }
2634@@ -63,7 +77,7 @@
2635 card := launch_helper.Card{Persist: true}
2636 notif := launch_helper.Notification{Card: &card}
2637
2638- mmu.Present(ms.app, "notif-id", &notif)
2639+ c.Check(mmu.Present(ms.app, "notif-id", &notif), Equals, false)
2640
2641 c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*")
2642 }
2643@@ -73,54 +87,248 @@
2644 card := launch_helper.Card{Summary: "ehlo"}
2645 notif := launch_helper.Notification{Card: &card}
2646
2647- mmu.Present(ms.app, "notif-id", &notif)
2648+ c.Check(mmu.Present(ms.app, "notif-id", &notif), Equals, false)
2649
2650 c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*")
2651 }
2652
2653-func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNil(c *C) {
2654+func (ms *MessagingSuite) TestPresentPanicsIfNil(c *C) {
2655 mmu := New(ms.log)
2656- mmu.Present(ms.app, "notif-id", nil)
2657- c.Check(ms.log.Captured(), Matches, "(?sm).*no notification.*")
2658+ c.Check(func() { mmu.Present(ms.app, "notif-id", nil) }, Panics, `please check notification is not nil before calling present`)
2659 }
2660
2661 func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNilCard(c *C) {
2662 mmu := New(ms.log)
2663- mmu.Present(ms.app, "notif-id", &launch_helper.Notification{})
2664- c.Check(ms.log.Captured(), Matches, "(?sm).*no notification.*")
2665+ c.Check(mmu.Present(ms.app, "notif-id", &launch_helper.Notification{}), Equals, false)
2666+ c.Check(ms.log.Captured(), Matches, "(?sm).*no persistable card.*")
2667 }
2668
2669 func (ms *MessagingSuite) TestPresentWithActions(c *C) {
2670 mmu := New(ms.log)
2671 card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}}
2672- notif := launch_helper.Notification{Card: &card}
2673+ notif := launch_helper.Notification{Card: &card, Tag: "a-tag"}
2674
2675- mmu.Present(ms.app, "notif-id", &notif)
2676+ c.Check(mmu.Present(ms.app, "notif-id", &notif), Equals, true)
2677
2678 c.Check(ms.log.Captured(), Matches, `(?s).* ADD:.*notif-id.*`)
2679
2680 payload, _ := mmu.notifications["notif-id"]
2681 c.Check(payload.Ch, Equals, mmu.Ch)
2682 c.Check(len(payload.Actions), Equals, 2)
2683+ c.Check(payload.Tag, Equals, "a-tag")
2684 rawAction := "{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}"
2685 c.Check(payload.Actions[0], Equals, rawAction)
2686 c.Check(payload.Actions[1], Equals, "action-1")
2687 }
2688
2689+func (ms *MessagingSuite) TestTagsListsTags(c *C) {
2690+ mmu := New(ms.log)
2691+ f := func(s string) *launch_helper.Notification {
2692+ card := launch_helper.Card{Summary: "tag: \"" + s + "\"", Persist: true}
2693+ return &launch_helper.Notification{Card: &card, Tag: s}
2694+ }
2695+
2696+ c.Check(mmu.Tags(ms.app), IsNil)
2697+ c.Assert(mmu.Present(ms.app, "notif1", f("one")), Equals, true)
2698+ c.Check(mmu.Tags(ms.app), DeepEquals, []string{"one"})
2699+ c.Assert(mmu.Present(ms.app, "notif2", f("")), Equals, true)
2700+ c.Check(mmu.Tags(ms.app), DeepEquals, []string{"one", ""})
2701+ // and an empty notification doesn't count
2702+ c.Assert(mmu.Present(ms.app, "notif3", &launch_helper.Notification{Tag: "X"}), Equals, false)
2703+ c.Check(mmu.Tags(ms.app), DeepEquals, []string{"one", ""})
2704+ // and they go away if we remove one
2705+ mmu.RemoveNotification("notif1", false)
2706+ c.Check(mmu.Tags(ms.app), DeepEquals, []string{""})
2707+ mmu.RemoveNotification("notif2", false)
2708+ c.Check(mmu.Tags(ms.app), IsNil)
2709+}
2710+
2711+func (ms *MessagingSuite) TestClearClears(c *C) {
2712+ app1 := ms.app
2713+ app2 := clickhelp.MustParseAppId("com.example.test_test-2_0")
2714+ app3 := clickhelp.MustParseAppId("com.example.test_test-3_0")
2715+ mm := New(ms.log)
2716+ f := func(app *click.AppId, nid string, tag string, withCard bool) bool {
2717+ notif := launch_helper.Notification{Tag: tag}
2718+ card := launch_helper.Card{Summary: "tag: \"" + tag + "\"", Persist: true}
2719+ if withCard {
2720+ notif.Card = &card
2721+ }
2722+ return mm.Present(app, nid, &notif)
2723+ }
2724+ // create a bunch
2725+ c.Assert(f(app1, "notif1", "one", true), Equals, true)
2726+ c.Assert(f(app1, "notif2", "two", true), Equals, true)
2727+ c.Assert(f(app1, "notif3", "", true), Equals, true)
2728+ c.Assert(f(app2, "notif4", "one", true), Equals, true)
2729+ c.Assert(f(app2, "notif5", "two", true), Equals, true)
2730+ c.Assert(f(app3, "notif6", "one", true), Equals, true)
2731+ c.Assert(f(app3, "notif7", "", true), Equals, true)
2732+
2733+ // that is:
2734+ // app 1: "one", "two", "";
2735+ // app 2: "one", "two";
2736+ // app 3: "one", ""
2737+ c.Assert(mm.Tags(app1), DeepEquals, []string{"one", "two", ""})
2738+ c.Assert(mm.Tags(app2), DeepEquals, []string{"one", "two"})
2739+ c.Assert(mm.Tags(app3), DeepEquals, []string{"one", ""})
2740+
2741+ // clearing a non-existent tag does nothing
2742+ c.Check(mm.Clear(app1, "foo"), Equals, 0)
2743+ c.Check(mm.Tags(app1), HasLen, 3)
2744+ c.Check(mm.Tags(app2), HasLen, 2)
2745+ c.Check(mm.Tags(app3), HasLen, 2)
2746+
2747+ // asking to clear a tag that exists only for another app does nothing
2748+ c.Check(mm.Clear(app3, "two"), Equals, 0)
2749+ c.Check(mm.Tags(app1), HasLen, 3)
2750+ c.Check(mm.Tags(app2), HasLen, 2)
2751+ c.Check(mm.Tags(app3), HasLen, 2)
2752+
2753+ // asking to clear a list of tags, only one of which is yours, only clears yours
2754+ c.Check(mm.Clear(app3, "one", "two"), Equals, 1)
2755+ c.Check(mm.Tags(app1), HasLen, 3)
2756+ c.Check(mm.Tags(app2), HasLen, 2)
2757+ c.Check(mm.Tags(app3), HasLen, 1)
2758+
2759+ // clearing with no args just empties it
2760+ c.Check(mm.Clear(app1), Equals, 3)
2761+ c.Check(mm.Tags(app1), IsNil)
2762+ c.Check(mm.Tags(app2), HasLen, 2)
2763+ c.Check(mm.Tags(app3), HasLen, 1)
2764+
2765+ // asking to clear all the tags from an already tagless app does nothing
2766+ c.Check(mm.Clear(app1), Equals, 0)
2767+ c.Check(mm.Tags(app1), IsNil)
2768+ c.Check(mm.Tags(app2), HasLen, 2)
2769+ c.Check(mm.Tags(app3), HasLen, 1)
2770+
2771+ // check we work ok with a "" tag, too.
2772+ c.Check(mm.Clear(app1, ""), Equals, 0)
2773+ c.Check(mm.Clear(app2, ""), Equals, 0)
2774+ c.Check(mm.Clear(app3, ""), Equals, 1)
2775+ c.Check(mm.Tags(app1), IsNil)
2776+ c.Check(mm.Tags(app2), HasLen, 2)
2777+ c.Check(mm.Tags(app3), HasLen, 0)
2778+}
2779+
2780 func (ms *MessagingSuite) TestRemoveNotification(c *C) {
2781 mmu := New(ms.log)
2782 card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}}
2783 actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"}
2784- mmu.addNotification(ms.app.DesktopId(), "notif-id", &card, actions)
2785+ mmu.addNotification(ms.app, "notif-id", "a-tag", &card, actions)
2786
2787 // check it's there
2788 payload, ok := mmu.notifications["notif-id"]
2789 c.Check(ok, Equals, true)
2790 c.Check(payload.Actions, DeepEquals, actions)
2791+ c.Check(payload.Tag, Equals, "a-tag")
2792 c.Check(payload.Ch, Equals, mmu.Ch)
2793+ // remove the notification (internal only)
2794+ mmu.RemoveNotification("notif-id", false)
2795+ // check it's gone
2796+ _, ok = mmu.notifications["notif-id"]
2797+ c.Check(ok, Equals, false)
2798+}
2799+
2800+func (ms *MessagingSuite) TestRemoveNotificationsFromUI(c *C) {
2801+ mmu := New(ms.log)
2802+ card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}}
2803+ actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"}
2804+ mmu.addNotification(ms.app, "notif-id", "a-tag", &card, actions)
2805+
2806+ // check it's there
2807+ _, ok := mmu.notifications["notif-id"]
2808+ c.Assert(ok, Equals, true)
2809+ // remove the notification (both internal and from UI)
2810+ mmu.RemoveNotification("notif-id", true)
2811+ // check it's gone
2812+ _, ok = mmu.notifications["notif-id"]
2813+ c.Check(ok, Equals, false)
2814+
2815+ // and check it's been removed from the UI too
2816+ c.Check(ms.log.Captured(), Matches, `(?s).* REMOVE:.*notif-id.*`)
2817+}
2818+
2819+func (ms *MessagingSuite) TestCleanupStaleNotification(c *C) {
2820+ mmu := New(ms.log)
2821+ card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}}
2822+ actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"}
2823+ mmu.addNotification(ms.app, "notif-id", "", &card, actions)
2824+
2825+ // check it's there
2826+ _, ok := mmu.notifications["notif-id"]
2827+ c.Check(ok, Equals, true)
2828+
2829+ // patch cnotificationexists to return true
2830+ cNotificationExists = func(did string, nid string) bool {
2831+ return true
2832+ }
2833 // remove the notification
2834- mmu.RemoveNotification("notif-id")
2835+ mmu.cleanUpNotifications()
2836+ // check it's still there
2837+ _, ok = mmu.notifications["notif-id"]
2838+ c.Check(ok, Equals, true)
2839+ // patch cnotificationexists to return false
2840+ cNotificationExists = func(did string, nid string) bool {
2841+ return false
2842+ }
2843+ // mark the notification
2844+ mmu.cleanUpNotifications()
2845+ // check it's gone
2846+ _, ok = mmu.notifications["notif-id"]
2847+ c.Check(ok, Equals, true)
2848+ // sweep the notification
2849+ mmu.cleanUpNotifications()
2850 // check it's gone
2851 _, ok = mmu.notifications["notif-id"]
2852 c.Check(ok, Equals, false)
2853 }
2854+
2855+func (ms *MessagingSuite) TestCleanupLoop(c *C) {
2856+ mmu := New(ms.log)
2857+ tickerCh := make(chan time.Time)
2858+ mmu.tickerCh = tickerCh
2859+ cleanupCh := make(chan bool)
2860+ cleanupFunc := func() {
2861+ cleanupCh <- true
2862+ }
2863+ // start the cleanup loop
2864+ mmu.doStartCleanupLoop(cleanupFunc)
2865+ // mark
2866+ tickerCh <- time.Now()
2867+ // check it was called
2868+ <-cleanupCh
2869+ // stop the loop and check that it's actually stopped.
2870+ mmu.StopCleanupLoop()
2871+ c.Check(ms.log.Captured(), Matches, "(?s).*DEBUG CleanupLoop stopped.*")
2872+}
2873+
2874+func (ms *MessagingSuite) TestStartCleanupLoop(c *C) {
2875+ mmu := New(ms.log)
2876+ tickerCh := make(chan time.Time)
2877+ mmu.tickerCh = tickerCh
2878+ card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}}
2879+ actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"}
2880+ mmu.addNotification(ms.app, "notif-id", "", &card, actions)
2881+ // patch cnotificationexists to return true and signal when it's called
2882+ notifExistsCh := make(chan bool)
2883+ cNotificationExists = func(did string, nid string) bool {
2884+ notifExistsCh <- true
2885+ return true
2886+ }
2887+ // statr the cleanup loop
2888+ mmu.StartCleanupLoop()
2889+ // mark
2890+ tickerCh <- time.Now()
2891+ // check it's there, and marked
2892+ <-notifExistsCh
2893+ // stop the loop
2894+ mmu.StopCleanupLoop()
2895+}
2896+
2897+func (ms *MessagingSuite) TestGetCh(c *C) {
2898+ mmu := New(ms.log)
2899+ mmu.Ch = make(chan *reply.MMActionReply)
2900+ c.Check(mmu.GetCh(), Equals, mmu.Ch)
2901+}
2902
2903=== modified file 'messaging/reply/reply.go'
2904--- messaging/reply/reply.go 2014-07-16 16:10:03 +0000
2905+++ messaging/reply/reply.go 2014-07-29 16:56:22 +0000
2906@@ -18,8 +18,11 @@
2907 // messaging and cmessaging without going circular about it
2908 package reply
2909
2910+import "launchpad.net/ubuntu-push/click"
2911+
2912 // MMActionReply holds the reply from a MessagingMenu action
2913 type MMActionReply struct {
2914 Notification string
2915 Action string
2916+ App *click.AppId
2917 }
2918
2919=== modified file 'sounds/sounds.go'
2920--- sounds/sounds.go 2014-07-09 00:13:19 +0000
2921+++ sounds/sounds.go 2014-07-29 16:56:22 +0000
2922@@ -19,7 +19,7 @@
2923 import (
2924 "os"
2925 "os/exec"
2926- "path"
2927+ "path/filepath"
2928
2929 "launchpad.net/go-xdg/v0"
2930
2931@@ -40,8 +40,12 @@
2932 }
2933
2934 func (snd *Sound) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool {
2935- if notification == nil || notification.Sound == "" {
2936- snd.log.Debugf("[%s] no notification or no Sound in the notification; doing nothing: %#v", nid, notification)
2937+ if notification == nil {
2938+ panic("please check notification is not nil before calling present")
2939+ }
2940+
2941+ if notification.Sound == "" {
2942+ snd.log.Debugf("[%s] notification has no Sound: %#v", nid, notification.Sound)
2943 return false
2944 }
2945 absPath := snd.findSoundFile(app, nid, notification.Sound)
2946@@ -68,7 +72,7 @@
2947 func (snd *Sound) findSoundFile(app *click.AppId, nid string, sound string) string {
2948 // XXX also support legacy appIds?
2949 // first, check package-specific
2950- absPath, err := snd.dataFind(path.Join(app.Package, sound))
2951+ absPath, err := snd.dataFind(filepath.Join(app.Package, sound))
2952 if err == nil {
2953 // ffffound
2954 return absPath
2955@@ -76,7 +80,7 @@
2956 // next, check the XDG data dirs (but skip the first one -- that's "home")
2957 // XXX should we only check in $XDG/sounds ? (that's for sound *themes*...)
2958 for _, dir := range snd.dataDirs()[1:] {
2959- absPath := path.Join(dir, sound)
2960+ absPath := filepath.Join(dir, sound)
2961 _, err := os.Stat(absPath)
2962 if err == nil {
2963 return absPath
2964
2965=== modified file 'sounds/sounds_test.go'
2966--- sounds/sounds_test.go 2014-07-11 19:42:57 +0000
2967+++ sounds/sounds_test.go 2014-07-29 16:56:22 +0000
2968@@ -70,7 +70,7 @@
2969 }
2970
2971 // nil notification
2972- c.Check(s.Present(ss.app, "", nil), Equals, false)
2973+ c.Check(func() { s.Present(ss.app, "", nil) }, Panics, `please check notification is not nil before calling present`)
2974 // no Sound
2975 c.Check(s.Present(ss.app, "", &launch_helper.Notification{}), Equals, false)
2976 // bad player

Subscribers

People subscribed via source and target branches