Merge lp:~chipaca/ubuntu-push/cherrypickings into lp:ubuntu-push

Proposed by John Lenton
Status: Merged
Merged at revision: 101
Proposed branch: lp:~chipaca/ubuntu-push/cherrypickings
Merge into: lp:ubuntu-push
Diff against target: 1220 lines (+640/-59)
15 files modified
client/client.go (+6/-9)
client/client_test.go (+15/-1)
client/gethosts/gethost.go (+7/-5)
client/gethosts/gethost_test.go (+6/-2)
client/session/session.go (+52/-4)
client/session/session_test.go (+181/-11)
config/config.go (+130/-21)
config/config_test.go (+105/-0)
debian/changelog (+19/-0)
logger/logger.go (+27/-0)
logger/logger_test.go (+25/-0)
server/acceptance/cmd/acceptanceclient.go (+7/-2)
ubuntu-push-client.go (+25/-0)
whoopsie/identifier/identifier.go (+20/-4)
whoopsie/identifier/identifier_test.go (+15/-0)
To merge this branch: bzr merge lp:~chipaca/ubuntu-push/cherrypickings
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+216466@code.launchpad.net

Commit message

[ Samuele Pedroni ]
  * gave the client the ability to get config from commandline
    ( => easier automated testing) (LP: #1311600)

  [ John Lenton ]
  * Ensure ubuntu-push-client is the only one running in the session.
    (LP: #1309432)
  * Remove supurious numbers in brackets in notifications. (LP: #1308145)
  * Check the server certificate and server name. (LP: #1297969)
  * Loop whoopsie_identifier_generate until it starts working. (LP: #1309237)
  * In the session: set a flag on connect, clear it on successfully
    replying to ping or broadcast messages, check it at the top of
    autoredial. Also track the last autoredial, and set the delay flag if
    autoredial is re-called too quickly. (LP: #1309231)

Description of the change

[ Samuele Pedroni ]
  * gave the client the ability to get config from commandline
    ( => easier automated testing) (LP: #1311600)

  [ John Lenton ]
  * Ensure ubuntu-push-client is the only one running in the session.
    (LP: #1309432)
  * Remove supurious numbers in brackets in notifications. (LP: #1308145)
  * Check the server certificate and server name. (LP: #1297969)
  * Loop whoopsie_identifier_generate until it starts working. (LP: #1309237)
  * In the session: set a flag on connect, clear it on successfully
    replying to ping or broadcast messages, check it at the top of
    autoredial. Also track the last autoredial, and set the delay flag if
    autoredial is re-called too quickly. (LP: #1309231)

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

fix changelog

110. By John Lenton

updated changelog with link to lp:216466

111. By John Lenton

updated changelog with link to lp:1311600 which is what it should be

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'client/client.go'
2--- client/client.go 2014-04-11 16:37:48 +0000
3+++ client/client.go 2014-04-23 11:53:01 +0000
4@@ -57,7 +57,7 @@
5 // The PEM-encoded server certificate
6 CertPEMFile string `json:"cert_pem_file"`
7 // The logging level (one of "debug", "info", "error")
8- LogLevel string `json:"log_level"`
9+ LogLevel logger.ConfigLogLevel `json:"log_level"`
10 }
11
12 // PushClient is the Ubuntu Push Notifications client-side daemon.
13@@ -95,13 +95,13 @@
14
15 // configure loads its configuration, and sets it up.
16 func (client *PushClient) configure() error {
17- f, err := os.Open(client.configPath)
18+ _, err := os.Stat(client.configPath)
19 if err != nil {
20- return fmt.Errorf("opening config: %v", err)
21+ return fmt.Errorf("config: %v", err)
22 }
23- err = config.ReadConfig(f, &client.config)
24+ err = config.ReadFiles(&client.config, client.configPath, "<flags>")
25 if err != nil {
26- return fmt.Errorf("reading config: %v", err)
27+ return fmt.Errorf("config: %v", err)
28 }
29 // ignore spaces
30 client.config.Addr = strings.Replace(client.config.Addr, " ", "", -1)
31@@ -110,7 +110,7 @@
32 }
33
34 // later, we'll be specifying more logging options in the config file
35- client.log = logger.NewSimpleLogger(os.Stderr, client.config.LogLevel)
36+ client.log = logger.NewSimpleLogger(os.Stderr, client.config.LogLevel.Level())
37
38 // overridden for testing
39 client.idder = identifier.New()
40@@ -285,9 +285,6 @@
41 h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}
42 nots := notifications.Raw(client.notificationsEndp, client.log)
43 body := "Tap to open the system updater."
44- if msg != nil {
45- body = fmt.Sprintf("[%d] %s", msg.TopLevel, body)
46- }
47 not_id, err := nots.Notify(
48 "ubuntu-push-client", // app name
49 uint32(0), // id
50
51=== modified file 'client/client_test.go'
52--- client/client_test.go 2014-04-11 16:21:45 +0000
53+++ client/client_test.go 2014-04-23 11:53:01 +0000
54@@ -19,10 +19,12 @@
55 import (
56 "encoding/json"
57 "errors"
58+ "flag"
59 "fmt"
60 "io/ioutil"
61 "net/http"
62 "net/http/httptest"
63+ "os"
64 "path/filepath"
65 "reflect"
66 "testing"
67@@ -37,6 +39,7 @@
68 testibus "launchpad.net/ubuntu-push/bus/testing"
69 "launchpad.net/ubuntu-push/client/session"
70 "launchpad.net/ubuntu-push/client/session/levelmap"
71+ "launchpad.net/ubuntu-push/config"
72 helpers "launchpad.net/ubuntu-push/testing"
73 "launchpad.net/ubuntu-push/testing/condition"
74 "launchpad.net/ubuntu-push/util"
75@@ -79,6 +82,7 @@
76 }
77
78 func (cs *clientSuite) SetUpSuite(c *C) {
79+ config.IgnoreParsedFlags = true // because configure() uses <flags>
80 cs.timeouts = util.SwapTimeouts([]time.Duration{0})
81 cs.leveldbPath = ""
82 }
83@@ -142,6 +146,16 @@
84 c.Check(cli.config.ExchangeTimeout.TimeDuration(), Equals, time.Duration(10*time.Millisecond))
85 }
86
87+func (cs *clientSuite) TestConfigureWorksWithFlags(c *C) {
88+ flag.CommandLine = flag.NewFlagSet("client", flag.ContinueOnError)
89+ os.Args = []string{"client", "-addr", "foo:7777"}
90+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
91+ err := cli.configure()
92+ c.Assert(err, IsNil)
93+ c.Assert(cli.config, NotNil)
94+ c.Check(cli.config.Addr, Equals, "foo:7777")
95+}
96+
97 func (cs *clientSuite) TestConfigureSetsUpLog(c *C) {
98 cli := NewPushClient(cs.configPath, cs.leveldbPath)
99 c.Check(cli.log, IsNil)
100@@ -163,7 +177,7 @@
101 c.Check(cli.idder, IsNil)
102 err := cli.configure()
103 c.Assert(err, IsNil)
104- c.Assert(cli.idder, DeepEquals, identifier.New())
105+ c.Assert(cli.idder, FitsTypeOf, identifier.New())
106 }
107
108 func (cs *clientSuite) TestConfigureSetsUpEndpoints(c *C) {
109
110=== modified file 'client/gethosts/gethost.go'
111--- client/gethosts/gethost.go 2014-03-24 15:32:29 +0000
112+++ client/gethosts/gethost.go 2014-04-23 11:53:01 +0000
113@@ -49,8 +49,10 @@
114 }
115 }
116
117-type expected struct {
118- Hosts []string
119+// Host contains the domain and hosts returned by the remote endpoint
120+type Host struct {
121+ Domain string
122+ Hosts []string
123 }
124
125 var (
126@@ -60,7 +62,7 @@
127 )
128
129 // Get gets a list of hosts consulting the endpoint.
130-func (gh *GetHost) Get() ([]string, error) {
131+func (gh *GetHost) Get() (*Host, error) {
132 resp, err := gh.cli.Get(gh.endpointUrl + "?h=" + gh.hash)
133 if err != nil {
134 return nil, err
135@@ -80,7 +82,7 @@
136 if err != nil {
137 return nil, err
138 }
139- var parsed expected
140+ var parsed Host
141 err = json.Unmarshal(body, &parsed)
142 if err != nil {
143 return nil, ErrTemporary
144@@ -88,5 +90,5 @@
145 if len(parsed.Hosts) == 0 {
146 return nil, ErrTemporary
147 }
148- return parsed.Hosts, nil
149+ return &parsed, nil
150 }
151
152=== modified file 'client/gethosts/gethost_test.go'
153--- client/gethosts/gethost_test.go 2014-03-31 14:31:07 +0000
154+++ client/gethosts/gethost_test.go 2014-04-23 11:53:01 +0000
155@@ -45,7 +45,8 @@
156 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157 x := r.FormValue("h")
158 b, err := json.Marshal(map[string]interface{}{
159- "hosts": []string{"http://" + x},
160+ "domain": "example.com",
161+ "hosts": []string{"http://" + x},
162 })
163 if err != nil {
164 panic(err)
165@@ -57,7 +58,8 @@
166 gh := New("foobar", ts.URL, 1*time.Second)
167 res, err := gh.Get()
168 c.Assert(err, IsNil)
169- c.Check(res, DeepEquals, []string{"http://c1130408a700afe0"})
170+ c.Check(*res, DeepEquals,
171+ Host{Domain: "example.com", Hosts: []string{"http://c1130408a700afe0"}})
172 }
173
174 func (s *getHostsSuite) TestGetTimeout(c *C) {
175@@ -97,4 +99,6 @@
176
177 scenario(http.StatusOK, "{", ErrTemporary)
178 scenario(http.StatusOK, "{}", ErrTemporary)
179+ scenario(http.StatusOK, `{"domain": "example.com"}`, ErrTemporary)
180+ scenario(http.StatusOK, `{"hosts": ["one"]}`, nil)
181 }
182
183=== modified file 'client/session/session.go'
184--- client/session/session.go 2014-04-04 13:55:00 +0000
185+++ client/session/session.go 2014-04-23 11:53:01 +0000
186@@ -73,7 +73,7 @@
187 )
188
189 type hostGetter interface {
190- Get() ([]string, error)
191+ Get() (*gethosts.Host, error)
192 }
193
194 // ClientSessionConfig groups the client session configuration.
195@@ -115,6 +115,26 @@
196 stateP *uint32
197 ErrCh chan error
198 MsgCh chan *Notification
199+ // autoredial knobs
200+ shouldDelayP *uint32
201+ lastAutoRedial time.Time
202+ redialDelay func(*ClientSession) time.Duration
203+ redialJitter func(time.Duration) time.Duration
204+ redialDelays []time.Duration
205+ redialDelaysIdx int
206+}
207+
208+func redialDelay(sess *ClientSession) time.Duration {
209+ if sess.ShouldDelay() {
210+ t := sess.redialDelays[sess.redialDelaysIdx]
211+ if len(sess.redialDelays) > sess.redialDelaysIdx+1 {
212+ sess.redialDelaysIdx++
213+ }
214+ return t + sess.redialJitter(t)
215+ } else {
216+ sess.redialDelaysIdx = 0
217+ return 0
218+ }
219 }
220
221 func NewSession(serverAddrSpec string, conf ClientSessionConfig,
222@@ -131,6 +151,7 @@
223 if hostsEndpoint != "" {
224 getHost = gethosts.New(deviceId, hostsEndpoint, conf.ExchangeTimeout)
225 }
226+ var shouldDelay uint32 = 0
227 sess := &ClientSession{
228 ClientSessionConfig: conf,
229 getHost: getHost,
230@@ -139,10 +160,14 @@
231 Log: log,
232 Protocolator: protocol.NewProtocol0,
233 Levels: levels,
234- TLS: &tls.Config{InsecureSkipVerify: true}, // XXX
235+ TLS: &tls.Config{},
236 stateP: &state,
237 timeSince: time.Since,
238+ shouldDelayP: &shouldDelay,
239+ redialDelay: redialDelay,
240+ redialDelays: util.Timeouts(),
241 }
242+ sess.redialJitter = sess.Jitter
243 if sess.PEM != nil {
244 cp := x509.NewCertPool()
245 ok := cp.AppendCertsFromPEM(sess.PEM)
246@@ -154,6 +179,18 @@
247 return sess, nil
248 }
249
250+func (sess *ClientSession) ShouldDelay() bool {
251+ return atomic.LoadUint32(sess.shouldDelayP) != 0
252+}
253+
254+func (sess *ClientSession) setShouldDelay() {
255+ atomic.StoreUint32(sess.shouldDelayP, uint32(1))
256+}
257+
258+func (sess *ClientSession) clearShouldDelay() {
259+ atomic.StoreUint32(sess.shouldDelayP, uint32(0))
260+}
261+
262 func (sess *ClientSession) State() ClientSessionState {
263 return ClientSessionState(atomic.LoadUint32(sess.stateP))
264 }
265@@ -180,14 +217,17 @@
266 if sess.deliveryHosts != nil && sess.timeSince(sess.deliveryHostsTimestamp) < sess.HostsCachingExpiryTime {
267 return nil
268 }
269- hosts, err := sess.getHost.Get()
270+ host, err := sess.getHost.Get()
271 if err != nil {
272 sess.Log.Errorf("getHosts: %v", err)
273 sess.setState(Error)
274 return err
275 }
276 sess.deliveryHostsTimestamp = time.Now()
277- sess.deliveryHosts = hosts
278+ sess.deliveryHosts = host.Hosts
279+ if sess.TLS != nil {
280+ sess.TLS.ServerName = host.Domain
281+ }
282 } else {
283 sess.deliveryHosts = sess.fallbackHosts
284 }
285@@ -234,6 +274,7 @@
286 // connect to a server using the configuration in the ClientSession
287 // and set up the connection.
288 func (sess *ClientSession) connect() error {
289+ sess.setShouldDelay()
290 sess.startConnectionAttempt()
291 var err error
292 var conn net.Conn
293@@ -263,7 +304,12 @@
294
295 func (sess *ClientSession) AutoRedial(doneCh chan uint32) {
296 sess.stopRedial()
297+ if time.Since(sess.lastAutoRedial) < 2*time.Second {
298+ sess.setShouldDelay()
299+ }
300+ time.Sleep(sess.redialDelay(sess))
301 sess.retrier = util.NewAutoRedialer(sess)
302+ sess.lastAutoRedial = time.Now()
303 go func() { doneCh <- sess.retrier.Redial() }()
304 }
305
306@@ -289,6 +335,7 @@
307 err := sess.proto.WriteMessage(protocol.PingPongMsg{Type: "pong"})
308 if err == nil {
309 sess.Log.Debugf("ping.")
310+ sess.clearShouldDelay()
311 } else {
312 sess.setState(Error)
313 sess.Log.Errorf("unable to pong: %s", err)
314@@ -330,6 +377,7 @@
315 sess.Log.Errorf("unable to ack broadcast: %s", err)
316 return err
317 }
318+ sess.clearShouldDelay()
319 sess.Log.Debugf("broadcast chan:%v app:%v topLevel:%d payloads:%s",
320 bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads)
321 if bcast.ChanId == protocol.SystemChannelId {
322
323=== modified file 'client/session/session_test.go'
324--- client/session/session_test.go 2014-04-04 13:55:00 +0000
325+++ client/session/session_test.go 2014-04-23 11:53:01 +0000
326@@ -32,12 +32,13 @@
327
328 . "launchpad.net/gocheck"
329
330+ "launchpad.net/ubuntu-push/client/gethosts"
331 "launchpad.net/ubuntu-push/client/session/levelmap"
332- //"launchpad.net/ubuntu-push/client/gethosts"
333 "launchpad.net/ubuntu-push/logger"
334 "launchpad.net/ubuntu-push/protocol"
335 helpers "launchpad.net/ubuntu-push/testing"
336 "launchpad.net/ubuntu-push/testing/condition"
337+ "launchpad.net/ubuntu-push/util"
338 )
339
340 func TestSession(t *testing.T) { TestingT(t) }
341@@ -214,6 +215,10 @@
342 c.Check(sess, NotNil)
343 c.Check(err, IsNil)
344 c.Check(sess.fallbackHosts, DeepEquals, []string{"foo:443"})
345+ // the session is happy and redial delayer is default
346+ c.Check(sess.ShouldDelay(), Equals, false)
347+ c.Check(fmt.Sprintf("%#v", sess.redialDelay), Equals, fmt.Sprintf("%#v", redialDelay))
348+ c.Check(sess.redialDelays, DeepEquals, util.Timeouts())
349 // but no root CAs set
350 c.Check(sess.TLS.RootCAs, IsNil)
351 c.Check(sess.State(), Equals, Disconnected)
352@@ -264,16 +269,17 @@
353 }
354
355 type testHostGetter struct {
356- hosts []string
357- err error
358+ domain string
359+ hosts []string
360+ err error
361 }
362
363-func (thg *testHostGetter) Get() ([]string, error) {
364- return thg.hosts, thg.err
365+func (thg *testHostGetter) Get() (*gethosts.Host, error) {
366+ return &gethosts.Host{thg.domain, thg.hosts}, thg.err
367 }
368
369 func (cs *clientSessionSuite) TestGetHostsRemote(c *C) {
370- hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
371+ hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil}
372 sess := &ClientSession{getHost: hostGetter, timeSince: time.Since}
373 err := sess.getHosts()
374 c.Assert(err, IsNil)
375@@ -284,7 +290,7 @@
376 sess, err := NewSession("", dummyConf, "", cs.lvls, cs.log)
377 c.Assert(err, IsNil)
378 hostsErr := errors.New("failed")
379- hostGetter := &testHostGetter{nil, hostsErr}
380+ hostGetter := &testHostGetter{"", nil, hostsErr}
381 sess.getHost = hostGetter
382 err = sess.getHosts()
383 c.Assert(err, Equals, hostsErr)
384@@ -293,7 +299,7 @@
385 }
386
387 func (cs *clientSessionSuite) TestGetHostsRemoteCaching(c *C) {
388- hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
389+ hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil}
390 sess := &ClientSession{
391 getHost: hostGetter,
392 ClientSessionConfig: ClientSessionConfig{
393@@ -318,7 +324,7 @@
394 }
395
396 func (cs *clientSessionSuite) TestGetHostsRemoteCachingReset(c *C) {
397- hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
398+ hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil}
399 sess := &ClientSession{
400 getHost: hostGetter,
401 ClientSessionConfig: ClientSessionConfig{
402@@ -427,7 +433,9 @@
403 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
404 c.Assert(err, IsNil)
405 sess.deliveryHosts = []string{"nowhere"}
406+ sess.clearShouldDelay()
407 err = sess.connect()
408+ c.Check(sess.ShouldDelay(), Equals, true)
409 c.Check(err, ErrorMatches, ".*connect.*address.*")
410 c.Check(sess.State(), Equals, Error)
411 }
412@@ -439,7 +447,9 @@
413 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
414 c.Assert(err, IsNil)
415 sess.deliveryHosts = []string{srv.Addr().String()}
416+ sess.clearShouldDelay()
417 err = sess.connect()
418+ c.Check(sess.ShouldDelay(), Equals, true)
419 c.Check(err, IsNil)
420 c.Check(sess.Connection, NotNil)
421 c.Check(sess.State(), Equals, Connected)
422@@ -452,7 +462,9 @@
423 sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
424 c.Assert(err, IsNil)
425 sess.deliveryHosts = []string{"nowhere", srv.Addr().String()}
426+ sess.clearShouldDelay()
427 err = sess.connect()
428+ c.Check(sess.ShouldDelay(), Equals, true)
429 c.Check(err, IsNil)
430 c.Check(sess.Connection, NotNil)
431 c.Check(sess.State(), Equals, Connected)
432@@ -466,7 +478,9 @@
433 srv.Close()
434 c.Assert(err, IsNil)
435 sess.deliveryHosts = []string{srv.Addr().String()}
436+ sess.clearShouldDelay()
437 err = sess.connect()
438+ c.Check(sess.ShouldDelay(), Equals, true)
439 c.Check(err, ErrorMatches, ".*connection refused")
440 c.Check(sess.State(), Equals, Error)
441 }
442@@ -548,6 +562,27 @@
443 c.Check(<-ch, Not(Equals), 0)
444 }
445
446+func (cs *clientSessionSuite) TestAutoRedialCallsRedialDelay(c *C) {
447+ sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
448+ c.Assert(err, IsNil)
449+ flag := false
450+ sess.redialDelay = func(sess *ClientSession) time.Duration { flag = true; return 0 }
451+ sess.AutoRedial(nil)
452+ c.Check(flag, Equals, true)
453+}
454+
455+func (cs *clientSessionSuite) TestAutoRedialSetsRedialDelayIfTooQuick(c *C) {
456+ sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
457+ c.Assert(err, IsNil)
458+ sess.redialDelay = func(sess *ClientSession) time.Duration { return 0 }
459+ sess.AutoRedial(nil)
460+ c.Check(sess.ShouldDelay(), Equals, false)
461+ sess.stopRedial()
462+ sess.clearShouldDelay()
463+ sess.AutoRedial(nil)
464+ c.Check(sess.ShouldDelay(), Equals, true)
465+}
466+
467 /****************************************************************
468 handlePing() tests
469 ****************************************************************/
470@@ -594,6 +629,24 @@
471 c.Check(s.sess.State(), Equals, Error)
472 }
473
474+func (s *msgSuite) TestHandlePingClearsDelay(c *C) {
475+ s.sess.setShouldDelay()
476+ s.upCh <- nil // no error
477+ c.Check(s.sess.handlePing(), IsNil)
478+ c.Assert(len(s.downCh), Equals, 1)
479+ c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"})
480+ c.Check(s.sess.ShouldDelay(), Equals, false)
481+}
482+
483+func (s *msgSuite) TestHandlePingDoesNotClearsDelayOnError(c *C) {
484+ s.sess.setShouldDelay()
485+ s.upCh <- errors.New("Pong")
486+ c.Check(s.sess.handlePing(), NotNil)
487+ c.Assert(len(s.downCh), Equals, 1)
488+ c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"})
489+ c.Check(s.sess.ShouldDelay(), Equals, true)
490+}
491+
492 /****************************************************************
493 handleBroadcast() tests
494 ****************************************************************/
495@@ -687,6 +740,32 @@
496 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
497 }
498
499+func (s *msgSuite) TestHandleBroadcastClearsDelay(c *C) {
500+ s.sess.setShouldDelay()
501+
502+ msg := serverMsg{"broadcast", protocol.BroadcastMsg{},
503+ protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
504+ go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
505+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
506+ s.upCh <- nil // ack ok
507+ c.Check(<-s.errCh, IsNil)
508+
509+ c.Check(s.sess.ShouldDelay(), Equals, false)
510+}
511+
512+func (s *msgSuite) TestHandleBroadcastDoesNotClearDelayOnError(c *C) {
513+ s.sess.setShouldDelay()
514+
515+ msg := serverMsg{"broadcast", protocol.BroadcastMsg{},
516+ protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
517+ go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
518+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
519+ s.upCh <- errors.New("bcast")
520+ c.Check(<-s.errCh, NotNil)
521+
522+ c.Check(s.sess.ShouldDelay(), Equals, true)
523+}
524+
525 /****************************************************************
526 handleConnBroken() tests
527 ****************************************************************/
528@@ -1087,9 +1166,64 @@
529
530 var (
531 dialTestTimeout = 100 * time.Millisecond
532- dialTestConf = ClientSessionConfig{ExchangeTimeout: dialTestTimeout}
533+ dialTestConf = ClientSessionConfig{
534+ ExchangeTimeout: dialTestTimeout,
535+ PEM: helpers.TestCertPEMBlock,
536+ }
537 )
538
539+func (cs *clientSessionSuite) TestDialBadServerName(c *C) {
540+ // a borked server name
541+ cert, err := tls.X509KeyPair(helpers.TestCertPEMBlock, helpers.TestKeyPEMBlock)
542+ c.Assert(err, IsNil)
543+ tlsCfg := &tls.Config{
544+ Certificates: []tls.Certificate{cert},
545+ SessionTicketsDisabled: true,
546+ }
547+
548+ lst, err := tls.Listen("tcp", "localhost:0", tlsCfg)
549+ c.Assert(err, IsNil)
550+ // advertise
551+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
552+ b, err := json.Marshal(map[string]interface{}{
553+ "domain": "xyzzy", // <-- *** THIS *** is the bit that'll break it
554+ "hosts": []string{"nowhere", lst.Addr().String()},
555+ })
556+ if err != nil {
557+ panic(err)
558+ }
559+ w.Header().Set("Content-Type", "application/json")
560+ w.Write(b)
561+ }))
562+ defer ts.Close()
563+
564+ sess, err := NewSession(ts.URL, dialTestConf, "wah", cs.lvls, cs.log)
565+ c.Assert(err, IsNil)
566+ tconn := &testConn{}
567+ sess.Connection = tconn
568+
569+ upCh := make(chan interface{}, 5)
570+ downCh := make(chan interface{}, 5)
571+ errCh := make(chan error, 1)
572+ proto := &testProtocol{up: upCh, down: downCh}
573+ sess.Protocolator = func(net.Conn) protocol.Protocol { return proto }
574+
575+ go func() {
576+ errCh <- sess.Dial()
577+ }()
578+
579+ srv, err := lst.Accept()
580+ c.Assert(err, IsNil)
581+
582+ // connect done
583+
584+ _, err = protocol.ReadWireFormatVersion(srv, dialTestTimeout)
585+ c.Check(err, NotNil)
586+
587+ c.Check(<-errCh, NotNil)
588+ c.Check(sess.State(), Equals, Error)
589+}
590+
591 func (cs *clientSessionSuite) TestDialWorks(c *C) {
592 // happy path thoughts
593 cert, err := tls.X509KeyPair(helpers.TestCertPEMBlock, helpers.TestKeyPEMBlock)
594@@ -1104,7 +1238,8 @@
595 // advertise
596 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
597 b, err := json.Marshal(map[string]interface{}{
598- "hosts": []string{"nowhere", lst.Addr().String()},
599+ "domain": "localhost",
600+ "hosts": []string{"nowhere", lst.Addr().String()},
601 })
602 if err != nil {
603 panic(err)
604@@ -1223,3 +1358,38 @@
605 c.Assert(err, IsNil)
606 // connect done
607 }
608+
609+/****************************************************************
610+ redialDelay() tests
611+****************************************************************/
612+
613+func (cs *clientSessionSuite) TestShouldDelay(c *C) {
614+ sess, err := NewSession("foo:443", dummyConf, "", cs.lvls, cs.log)
615+ c.Assert(err, IsNil)
616+ c.Check(sess.ShouldDelay(), Equals, false)
617+ sess.setShouldDelay()
618+ c.Check(sess.ShouldDelay(), Equals, true)
619+ sess.clearShouldDelay()
620+ c.Check(sess.ShouldDelay(), Equals, false)
621+}
622+
623+func (cs *clientSessionSuite) TestRedialDelay(c *C) {
624+ sess, err := NewSession("foo:443", dummyConf, "", cs.lvls, cs.log)
625+ c.Assert(err, IsNil)
626+ sess.redialDelays = []time.Duration{17, 42}
627+ n := 0
628+ sess.redialJitter = func(time.Duration) time.Duration { n++; return 0 }
629+ // we get increasing delays while we're unhappy
630+ sess.setShouldDelay()
631+ c.Check(redialDelay(sess), Equals, time.Duration(17))
632+ c.Check(redialDelay(sess), Equals, time.Duration(42))
633+ c.Check(redialDelay(sess), Equals, time.Duration(42))
634+ // once we're happy, delays drop to 0
635+ sess.clearShouldDelay()
636+ c.Check(redialDelay(sess), Equals, time.Duration(0))
637+ // and start again from the top if we become unhappy again
638+ sess.setShouldDelay()
639+ c.Check(redialDelay(sess), Equals, time.Duration(17))
640+ // and redialJitter got called every time shouldDelay was true
641+ c.Check(n, Equals, 4)
642+}
643
644=== modified file 'config/config.go'
645--- config/config.go 2014-03-25 18:49:18 +0000
646+++ config/config.go 2014-04-23 11:53:01 +0000
647@@ -20,6 +20,7 @@
648 import (
649 "encoding/json"
650 "errors"
651+ "flag"
652 "fmt"
653 "io"
654 "io/ioutil"
655@@ -27,6 +28,7 @@
656 "os"
657 "path/filepath"
658 "reflect"
659+ "strconv"
660 "strings"
661 "time"
662 )
663@@ -118,6 +120,22 @@
664 return fillDestConfig(destValue, p1)
665 }
666
667+// FromString are config holders that can be set by parsing a string.
668+type FromString interface {
669+ SetFromString(enc string) error
670+}
671+
672+// UnmarshalJSONViaString helps unmarshalling from JSON for FromString
673+// supporting config holders.
674+func UnmarshalJSONViaString(dest FromString, b []byte) error {
675+ var enc string
676+ err := json.Unmarshal(b, &enc)
677+ if err != nil {
678+ return err
679+ }
680+ return dest.SetFromString(enc)
681+}
682+
683 // ConfigTimeDuration can hold a time.Duration in a configuration struct,
684 // that is parsed from a string as supported by time.ParseDuration.
685 type ConfigTimeDuration struct {
686@@ -125,13 +143,11 @@
687 }
688
689 func (ctd *ConfigTimeDuration) UnmarshalJSON(b []byte) error {
690- var enc string
691- var v time.Duration
692- err := json.Unmarshal(b, &enc)
693- if err != nil {
694- return err
695- }
696- v, err = time.ParseDuration(enc)
697+ return UnmarshalJSONViaString(ctd, b)
698+}
699+
700+func (ctd *ConfigTimeDuration) SetFromString(enc string) error {
701+ v, err := time.ParseDuration(enc)
702 if err != nil {
703 return err
704 }
705@@ -148,12 +164,11 @@
706 type ConfigHostPort string
707
708 func (chp *ConfigHostPort) UnmarshalJSON(b []byte) error {
709- var enc string
710- err := json.Unmarshal(b, &enc)
711- if err != nil {
712- return err
713- }
714- _, _, err = net.SplitHostPort(enc)
715+ return UnmarshalJSONViaString(chp, b)
716+}
717+
718+func (chp *ConfigHostPort) SetFromString(enc string) error {
719+ _, _, err := net.SplitHostPort(enc)
720 if err != nil {
721 return err
722 }
723@@ -198,23 +213,117 @@
724 return ioutil.ReadFile(p)
725 }
726
727-// ReadFiles reads configuration from a set of files. Uses ReadConfig internally.
728+// used to implement getting config values with flag.Parse()
729+type val struct {
730+ destField destField
731+ accu map[string]json.RawMessage
732+}
733+
734+func (v *val) String() string { // used to show default
735+ return string(v.accu[v.destField.configName()])
736+}
737+
738+func (v *val) IsBoolFlag() bool {
739+ return v.destField.fld.Type.Kind() == reflect.Bool
740+}
741+
742+func (v *val) marshalAsNeeded(s string) (json.RawMessage, error) {
743+ var toMarshal interface{}
744+ switch v.destField.dest.(type) {
745+ case *string, FromString:
746+ toMarshal = s
747+ case *bool:
748+ bit, err := strconv.ParseBool(s)
749+ if err != nil {
750+ return nil, err
751+ }
752+ toMarshal = bit
753+ default:
754+ return json.RawMessage(s), nil
755+ }
756+ return json.Marshal(toMarshal)
757+}
758+
759+func (v *val) Set(s string) error {
760+ marshalled, err := v.marshalAsNeeded(s)
761+ if err != nil {
762+ return err
763+ }
764+ v.accu[v.destField.configName()] = marshalled
765+ return nil
766+}
767+
768+func readOneConfig(accu map[string]json.RawMessage, cfgPath string) error {
769+ r, err := os.Open(cfgPath)
770+ if err != nil {
771+ return err
772+ }
773+ defer r.Close()
774+ err = json.NewDecoder(r).Decode(&accu)
775+ if err != nil {
776+ return err
777+ }
778+ return nil
779+}
780+
781+// used to implement -cfg@=
782+type readConfigAtVal struct {
783+ accu map[string]json.RawMessage
784+}
785+
786+func (v *readConfigAtVal) String() string {
787+ return "<config.json>"
788+}
789+
790+func (v *readConfigAtVal) Set(path string) error {
791+ return readOneConfig(v.accu, path)
792+}
793+
794+// readUsingFlags gets config values from command line flags.
795+func readUsingFlags(accu map[string]json.RawMessage, destValue reflect.Value) error {
796+ if flag.Parsed() {
797+ if IgnoreParsedFlags {
798+ return nil
799+ }
800+ return fmt.Errorf("too late, flags already parsed")
801+ }
802+ destStruct := destValue.Elem()
803+ for destField := range traverseStruct(destStruct) {
804+ help := destField.fld.Tag.Get("help")
805+ flag.Var(&val{destField, accu}, destField.configName(), help)
806+ }
807+ flag.Var(&readConfigAtVal{accu}, "cfg@", "get config values from file")
808+ flag.Parse()
809+ return nil
810+}
811+
812+// IgnoreParsedFlags will just have ReadFiles ignore <flags> if the
813+// command line was already parsed.
814+var IgnoreParsedFlags = false
815+
816+// ReadFiles reads configuration from a set of files. The string
817+// "<flags>" can be used as a pseudo file-path, it will consider
818+// command line flags, invoking flag.Parse(). Among those the flag
819+// -cfg@=FILE can be used to get further config values from FILE.
820 func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
821 destValue, err := checkDestConfig("destConfig", destConfig)
822 if err != nil {
823 return err
824 }
825 // do the parsing in two phases for better error handling
826- var p1 map[string]json.RawMessage
827+ p1 := make(map[string]json.RawMessage)
828 readOne := false
829 for _, cfgPath := range cfgFpaths {
830+ if cfgPath == "<flags>" {
831+ err := readUsingFlags(p1, destValue)
832+ if err != nil {
833+ return err
834+ }
835+ readOne = true
836+ continue
837+ }
838 if _, err := os.Stat(cfgPath); err == nil {
839- r, err := os.Open(cfgPath)
840- if err != nil {
841- return err
842- }
843- defer r.Close()
844- err = json.NewDecoder(r).Decode(&p1)
845+ err := readOneConfig(p1, cfgPath)
846 if err != nil {
847 return err
848 }
849
850=== modified file 'config/config_test.go'
851--- config/config_test.go 2014-03-25 18:49:18 +0000
852+++ config/config_test.go 2014-04-23 11:53:01 +0000
853@@ -18,6 +18,9 @@
854
855 import (
856 "bytes"
857+ "encoding/json"
858+ "flag"
859+ "fmt"
860 "io/ioutil"
861 "os"
862 "path/filepath"
863@@ -230,3 +233,105 @@
864 c.Check(res, DeepEquals, []string{"b", "c_list", "d"})
865
866 }
867+
868+type testConfig3 struct {
869+ A bool
870+ B string
871+ C []string `json:"c_list"`
872+ D ConfigTimeDuration `help:"duration"`
873+ E ConfigHostPort
874+ F string
875+}
876+
877+type configFlagsSuite struct{}
878+
879+var _ = Suite(&configFlagsSuite{})
880+
881+func (s *configFlagsSuite) SetUpTest(c *C) {
882+ flag.CommandLine = flag.NewFlagSet("cmd", flag.PanicOnError)
883+ // supress outputs
884+ flag.Usage = func() { flag.PrintDefaults() }
885+ flag.CommandLine.SetOutput(ioutil.Discard)
886+}
887+
888+func (s *configFlagsSuite) TestReadUsingFlags(c *C) {
889+ os.Args = []string{"cmd", "-a=1", "-b=foo", "-c_list", `["x","y"]`, "-d", "10s", "-e=localhost:80"}
890+ var cfg testConfig3
891+ p := make(map[string]json.RawMessage)
892+ err := readUsingFlags(p, reflect.ValueOf(&cfg))
893+ c.Assert(err, IsNil)
894+ c.Check(p, DeepEquals, map[string]json.RawMessage{
895+ "a": json.RawMessage("true"),
896+ "b": json.RawMessage(`"foo"`),
897+ "c_list": json.RawMessage(`["x","y"]`),
898+ "d": json.RawMessage(`"10s"`),
899+ "e": json.RawMessage(`"localhost:80"`),
900+ })
901+}
902+
903+func (s *configFlagsSuite) TestReadUsingFlagsBoolError(c *C) {
904+ os.Args = []string{"cmd", "-a=zoo"}
905+ var cfg testConfig3
906+ p := make(map[string]json.RawMessage)
907+ c.Check(func() { readUsingFlags(p, reflect.ValueOf(&cfg)) }, PanicMatches, ".*invalid boolean.*-a.*")
908+}
909+
910+func (s *configFlagsSuite) TestReadFilesAndFlags(c *C) {
911+ // test <flags> pseudo file
912+ os.Args = []string{"cmd", "-b=x"}
913+ tmpDir := c.MkDir()
914+ cfgPath := filepath.Join(tmpDir, "cfg.json")
915+ err := ioutil.WriteFile(cfgPath, []byte(`{"a": 42, "c_list": ["y", "z"]}`), os.ModePerm)
916+ c.Assert(err, IsNil)
917+ var cfg testConfig1
918+ err = ReadFiles(&cfg, cfgPath, "<flags>")
919+ c.Assert(err, IsNil)
920+ c.Check(cfg.A, Equals, 42)
921+ c.Check(cfg.B, Equals, "x")
922+ c.Check(cfg.C, DeepEquals, []string{"y", "z"})
923+}
924+
925+func (s *configFlagsSuite) TestReadFilesAndFlagsConfigAtSupport(c *C) {
926+ // test <flags> pseudo file
927+ tmpDir := c.MkDir()
928+ cfgPath := filepath.Join(tmpDir, "cfg.json")
929+ os.Args = []string{"cmd", "-a=42", fmt.Sprintf("-cfg@=%s", cfgPath)}
930+ err := ioutil.WriteFile(cfgPath, []byte(`{"b": "x", "c_list": ["y", "z"]}`), os.ModePerm)
931+ c.Assert(err, IsNil)
932+ var cfg testConfig1
933+ err = ReadFiles(&cfg, "<flags>")
934+ c.Assert(err, IsNil)
935+ c.Check(cfg.A, Equals, 42)
936+ c.Check(cfg.B, Equals, "x")
937+ c.Check(cfg.C, DeepEquals, []string{"y", "z"})
938+}
939+
940+func (s *configFlagsSuite) TestReadUsingFlagsHelp(c *C) {
941+ os.Args = []string{"cmd", "-h"}
942+ buf := bytes.NewBufferString("")
943+ flag.CommandLine.Init("cmd", flag.ContinueOnError)
944+ flag.CommandLine.SetOutput(buf)
945+ var cfg testConfig3
946+ p := map[string]json.RawMessage{
947+ "d": json.RawMessage(`"2s"`),
948+ }
949+ readUsingFlags(p, reflect.ValueOf(&cfg))
950+ c.Check(buf.String(), Matches, `(?s).*-cfg@=<config.json>: get config values from file\n.*-d="2s": duration.*`)
951+}
952+
953+func (s *configFlagsSuite) TestReadUsingFlagsAlreadyParsed(c *C) {
954+ os.Args = []string{"cmd"}
955+ flag.Parse()
956+ var cfg struct{}
957+ p := make(map[string]json.RawMessage)
958+ err := readUsingFlags(p, reflect.ValueOf(&cfg))
959+ c.Assert(err, ErrorMatches, "too late, flags already parsed")
960+ err = ReadFiles(&cfg, "<flags>")
961+ c.Assert(err, ErrorMatches, "too late, flags already parsed")
962+ IgnoreParsedFlags = true
963+ defer func() {
964+ IgnoreParsedFlags = false
965+ }()
966+ err = ReadFiles(&cfg, "<flags>")
967+ c.Assert(err, IsNil)
968+}
969
970=== modified file 'debian/changelog'
971--- debian/changelog 2014-04-11 18:31:57 +0000
972+++ debian/changelog 2014-04-23 11:53:01 +0000
973@@ -1,3 +1,22 @@
974+ubuntu-push (0.2.1-0ubuntu1) UNRELEASED; urgency=high
975+
976+ [ Samuele Pedroni ]
977+ * gave the client the ability to get config from commandline
978+ ( => easier automated testing) (LP: #1311600)
979+
980+ [ John Lenton ]
981+ * Ensure ubuntu-push-client is the only one running in the session.
982+ (LP: #1309432)
983+ * Remove supurious numbers in brackets in notifications. (LP: #1308145)
984+ * Check the server certificate and server name. (LP: #1297969)
985+ * Loop whoopsie_identifier_generate until it starts working. (LP: #1309237)
986+ * In the session: set a flag on connect, clear it on successfully
987+ replying to ping or broadcast messages, check it at the top of
988+ autoredial. Also track the last autoredial, and set the delay flag if
989+ autoredial is re-called too quickly. (LP: #1309231)
990+
991+ -- John Lenton <john.lenton@canonical.com> Fri, 18 Apr 2014 10:42:31 +0100
992+
993 ubuntu-push (0.2+14.04.20140411-0ubuntu1) trusty; urgency=medium
994
995 [ John Lenton ]
996
997=== modified file 'logger/logger.go'
998--- logger/logger.go 2014-02-24 10:27:38 +0000
999+++ logger/logger.go 2014-04-23 11:53:01 +0000
1000@@ -23,6 +23,8 @@
1001 "log"
1002 "os"
1003 "runtime"
1004+
1005+ "launchpad.net/ubuntu-push/config"
1006 )
1007
1008 // Logger is a simple logger interface with logging at levels.
1009@@ -119,3 +121,28 @@
1010 lg.outputFunc(2, fmt.Sprintf("DEBUG "+format, v...))
1011 }
1012 }
1013+
1014+// config bits
1015+
1016+// ConfigLogLevel can hold a log level in a configuration struct.
1017+type ConfigLogLevel string
1018+
1019+func (cll *ConfigLogLevel) ConfigFromJSONString() {}
1020+
1021+func (cll *ConfigLogLevel) UnmarshalJSON(b []byte) error {
1022+ return config.UnmarshalJSONViaString(cll, b)
1023+}
1024+
1025+func (cll *ConfigLogLevel) SetFromString(enc string) error {
1026+ _, ok := levelToNLevel[enc]
1027+ if !ok {
1028+ return fmt.Errorf("not a log level: %s", enc)
1029+ }
1030+ *cll = ConfigLogLevel(enc)
1031+ return nil
1032+}
1033+
1034+// Level returns the log level string held in cll.
1035+func (cll ConfigLogLevel) Level() string {
1036+ return string(cll)
1037+}
1038
1039=== modified file 'logger/logger_test.go'
1040--- logger/logger_test.go 2014-02-10 22:51:43 +0000
1041+++ logger/logger_test.go 2014-04-23 11:53:01 +0000
1042@@ -25,6 +25,8 @@
1043 "testing"
1044
1045 . "launchpad.net/gocheck"
1046+
1047+ "launchpad.net/ubuntu-push/config"
1048 )
1049
1050 func TestLogger(t *testing.T) { TestingT(t) }
1051@@ -138,3 +140,26 @@
1052 logger.Output(1, "foobaz")
1053 c.Check(buf.String(), Matches, "logger_test.go:[0-9]+: foobar\nlogger_test.go:[0-9]+: foobaz\n")
1054 }
1055+
1056+type testLogLevelConfig struct {
1057+ Lvl ConfigLogLevel
1058+}
1059+
1060+func (s *loggerSuite) TestReadConfigLogLevel(c *C) {
1061+ buf := bytes.NewBufferString(`{"lvl": "debug"}`)
1062+ var cfg testLogLevelConfig
1063+ err := config.ReadConfig(buf, &cfg)
1064+ c.Assert(err, IsNil)
1065+ c.Check(cfg.Lvl.Level(), Equals, "debug")
1066+}
1067+
1068+func (s *loggerSuite) TestReadConfigLogLevelErrors(c *C) {
1069+ var cfg testLogLevelConfig
1070+ checkError := func(jsonCfg string, expectedError string) {
1071+ buf := bytes.NewBufferString(jsonCfg)
1072+ err := config.ReadConfig(buf, &cfg)
1073+ c.Check(err, ErrorMatches, expectedError)
1074+ }
1075+ checkError(`{"lvl": 1}`, "lvl:.*type string")
1076+ checkError(`{"lvl": "foo"}`, "lvl: not a log level: foo")
1077+}
1078
1079=== modified file 'server/acceptance/cmd/acceptanceclient.go'
1080--- server/acceptance/cmd/acceptanceclient.go 2014-04-10 13:52:31 +0000
1081+++ server/acceptance/cmd/acceptanceclient.go 2014-04-23 11:53:01 +0000
1082@@ -48,13 +48,18 @@
1083 fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] <config.json> <device id>\n")
1084 flag.PrintDefaults()
1085 }
1086+ missingArg := func(what string) {
1087+ fmt.Fprintf(os.Stderr, "missing %s\n", what)
1088+ flag.Usage()
1089+ os.Exit(2)
1090+ }
1091 flag.Parse()
1092 narg := flag.NArg()
1093 switch {
1094 case narg < 1:
1095- log.Fatal("missing config file")
1096+ missingArg("config file")
1097 case narg < 2:
1098- log.Fatal("missing device-id")
1099+ missingArg("device-id")
1100 }
1101 configFName := flag.Arg(0)
1102 f, err := os.Open(configFName)
1103
1104=== modified file 'ubuntu-push-client.go'
1105--- ubuntu-push-client.go 2014-03-12 13:25:20 +0000
1106+++ ubuntu-push-client.go 2014-04-23 11:53:01 +0000
1107@@ -19,12 +19,37 @@
1108 import (
1109 "log"
1110
1111+ "launchpad.net/go-dbus/v1"
1112 "launchpad.net/go-xdg/v0"
1113
1114 "launchpad.net/ubuntu-push/client"
1115 )
1116
1117+const NAME = "com.ubuntu.PushNotifications"
1118+
1119+// grabName grabs ownership of the dbus name, and bails the client as
1120+// soon as somebody else grabs it.
1121+func grabName() {
1122+ conn, err := dbus.Connect(dbus.SessionBus)
1123+ if err != nil {
1124+ log.Fatalf("bus: %v", err)
1125+ }
1126+
1127+ flags := dbus.NameFlagAllowReplacement | dbus.NameFlagReplaceExisting
1128+ n := conn.RequestName(NAME, flags)
1129+ go func() {
1130+ for err := range n.C {
1131+ if err != nil {
1132+ log.Fatalf("FATAL: name channel got: %v", err)
1133+ }
1134+ }
1135+ }()
1136+}
1137+
1138 func main() {
1139+ // XXX: this is a quick hack to ensure unicity
1140+ grabName()
1141+
1142 cfgFname, err := xdg.Config.Find("ubuntu-push-client/config.json")
1143 if err != nil {
1144 log.Fatalf("unable to find a configuration file: %v", err)
1145
1146=== modified file 'whoopsie/identifier/identifier.go'
1147--- whoopsie/identifier/identifier.go 2014-02-21 16:17:28 +0000
1148+++ whoopsie/identifier/identifier.go 2014-04-23 11:53:01 +0000
1149@@ -27,6 +27,7 @@
1150 import "C"
1151 import "unsafe"
1152 import "errors"
1153+import "time"
1154
1155 // an Id knows how to generate itself, and how to stringify itself.
1156 type Id interface {
1157@@ -36,12 +37,17 @@
1158
1159 // Identifier is the default Id implementation.
1160 type Identifier struct {
1161- value string
1162+ value string
1163+ generator func(**C.char, **C.GError)
1164+}
1165+
1166+func generator(csp **C.char, errp **C.GError) {
1167+ C.whoopsie_identifier_generate(csp, errp)
1168 }
1169
1170 // New creates an Identifier, but does not call Generate() on it.
1171 func New() Id {
1172- return &Identifier{}
1173+ return &Identifier{generator: generator}
1174 }
1175
1176 // Generate makes the Identifier create the identifier itself.
1177@@ -49,8 +55,18 @@
1178 var gerr *C.GError
1179 var cs *C.char
1180 defer C.g_free((C.gpointer)(unsafe.Pointer(cs)))
1181- C.whoopsie_identifier_generate(&cs, &gerr)
1182-
1183+
1184+ for i := 0; i < 200; i++ {
1185+ id.generator(&cs, &gerr)
1186+
1187+ if cs != nil || gerr != nil {
1188+ goto SuccessMaybe
1189+ }
1190+ time.Sleep(600 * time.Millisecond)
1191+ }
1192+ return errors.New("whoopsie_identifier_generate still bad after 2m; giving up")
1193+
1194+SuccessMaybe:
1195 if gerr != nil {
1196 return errors.New(C.GoString((*C.char)(gerr.message)))
1197 } else {
1198
1199=== modified file 'whoopsie/identifier/identifier_test.go'
1200--- whoopsie/identifier/identifier_test.go 2014-01-15 15:51:50 +0000
1201+++ whoopsie/identifier/identifier_test.go 2014-04-23 11:53:01 +0000
1202@@ -41,3 +41,18 @@
1203 func (s *IdentifierSuite) TestIdentifierInterface(c *C) {
1204 _ = []Id{New()}
1205 }
1206+
1207+// TestFailure checks that Identifier survives whoopsie shenanigans
1208+func (s *IdentifierSuite) TestIdentifierSurvivesShenanigans(c *C) {
1209+ count := 0
1210+ // using _Ctype* as a workaround for gocheck also having a C
1211+ gen := func(csp **_Ctype_char, errp **_Ctype_GError) {
1212+ count++
1213+ if count > 3 {
1214+ generator(csp, errp)
1215+ }
1216+ }
1217+ id := &Identifier{generator: gen}
1218+ id.Generate()
1219+ c.Check(id.String(), HasLen, 128)
1220+}

Subscribers

People subscribed via source and target branches