Merge lp:~thomir-deactivatedaccount/autopilot/trunk-fix-ProcessSearchError-message into lp:autopilot

Proposed by Thomi Richards
Status: Merged
Approved by: Christopher Lee
Approved revision: 420
Merged at revision: 417
Proposed branch: lp:~thomir-deactivatedaccount/autopilot/trunk-fix-ProcessSearchError-message
Merge into: lp:autopilot
Diff against target: 494 lines (+416/-19)
3 files modified
autopilot/introspection/__init__.py (+98/-14)
autopilot/tests/functional/test_ap_apps.py (+1/-2)
autopilot/tests/unit/test_introspection_features.py (+317/-3)
To merge this branch: bzr merge lp:~thomir-deactivatedaccount/autopilot/trunk-fix-ProcessSearchError-message
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Christopher Lee (community) Approve
Review via email: mp+202238@code.launchpad.net

Commit message

Improve the error message in the ProcessSearchError exception.

Description of the change

Improve the error message in the ProcessSearchError exception.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
419. By Thomi Richards

Fix failing functional test.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
420. By Thomi Richards

Fixed issues from code review.

Revision history for this message
Christopher Lee (veebers) wrote :

LGTM

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/introspection/__init__.py'
2--- autopilot/introspection/__init__.py 2014-01-16 23:28:01 +0000
3+++ autopilot/introspection/__init__.py 2014-01-20 20:59:59 +0000
4@@ -33,6 +33,7 @@
5 from functools import partial
6 import os
7 import psutil
8+from six import u
9
10 from autopilot.dbus_handler import (
11 get_session_bus,
12@@ -143,14 +144,7 @@
13 ``process.pid != pid``.
14
15 """
16- if process is not None:
17- if pid is None:
18- pid = process.pid
19- elif pid != process.pid:
20- raise RuntimeError("Supplied PID and process.pid do not match.")
21-
22- if pid is not None and not _pid_is_running(pid):
23- raise ProcessSearchError("PID %d could not be found" % pid)
24+ pid = _check_process_and_pid_details(process, pid)
25
26 dbus_addresses = _get_dbus_addresses_from_search_parameters(
27 pid,
28@@ -160,19 +154,109 @@
29 process
30 )
31
32- if application_name:
33- app_name_check_fn = lambda i: get_classname_from_path(
34- i.introspection_iface.GetState('/')[0][0]) == application_name
35- dbus_addresses = list(filter(app_name_check_fn, dbus_addresses))
36+ dbus_addresses = _maybe_filter_connections_by_app_name(
37+ application_name,
38+ dbus_addresses
39+ )
40
41 if dbus_addresses is None or len(dbus_addresses) == 0:
42- raise ProcessSearchError("Search criteria returned no results")
43+ criteria_string = _get_search_criteria_string_representation(
44+ pid,
45+ dbus_bus,
46+ connection_name,
47+ process,
48+ object_path,
49+ application_name
50+ )
51+ message_string = "Search criteria (%s) returned no results" % \
52+ (criteria_string)
53+ raise ProcessSearchError(message_string)
54 if len(dbus_addresses) > 1:
55- raise RuntimeError("Search criteria returned multiple results")
56+ criteria_string = _get_search_criteria_string_representation(
57+ pid,
58+ dbus_bus,
59+ connection_name,
60+ process,
61+ object_path,
62+ application_name
63+ )
64+ message_string = "Search criteria (%s) returned multiple results" % \
65+ (criteria_string)
66+ raise RuntimeError(message_string)
67
68 return _make_proxy_object(dbus_addresses[0], emulator_base)
69
70
71+def _check_process_and_pid_details(process=None, pid=None):
72+ """Do error checking on process and pid specification.
73+
74+ :raises RuntimeError: if both process and pid are specified, but the
75+ process's 'pid' attribute is different to the pid attribute specified.
76+ :raises ProcessSearchError: if the process specified is not running.
77+ :returns: the pid to use in all search queries.
78+
79+ """
80+ if process is not None:
81+ if pid is None:
82+ pid = process.pid
83+ elif pid != process.pid:
84+ raise RuntimeError("Supplied PID and process.pid do not match.")
85+
86+ if pid is not None and not _pid_is_running(pid):
87+ raise ProcessSearchError("PID %d could not be found" % pid)
88+ return pid
89+
90+
91+def _maybe_filter_connections_by_app_name(application_name, dbus_addresses):
92+ """Filter `dbus_addresses` by the application name exported, if
93+ `application_name` has been specified.
94+
95+ :returns: a filtered list of connections.
96+ """
97+
98+ if application_name:
99+ dbus_addresses = [
100+ a for a in dbus_addresses
101+ if _get_application_name_from_dbus_address(a) == application_name
102+ ]
103+ return dbus_addresses
104+
105+
106+def _get_application_name_from_dbus_address(dbus_address):
107+ """Return the application name from a dbus_address object."""
108+ return get_classname_from_path(
109+ dbus_address.introspection_iface.GetState('/')[0][0]
110+ )
111+
112+
113+def _get_search_criteria_string_representation(
114+ pid=None, dbus_bus=None, connection_name=None, process=None,
115+ object_path=None, application_name=None):
116+ """Get a string representation of the search criteria.
117+
118+ Used to represent the search criteria to users in error messages.
119+ """
120+ description_parts = []
121+ if pid is not None:
122+ description_parts.append(u('pid = %d') % pid)
123+ if dbus_bus is not None:
124+ description_parts.append(u("dbus bus = '%s'") % dbus_bus)
125+ if connection_name is not None:
126+ description_parts.append(
127+ u("connection name = '%s'") % connection_name
128+ )
129+ if object_path is not None:
130+ description_parts.append(u("object path = '%s'") % object_path)
131+ if application_name is not None:
132+ description_parts.append(
133+ u("application name = '%s'") % application_name
134+ )
135+ if process is not None:
136+ description_parts.append(u("process object = '%r'") % process)
137+
138+ return ", ".join(description_parts)
139+
140+
141 def _get_dbus_addresses_from_search_parameters(
142 pid, dbus_bus, connection_name, object_path, process):
143 """Returns a list of :py:class: `DBusAddress` for all successfully matched
144
145=== modified file 'autopilot/tests/functional/test_ap_apps.py'
146--- autopilot/tests/functional/test_ap_apps.py 2013-12-16 03:55:22 +0000
147+++ autopilot/tests/functional/test_ap_apps.py 2014-01-20 20:59:59 +0000
148@@ -105,10 +105,9 @@
149 sleep(1)
150 """ % sys.executable))
151
152- expected_error = "Search criteria returned no results"
153 self.assertThat(
154 lambda: self.launch_test_application(path, app_type='qt'),
155- raises(ProcessSearchError(expected_error))
156+ raises(ProcessSearchError)
157 )
158
159 def test_creating_app_for_non_running_app_fails(self):
160
161=== modified file 'autopilot/tests/unit/test_introspection_features.py'
162--- autopilot/tests/unit/test_introspection_features.py 2013-11-07 22:15:29 +0000
163+++ autopilot/tests/unit/test_introspection_features.py 2014-01-20 20:59:59 +0000
164@@ -33,9 +33,22 @@
165 raises,
166 )
167 from testscenarios import TestWithScenarios
168-from six import StringIO
169-
170-
171+from six import StringIO, u, PY3
172+from contextlib import contextmanager
173+if PY3:
174+ from contextlib import ExitStack
175+else:
176+ from contextlib2 import ExitStack
177+
178+
179+from autopilot.introspection import (
180+ _check_process_and_pid_details,
181+ _get_application_name_from_dbus_address,
182+ _get_search_criteria_string_representation,
183+ _maybe_filter_connections_by_app_name,
184+ get_proxy_object_for_existing_process,
185+ ProcessSearchError,
186+)
187 from autopilot.introspection.dbus import (
188 _get_filter_string_for_key_value_pair,
189 _is_valid_server_side_filter_param,
190@@ -279,3 +292,304 @@
191 path: '/some/path'
192 text: 'Hello'
193 """))
194+
195+
196+class ProcessSearchErrorStringRepTests(TestCase):
197+
198+ """Various tests for the _get_search_criteria_string_representation
199+ function.
200+
201+ """
202+
203+ def test_get_string_rep_defaults_to_empty_string(self):
204+ observed = _get_search_criteria_string_representation()
205+ self.assertEqual("", observed)
206+
207+ def test_pid(self):
208+ self.assertEqual(
209+ u('pid = 123'),
210+ _get_search_criteria_string_representation(pid=123)
211+ )
212+
213+ def test_dbus_bus(self):
214+ self.assertEqual(
215+ u("dbus bus = 'foo'"),
216+ _get_search_criteria_string_representation(dbus_bus='foo')
217+ )
218+
219+ def test_connection_name(self):
220+ self.assertEqual(
221+ u("connection name = 'foo'"),
222+ _get_search_criteria_string_representation(connection_name='foo')
223+ )
224+
225+ def test_object_path(self):
226+ self.assertEqual(
227+ u("object path = 'foo'"),
228+ _get_search_criteria_string_representation(object_path='foo')
229+ )
230+
231+ def test_application_name(self):
232+ self.assertEqual(
233+ u("application name = 'foo'"),
234+ _get_search_criteria_string_representation(application_name='foo')
235+ )
236+
237+ def test_process_object(self):
238+ class FakeProcess(object):
239+
240+ def __repr__(self):
241+ return 'foo'
242+ process = FakeProcess()
243+ self.assertEqual(
244+ u("process object = 'foo'"),
245+ _get_search_criteria_string_representation(process=process)
246+ )
247+
248+ def test_all_parameters_combined(self):
249+ class FakeProcess(object):
250+
251+ def __repr__(self):
252+ return 'foo'
253+ process = FakeProcess()
254+ observed = _get_search_criteria_string_representation(
255+ pid=123,
256+ dbus_bus='session_bus',
257+ connection_name='com.Canonical.Unity',
258+ object_path='/com/Canonical/Autopilot',
259+ application_name='MyApp',
260+ process=process
261+ )
262+ expected = "pid = 123, dbus bus = 'session_bus', " \
263+ "connection name = 'com.Canonical.Unity', " \
264+ "object path = '/com/Canonical/Autopilot', " \
265+ "application name = 'MyApp', process object = 'foo'"
266+ self.assertEqual(expected, observed)
267+
268+
269+class ProcessAndPidErrorCheckingTests(TestCase):
270+
271+ def test_raises_ProcessSearchError_when_process_is_not_running(self):
272+ with patch('autopilot.introspection._pid_is_running') as pir:
273+ pir.return_value = False
274+
275+ self.assertThat(
276+ lambda: _check_process_and_pid_details(pid=123),
277+ raises(ProcessSearchError("PID 123 could not be found"))
278+ )
279+
280+ def test_raises_RuntimeError_when_pid_and_process_disagree(self):
281+ mock_process = Mock()
282+ mock_process.pid = 1
283+
284+ self.assertThat(
285+ lambda: _check_process_and_pid_details(mock_process, 2),
286+ raises(RuntimeError("Supplied PID and process.pid do not match."))
287+ )
288+
289+ def test_returns_pid_when_specified(self):
290+ expected = self.getUniqueInteger()
291+ with patch('autopilot.introspection._pid_is_running') as pir:
292+ pir.return_value = True
293+
294+ observed = _check_process_and_pid_details(pid=expected)
295+
296+ self.assertEqual(expected, observed)
297+
298+ def test_returns_process_pid_attr_when_specified(self):
299+ fake_process = Mock()
300+ fake_process.pid = self.getUniqueInteger()
301+
302+ with patch('autopilot.introspection._pid_is_running') as pir:
303+ pir.return_value = True
304+ observed = _check_process_and_pid_details(fake_process)
305+
306+ self.assertEqual(fake_process.pid, observed)
307+
308+ def test_returns_None_when_neither_parameters_present(self):
309+ self.assertEqual(
310+ None,
311+ _check_process_and_pid_details()
312+ )
313+
314+ def test_returns_pid_when_both_specified(self):
315+ fake_process = Mock()
316+ fake_process.pid = self.getUniqueInteger()
317+ with patch('autopilot.introspection._pid_is_running') as pir:
318+ pir.return_value = True
319+ observed = _check_process_and_pid_details(
320+ fake_process,
321+ fake_process.pid
322+ )
323+ self.assertEqual(fake_process.pid, observed)
324+
325+
326+class ApplicationFilteringTests(TestCase):
327+
328+ def get_mock_dbus_address_with_application_name(slf, app_name):
329+ mock_dbus_address = Mock()
330+ mock_dbus_address.introspection_iface.GetState.return_value = (
331+ ('/' + app_name, {}),
332+ )
333+ return mock_dbus_address
334+
335+ def test_can_extract_application_name(self):
336+ mock_connection = self.get_mock_dbus_address_with_application_name(
337+ 'SomeAppName'
338+ )
339+ self.assertEqual(
340+ 'SomeAppName',
341+ _get_application_name_from_dbus_address(mock_connection)
342+ )
343+
344+ def test_maybe_filter_returns_addresses_when_app_name_not_specified(self):
345+ self.assertEqual(
346+ [],
347+ _maybe_filter_connections_by_app_name(None, [])
348+ )
349+
350+ def test_maybe_filter_works_with_partial_match(self):
351+ mock_connections = [
352+ self.get_mock_dbus_address_with_application_name('Match'),
353+ self.get_mock_dbus_address_with_application_name('Mismatch'),
354+ ]
355+ expected = mock_connections[:1]
356+ observed = _maybe_filter_connections_by_app_name(
357+ 'Match',
358+ mock_connections
359+ )
360+ self.assertEqual(expected, observed)
361+
362+ def test_maybe_filter_works_with_no_match(self):
363+ mock_connections = [
364+ self.get_mock_dbus_address_with_application_name('Mismatch1'),
365+ self.get_mock_dbus_address_with_application_name('Mismatch2'),
366+ ]
367+ expected = []
368+ observed = _maybe_filter_connections_by_app_name(
369+ 'Match',
370+ mock_connections
371+ )
372+ self.assertEqual(expected, observed)
373+
374+ def test_maybe_filter_works_with_full_match(self):
375+ mock_connections = [
376+ self.get_mock_dbus_address_with_application_name('Match'),
377+ self.get_mock_dbus_address_with_application_name('Match'),
378+ ]
379+ expected = mock_connections
380+ observed = _maybe_filter_connections_by_app_name(
381+ 'Match',
382+ mock_connections
383+ )
384+ self.assertEqual(expected, observed)
385+
386+
387+class ProxyObjectGenerationTests(TestCase):
388+
389+ @contextmanager
390+ def mock_all_child_calls(self):
391+ mock_dict = {}
392+ with ExitStack() as all_the_mocks:
393+ mock_dict['check_process'] = all_the_mocks.enter_context(
394+ patch(
395+ 'autopilot.introspection._check_process_and_pid_details'
396+ )
397+ )
398+ mock_dict['get_addresses'] = all_the_mocks.enter_context(
399+ patch(
400+ 'autopilot.introspection.'
401+ '_get_dbus_addresses_from_search_parameters'
402+ )
403+ )
404+ mock_dict['filter_addresses'] = all_the_mocks.enter_context(
405+ patch(
406+ 'autopilot.introspection.'
407+ '_maybe_filter_connections_by_app_name'
408+ )
409+ )
410+ mock_dict['make_proxy_object'] = all_the_mocks.enter_context(
411+ patch(
412+ 'autopilot.introspection._make_proxy_object'
413+ )
414+ )
415+ yield mock_dict
416+
417+ def test_makes_child_calls(self):
418+ """Mock out all child functions, and assert that they're called.
419+
420+ This test is somewhat ugly, and should be refactored once the search
421+ criteria has been refactored into a separate object, rather than a
422+ bunch of named parameters.
423+
424+ """
425+ with self.mock_all_child_calls() as mocks:
426+ fake_address_list = [Mock()]
427+ mocks['get_addresses'].return_value = fake_address_list
428+ mocks['filter_addresses'].return_value = fake_address_list
429+
430+ get_proxy_object_for_existing_process()
431+
432+ self.assertEqual(
433+ 1,
434+ mocks['check_process'].call_count
435+ )
436+ self.assertEqual(
437+ 1,
438+ mocks['get_addresses'].call_count
439+ )
440+ self.assertEqual(
441+ 1,
442+ mocks['make_proxy_object'].call_count
443+ )
444+
445+ def test_raises_ProcessSearchError(self):
446+ """Function must raise ProcessSearchError if no addresses are found.
447+
448+ This test is somewhat ugly, and should be refactored once the search
449+ criteria has been refactored into a separate object, rather than a
450+ bunch of named parameters.
451+
452+ """
453+ with self.mock_all_child_calls() as mocks:
454+ fake_address_list = [Mock()]
455+ mocks['check_process'].return_value = 123
456+ mocks['get_addresses'].return_value = fake_address_list
457+ mocks['filter_addresses'].return_value = []
458+
459+ self.assertThat(
460+ lambda: get_proxy_object_for_existing_process(),
461+ raises(
462+ ProcessSearchError(
463+ "Search criteria (pid = 123, dbus bus = 'session', "
464+ "object path = "
465+ "'/com/canonical/Autopilot/Introspection') returned "
466+ "no results"
467+ )
468+ )
469+ )
470+
471+ def test_raises_RuntimeError(self):
472+ """Function must raise RuntimeError if several addresses are found.
473+
474+ This test is somewhat ugly, and should be refactored once the search
475+ criteria has been refactored into a separate object, rather than a
476+ bunch of named parameters.
477+
478+ """
479+ with self.mock_all_child_calls() as mocks:
480+ fake_address_list = [Mock(), Mock()]
481+ mocks['get_addresses'].return_value = fake_address_list
482+ mocks['filter_addresses'].return_value = fake_address_list
483+
484+ self.assertThat(
485+ lambda: get_proxy_object_for_existing_process(),
486+ raises(
487+ RuntimeError(
488+ "Search criteria (pid = 1, dbus bus = 'session', "
489+ "object path = "
490+ "'/com/canonical/Autopilot/Introspection') "
491+ "returned multiple results"
492+ )
493+ )
494+ )

Subscribers

People subscribed via source and target branches