Merge lp:~robru/friends/refresh-fixup into lp:friends

Proposed by Robert Bruce Park
Status: Merged
Merge reported by: Robert Bruce Park
Merged at revision: not available
Proposed branch: lp:~robru/friends/refresh-fixup
Merge into: lp:friends
Diff against target: 833 lines (+160/-139)
24 files modified
friends-service.sh (+1/-1)
friends/main.py (+4/-2)
friends/protocols/facebook.py (+1/-1)
friends/protocols/flickr.py (+1/-1)
friends/protocols/foursquare.py (+1/-1)
friends/protocols/twitter.py (+6/-7)
friends/service/connection.py (+1/-1)
friends/service/dispatcher.py (+2/-1)
friends/service/shortener.py (+1/-1)
friends/testing/mocks.py (+3/-3)
friends/testing/service.py (+1/-1)
friends/tests/test_authentication.py (+15/-9)
friends/tests/test_download.py (+14/-8)
friends/tests/test_facebook.py (+2/-0)
friends/tests/test_flickr.py (+16/-2)
friends/tests/test_logging.py (+16/-37)
friends/utils/account.py (+1/-1)
friends/utils/authentication.py (+15/-15)
friends/utils/avatar.py (+1/-1)
friends/utils/base.py (+8/-3)
friends/utils/download.py (+4/-11)
friends/utils/logging.py (+24/-11)
friends/utils/menus.py (+1/-1)
tools/debug_live.py (+21/-20)
To merge this branch: bzr merge lp:~robru/friends/refresh-fixup
Reviewer Review Type Date Requested Status
Robert Bruce Park Approve
Review via email: mp+130456@code.launchpad.net

Commit message

Workaround for Authentication.login hanging, which was also causing Dispatcher.Refresh to hang thanks to it's use of thread.join(). Authentication.login was using an extraneous GObject MainLoop for the purposes of blocking it's thread synchronously until an async callback got called, but that was a dain bramaged approach because not only does it interfere with the primary mainloop, but Signon.AuthSession.process seems to have a bug where the callback often won't be called, causing the mainloop to block forever.

I've replaced this mainloop with a while loop that calls time.sleep() a limited number of times. The advantage here is that we have direct control over how many times time.sleep() gets called and never have to worry about waiting for a callback that might never come to unblock us.

This commit also makes sweeping changes to the logging infrastructure, which were necessary to track down this bug, and generally beneficial, so they're being included rather than reverted. Logging is now more verbose, and makes extensive use of the "logging.getLogger(__name__)" idiom for finer-grained data on which log is coming from where. debug_live.py also received an overhaul in order to better accomodate testing.

Description of the change

This branch fixes one of the major causes of dispatcher.Refresh hanging, in particular with Authentication.login, in the event that the server did not return any authentication data, it would just hang forever rather than returning properly. This branch converts the incorrect usage of a seconday GObject MainLoop into a much simpler threading.Lock, which serves the same purpose, doesn't interfere with the primary MainLoop, and allows to code to go on to raise existing exceptions when no authentication data is received.

I'm afraid there may be more conditions in which dispatcher.Refresh will hang, but they've been much more intermittent and difficult to pinpoint thus far. But this should still be merged as-is because it's an important fix to a really stupid part of old gwibber code that somehow managed to survive into the friends-service transition.

To post a comment you must log in.
Revision history for this message
Robert Bruce Park (robru) wrote :

This branch also initiates a dispatcher.Refresh on friends-service launch, so that we don't have to wait 15 minutes for the first batch of data to be downloaded ;-)

lp:~robru/friends/refresh-fixup updated
14. By Robert Bruce Park

Drop extraneous instance attribute from Authentication class.

This commit also does some minor cleanups, and introduces some new
debugging statements that may prove helpful in tracking down any
future hangs we discover.

Authentication._authenticating turned out to be redundant, I've
dropped it in favor of testing for the presence of self._reply
directly. Authentication.login sees that self._reply isn't populated
yet, so it blocks, and then the callback populates that value and
stops the lock, allowing login() to return that value.

Revision history for this message
Robert Bruce Park (robru) wrote :

Ken, I've added a couple log.debug statements at the beginning and ending of our Thread.run subclass, so that now it should be really obvious to us in the debugging output when threads are starting and stopping. I still can no longer reproduce any more hangs in dispatcher.Refresh, I've let it run for quite a while with many iterations and no issues.

Please merge and then we'll just have to keep an eye out for any hangs if they return in the future.

lp:~robru/friends/refresh-fixup updated
15. By Robert Bruce Park

Set self._reply sooner, and unconditionally.

16. By Robert Bruce Park

Test suite fixups ;-)

17. By Robert Bruce Park

Replace threading.Lock() with time.sleep()

Yes, yes, I know time.sleep() is ugly, but this is looking like the
only way I can get this working quite right. The previous attempts
using threading.Lock and GObject.MainLoops both had very subtle hangs
that basically screwed up everything.

18. By Robert Bruce Park

Make logging *much* more explicit about what is being logged from where.

19. By Robert Bruce Park

Overhaul debug_live.py to make it compatible with Authentication changes.

20. By Robert Bruce Park

Update tests to no longer expect HTTPError to be raised.

This brings us much more into line with the Soup way of doing things,
rather than trying to force urllib-like behavior on top of Soup.

21. By Robert Bruce Park

Resurrect logging from friends-service.sh

22. By Robert Bruce Park

Overhaul a lot of the logging logic.

Logging now works in both friends-service and in debug_live. I found a
workaround for that duplicate-records bug. Tests have been updated.

23. By Robert Bruce Park

Fix the last of the test failures.

24. By Robert Bruce Park

Greatly simplify the logging logic. Was overthinking it before...

25. By Robert Bruce Park

Simplify debug_live.py

26. By Robert Bruce Park

debug_live.py doesn't need to be on a loop anymore.

The looping of it was primarily useful before we had the dispatcher
actually running. Now that we have the dispatcher, debug_live is more
just for firing single messages.

Revision history for this message
Robert Bruce Park (robru) wrote :

All tests pass, friends-service ran all overnight with no hangs, all good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'friends-service.sh'
2--- friends-service.sh 2012-10-18 17:48:42 +0000
3+++ friends-service.sh 2012-10-20 00:36:21 +0000
4@@ -1,3 +1,3 @@
5 #! /bin/sh
6
7-PYTHONPATH=. python3 -m friends.main -do
8+PYTHONPATH=. python3 -m friends.main --debug --console
9
10=== modified file 'friends/main.py'
11--- friends/main.py 2012-10-15 18:56:26 +0000
12+++ friends/main.py 2012-10-20 00:36:21 +0000
13@@ -26,7 +26,9 @@
14 import logging
15
16 from dbus.mainloop.glib import DBusGMainLoop
17-from gi.repository import Gio, GLib
18+from gi.repository import Gio, GLib, GObject
19+
20+GObject.threads_init(None)
21
22 from friends.service.connection import ConnectionMonitor
23 from friends.service.dispatcher import Dispatcher
24@@ -56,7 +58,7 @@
25 gsettings = Gio.Settings.new('org.gwibber.preferences')
26 initialize(console=args.console,
27 debug=args.debug or gsettings.get_boolean('debug'))
28- log = logging.getLogger('friends.service')
29+ log = logging.getLogger(__name__)
30 log.info('Friends backend service starting')
31
32 # Set up the DBus main loop.
33
34=== modified file 'friends/protocols/facebook.py'
35--- friends/protocols/facebook.py 2012-10-16 04:10:50 +0000
36+++ friends/protocols/facebook.py 2012-10-20 00:36:21 +0000
37@@ -37,7 +37,7 @@
38 ME_URL = API_BASE.format(id='me')
39
40
41-log = logging.getLogger('friends.service')
42+log = logging.getLogger(__name__)
43
44
45 class Facebook(Base):
46
47=== modified file 'friends/protocols/flickr.py'
48--- friends/protocols/flickr.py 2012-10-13 01:27:15 +0000
49+++ friends/protocols/flickr.py 2012-10-20 00:36:21 +0000
50@@ -28,7 +28,7 @@
51 from friends.utils.time import iso8601utc, parsetime
52
53
54-log = logging.getLogger('friends.service')
55+log = logging.getLogger(__name__)
56
57
58 # This is contact information for the Flickr REST service.
59
60=== modified file 'friends/protocols/foursquare.py'
61--- friends/protocols/foursquare.py 2012-10-13 01:27:15 +0000
62+++ friends/protocols/foursquare.py 2012-10-20 00:36:21 +0000
63@@ -28,7 +28,7 @@
64 from friends.utils.time import iso8601utc
65
66
67-log = logging.getLogger('friends.service')
68+log = logging.getLogger(__name__)
69
70
71 # The '&v=YYYYMMDD' defines the date that the API was last confirmed to be
72
73=== modified file 'friends/protocols/twitter.py'
74--- friends/protocols/twitter.py 2012-10-16 04:10:50 +0000
75+++ friends/protocols/twitter.py 2012-10-20 00:36:21 +0000
76@@ -34,17 +34,16 @@
77 from friends.utils.time import parsetime, iso8601utc
78
79
80-log = logging.getLogger('friends.service')
81+log = logging.getLogger(__name__)
82
83
84 # https://dev.twitter.com/docs/api/1.1
85 class Twitter(Base):
86- # StatusNet claims to mimick the Twitter API very closely (so closely to
87- # the point that they refer you to the Twitter API reference docs as a
88- # starting point for learning about their API). So these prefixes are
89- # defined here as class attributes instead of the usual module globals, in
90- # the hopes that the StatusNet class will be able to subclass Twitter and
91- # change only the URLs, with minimal other changes, and magically work.
92+ # Identi.ca's API mimicks Twitter's to such a high degree that it
93+ # is implemented just as a subclass of this, hence we need these
94+ # constants defined as instance attributes, so that the Identica
95+ # class can override them. If you make any changes to this class
96+ # you must confirm that your changes do not break Identi.ca!
97 _api_base = 'https://api.twitter.com/1.1/{endpoint}.json'
98
99 _timeline = _api_base.format(endpoint='statuses/{}_timeline')
100
101=== modified file 'friends/service/connection.py'
102--- friends/service/connection.py 2012-10-13 01:27:15 +0000
103+++ friends/service/connection.py 2012-10-20 00:36:21 +0000
104@@ -50,7 +50,7 @@
105 DISCONNECTED = 4
106
107
108-log = logging.getLogger('friends.service')
109+log = logging.getLogger(__name__)
110 _State = None
111
112
113
114=== modified file 'friends/service/dispatcher.py'
115--- friends/service/dispatcher.py 2012-10-13 01:27:15 +0000
116+++ friends/service/dispatcher.py 2012-10-20 00:36:21 +0000
117@@ -34,7 +34,7 @@
118 from friends.utils.manager import protocol_manager
119 from friends.utils.signaler import signaler
120
121-log = logging.getLogger('friends.service')
122+log = logging.getLogger(__name__)
123
124
125 class Dispatcher(dbus.service.Object):
126@@ -53,6 +53,7 @@
127 signaler.add_signal('ConnectionOnline', self._on_connection_online)
128 signaler.add_signal('ConnectionOffline', self._on_connection_offline)
129 self._on_connection_online()
130+ self.Refresh()
131
132 def _on_connection_online(self):
133 if not self._timer_id:
134
135=== modified file 'friends/service/shortener.py'
136--- friends/service/shortener.py 2012-10-13 01:27:15 +0000
137+++ friends/service/shortener.py 2012-10-20 00:36:21 +0000
138@@ -27,7 +27,7 @@
139
140 from friends.shorteners import lookup
141
142-log = logging.getLogger('friends.service')
143+log = logging.getLogger(__name__)
144
145
146 class URLShorten(dbus.service.Object):
147
148=== modified file 'friends/testing/mocks.py'
149--- friends/testing/mocks.py 2012-10-17 18:55:49 +0000
150+++ friends/testing/mocks.py 2012-10-20 00:36:21 +0000
151@@ -168,7 +168,7 @@
152 """
153 def __init__(self, *modules):
154 self._queue = Queue()
155- self._log = logging.getLogger('friends.test')
156+ self._log = logging.getLogger(__name__)
157 handler = QueueHandler(self._queue)
158 formatter = logging.Formatter(LOG_FORMAT, style='{')
159 handler.setFormatter(formatter)
160@@ -205,8 +205,8 @@
161 self.empty()
162 for patcher in self._patchers:
163 patcher.stop()
164- # Get rid of the friends.test logger.
165- del logging.Logger.manager.loggerDict['friends.test']
166+ # Get rid of the mock logger.
167+ del logging.Logger.manager.loggerDict[__name__]
168
169 def empty(self, trim=True):
170 """Return all the log messages written to this log.
171
172=== modified file 'friends/testing/service.py'
173--- friends/testing/service.py 2012-10-13 01:27:15 +0000
174+++ friends/testing/service.py 2012-10-20 00:36:21 +0000
175@@ -28,7 +28,7 @@
176 from friends.utils.signaler import signaler
177
178
179-log = logging.getLogger('friends.service')
180+log = logging.getLogger(__name__)
181
182
183 class TestService(dbus.service.Object):
184
185=== modified file 'friends/tests/test_authentication.py'
186--- friends/tests/test_authentication.py 2012-10-13 01:27:15 +0000
187+++ friends/tests/test_authentication.py 2012-10-20 00:36:21 +0000
188@@ -62,6 +62,11 @@
189 def error(self, message, *args):
190 self.error_messages.append(message.format(*args))
191
192+ reset = __init__
193+
194+
195+logger = Logger()
196+
197
198 class TestAuthentication(unittest.TestCase):
199 """Test authentication."""
200@@ -72,28 +77,29 @@
201 self.account.auth.method = 'some method'
202 self.account.auth.parameters = 'change me'
203 self.account.auth.mechanism = ['whatever']
204- self.logger = Logger()
205+ logger.reset()
206
207+ @mock.patch('friends.utils.authentication.log', logger)
208 @mock.patch('friends.utils.authentication.Signon', FakeSignon)
209 def test_successful_login(self):
210 # Prevent an error in the callback.
211 self.account.auth.parameters = False
212- authenticator = Authentication(self.account, self.logger)
213+ authenticator = Authentication(self.account)
214 reply = authenticator.login()
215 self.assertEqual(reply, 'auth reply')
216- self.assertEqual(self.logger.debug_messages,
217- ['Login completed'])
218- self.assertEqual(self.logger.error_messages, [])
219+ self.assertEqual(logger.debug_messages, ['Login completed'])
220+ self.assertEqual(logger.error_messages, [])
221
222+ @mock.patch('friends.utils.authentication.log', logger)
223 @mock.patch('friends.utils.authentication.Signon', FakeSignon)
224 def test_failed_login(self):
225 # Trigger an error in the callback.
226 class Error:
227 message = 'who are you?'
228 self.account.auth.parameters = Error
229- authenticator = Authentication(self.account, self.logger)
230+ authenticator = Authentication(self.account)
231 reply = authenticator.login()
232- self.assertIsNone(reply)
233- self.assertEqual(self.logger.debug_messages, ['Login completed'])
234- self.assertEqual(self.logger.error_messages,
235+ self.assertEqual(reply, 'auth reply')
236+ self.assertEqual(logger.debug_messages, ['Login completed'])
237+ self.assertEqual(logger.error_messages,
238 ['Got authentication error: who are you?'])
239
240=== modified file 'friends/tests/test_download.py'
241--- friends/tests/test_download.py 2012-10-13 01:27:15 +0000
242+++ friends/tests/test_download.py 2012-10-20 00:36:21 +0000
243@@ -33,7 +33,7 @@
244 from wsgiref.simple_server import WSGIRequestHandler, make_server
245 from wsgiref.util import setup_testing_defaults
246
247-from friends.testing.mocks import FakeSoupMessage
248+from friends.testing.mocks import FakeSoupMessage, LogMock
249 from friends.utils.download import Downloader, get_json
250
251 try:
252@@ -123,6 +123,12 @@
253 class TestDownloader(unittest.TestCase):
254 """Test the downloading utilities."""
255
256+ def setUp(self):
257+ self.log_mock = LogMock('friends.utils.download')
258+
259+ def tearDown(self):
260+ self.log_mock.stop()
261+
262 @classmethod
263 def setUpClass(cls):
264 cls.server = make_server('', 9180, _app, handler_class=_SilentHandler)
265@@ -231,17 +237,17 @@
266
267 def test_unauthorized(self):
268 # Test a URL that requires authorization.
269- with self.assertRaises(HTTPError) as cm:
270- Downloader('http://localhost:9180/auth').get_string()
271- self.assertEqual(cm.exception.code, 401)
272+ Downloader('http://localhost:9180/auth').get_string()
273+ self.assertEqual(self.log_mock.empty(),
274+ 'http://localhost:9180/auth: 401 Unauthorized\n')
275
276 def test_bad_authorization(self):
277 # Test a URL that requires authorization, but doesn't have the correct
278 # username or password.
279- with self.assertRaises(HTTPError) as cm:
280- Downloader('http://localhost:9180/auth',
281- username='bob', password='wrong').get_string()
282- self.assertEqual(cm.exception.code, 401)
283+ Downloader('http://localhost:9180/auth',
284+ username='bob', password='wrong').get_string()
285+ self.assertEqual(self.log_mock.empty(),
286+ 'http://localhost:9180/auth: 401 Unauthorized\n')
287
288 def test_authorized(self):
289 # Test a URL that requires authorization, and has the proper
290
291=== modified file 'friends/tests/test_facebook.py'
292--- friends/tests/test_facebook.py 2012-10-17 18:55:49 +0000
293+++ friends/tests/test_facebook.py 2012-10-20 00:36:21 +0000
294@@ -102,9 +102,11 @@
295 self.protocol('receive')
296 contents = log_mock.empty(trim=False)
297 self.assertEqual(contents, """\
298+Facebook.receive is starting in a new thread.
299 Logging in to Facebook
300 Facebook UID: None
301 Facebook error (190 OAuthException): Bad access token
302+Facebook.receive has completed, thread exiting.
303 """)
304
305 @mock.patch('friends.utils.download.Soup.Message',
306
307=== modified file 'friends/tests/test_flickr.py'
308--- friends/tests/test_flickr.py 2012-10-17 18:55:49 +0000
309+++ friends/tests/test_flickr.py 2012-10-20 00:36:21 +0000
310@@ -71,12 +71,14 @@
311 with mock.patch.object(self.protocol, '_get_nsid', return_value=None):
312 self.protocol('receive')
313 self.assertEqual(self.log_mock.empty(), """\
314+Flickr.receive is starting in a new thread.
315 Flickr: No NSID available
316 Friends operation exception:
317 Traceback (most recent call last):
318 ...
319 friends.errors.AuthorizationError:\
320 No Flickr user id available (account: faker/than fake)
321+Flickr.receive has completed, thread exiting.
322 """)
323
324 @mock.patch('friends.utils.download.Soup.Message',
325@@ -90,7 +92,10 @@
326 # AuthorizationError.
327 self.protocol('receive')
328 # No error messages.
329- self.assertEqual(self.log_mock.empty(), '')
330+ self.assertEqual(self.log_mock.empty(), """\
331+Flickr.receive is starting in a new thread.
332+Flickr.receive has completed, thread exiting.
333+""")
334 # But also no photos.
335 self.assertEqual(TestModel.get_n_rows(), 0)
336
337@@ -108,11 +113,13 @@
338 side_effect=side_effect):
339 self.protocol('receive')
340 self.assertEqual(self.log_mock.empty(), """\
341+Flickr.receive is starting in a new thread.
342 Friends operation exception:
343 Traceback (most recent call last):
344 ...
345 friends.errors.AuthorizationError:\
346 No Flickr user id available (account: faker/than fake)
347+Flickr.receive has completed, thread exiting.
348 """)
349
350 @mock.patch('friends.utils.download.Soup.Message',
351@@ -129,7 +136,10 @@
352 side_effect=side_effect):
353 self.protocol('receive')
354 # No error message.
355- self.assertEqual(self.log_mock.empty(), '')
356+ self.assertEqual(self.log_mock.empty(), """\
357+Flickr.receive is starting in a new thread.
358+Flickr.receive has completed, thread exiting.
359+""")
360 # But also no photos.
361 self.assertEqual(TestModel.get_n_rows(), 0)
362
363@@ -145,6 +155,7 @@
364 # AccessToken, but this fails.
365 self.protocol('receive')
366 self.assertEqual(self.log_mock.empty(), """\
367+Flickr.receive is starting in a new thread.
368 Logging in to Flickr
369 No AccessToken in Flickr session:\
370 {'TokenSecret': 'abc', 'username': 'Bob Dobbs', 'user_nsid': 'bob'}
371@@ -154,6 +165,7 @@
372 ...
373 friends.errors.AuthorizationError:\
374 No Flickr user id available (account: faker/than fake)
375+Flickr.receive has completed, thread exiting.
376 """)
377
378 @mock.patch('friends.utils.download.Soup.Message',
379@@ -166,6 +178,7 @@
380 # AccessToken, but this fails.
381 self.protocol('receive')
382 self.assertEqual(self.log_mock.empty(), """\
383+Flickr.receive is starting in a new thread.
384 Logging in to Flickr
385 No Flickr authentication results received.
386 Flickr: No NSID available
387@@ -174,6 +187,7 @@
388 ...
389 friends.errors.AuthorizationError:\
390 No Flickr user id available (account: faker/than fake)
391+Flickr.receive has completed, thread exiting.
392 """)
393
394 @mock.patch('friends.utils.download.Soup.Message',
395
396=== modified file 'friends/tests/test_logging.py'
397--- friends/tests/test_logging.py 2012-10-13 01:27:15 +0000
398+++ friends/tests/test_logging.py 2012-10-20 00:36:21 +0000
399@@ -35,30 +35,23 @@
400 def setUp(self):
401 # Preserve the original logger, but restore the global logging system
402 # to pre-initialized state.
403- self._logger = logging.getLogger('friends.service')
404- del logging.Logger.manager.loggerDict['friends.service']
405+ self._loggers = logging.Logger.manager.loggerDict.copy()
406+ logging.Logger.manager.loggerDict.clear()
407
408 def tearDown(self):
409- # Restore the original logger.
410- logging.Logger.manager.loggerDict['friends.service'] = self._logger
411+ # Restore the original loggers.
412+ logging.Logger.manager.loggerDict.update(self._loggers)
413
414 def test_logger_has_filehandler(self):
415 initialize()
416- # The top level 'friends.service' logger should be available.
417- log = logging.getLogger('friends.service')
418+ # The top level logger should be available.
419+ log = logging.getLogger()
420 # Try to find the file opened by the default file handler.
421 filenames = []
422 for handler in log.handlers:
423 if hasattr(handler, 'baseFilename'):
424 filenames.append(handler.baseFilename)
425- self.assertEqual(len(filenames), 1)
426-
427- def _get_log_filename(self, log):
428- filenames = []
429- for handler in log.handlers:
430- if hasattr(handler, 'baseFilename'):
431- filenames.append(handler.baseFilename)
432- return filenames
433+ self.assertGreater(len(filenames), 0)
434
435 def test_logger_message(self):
436 # Write an error message to the log and test that it shows up.
437@@ -67,11 +60,7 @@
438 logfile = os.path.join(tempdir, 'friends-test.log')
439 initialize(filename=logfile)
440 # Get another handle on the log file.
441- log = logging.getLogger('friends.service')
442- # Try to find the file opened by the default file handler.
443- filenames = self._get_log_filename(log)
444- self.assertEqual(len(filenames), 1)
445- logfile = filenames[0]
446+ log = logging.getLogger()
447 self.assertEqual(os.stat(logfile).st_size, 0)
448 # Log messages at INFO or higher should be written.
449 log.info('friends at your service')
450@@ -81,12 +70,11 @@
451 contents = fp.read()
452 lines = contents.splitlines()
453 self.assertEqual(len(lines), 1)
454- # The log message will have a variable timestamp at the front, so
455- # ignore that, but check everything else.
456+ # The log message will have a variable timestamp, so ignore
457+ # that, but check everything else.
458 self.assertRegex(
459 lines[0],
460- r'friends.service\s+MainThread\s+:\s+INFO\s+-\s+'
461- r'friends at your service')
462+ r'INFO\s+MainThread.*root\s+friends at your service')
463
464 def test_console_logger(self):
465 # The logger can support an optional console logger.
466@@ -94,10 +82,10 @@
467 self.addCleanup(shutil.rmtree, tempdir)
468 logfile = os.path.join(tempdir, 'friends-test.log')
469 initialize(console=True, filename=logfile)
470- log = logging.getLogger('friends.service')
471+ log = logging.getLogger()
472 # Can we do better than testing that there are now two handlers for
473 # the logger?
474- self.assertEqual(2, sum(1 for handler in log.handlers))
475+ self.assertEqual(3, sum(1 for handler in log.handlers))
476
477 def test_default_logger_level(self):
478 # By default, the logger level is INFO.
479@@ -106,11 +94,7 @@
480 logfile = os.path.join(tempdir, 'friends-test.log')
481 initialize(filename=logfile)
482 # Get another handle on the log file.
483- log = logging.getLogger('friends.service')
484- # Try to find the file opened by the default file handler.
485- filenames = self._get_log_filename(log)
486- self.assertEqual(len(filenames), 1)
487- logfile = filenames[0]
488+ log = logging.getLogger()
489 # By default, debug messages won't get written since they are less
490 # severe than INFO.
491 log.debug('friends is ready')
492@@ -123,11 +107,7 @@
493 logfile = os.path.join(tempdir, 'friends-test.log')
494 initialize(filename=logfile, debug=True)
495 # Get another handle on the log file.
496- log = logging.getLogger('friends.service')
497- # Try to find the file opened by the default file handler.
498- filenames = self._get_log_filename(log)
499- self.assertEqual(len(filenames), 1)
500- logfile = filenames[0]
501+ log = logging.getLogger()
502 log.debug('friends is ready')
503 self.assertGreater(os.stat(logfile).st_size, 0)
504 # Read the contents, which should be just one line of output.
505@@ -139,5 +119,4 @@
506 # ignore that, but check everything else.
507 self.assertRegex(
508 lines[0],
509- r'friends.service\s+MainThread\s+:\s+DEBUG\s+-\s+'
510- r'friends is ready')
511+ r'DEBUG\s+MainThread.*root\s+friends is ready')
512
513=== modified file 'friends/utils/account.py'
514--- friends/utils/account.py 2012-10-18 18:41:58 +0000
515+++ friends/utils/account.py 2012-10-20 00:36:21 +0000
516@@ -31,7 +31,7 @@
517 from friends.utils.model import COLUMN_INDICES, Model
518
519
520-log = logging.getLogger('friends.service')
521+log = logging.getLogger(__name__)
522
523
524 class AccountManager:
525
526=== modified file 'friends/utils/authentication.py'
527--- friends/utils/authentication.py 2012-10-13 01:27:15 +0000
528+++ friends/utils/authentication.py 2012-10-20 00:36:21 +0000
529@@ -21,37 +21,37 @@
530
531
532 import logging
533+import time
534
535 from gi.repository import GObject, Signon
536 GObject.threads_init(None)
537
538
539+log = logging.getLogger(__name__)
540+
541+
542 class Authentication:
543- def __init__(self, account, log=None):
544+ def __init__(self, account):
545 self.account = account
546- self.log = (logging.getLogger('friends.service')
547- if log is None
548- else log)
549 self._reply = None
550- self._authenticating = False
551
552 def login(self):
553 auth = self.account.auth
554 self.auth_session = Signon.AuthSession.new(auth.id, auth.method)
555- self._authenticating = True
556- self._loop = GObject.MainLoop()
557 self.auth_session.process(
558 auth.parameters, auth.mechanism,
559 self._login_cb, None)
560- if self._authenticating:
561- self._loop.run()
562+ timeout = 30
563+ while self._reply is None and timeout > 0:
564+ # We're building a synchronous API on top of an inherently
565+ # async library, so we need to block this thread until the
566+ # callback gets called to give us the response to return.
567+ time.sleep(0.5)
568+ timeout -= 1
569 return self._reply
570
571 def _login_cb(self, session, reply, error, user_data):
572- self._authenticating = False
573+ self._reply = reply
574 if error:
575- self.log.error('Got authentication error: {}', error.message)
576- else:
577- self._reply = reply
578- self.log.debug('Login completed')
579- self._loop.quit()
580+ log.error('Got authentication error: {}'.format(error.message))
581+ log.debug('Login completed')
582
583=== modified file 'friends/utils/avatar.py'
584--- friends/utils/avatar.py 2012-10-13 01:27:15 +0000
585+++ friends/utils/avatar.py 2012-10-20 00:36:21 +0000
586@@ -31,7 +31,7 @@
587 from friends.utils.download import Downloader
588
589
590-log = logging.getLogger('friends.avatars')
591+log = logging.getLogger(__name__)
592 CACHE_DIR = os.path.realpath(os.path.join(
593 GLib.get_user_cache_dir(), 'friends', 'avatars'))
594 CACHE_AGE = timedelta(weeks=4)
595
596=== modified file 'friends/utils/base.py'
597--- friends/utils/base.py 2012-10-16 18:58:49 +0000
598+++ friends/utils/base.py 2012-10-20 00:36:21 +0000
599@@ -52,7 +52,7 @@
600 _publish_lock = threading.Lock()
601
602
603-log = logging.getLogger('friends.service')
604+log = logging.getLogger(__name__)
605
606
607 def feature(method):
608@@ -105,9 +105,10 @@
609 class _OperationThread(threading.Thread):
610 """Catch, log, and swallow all exceptions in the sub-thread."""
611
612- def __init__(self, barrier, *args, **kws):
613+ def __init__(self, barrier, *args, identifier=None, **kws):
614 # The barrier will only be provided when the system is under test.
615 self._barrier = barrier
616+ self._id = identifier
617 super().__init__(*args, **kws)
618
619 # Always run these as daemon threads, so they don't block the main thread,
620@@ -115,10 +116,12 @@
621 daemon = True
622
623 def run(self):
624+ log.debug('{} is starting in a new thread.'.format(self._id))
625 try:
626 super().run()
627 except Exception:
628 log.exception('Friends operation exception:\n')
629+ log.debug('{} has completed, thread exiting.'.format(self._id))
630 # If the system is under test, indicate that we've reached the
631 # barrier, so that the main thread, i.e. the test thread waiting for
632 # the results, can then proceed.
633@@ -160,6 +163,8 @@
634 # thread to assert the results of the sub-thread.
635 barrier = (threading.Barrier(parties=2) if Base._SYNCHRONIZE else None)
636 _OperationThread(barrier,
637+ identifier='{}.{}'.format(self.__class__.__name__,
638+ operation),
639 target=method, args=args, kwargs=kwargs).start()
640 # When under synchronous testing, wait until the sub-thread completes
641 # before returning.
642@@ -299,7 +304,7 @@
643 log.debug('{} to {}'.format(
644 'Re-authenticating' if old_token else 'Logging in', protocol))
645
646- result = Authentication(self._account, log).login()
647+ result = Authentication(self._account).login()
648 if result is None:
649 log.error(
650 'No {} authentication results received.'.format(protocol))
651
652=== modified file 'friends/utils/download.py'
653--- friends/utils/download.py 2012-10-18 18:41:54 +0000
654+++ friends/utils/download.py 2012-10-20 00:36:21 +0000
655@@ -31,7 +31,7 @@
656 from urllib.error import HTTPError
657 from urllib.parse import urlencode
658
659-log = logging.getLogger('friends.service')
660+log = logging.getLogger(__name__)
661
662
663 # Global libsoup session instance.
664@@ -139,16 +139,9 @@
665 message = self._build_request()
666 _soup.send_message(message)
667 if message.status_code != 200:
668- # Even though we're using libsoup instead of Python's urllib
669- # libraries, it's helpful for clients to receive an exception when
670- # non-success error codes are received. This lets the clients do
671- # additional work if they want, and it prevents rate limiting from
672- # occurring.
673- raise HTTPError(self.url,
674- message.status_code,
675- message.reason_phrase,
676- self.headers,
677- None)
678+ log.error('{}: {} {}'.format(self.url,
679+ message.status_code,
680+ message.reason_phrase))
681 yield message
682 self._rate_limiter.update(message)
683
684
685=== modified file 'friends/utils/logging.py'
686--- friends/utils/logging.py 2012-10-13 01:27:15 +0000
687+++ friends/utils/logging.py 2012-10-20 00:36:21 +0000
688@@ -19,15 +19,25 @@
689 import errno
690 import logging
691 import logging.handlers
692+import oauthlib.oauth1
693
694 from gi.repository import GLib
695
696
697+# Set a global default of no logging. This is a workaround for a bug
698+# where we were getting duplicated log records.
699+logging.basicConfig(filename='/dev/null', level=100)
700+
701+
702+# Disable logging in oauthlib because it is very verbose.
703+oauthlib.oauth1.rfc5849.logging.debug = lambda *ignore: None
704+
705+
706 LOG_FILENAME = os.path.join(
707 os.path.realpath(os.path.abspath(GLib.get_user_cache_dir())),
708 'friends', 'friends.log')
709-LOG_FORMAT = '{asctime} - {name:12} {threadName:14}: {levelname:8} - {message}'
710-CSL_FORMAT = '{name:12} {threadName:12}: {levelname:8} {message}'
711+LOG_FORMAT = '{levelname:5} {threadName:10} {asctime} {name:18} {message}'
712+CSL_FORMAT = LOG_FORMAT.replace(' {asctime}', '')
713
714
715 def initialize(console=False, debug=False, filename=None):
716@@ -48,11 +58,7 @@
717 except OSError as error:
718 if error.errno != errno.EEXIST:
719 raise
720- log = logging.getLogger('friends.service')
721- if debug:
722- log.setLevel(logging.DEBUG)
723- else:
724- log.setLevel(logging.INFO)
725+
726 # Install a rotating log file handler. XXX There should be a
727 # configuration file rather than hard-coded values.
728 text_handler = logging.handlers.RotatingFileHandler(
729@@ -60,9 +66,16 @@
730 # Use str.format() style format strings.
731 text_formatter = logging.Formatter(LOG_FORMAT, style='{')
732 text_handler.setFormatter(text_formatter)
733- log.addHandler(text_handler)
734+
735+ console_handler = logging.StreamHandler()
736+ console_formatter = logging.Formatter(CSL_FORMAT, style='{')
737+ console_handler.setFormatter(console_formatter)
738+
739+ log = logging.getLogger()
740+ if debug:
741+ log.setLevel(logging.DEBUG)
742+ else:
743+ log.setLevel(logging.INFO)
744 if console:
745- console_handler = logging.StreamHandler()
746- console_formatter = logging.Formatter(CSL_FORMAT, style='{')
747- console_handler.setFormatter(console_formatter)
748 log.addHandler(console_handler)
749+ log.addHandler(text_handler)
750
751=== modified file 'friends/utils/menus.py'
752--- friends/utils/menus.py 2012-10-13 01:27:15 +0000
753+++ friends/utils/menus.py 2012-10-20 00:36:21 +0000
754@@ -34,7 +34,7 @@
755
756
757 DESKTOP_ID = 'friends.desktop'
758-log = logging.getLogger('friends.service')
759+log = logging.getLogger(__name__)
760
761
762 def helper(executable):
763
764=== modified file 'tools/debug_live.py'
765--- tools/debug_live.py 2012-10-13 01:27:15 +0000
766+++ tools/debug_live.py 2012-10-20 00:36:21 +0000
767@@ -20,30 +20,29 @@
768 """
769
770 import sys
771+import time
772+import logging
773
774 sys.path.insert(0, '.')
775
776 from gi.repository import GObject
777+from friends.utils.logging import initialize
778 from friends.utils.account import AccountManager
779 from friends.utils.base import Base
780 from friends.utils.model import Model
781
782-
783-# Ensure synchronous operation of Base.__call__() for easier testing.
784-Base._SYNCHRONIZE = True
785-
786-
787-def refresh(account):
788+# Print all logs for debugging purposes
789+initialize(debug=True, console=True)
790+
791+
792+log = logging.getLogger('friends.debug_live')
793+
794+
795+def row_added(model, itr):
796+ row = model.get_row(itr)
797+ print(row)
798+ log.info('ROWS: {}'.format(len(model)))
799 print()
800- print('#' * 80)
801- print('Performing "{}" operation!'.format(args[0]))
802- print('#' * 80)
803-
804- account.protocol(*args)
805- for row in Model:
806- print([col for col in row])
807- print()
808- print('ROWS: ', len(Model))
809
810
811 if __name__ == '__main__':
812@@ -56,14 +55,16 @@
813 found = False
814 a = AccountManager(None)
815
816+ Model.connect('row-added', row_added)
817+
818 for account_id, account in a._accounts.items():
819 if account_id.endswith(protocol):
820 found = True
821- refresh(account)
822- GObject.timeout_add_seconds(300, refresh, account)
823+ account.protocol(*args)
824
825 if not found:
826- print('No {} account found in your Ubuntu Online Accounts!'.format(
827- protocol))
828+ log.error('No {} found in Ubuntu Online Accounts!'.format(protocol))
829 else:
830- GObject.MainLoop().run()
831+ loop = GObject.MainLoop()
832+ GObject.timeout_add_seconds(10, loop.quit)
833+ loop.run()

Subscribers

People subscribed via source and target branches