Merge lp:~elopio/u1-test-utils/logger_decorator into lp:u1-test-utils

Proposed by Leo Arias
Status: Merged
Approved by: Natalia Bidart
Approved revision: 55
Merged at revision: 45
Proposed branch: lp:~elopio/u1-test-utils/logger_decorator
Merge into: lp:u1-test-utils
Prerequisite: lp:~elopio/u1-test-utils/assert_page_path
Diff against target: 221 lines (+99/-1)
3 files modified
u1testutils/sst/__init__.py (+24/-0)
u1testutils/sst/selftests/unit/test_pages.py (+60/-1)
u1testutils/sst/sso/pages.py (+15/-0)
To merge this branch: bzr merge lp:~elopio/u1-test-utils/logger_decorator
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+157925@code.launchpad.net

Commit message

Added the actions logger decorator for the page objects.

To post a comment you must log in.
48. By Leo Arias

Added the decorator to the SSO pages.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good. About the decorator, instead of

11 + def log_and_call(*args, **kwargs):
12 + class_name = str(args[0].__class__.__name__)

I would suggest using this approach, which is more explicit:

11 + def log_and_call(instance, *args, **kwargs):
12 + class_name = str(instance.__class__.__name__)
...
25 + return method(instance, *args, **kwargs)

Futhermore, I would suggest adding a decorator that will receive the logging function as a param, thus allowing the caller to log using different levels. For example:

http://bazaar.launchpad.net/~pycassos/pycasa/trunk/view/head:/pycasa/utils.py#L120

Nitpicking detail: there should be an empty blank line between the import <module> block and the from <module> import <feature> block.

review: Needs Fixing
49. By Leo Arias

Improved the logger using a more explicit approach with the instance as parameter.

50. By Leo Arias

Improve the logger decorator to receive the logging funciton.

51. By Leo Arias

Removed the log_func default.

52. By Leo Arias

Remove the unnecessary test.

53. By Leo Arias

Fixed lint.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

I made some tweaks to this logging function and pasted them here:

https://pastebin.canonical.com/89011/

I added comments inline explaining the changes, but will duplicate those here for the sake of the review:

"Creating several strings depending on conditions is expensive.
Also, is better to have non-chaging log messages to be able to
grep for specific keywords when debugging, so, I prefer to always
log the args and kwargs even if they are not set, since the fact
that no args nor kwargs were given is an important info to log.
Is particularly important to use %r and not %s.

You should not format the string to log outside the logging
function, for mainly 2 reasons: the formatting may not be needed
since the currently set log level may be higher than the level of
log_func, and because there may be a formatting error that could
raise an exception making the whole call to 'f' fail (and we want
to avoid that)."

review: Needs Fixing
54. By Leo Arias

Implemented nessita's suggestion for the log line.

55. By Leo Arias

Empty line between import forms.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks great! Thanks a lot.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'u1testutils/sst/__init__.py'
2--- u1testutils/sst/__init__.py 2013-04-10 19:25:21 +0000
3+++ u1testutils/sst/__init__.py 2013-04-12 15:18:27 +0000
4@@ -18,10 +18,34 @@
5
6 import sst.actions
7
8+from functools import wraps
9+
10
11 logger = logging.getLogger('User test')
12
13
14+def log_action(log_func):
15+ """Decorator to log the call of an action method."""
16+
17+ def middle(f):
18+
19+ @wraps(f)
20+ def inner(instance, *args, **kwargs):
21+ class_name = str(instance.__class__.__name__)
22+ docstring = f.__doc__
23+ if docstring:
24+ docstring = docstring.split('\n')[0].strip()
25+ else:
26+ docstring = f.__name__
27+ log_line = '%r: %r. Arguments %r. Keyword arguments: %r.'
28+ log_func(log_line, class_name, docstring, args, kwargs)
29+ return f(instance, *args, **kwargs)
30+
31+ return inner
32+
33+ return middle
34+
35+
36 class Page(object):
37 """Base class for the page objects used in acceptance testing.
38
39
40=== modified file 'u1testutils/sst/selftests/unit/test_pages.py'
41--- u1testutils/sst/selftests/unit/test_pages.py 2013-04-08 18:43:37 +0000
42+++ u1testutils/sst/selftests/unit/test_pages.py 2013-04-12 15:18:27 +0000
43@@ -12,10 +12,13 @@
44 # You should have received a copy of the GNU General Public License along
45 # with this program. If not, see <http://www.gnu.org/licenses/>.
46
47+import logging
48 import mock
49 import testtools
50
51-from u1testutils.sst import Page
52+import u1testutils.logging
53+
54+from u1testutils.sst import log_action, Page
55
56
57 class PageTestCase(testtools.TestCase):
58@@ -58,3 +61,59 @@
59 page.headings2 = []
60 page.assert_page_is_open()
61 assert not mock_assert.called
62+
63+
64+class PageWithLogDecorator(Page):
65+
66+ @log_action(logging.info)
67+ def do_something_without_docstring(self, *args, **kwargs):
68+ pass
69+
70+ @log_action(logging.info)
71+ def do_something_with_docstring(self, *args, **kwargs):
72+ """Do something with docstring."""
73+ pass
74+
75+ @log_action(logging.info)
76+ def do_something_with_multiline_docstring(self, *args, **kwargs):
77+ """Do something with a multiline docstring.
78+
79+ This should not be logged.
80+ """
81+ pass
82+
83+
84+class PageLoggingTestCase(u1testutils.logging.LogHandlerTestCase):
85+
86+ def setUp(self):
87+ super(PageLoggingTestCase, self).setUp()
88+ self.root_logger.setLevel(logging.INFO)
89+ self.page = PageWithLogDecorator(check=False)
90+
91+ def test_logged_action_without_docstring(self):
92+ self.page.do_something_without_docstring(
93+ 'arg1', 'arg2', arg3='arg3', arg4='arg4')
94+ self.assertLogLevelContains(
95+ 'INFO',
96+ "'PageWithLogDecorator': 'do_something_without_docstring'. "
97+ "Arguments ('arg1', 'arg2'). "
98+ "Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
99+
100+ def test_logged_action_with_docstring(self):
101+ self.page.do_something_with_docstring(
102+ 'arg1', 'arg2', arg3='arg3', arg4='arg4')
103+ self.assertLogLevelContains(
104+ 'INFO',
105+ "'PageWithLogDecorator': 'Do something with docstring.'. "
106+ "Arguments ('arg1', 'arg2'). "
107+ "Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
108+
109+ def test_logged_action_with_multiline_docstring(self):
110+ self.page.do_something_with_multiline_docstring(
111+ 'arg1', 'arg2', arg3='arg3', arg4='arg4')
112+ self.assertLogLevelContains(
113+ 'INFO',
114+ "'PageWithLogDecorator': "
115+ "'Do something with a multiline docstring.'. "
116+ "Arguments ('arg1', 'arg2'). "
117+ "Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
118
119=== modified file 'u1testutils/sst/sso/pages.py'
120--- u1testutils/sst/sso/pages.py 2013-04-10 19:47:20 +0000
121+++ u1testutils/sst/sso/pages.py 2013-04-12 15:18:27 +0000
122@@ -12,10 +12,14 @@
123 # You should have received a copy of the GNU General Public License along
124 # with this program. If not, see <http://www.gnu.org/licenses/>.
125
126+import logging
127+
128 import sst.actions
129
130 import u1testutils.sst
131
132+from u1testutils.sst import log_action
133+
134
135 class LogIn(u1testutils.sst.Page):
136 """Log in page of the Ubuntu Single Sign On website.
137@@ -39,6 +43,7 @@
138 sst.actions.assert_element(tag='h1', text='Ubuntu Single Sign On')
139 sst.actions.assert_element(tag='h2', text='Are you new?')
140
141+ @log_action(logging.info)
142 def log_in_to_site_recognized(self, user=None):
143 """Fill the log in form and continue to the site that requested it.
144
145@@ -51,6 +56,7 @@
146 """
147 self._log_in(user)
148
149+ @log_action(logging.info)
150 def log_in_to_site_not_recognized(self, user=None):
151 """Fill the log in form and continue to the next step.
152
153@@ -87,6 +93,7 @@
154 css_class='btn', name='continue')
155 sst.actions.click_button(continue_button)
156
157+ @log_action(logging.info)
158 def go_to_create_new_account(self):
159 """Go to the Create new account page."""
160 new_account_link = sst.actions.get_element(id='new-account-link')
161@@ -109,6 +116,7 @@
162 title = 'Create account'
163 url_path = '/\+new_account'
164
165+ @log_action(logging.info)
166 def create_ubuntu_sso_account(self, user):
167 """Fill the new account form and continue to the next step.
168
169@@ -147,6 +155,7 @@
170 title = 'Registration mail sent'
171 url_path = '/\+new_account'
172
173+ @log_action(logging.info)
174 def confirm_email_to_site_recognized(self, confirmation_code):
175 """Confirm email and continue to the site that requested the log in.
176
177@@ -157,6 +166,7 @@
178 """
179 self._confirm_email(confirmation_code)
180
181+ @log_action(logging.info)
182 def confirm_email_to_site_not_recognized(self, confirmation_code):
183 """Enter the confirmation code and continue to the next step.
184
185@@ -215,10 +225,12 @@
186 cancel_link = sst.actions.get_element(tag='a', text='cancel')
187 sst.actions.click_link(cancel_link)
188
189+ @log_action(logging.info)
190 def confirm(self):
191 self._click_continue_button()
192 return YourAccount(self.user_name)
193
194+ @log_action(logging.info)
195 def cancel(self):
196 return YourAccount(self.user_name)
197
198@@ -249,6 +261,7 @@
199 """
200 sst.actions.assert_title_contains(self.title, regex=True)
201
202+ @log_action(logging.info)
203 def make_all_information_available_to_website(self):
204 """Select all the user available information.
205
206@@ -265,6 +278,7 @@
207 'form[name="decideform"] > .info-items > .list > li > '
208 'input[type="checkbox"]')
209
210+ @log_action(logging.info)
211 def yes_sign_me_in(self):
212 """Accept to sign in to the site not recognized and go back to it."""
213 sign_me_in_button = sst.actions.get_element(css_class='btn',
214@@ -299,6 +313,7 @@
215
216 class _UserSubHeader(object):
217
218+ @log_action(logging.info)
219 def log_out(self):
220 """Log out from the web site."""
221 sst.actions.click_link('logout-link')

Subscribers

People subscribed via source and target branches

to all changes: