Merge lp:~adam-disc0tech/ubuntu-autopilot-tests/rhythmbox into lp:ubuntu-autopilot-tests
- rhythmbox
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Dan Chapman | ||||
Approved revision: | 111 | ||||
Merged at revision: | 66 | ||||
Proposed branch: | lp:~adam-disc0tech/ubuntu-autopilot-tests/rhythmbox | ||||
Merge into: | lp:ubuntu-autopilot-tests | ||||
Diff against target: |
1138 lines (+1093/-0) 6 files modified
ubuntu_autopilot_tests/rhythmbox/emulators/gtk.py (+437/-0) ubuntu_autopilot_tests/rhythmbox/resources/playlists.xml (+34/-0) ubuntu_autopilot_tests/rhythmbox/resources/rhythmdb.xml (+177/-0) ubuntu_autopilot_tests/rhythmbox/tests/base.py (+247/-0) ubuntu_autopilot_tests/rhythmbox/tests/test_import_and_play.py (+107/-0) ubuntu_autopilot_tests/rhythmbox/tests/test_rbox.py (+91/-0) |
||||
To merge this branch: | bzr merge lp:~adam-disc0tech/ubuntu-autopilot-tests/rhythmbox | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Dan Chapman (community) | Approve | ||
Nicholas Skaggs (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
First release of tests for Rhythmbox, including launch test, launching help, playing internet radio, playing WAV and OGG files, play / pause / skip track buttons

Nicholas Skaggs (nskaggs) wrote : | # |
Once that's fixed I'll give it a run again. A couple quick comments; we might need to change how you are deriving the home directory, jenkins may not like it. Did the emulator for gtk work out ok?
- 68. By Adam Smith
-
Added resources

Adam Smith (adam-disc0tech) wrote : | # |
> I'll be away next week and won't have proper time to give this a good review.
> However, it looks like you are missing the resources with this commit :-)
Resources added!

Adam Smith (adam-disc0tech) wrote : | # |
> Once that's fixed I'll give it a run again. A couple quick comments; we might
> need to change how you are deriving the home directory, jenkins may not like
> it. Did the emulator for gtk work out ok?
Sure, should I use an absolute path running under jenkins?
The emulator - required significant hacking... Mostly because I couldn't rely on pressing HOME to get to the start of a drop down - as that would select my home directory and rhythmbox would immediately start importing everything from there.

Dan Chapman (dpniel) wrote : | # |
I did think the combobox emulator would need a better solution. I ripped those emulators out of ubiquity tests which it works for it's purpose there. This seems to be the tricky part with gtk emulators as it ultimately depends on the developers implementation choices. I will give your modifications a go on the ubiquity tests and a few other apps and see if it can be used as a more general purpose solution rather than just wacking 'Home'.

Adam Smith (adam-disc0tech) wrote : | # |
OK - but note that it is very hacky emulator code... needs revisiting.
A couple of key changes I made were to ignore items with an accessible role of 49 (invisible separators), and to check which item is focussed (unreliable).
I also disabled some of the code that checks the item by name rather than by index. With hindsight, the reasons I disabled it may be invalid now I have implemented the above.

Dan Chapman (dpniel) wrote : | # |
This test is looking good, there are a couple of minor errors that are a quick fix.
see http://
The first is just a case of cleaning up tabs n spaces. As a side note tests ideally should be python3 compliant so I would recommend using that going forward as it will be used when running tests on Jenkins. it's just a case of using the python3-autopilot package and autopilot3 run/list etc
The Second is just a case of fixing imports in general all testsuites are usually run from the ubuntu-
Also to expand on what Nicholas said about deriving the home directory i personally use os.getenv("HOME") to get the current users home.
If you could make these little fixes first then I will give them a good run :-)
Great job Adam
- 69. By Adam Smith
-
Fixes following review
- 70. By Adam Smith
-
corrected import statement
- 71. By Adam Smith
-
Fixed tabulation issues
- 72. By Adam Smith
-
Added init file
- 73. By Adam Smith
-
fixed import error
- 74. By Adam Smith
-
Fixed properly this time :)
- 75. By Adam Smith
-
Fixed path errors to handle remote directory structure for jenkins
- 76. By Adam Smith
-
...
- 77. By Adam Smith
-
Now checks for existence of Rhythmbox config dir and creates if necessary
- 78. By Adam Smith
-
referenced test_dir correctly
- 79. By Adam Smith
-
Changed filter() to enumeration as failing with py3
- 80. By Adam Smith
-
Split out base class in new structure
- 81. By Adam Smith
-
Added main_window as a property
- 82. By Adam Smith
-
Adjusted properties
- 83. By Adam Smith
-
Modified to work with python 3, refactored. Combo box is now not working in local test environment - pushing for Test Runner test
- 84. By Adam Smith
-
Changed assertion type in emulator
- 85. By Adam Smith
-
Removed assertion in emulator
- 86. By Adam Smith
-
Fixed bug where combo box goes out of range if last item is a separator
- 87. By Adam Smith
-
Additional assertions added
- 88. By Adam Smith
-
Corrected max_index naming error
- 89. By Adam Smith
-
Additional debug statement to investigate out of range issue
- 90. By Adam Smith
-
fix
- 91. By Adam Smith
-
fix
- 92. By Adam Smith
-
Refactored GTK emulator. Changed fallback mode to first assume gail box will be returned in display order, if that fails use combo accessible name to fall back.
Changed import tests to use Pictures directory rather than Downloads, to support local testing.
- 93. By Adam Smith
-
Stable on local
- 94. By Adam Smith
-
Moved library parser to use xpath directly
- 95. By Adam Smith
-
Works locally for single import test
- 96. By Adam Smith
-
hacks
- 97. By Adam Smith
-
Added additional debug logging around expected track names
- 98. By Adam Smith
-
Reversed the order of track play to match that on live image
- 99. By Adam Smith
-
Performance improvement, only check the treeview tracklist once
- 100. By Adam Smith
-
Tested all positive import scenarios and work locally. Enabled all tests

Adam Smith (adam-disc0tech) wrote : | # |
Fixed issues - now runs fine in test-runner
- 101. By Adam Smith
-
Fixed loggin convention
Refactored variable scope
Removed hardcoded resource paths

Nicholas Skaggs (nskaggs) wrote : | # |
First of all, this looks like a really solid base. Lots of cool stuff being explored here; umockdev, emulator tweaks. Kudos to you Adam!
My one comment would be the function names. I would rather we dropped the given, when, and then from the function names and used comments and logger debug messages instead to achieve a similar thing. Going forward, we're attempting to adopt a new model, based upon the page-object model -- all objects are classes. This tends to make the tests more readable in a similar way without having to name utility functions in such a way.
Overall, good work. I support merging this with the naming tweaks I mentioned.
Ohh, one other thing -- this might also be a good candidate to mock the /home env for as well to avoid colliding with user's data.

Dan Chapman (dpniel) wrote : | # |
Nice job Adam :-)
I agree with what Nicholas mentions above but, i think those kind of refactoring changes could be incorporated into you current working branch lp:~adam-disc0tech/ubuntu-autopilot-tests/rhythmbox2.
The only issue I would want resolved before this merges is in combobox.
Also the select_item looks like it has become a little over complex so i think we could refactor that as well at some point.
All in all though I think it's great, and once the combox selection has changed I happy for this to merge

Adam Smith (adam-disc0tech) wrote : | # |
OK thanks for the feedback everyone.
So in this branch I will fix the combox/select item issues. For the second branch mentioned above I'll (1) move to a page object model (2) add additional comments / logging (3) look at mocking the home directory
I also need to remove umockdev from this branch, as I'm actually not using it yet...
- 102. By Adam Smith
-
Removed unnecessary resources
- 103. By Adam Smith
-
Updated the emulator to do extra checks around whether focus is held, and ensure it does not enter an endless loop
- 104. By Adam Smith
-
Included minor changes from second branch
- 105. By Adam Smith
-
Spelling in comment corrected
- 106. By Adam Smith
-
Change to print tree on failure to select combo box
- 107. By Adam Smith
-
Added another exception handling situation that generates a print_tree
- 108. By Adam Smith
-
Change print tree to look at root object
- 109. By Adam Smith
-
Added some attempts to regain focus where necessary
- 110. By Adam Smith
-
fixed tab issue
- 111. By Adam Smith
-
Added exceptions and stack trace logging

Nicholas Skaggs (nskaggs) wrote : | # |
I believe the other work mentioned above is slated for refactoring, so +1 from me.

Dan Chapman (dpniel) wrote : | # |
This is good to merge now since all other changes are being made in the new branch
Preview Diff
1 | === added directory 'ubuntu_autopilot_tests/rhythmbox' |
2 | === added file 'ubuntu_autopilot_tests/rhythmbox/__init__.py' |
3 | === added directory 'ubuntu_autopilot_tests/rhythmbox/emulators' |
4 | === added file 'ubuntu_autopilot_tests/rhythmbox/emulators/__init__.py' |
5 | === added file 'ubuntu_autopilot_tests/rhythmbox/emulators/gtk.py' |
6 | --- ubuntu_autopilot_tests/rhythmbox/emulators/gtk.py 1970-01-01 00:00:00 +0000 |
7 | +++ ubuntu_autopilot_tests/rhythmbox/emulators/gtk.py 2014-03-05 19:06:22 +0000 |
8 | @@ -0,0 +1,437 @@ |
9 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
10 | +# |
11 | +# Copyright (C) Canonical Ltd 2013 |
12 | +# |
13 | +# Author: Daniel Chapman daniel@chapman-mail.com |
14 | +# Modified by: Adam Smith adam@disc0tech.com |
15 | +# |
16 | +# This program is free software; you can redistribute it and/or modify |
17 | +# it under the terms of the GNU Lesser General Public License as published by |
18 | +# the Free Software Foundation; version 3. |
19 | +# |
20 | +# This program is distributed in the hope that it will be useful, |
21 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | +# GNU Lesser General Public License for more details. |
24 | +# |
25 | +# You should have received a copy of the GNU Lesser General Public License |
26 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
27 | +import logging, time |
28 | +import os, inspect |
29 | +from autopilot.input import Pointer, Mouse, Keyboard |
30 | +from autopilot.introspection.dbus import StateNotFoundError |
31 | +from autopilot.introspection import CustomEmulatorBase |
32 | +from autopilot.testcase import AutopilotTestCase |
33 | +from testtools.matchers import Equals, NotEquals, GreaterThan, FileExists, StartsWith, Not |
34 | +from autopilot.matchers import Eventually |
35 | +from autopilot.testcase import AutopilotTestCase |
36 | +from testtools.matchers import Equals, NotEquals, GreaterThan, FileExists, StartsWith, Not |
37 | + |
38 | +logger = logging.getLogger('__name__') |
39 | + |
40 | + |
41 | +class AutopilotGtkEmulatorBase(CustomEmulatorBase): |
42 | + """ Base class for the Gtk Autopilot Emulator """ |
43 | + |
44 | + |
45 | +class GtkTreeView(AutopilotGtkEmulatorBase): |
46 | + """ Emulator for a GtkTreeView instance """ |
47 | + |
48 | + def __init__(self, *args): |
49 | + super(GtkTreeView, self).__init__(*args) |
50 | + self.pointing_device = Pointer(Mouse.create()) |
51 | + |
52 | + def select_item(self, labelText): |
53 | + """ Selects an item in a GtkTreeView by its UI label text |
54 | + |
55 | + :param labelText: The label value of the tree item as seen on the UI |
56 | + :returns: An object of the requested treeitem |
57 | + |
58 | + e.g. If you want to click say an item displaying 'Home' in a treeview |
59 | + then it would be:: |
60 | + |
61 | + >>> treeitem = treeview.select_item('Home') |
62 | + >>> self.mouse.click_object(treeitem) |
63 | + |
64 | + """ |
65 | + logger.debug('Selecting "{0}" item'.format(labelText)) |
66 | + try: |
67 | + #lets try and get the corresponding GailTreeView |
68 | + # so we can assure we are selecting the item from the correct |
69 | + # treeview |
70 | + treeview = self._get_gail_treeview() |
71 | + treeview_item = treeview.select_item(str(labelText)) |
72 | + except StateNotFoundError: |
73 | + #lets catch the exception and have one last go at selecting the |
74 | + # item from a root instance |
75 | + # This may return more than one though. |
76 | + logger.warning('Could not get corresponding ' |
77 | + 'GtkTreeViewAccessibleObject \ |
78 | + with globalRect {0}. Trying to select GtkTreeView ' |
79 | + 'item \ |
80 | + from root object instead'.format(self.globalRect)) |
81 | + treeview_item = self._select_item_from_root(labelText) |
82 | + |
83 | + logger.debug('Corresponding Item Found in treeview. Returning item.') |
84 | + return treeview_item |
85 | + |
86 | + def select_item_by_index(self, index): |
87 | + """ Selects an item in a GtkTreeVIew by its index |
88 | + |
89 | + :param index: Index of the wanted tree view item |
90 | + :returns: Object of the tree view item at the given index |
91 | + """ |
92 | + treeview = self._get_gail_treeview() |
93 | + treeview_item = treeview.select_item_by_index(index) |
94 | + if treeview_item is None: |
95 | + raise ValueError( |
96 | + "Could not select item with index '{0}'".format(index)) |
97 | + return treeview_item |
98 | + |
99 | + |
100 | + def get_all_items(self, startWith=None): |
101 | + """ Gets all items in GtkTreeView |
102 | + |
103 | + :param startWith: gets a list of items that start with given string |
104 | + :returns: List of items in tree view |
105 | + |
106 | + """ |
107 | + treeview = self._get_gail_treeview() |
108 | + items = treeview.get_all_items(startWith) |
109 | + if items is None: |
110 | + raise ValueError("NoneType object: Could not get list of items") |
111 | + return items |
112 | + |
113 | + def _select_item_from_root(self, labelText): |
114 | + root = self.get_root_instance() |
115 | + |
116 | + treeview_item = root.select_single('GtkTextCellAccessible', |
117 | + accessible_name=str(labelText)) |
118 | + |
119 | + if treeview_item is None: |
120 | + raise ValueError( |
121 | + "Could not select item with label '{0}'".format(labelText)) |
122 | + |
123 | + return treeview_item |
124 | + |
125 | + def _get_gail_treeview(self, ): |
126 | + """ Gets the GtkTreeViews corresponding GtkTreeViewAccessible object |
127 | + """ |
128 | + logger.debug('Getting corresponding GtkTreeViewAccessible object') |
129 | + # lets get a root instance |
130 | + root = self.get_root_instance() |
131 | + assert root is not None |
132 | + # As the treeview item is in the GAILWindow tree and not our current |
133 | + # tree |
134 | + # We want to select the treeviewaccessible with the same globalRect |
135 | + # as us |
136 | + logger.debug('Selecting GtkTreeViewAccessible with same globalRect') |
137 | + treeviews = root.select_many('GtkTreeViewAccessible', |
138 | + globalRect=self.globalRect) |
139 | + # if the treeviews are nested they could potentially have the |
140 | + # same globalRect so lets pick out the one thats visible |
141 | + for treeview in treeviews: |
142 | + if treeview.visible: |
143 | + logger.debug( |
144 | + 'GtkTreeViewAccessible object found, returning object.') |
145 | + return treeview |
146 | + raise ValueError( |
147 | + "No treeview visible with globalRect {0}".format(self.globalRect) |
148 | + ) |
149 | + |
150 | + |
151 | +class GtkComboBox(AutopilotGtkEmulatorBase): |
152 | + """ Emulator class for a GtkComboBox instance""" |
153 | + last_index_selected = -1 |
154 | + lack_of_focus_events = 0 |
155 | + lack_of_focus_max = 5 |
156 | + skip_items = [] |
157 | + |
158 | + def set_last_label_selected(self, label): |
159 | + combo = self._get_gail_combobox() |
160 | + items = combo.select_many('GtkMenuItemAccessible') |
161 | + n = 0 |
162 | + for i in items: |
163 | + if(i.accessible_name==label): |
164 | + self.last_index_selected = n |
165 | + n+=1 |
166 | + |
167 | + |
168 | + def __init__(self, *args): |
169 | + super(GtkComboBox, self).__init__(*args) |
170 | + self.pointing_device = Pointer(Mouse.create()) |
171 | + self.kbd = Keyboard.create() |
172 | + |
173 | + def select_item(self, labelText): |
174 | + """ Selects an item in a GtkComboBox by its UI label text value |
175 | + |
176 | + :param labelText: The label value of the list item as seen on the UI |
177 | + |
178 | + e.g. :: |
179 | + |
180 | + >>> combobox = self.main_window.select_single('GtkComboBox') |
181 | + >>> combobox.select_item('ListItem') |
182 | + |
183 | + """ |
184 | + logger.debug('Selecting "{0}" item'.format(labelText)) |
185 | + #get our gail combo to start |
186 | + combo = self._get_gail_combobox() |
187 | + # get total number of items in the combo |
188 | + items = combo.select_many('GtkMenuItemAccessible') |
189 | + # lets check that the item is in the list before cycling through |
190 | + names = [] |
191 | + for i in items: |
192 | + names.append(i.accessible_name) |
193 | + if labelText not in names: |
194 | + raise ValueError( |
195 | + "Could not find '{0}' in combobox items".format(labelText)) |
196 | + |
197 | + logger.debug("Clicking on the combobox") |
198 | + self.pointing_device.click_object(self) |
199 | + logger.debug("Done") |
200 | + self.kbd.press_and_release('Enter') |
201 | + |
202 | + #find the focused item, if you can't assume based on the combo accessible name |
203 | + focused_index = self._find_item_with_focus(items) |
204 | + if focused_index == -1: |
205 | + focused_index = next((index for index, value in enumerate(items) if |
206 | + combo.accessible_name == value.accessible_name), None) |
207 | + logger.debug('Assuming focus is with: ' + combo.accessible_name + ' and that is at index ' + str(focused_index)) |
208 | + |
209 | + x = focused_index |
210 | + max_index = len(items) |
211 | + logger.debug("index has max items: " + str(max_index)) |
212 | + #add a counter to break out and end the test if 100 keypresses does not get us to the right place |
213 | + count = 0 |
214 | + max_count = 100 |
215 | + |
216 | + while True: |
217 | + if len(self.skip_items) > 0: |
218 | + while x in self.skip_items: |
219 | + logger.debug("Skipping item: " + str(x)) |
220 | + x = self.__get_next_pos(x, max_index) |
221 | + |
222 | + logger.debug('Inspecting item ' + str(x)) |
223 | + item = items[x] |
224 | + logger.debug("current index = " + str(x) + " ["+item.accessible_name +"] ") |
225 | + |
226 | + if labelText == item.accessible_name: |
227 | + logger.debug("Found MATCHING labels in combobox: labelText="+labelText+", item.accessible_name="+item.accessible_name) |
228 | + self.kbd.press_and_release('Enter') |
229 | + self.set_last_label_selected(item.accessible_name) |
230 | + logger.debug('Item is now selected, combo.accessible is = ' + combo.accessible_name) |
231 | + |
232 | + |
233 | + #if the displayed item is not as expected |
234 | + if not combo.accessible_name == labelText: |
235 | + while True: |
236 | + logger.debug('Fallback mode') |
237 | + x = self.__move_to_next(x, max_index) |
238 | + self.kbd.press_and_release('Enter') |
239 | + time.sleep(1) |
240 | + self.set_last_label_selected(item.accessible_name) |
241 | + logger.debug('New item now selected, combo.accessible is = ' + combo.accessible_name) |
242 | + if combo.accessible_name == labelText: |
243 | + logger.debug('Combo.accessible matches') |
244 | + logger.debug('Combo box selection complete') |
245 | + return |
246 | + else: |
247 | + logger.debug('Combo box selection complete') |
248 | + time.sleep(1) |
249 | + return |
250 | + |
251 | + else: |
252 | + #this section ensures we do not get stuck in an endless loop |
253 | + count += 1 |
254 | + if count > max_count: |
255 | + logger.error('Reached max_count on keypresses to select the right combo box item') |
256 | + raise Exception("Max number of keypresses in a GtkComboBox exceeded") |
257 | + |
258 | + x = self.__move_to_next(x, max_index) |
259 | + |
260 | + def __move_to_next(self, x, max_index): |
261 | + #check that we have focus then press down key |
262 | + if self._is_an_item_focussed() == False: |
263 | + self.lack_of_focus_events += 1 |
264 | + logger.error('Trying to manipulate a combox box which does not have focus, count: '+ str(self.lack_of_focus_events)) |
265 | + if self.lack_of_focus_events > self.lack_of_focus_max: |
266 | + raise Exception("Lack of focus on the object under test events exceeded maximium") |
267 | + #if more than one event, this is probably not due to a seperator, try some other things |
268 | + if self.lack_of_focus_events > 1: |
269 | + self.pointing_device.click_object(self) |
270 | + if self._is_an_item_focussed() == False: |
271 | + self.kbd.press_and_release('Escape') |
272 | + |
273 | + #move to next item |
274 | + self.kbd.press_and_release('Down') |
275 | + return self.__get_next_pos(x, max_index) |
276 | + |
277 | + def __get_next_pos(self, x, max_index): |
278 | + logger.debug('Getting next combo pos, x = ' + str(x)) |
279 | + if x==(max_index-1): |
280 | + x = 0 |
281 | + logger.debug('Going to the start of the list') |
282 | + else: |
283 | + x += 1 |
284 | + assert x is not None |
285 | + assert x < max_index |
286 | + return x |
287 | + |
288 | + |
289 | + def _is_an_item_focussed(self, ): |
290 | + #return true / false depending on whether one of the items holds focus |
291 | + combo = self._get_gail_combobox() |
292 | + items = combo.select_many('GtkMenuItemAccessible') |
293 | + for i in items: |
294 | + if i.focused: |
295 | + return True |
296 | + return False |
297 | + |
298 | + |
299 | + def _find_item_with_focus(self, items): |
300 | + logger.debug('Finding combo box focus') |
301 | + |
302 | + #determine currently selected item, and skip items with a blank label |
303 | + focused_index = -1 |
304 | + if self.last_index_selected > -1: |
305 | + focused_index = self.last_index_selected |
306 | + logger.debug('Last item selected was ' + str(focused_index)) |
307 | + return focused_index |
308 | + |
309 | + n = 0 |
310 | + for i in items: |
311 | + logger.debug("----------------") |
312 | + logger.debug("Index: " + str(n)) |
313 | + logger.debug("Accessible Name: " + i.accessible_name) |
314 | + logger.debug(" Accessible Role: " + str(i.accessible_role)) |
315 | + logger.debug(" Showing: " + str(i.showing)) |
316 | + logger.debug(" Selected: " + str(i.selected)) |
317 | + logger.debug(" Enabled: " + str(i.enabled)) |
318 | + logger.debug(" Visible: " + str(i.visible)) |
319 | + logger.debug(" Focused: " + str(i.focused)) |
320 | + logger.debug(" Pressed: " + str(i.pressed)) |
321 | + logger.debug(" Active: " + str(i.active)) |
322 | + |
323 | + if(self.last_index_selected < 0 and i.focused): |
324 | + focused_index = n |
325 | + logger.debug('Found item ' + i.accessible_name + ' which holds the focus') |
326 | + |
327 | + if(i.accessible_role == 49): |
328 | + self.skip_items.append(n) |
329 | + logger.debug('Adding to skip list index: ' + str(n) + ' [' + i.accessible_name + ']') |
330 | + |
331 | + n+=1 |
332 | + |
333 | + logger.debug('Focus on: ' + str(focused_index)) |
334 | + logger.debug(str(len(self.skip_items)) + ' items are separators and will be skipped') |
335 | + return focused_index |
336 | + |
337 | + def select_item_by_index(self, index): |
338 | + """ Selects an item by given index""" |
339 | + combo = self._get_gail_combobox() |
340 | + # get total number of items in the combo |
341 | + items = combo.select_many('GtkMenuItemAccessible') |
342 | + |
343 | + focused_index = self._find_item_with_focus(items) |
344 | + i = focused_index |
345 | + max_index = len(items)-1 |
346 | + |
347 | + logger.debug("index has max items: " + str(max_index)) |
348 | + |
349 | + while True: |
350 | + if i in self.skip_items: |
351 | + logger.debug("Skipping item: " + str(i)) |
352 | + i+=1 |
353 | + |
354 | + logger.debug("current index = " + str(i) + " ["+items[i].accessible_name +"] "+ "target = " + str(index)) |
355 | + if i != index: |
356 | + i = self.__move_to_next(i, max_index) |
357 | + else: |
358 | + assert i == index |
359 | + logger.debug("Found correct item, hitting ENTER") |
360 | + self.kbd.press_and_release('Enter') |
361 | + self.last_index_selected = i |
362 | + break |
363 | + |
364 | + def _get_gail_combobox(self, ): |
365 | + """ Gets the GtkComBox's corresponding GtkComboBoxAccessible object """ |
366 | + # lets get a root instance |
367 | + logger.debug('Getting corresponding GtkComboBoxAccessible object') |
368 | + root = self.get_root_instance() |
369 | + assert root is not None |
370 | + combos = root.select_many('GtkComboBoxAccessible', |
371 | + globalRect=self.globalRect) |
372 | + for combo in combos: |
373 | + if combo.visible: |
374 | + logger.debug('PICTURES found, returning combo') |
375 | + return combo |
376 | + raise ValueError( |
377 | + "No ComboBox visible with globalRect {0}".format(self.globalRect) |
378 | + ) |
379 | + |
380 | + |
381 | +class GtkComboBoxText(GtkComboBox): |
382 | + """ Emulator class for a GtkComboBoxText instance |
383 | + |
384 | + .. note:: `GtkComboBoxText` inherits from :func: `GtkComboBox` |
385 | + |
386 | + """ |
387 | + |
388 | + def __init__(self, *args): |
389 | + super(GtkComboBoxText, self).__init__(*args) |
390 | + self.pointing_device = Pointer(Mouse.create()) |
391 | + self.kbd = Keyboard.create() |
392 | + |
393 | + |
394 | +class GtkTreeViewAccessible(AutopilotGtkEmulatorBase): |
395 | + |
396 | + def __init__(self, *args): |
397 | + super(GtkTreeViewAccessible, self).__init__(*args) |
398 | + self.pointing_device = Pointer(Mouse.create()) |
399 | + |
400 | + def select_item(self, label): |
401 | + """ Selects an item in a GtkTreeViewAccessible object """ |
402 | + logger.debug('Selecting {0} item'.format(label)) |
403 | + item = self._get_item(label) |
404 | + if item is None: |
405 | + raise ValueError("Could not select item with label {0}".format( |
406 | + label)) |
407 | + logger.debug('{0} item found, returning item'.format(label)) |
408 | + return item |
409 | + |
410 | + def select_item_by_index(self, index): |
411 | + """ Select an item by its index """ |
412 | + return self._get_treeview_items()[index] |
413 | + |
414 | + def get_all_items(self, startWith=None): |
415 | + """ Gets all items in GtkTreeView |
416 | + |
417 | + :param startWith: gets a list of items that start with given string |
418 | + :rtype: List of items in treeview |
419 | + |
420 | + """ |
421 | + item_list = [] |
422 | + items = self._get_treeview_items() |
423 | + if startWith: |
424 | + logger.debug( |
425 | + "Searching for items beginning with '{0}'".format(startWith)) |
426 | + for item in items: |
427 | + if startWith in item.accessible_name[0:len(startWith)+1]: |
428 | + item_list.append(item) |
429 | + return item_list |
430 | + else: |
431 | + return items |
432 | + |
433 | + def _get_treeview_items(self, ): |
434 | + logger.debug('Getting list of items.....') |
435 | + items = self.select_many('GtkTextCellAccessible') |
436 | + if items is None: |
437 | + raise ValueError( |
438 | + "Could not get a list of treeview items" |
439 | + ) |
440 | + return items |
441 | + |
442 | + def _get_item(self, label): |
443 | + """ Gets an item in a GtkTreeView """ |
444 | + return self.select_many('GtkTextCellAccessible', |
445 | + accessible_name=label)[0] |
446 | |
447 | === added directory 'ubuntu_autopilot_tests/rhythmbox/resources' |
448 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/002.ogg' |
449 | Binary files ubuntu_autopilot_tests/rhythmbox/resources/002.ogg 1970-01-01 00:00:00 +0000 and ubuntu_autopilot_tests/rhythmbox/resources/002.ogg 2014-03-05 19:06:22 +0000 differ |
450 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/092.ogg' |
451 | Binary files ubuntu_autopilot_tests/rhythmbox/resources/092.ogg 1970-01-01 00:00:00 +0000 and ubuntu_autopilot_tests/rhythmbox/resources/092.ogg 2014-03-05 19:06:22 +0000 differ |
452 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/playlists.xml' |
453 | --- ubuntu_autopilot_tests/rhythmbox/resources/playlists.xml 1970-01-01 00:00:00 +0000 |
454 | +++ ubuntu_autopilot_tests/rhythmbox/resources/playlists.xml 2014-03-05 19:06:22 +0000 |
455 | @@ -0,0 +1,34 @@ |
456 | +<?xml version="1.0"?> |
457 | +<rhythmdb-playlists> |
458 | + <playlist name="My Top Rated" type="automatic" sort-key="Rating" sort-direction="1"> |
459 | + <conjunction> |
460 | + <equals prop="type">song</equals> |
461 | + <subquery> |
462 | + <conjunction> |
463 | + <greater prop="rating">4.000000</greater> |
464 | + </conjunction> |
465 | + </subquery> |
466 | + </conjunction> |
467 | + </playlist> |
468 | + <playlist name="Recently Added" type="automatic" sort-key="FirstSeen" sort-direction="1"> |
469 | + <conjunction> |
470 | + <equals prop="type">song</equals> |
471 | + <subquery> |
472 | + <conjunction> |
473 | + <current-time-within prop="first-seen">604800</current-time-within> |
474 | + </conjunction> |
475 | + </subquery> |
476 | + </conjunction> |
477 | + </playlist> |
478 | + <playlist name="Recently Played" type="automatic" sort-key="LastPlayed" sort-direction="1"> |
479 | + <conjunction> |
480 | + <equals prop="type">song</equals> |
481 | + <subquery> |
482 | + <conjunction> |
483 | + <current-time-within prop="last-played">604800</current-time-within> |
484 | + </conjunction> |
485 | + </subquery> |
486 | + </conjunction> |
487 | + </playlist> |
488 | + <playlist name="Play Queue" type="queue"/> |
489 | +</rhythmdb-playlists> |
490 | |
491 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/podcast-timestamp' |
492 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/rhythmdb.xml' |
493 | --- ubuntu_autopilot_tests/rhythmbox/resources/rhythmdb.xml 1970-01-01 00:00:00 +0000 |
494 | +++ ubuntu_autopilot_tests/rhythmbox/resources/rhythmdb.xml 2014-03-05 19:06:22 +0000 |
495 | @@ -0,0 +1,177 @@ |
496 | +<?xml version="1.0" standalone="yes"?> |
497 | +<rhythmdb version="1.8"> |
498 | + <entry type="iradio"> |
499 | + <title>WKNC 88.1 FM (NC State) (Low Quality)</title> |
500 | + <genre>Music</genre> |
501 | + <artist></artist> |
502 | + <album></album> |
503 | + <location>http://wknc.sma.ncsu.edu:8000/wkncmq.ogg.m3u</location> |
504 | + <date>0</date> |
505 | + <media-type>application/octet-stream</media-type> |
506 | + </entry> |
507 | + <entry type="iradio"> |
508 | + <title>WKNC 88.1 FM (NC State) (High Quality)</title> |
509 | + <genre>Music</genre> |
510 | + <artist></artist> |
511 | + <album></album> |
512 | + <location>http://wknc.sma.ncsu.edu:8000/wknchq.ogg.m3u</location> |
513 | + <date>0</date> |
514 | + <media-type>application/octet-stream</media-type> |
515 | + </entry> |
516 | + <entry type="iradio"> |
517 | + <title>Absolute Radio 80s (Modem)</title> |
518 | + <genre>80's</genre> |
519 | + <artist></artist> |
520 | + <album></album> |
521 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=a8</location> |
522 | + <date>0</date> |
523 | + <media-type>application/octet-stream</media-type> |
524 | + </entry> |
525 | + <entry type="iradio"> |
526 | + <title>Absolute Radio 80s (Broadband)</title> |
527 | + <genre>80's</genre> |
528 | + <artist></artist> |
529 | + <album></album> |
530 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=a8bb</location> |
531 | + <date>0</date> |
532 | + <media-type>application/octet-stream</media-type> |
533 | + </entry> |
534 | + <entry type="iradio"> |
535 | + <title>Absolute Classic Rock (Modem)</title> |
536 | + <genre>Rock'n'Roll</genre> |
537 | + <artist></artist> |
538 | + <album></album> |
539 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=vc</location> |
540 | + <date>0</date> |
541 | + <media-type>application/octet-stream</media-type> |
542 | + </entry> |
543 | + <entry type="iradio"> |
544 | + <title>Absolute Classic Rock (Broadband)</title> |
545 | + <genre>Rock'n'Roll</genre> |
546 | + <artist></artist> |
547 | + <album></album> |
548 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=vcbb</location> |
549 | + <date>0</date> |
550 | + <media-type>application/octet-stream</media-type> |
551 | + </entry> |
552 | + <entry type="iradio"> |
553 | + <title>Absolute Radio 90s (Broadband)</title> |
554 | + <genre>90's</genre> |
555 | + <artist></artist> |
556 | + <album></album> |
557 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=a9bb</location> |
558 | + <date>0</date> |
559 | + <media-type>application/octet-stream</media-type> |
560 | + </entry> |
561 | + <entry type="iradio"> |
562 | + <title>Absolute Radio 90s (Modem)</title> |
563 | + <genre>90's</genre> |
564 | + <artist></artist> |
565 | + <album></album> |
566 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=a9</location> |
567 | + <date>0</date> |
568 | + <media-type>application/octet-stream</media-type> |
569 | + </entry> |
570 | + <entry type="iradio"> |
571 | + <title>HBR1.com - Dream Factory</title> |
572 | + <genre>Ambient</genre> |
573 | + <artist></artist> |
574 | + <album></album> |
575 | + <location>http://ubuntu.hbr1.com:19800/ambient.ogg</location> |
576 | + <date>0</date> |
577 | + <media-type>application/octet-stream</media-type> |
578 | + </entry> |
579 | + <entry type="iradio"> |
580 | + <title>StartFM - 94.2MHz, Vilnius University, Lithuania</title> |
581 | + <genre>Underground</genre> |
582 | + <artist></artist> |
583 | + <album></album> |
584 | + <location>http://eteris.startfm.lt/startfm.ogg</location> |
585 | + <date>0</date> |
586 | + <media-type>application/octet-stream</media-type> |
587 | + </entry> |
588 | + <entry type="iradio"> |
589 | + <title>WSUM 91.7 FM (University of Wisconsin)</title> |
590 | + <genre>College Radio</genre> |
591 | + <artist></artist> |
592 | + <album></album> |
593 | + <location>http://vu.wsum.wisc.edu/wsumlive.m3u</location> |
594 | + <date>0</date> |
595 | + <media-type>application/octet-stream</media-type> |
596 | + </entry> |
597 | + <entry type="iradio"> |
598 | + <title>Radio Paradise (Low Quality)</title> |
599 | + <genre>Eclectic</genre> |
600 | + <artist></artist> |
601 | + <album></album> |
602 | + <location>http://stream-sd.radioparadise.com:9000/rp_96.ogg</location> |
603 | + <date>0</date> |
604 | + <media-type>application/octet-stream</media-type> |
605 | + </entry> |
606 | + <entry type="iradio"> |
607 | + <title>Radio Paradise (High Quality)</title> |
608 | + <genre>Eclectic</genre> |
609 | + <artist></artist> |
610 | + <album></album> |
611 | + <location>http://stream-tx1.radioparadise.com:9000/rp_192.ogg</location> |
612 | + <date>0</date> |
613 | + <media-type>application/octet-stream</media-type> |
614 | + </entry> |
615 | + <entry type="iradio"> |
616 | + <title>KWUR 90.3 FM St. Louis Underground Radio</title> |
617 | + <genre>Unknown</genre> |
618 | + <artist></artist> |
619 | + <album></album> |
620 | + <location>http://www.kwur.com/icecast/kwurogg.m3u</location> |
621 | + <date>0</date> |
622 | + <media-type>application/octet-stream</media-type> |
623 | + </entry> |
624 | + <entry type="iradio"> |
625 | + <title>wbur.org - Boston's NPR news source</title> |
626 | + <genre>News</genre> |
627 | + <artist></artist> |
628 | + <album></album> |
629 | + <location>http://audio.wbur.org/stream/live_ogg.m3u</location> |
630 | + <date>0</date> |
631 | + <media-type>application/octet-stream</media-type> |
632 | + </entry> |
633 | + <entry type="iradio"> |
634 | + <title>Absolute Radio (Modem)</title> |
635 | + <genre>Pop</genre> |
636 | + <artist></artist> |
637 | + <album></album> |
638 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=vr</location> |
639 | + <date>0</date> |
640 | + <media-type>application/octet-stream</media-type> |
641 | + </entry> |
642 | + <entry type="iradio"> |
643 | + <title>Absolute Radio (Broadband)</title> |
644 | + <genre>Pop</genre> |
645 | + <artist></artist> |
646 | + <album></album> |
647 | + <location>http://network.absoluteradio.co.uk/core/audio/ogg/live.pls?service=vrbb</location> |
648 | + <date>0</date> |
649 | + <media-type>application/octet-stream</media-type> |
650 | + </entry> |
651 | + <entry type="iradio"> |
652 | + <title>HBR1.com - I.D.M. Tranceponder</title> |
653 | + <genre>Trance</genre> |
654 | + <artist></artist> |
655 | + <album></album> |
656 | + <location>http://ubuntu.hbr1.com:19800/trance.ogg</location> |
657 | + <play-count>7</play-count> |
658 | + <last-played>1390659227</last-played> |
659 | + <bitrate>64</bitrate> |
660 | + <date>0</date> |
661 | + <media-type>application/octet-stream</media-type> |
662 | + </entry> |
663 | + <entry type="iradio"> |
664 | + <title>HBR1.com - Tronic Lounge</title> |
665 | + <genre>House</genre> |
666 | + <artist></artist> |
667 | + <album></album> |
668 | + <location>http://ubuntu.hbr1.com:19800/tronic.ogg</location> |
669 | + <date>0</date> |
670 | + <media-type>application/octet-stream</media-type> |
671 | + </entry> |
672 | +</rhythmdb> |
673 | |
674 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/trk1.wav' |
675 | Binary files ubuntu_autopilot_tests/rhythmbox/resources/trk1.wav 1970-01-01 00:00:00 +0000 and ubuntu_autopilot_tests/rhythmbox/resources/trk1.wav 2014-03-05 19:06:22 +0000 differ |
676 | === added file 'ubuntu_autopilot_tests/rhythmbox/resources/trk2.wav' |
677 | Binary files ubuntu_autopilot_tests/rhythmbox/resources/trk2.wav 1970-01-01 00:00:00 +0000 and ubuntu_autopilot_tests/rhythmbox/resources/trk2.wav 2014-03-05 19:06:22 +0000 differ |
678 | === added directory 'ubuntu_autopilot_tests/rhythmbox/tests' |
679 | === added file 'ubuntu_autopilot_tests/rhythmbox/tests/__init__.py' |
680 | === added file 'ubuntu_autopilot_tests/rhythmbox/tests/base.py' |
681 | --- ubuntu_autopilot_tests/rhythmbox/tests/base.py 1970-01-01 00:00:00 +0000 |
682 | +++ ubuntu_autopilot_tests/rhythmbox/tests/base.py 2014-03-05 19:06:22 +0000 |
683 | @@ -0,0 +1,247 @@ |
684 | +from autopilot.testcase import AutopilotTestCase |
685 | +from testtools.matchers import Equals, NotEquals, GreaterThan, FileExists, StartsWith, Not |
686 | +from autopilot.matchers import Eventually |
687 | +from autopilot.introspection import get_proxy_object_for_existing_process |
688 | +import subprocess, re, getpass, time, logging, os, sys, shutil, locale, traceback |
689 | +from rhythmbox.emulators import gtk |
690 | +from collections import namedtuple |
691 | +from autopilot.input import Keyboard, Mouse |
692 | + |
693 | + |
694 | +logger = logging.getLogger('__name__') |
695 | +logger.setLevel(10) |
696 | + |
697 | +class RhythmBoxTestCase(AutopilotTestCase): |
698 | + |
699 | + def setUp(self, ): |
700 | + super(AutopilotTestCase, self).setUp() |
701 | + logger.debug('sys.path = ' + str(sys.path)) |
702 | + |
703 | + #walk the full file system to confirm where the resources directory is |
704 | + res_dir = self._get_path('trk1.wav') |
705 | + |
706 | + #assume that the rhythmbox config remains in teh same laction |
707 | + test_dir = os.getenv("HOME") + "/.local/share/rhythmbox/" |
708 | + |
709 | + #define baselined config |
710 | + res_rhythmdb = res_dir + "rhythmdb.xml" |
711 | + test_rhythmdb = test_dir + 'rhythmdb.xml' |
712 | + res_playlists = res_dir + 'playlists.xml' |
713 | + test_playlists = test_dir + 'playlists.xml' |
714 | + res_podcasts = res_dir + 'podcast-timestamp' |
715 | + test_podcasts = test_dir + '/podcast-timestamp' |
716 | + |
717 | + #define objects for track view |
718 | + self.Track = namedtuple('Track', 'name genre artist album time') |
719 | + self.tracklist = [] |
720 | + |
721 | + #reset local database / playlist files |
722 | + try: |
723 | + os.stat(test_dir) |
724 | + except: |
725 | + os.mkdir(test_dir) |
726 | + |
727 | + shutil.copy (res_rhythmdb, test_rhythmdb) |
728 | + shutil.copy (res_playlists, test_playlists) |
729 | + shutil.copy (res_podcasts, test_podcasts) |
730 | + self.addCleanup(os.remove, (test_rhythmdb)) |
731 | + self.addCleanup(os.remove, (test_playlists)) |
732 | + self.addCleanup(os.remove, (test_podcasts)) |
733 | + self._mouse = None |
734 | + self._kb = None |
735 | + |
736 | + #we create our root object and since we are going to use the emulator |
737 | + # we need to inform autopilot when launching the application. |
738 | + self.root_obj = self.launch_test_application( |
739 | + 'rhythmbox', |
740 | + app_type='gtk', |
741 | + emulator_base=gtk.AutopilotGtkEmulatorBase) |
742 | + # then to make thing a bit easier create an object of the app window |
743 | + # to ensure we are always selecting from the correct tree |
744 | + main_window = self.root_obj.select_single('GtkApplicationWindow') |
745 | + |
746 | + #lets start with getting the left panel |
747 | + display_source = main_window.select_single('RBDisplayPageTree') |
748 | + # 'display_source' now contains all objects in the left panel |
749 | + # so we want the tree view inside this panel |
750 | + self.treeview = display_source.select_single('GtkTreeView') |
751 | + self.treeitems = self.treeview.get_all_items() |
752 | + #for item in self.treeitems: |
753 | + |
754 | + |
755 | + def _get_path(self, filename): |
756 | + # lets walk down all dirs and files of current directory |
757 | + for root, dirs, files in os.walk(os.getcwd()): |
758 | + # for each file we come across see if it's the one we want |
759 | + for name in files: |
760 | + #if it is then return the path |
761 | + if name == filename: |
762 | + logger.debug('Found resources root: ' + root) |
763 | + return os.path.abspath(os.path.join(root)) + '/' |
764 | + |
765 | + |
766 | + #PROPERTIES |
767 | + @property |
768 | + def main_window(self, ): |
769 | + return self.root_obj.select_single('GtkApplicationWindow') |
770 | + |
771 | + @property |
772 | + def keyboard(self): |
773 | + if self._kb is None: |
774 | + self._kb = Keyboard.create() |
775 | + return self._kb |
776 | + |
777 | + @property |
778 | + def mouse(self): |
779 | + if self._mouse is None: |
780 | + self._mouse = Mouse.create() |
781 | + return self._mouse |
782 | + |
783 | + def givenThisDirectoryIsEmpty(self, d): |
784 | + d = d.replace('$HOME',os.getenv("HOME")) |
785 | + for the_file in os.listdir(d): |
786 | + file_path = os.path.join(d, the_file) |
787 | + if os.path.isfile(file_path): |
788 | + os.unlink(file_path) |
789 | + |
790 | + def givenIHaveCopiedFile_To(self, f, target): |
791 | + target = target.replace('$HOME',os.getenv("HOME")) |
792 | + ps = subprocess.Popen("cp rhythmbox/resources/" + f + " " + target, shell=True, stdout=subprocess.PIPE) |
793 | + ps.wait() |
794 | + self.assertThat(target, FileExists()) |
795 | + logger.debug(str(f) + ' copied to ' + target) |
796 | + |
797 | + def whenISelectObjectByFilter(self, object_type, type_name='*', **kwargs): |
798 | + object = self.main_window.select_single(object_type, **kwargs) |
799 | + self.mouse.click_object(object) |
800 | + |
801 | + def thenObjectMustBeFocusable(self, object_type, type_name='*', **kwargs): |
802 | + object = self.main_window.select_single(object_type, **kwargs) |
803 | + self.assertThat(object.can_focus, Eventually(Equals(True))) |
804 | + |
805 | + def thenObjectLabelMustBe(self, text, object_type, type_name='*', **kwargs): |
806 | + object = self.main_window.select_single(object_type, **kwargs) |
807 | + logger.debug("Waiting for object " + object_type + " to have a label: " + text) |
808 | + self.assertThat(object.label, Eventually(Equals(text))) |
809 | + |
810 | + |
811 | + def whenISelectALeftPanelOption(self, option): |
812 | + try: |
813 | + self.assertTrue(self.main_window.visible) |
814 | + display_source = self.main_window.select_single('RBDisplayPageTree') |
815 | + # 'display_source' now contains all objects in the left panel |
816 | + # so we want the tree view inside this panel |
817 | + treeview = display_source.select_single('GtkTreeView') |
818 | + treeitems = treeview.get_all_items() |
819 | + self.assertThat(len(treeitems), GreaterThan(0)) |
820 | + # now we have the correct treeview, we use the emulated select_item() |
821 | + # to get the 'Radio' item. select_item() will only work on a |
822 | + # GtkTreeView object and just requires the label value exactly as you |
823 | + # see it on the UI. |
824 | + tree_item = treeview.select_item(option) |
825 | + self.mouse.click_object(tree_item) |
826 | + tree_item2 = treeview.select_item(option) |
827 | + #lets quickly assert the tree item is in selected state |
828 | + self.assertThat(tree_item2.selected, Eventually(Equals(True))) |
829 | + except: |
830 | + print(traceback.format_exc()) |
831 | + self.root_obj.print_tree() |
832 | + raise Exception("Test failed to select the left panel, check print_tree in logs") |
833 | + |
834 | + |
835 | + ####################### |
836 | + # THEN METHODS |
837 | + |
838 | + def thenStatusBarStartsWith(self, text): |
839 | + logger.debug("Waiting for status bar to start with: " + text) |
840 | + self.assertThat(self.getStatusBarText(), Eventually(StartsWith(text), timeout=30)) |
841 | + |
842 | + def thenStatusBarShows(self, text): |
843 | + logger.debug("Waiting for status bar to show: " + text) |
844 | + self.assertThat(self.getStatusBarText(), Eventually(Equals(self.getStatusBarText()), timeout=20)) |
845 | + |
846 | + |
847 | + def thenFindThisProcess(self, process_name ): |
848 | + ps = subprocess.Popen("ps aux | grep "+process_name, shell=True, stdout=subprocess.PIPE) |
849 | + output = ps.stdout.read() |
850 | + ps.stdout.close() |
851 | + ps.wait() |
852 | + return output |
853 | + |
854 | + def thenGetThisProcessPID(self, process_name): |
855 | + output = self.thenFindThisProcess(process_name) |
856 | + logger.debug('Raw process list obtained') |
857 | + user = getpass.getuser() |
858 | + self.assertThat(len(user), GreaterThan(0)) |
859 | + process_list = output.decode(locale.getdefaultlocale()[1]).split(user) |
860 | + self.assertThat(len(process_list), GreaterThan(1)) |
861 | + logger.debug('DECODED:') |
862 | + logger.debug(process_list) |
863 | + |
864 | + for x in range(0, len(process_list)): |
865 | + logger.debug('Enumerating the process list, item: ' + str(x) + ' [' + str(process_list[x] + ']')) |
866 | + #this ignores the grep process |
867 | + if not "grep" in process_list[x] and len(process_list[x]) > 0: |
868 | + target_process = list(filter(None, process_list[x].split(' '))) |
869 | + self.assertThat(len(target_process), GreaterThan(0)) |
870 | + logger.debug('Found process: ' + str(target_process)) |
871 | + pid = target_process[0] |
872 | + logger.debug("Target process is: " + pid) |
873 | + return int(pid) |
874 | + return -1 |
875 | + |
876 | + |
877 | + def thenGetTheLibraryView(self): |
878 | + self.tracklist = [] |
879 | + time.sleep(1) |
880 | + state_dicts = self.root_obj.get_state_by_path('/Root/GtkApplicationWindow/GtkBox/GtkPaned/GtkPaned/GtkBox/GtkNotebook/RBLibrarySource/GtkNotebook/GtkGrid/GtkPaned/RBEntryView/GtkTreeView') |
881 | + tracks_treeview = [self.root_obj.make_introspection_object(i) for i in state_dicts][0] |
882 | + |
883 | + #build the track list view |
884 | + x=1 |
885 | + while(True): |
886 | + try: |
887 | + logger.debug("Adding track to tracklist collection: " + tracks_treeview.select_item_by_index(x).accessible_name) |
888 | + track = self.Track(tracks_treeview.select_item_by_index(x).accessible_name, tracks_treeview.select_item_by_index(x+1).accessible_name, tracks_treeview.select_item_by_index(x+2).accessible_name, tracks_treeview.select_item_by_index(x+3).accessible_name, tracks_treeview.select_item_by_index(x+4).accessible_name) |
889 | + self.tracklist.append(track) |
890 | + logger.debug(str(x) + " index: " + track.name) |
891 | + x += 6 |
892 | + except: |
893 | + logger.exception("Caught exception parsing treeview") |
894 | + logger.debug("Finished parsing tracklist collection, objects added " + str(len(self.tracklist))) |
895 | + logger.debug("Exception thrown accessing treeview index " + str(x)) |
896 | + break |
897 | + |
898 | + def thenGetTrackNameFromLibraryPosition(self, x): |
899 | + try: |
900 | + if len(self.tracklist) < 1: |
901 | + self.thenGetTheLibraryView() |
902 | + except: |
903 | + self.thenGetTheLibraryView() |
904 | + self.assertThat(len(self.tracklist), GreaterThan(0)) |
905 | + return self.tracklist[x].name |
906 | + |
907 | + |
908 | + def thenCheckThatThisTrackIsInTheLibraryView(self, track): |
909 | + self.thenGetTheLibraryView() |
910 | + self.assertIn(track,self.tracklist) |
911 | + logger.debug('Copied ' + file + ' to ' + target) |
912 | + time.sleep(1) |
913 | + self.assertThat(target, FileExists()) |
914 | + self.addCleanup(os.remove, target) |
915 | + |
916 | + def getStatusBarText(self, ): |
917 | + tmp = self.main_window.select_single('RBStatusbar') |
918 | + status_bar = tmp.select_single('GtkLabel') |
919 | + status_bar_text = status_bar.label |
920 | + logger.debug('Status bar: ' + status_bar_text) |
921 | + return status_bar_text |
922 | + |
923 | + def TearDown(self, ): |
924 | + super(RhythmBoxTestCase, self).tearDown() |
925 | + shutil.copy (self.test_rhythmdb, '/tmp/last_rhythmdb.xml') |
926 | + shutil.copy (self.test_playlists, '/tmp/last_playlists.xml') |
927 | + shutil.copy (self.test_podcasts, '/tmp/last_podcast') |
928 | + |
929 | + |
930 | + |
931 | |
932 | === added file 'ubuntu_autopilot_tests/rhythmbox/tests/test_import_and_play.py' |
933 | --- ubuntu_autopilot_tests/rhythmbox/tests/test_import_and_play.py 1970-01-01 00:00:00 +0000 |
934 | +++ ubuntu_autopilot_tests/rhythmbox/tests/test_import_and_play.py 2014-03-05 19:06:22 +0000 |
935 | @@ -0,0 +1,107 @@ |
936 | +from autopilot.testcase import AutopilotTestCase |
937 | +from testtools.matchers import Equals, NotEquals, GreaterThan, FileExists, StartsWith, Not |
938 | +from autopilot.matchers import Eventually |
939 | +from autopilot.introspection import get_proxy_object_for_existing_process |
940 | +import subprocess, re, getpass, time, logging, os, sys, shutil |
941 | +from rhythmbox.emulators import gtk |
942 | +from rhythmbox.tests.test_rbox import RhythmBoxTestCase |
943 | +from collections import namedtuple |
944 | + |
945 | +logger = logging.getLogger('__name__') |
946 | +logger.setLevel(10) |
947 | + |
948 | +class ImportAndPlayTestCase(RhythmBoxTestCase): |
949 | + |
950 | + #TODO: Support file types that need extra plugins |
951 | + scenarios = [ |
952 | + ('wav/wav', {'file1': "trk1.wav", 'file2': "trk2.wav"}), |
953 | + ('ogg/ogg', {'file1': "002.ogg", 'file2': "092.ogg"}), |
954 | + ('ogg/wav', {'file1': "002.ogg", 'file2': "trk1.wav"}), |
955 | + ('wav/ogg', {'file1': "092.ogg", 'file2': "trk2.wav"}), |
956 | + |
957 | + #('2 mp3', {'file1': "001.mp3", 'file2': "091.mp3"}), |
958 | + # ('2 wmv', {'file1': "003.wmv", 'file2': "093.wmv"}), |
959 | + # ('2 ac3', {'file1': "004.ac3", 'file2': "094.ac3"}), |
960 | + # ('2 mp4', {'file1': "005.mp4", 'file2': "095.mp4"}), |
961 | + ] |
962 | + |
963 | + def test_import_and_play_music_and_skip_tracks(self, ): |
964 | + |
965 | + #check the environment is clean |
966 | + self.tracklist = None |
967 | + self.givenThisDirectoryIsEmpty('$HOME/Pictures') |
968 | + self.givenThisDirectoryIsEmpty('$HOME/Videos') |
969 | + |
970 | + logger.debug("TEST IMPORT AND PLAY MUSIC AND SKIP TRACKS. SCENARIO START") |
971 | + #setup the test data |
972 | + self.givenIHaveCopiedFile_To(self.file1, '$HOME/Videos/' + self.file1) |
973 | + self.givenIHaveCopiedFile_To(self.file2, '$HOME/Pictures/' + self.file2) |
974 | + |
975 | + #Select the music library |
976 | + self.whenISelectALeftPanelOption('Music') |
977 | + self.thenStatusBarStartsWith(u'0 songs') |
978 | + |
979 | + #import a song from the VIDEOS directory |
980 | + logger.debug("WHEN I IMPORT A SONG FROM THE VIDEOS DIRECTORY") |
981 | + self.whenISelectObjectByFilter('GtkButton',action_name=u'app.library-import') |
982 | + self.whenISelectObjectByFilter('GtkFileChooserButton', BuilderName='file-chooser-button') |
983 | + comboBox = self.main_window.select_single('GtkComboBox') |
984 | + try: |
985 | + comboBox.select_item('Videos') |
986 | + except: |
987 | + self.root_obj.print_tree() |
988 | + raise Exception("Failed to select the combobox item, check print_tree in logs") |
989 | + |
990 | + self.thenObjectMustBeFocusable('GtkButton', BuilderName=u'import-button') |
991 | + self.thenObjectLabelMustBe(u'Import 1 listed track', 'GtkButton', BuilderName=u'import-button') |
992 | + self.whenISelectObjectByFilter('GtkButton', BuilderName=u'import-button') |
993 | + self.thenStatusBarStartsWith(u'1 song, 0 minutes') |
994 | + |
995 | + #import a song from the PICTURES directory |
996 | + logger.debug("WHEN I IMPORT A SONG FROM THE PICTURES DIRECTORY") |
997 | + self.whenISelectObjectByFilter('GtkButton',action_name=u'app.library-import') |
998 | + self.whenISelectObjectByFilter('GtkFileChooserButton', BuilderName='file-chooser-button') |
999 | + comboBox = self.main_window.select_single('GtkComboBox') |
1000 | + comboBox.set_last_label_selected('Videos') |
1001 | + |
1002 | + try: |
1003 | + comboBox.select_item('Pictures') |
1004 | + except: |
1005 | + self.root_obj.print_tree() |
1006 | + raise Exception("Failed to select the combobox item, check print_tree in logs") |
1007 | + |
1008 | + self.thenObjectMustBeFocusable('GtkButton', BuilderName=u'import-button') |
1009 | + self.thenObjectLabelMustBe(u'Import 1 listed track', 'GtkButton', BuilderName=u'import-button') |
1010 | + self.whenISelectObjectByFilter('GtkButton', BuilderName=u'import-button') |
1011 | + |
1012 | + self.thenStatusBarStartsWith(u'2 songs, 0 minutes') |
1013 | + |
1014 | + # Below is reversed in order, deliberately, to match reverse alpha on track name, which is the default startup |
1015 | + track_one_name = (self.thenGetTrackNameFromLibraryPosition(1)) |
1016 | + track_two_name = (self.thenGetTrackNameFromLibraryPosition(0)) |
1017 | + |
1018 | + #When I play the first track |
1019 | + logger.debug("WHEN I PLAY THE FIRST TRACK") |
1020 | + self.whenISelectObjectByFilter('GtkButton',BuilderName=u'play-button') |
1021 | + |
1022 | + #Then the tile of the song should be in the window title bar |
1023 | + logger.debug("THEN THE TITLE OF THE SONG " + track_one_name + " SHOULD BE IN THE WINDOW TITLE BAR") |
1024 | + self.assertThat(self.main_window.title, Eventually(Equals(u'Unknown - '+ track_one_name))) |
1025 | + |
1026 | + #And then the second file should play after len track_1_length |
1027 | + logger.debug("AND THE SECOND TRACK " + track_two_name + " SHOULD PLAY AFTERWARDS") |
1028 | + self.assertThat(self.main_window.title, Eventually(Equals(u'Unknown - ' + track_two_name), timeout=20)) |
1029 | + #when I pause the track and press back |
1030 | + logger.debug("WHEN I PAUSE THE TRACK AND SKIP BACK") |
1031 | + self.whenISelectObjectByFilter('GtkButton',BuilderName=u'play-button') |
1032 | + self.whenISelectObjectByFilter('GtkButton',BuilderName=u'previous-button') |
1033 | + |
1034 | + #then track one should play again |
1035 | + logger.debug("THEN TRACK ONE" + track_one_name+ " SHOULD PLAY AGAIN") |
1036 | + self.assertThat(self.main_window.title, Eventually(Equals(u'Unknown - ' + track_one_name))) |
1037 | + |
1038 | + #shutdown cleanly so that the database is updated |
1039 | + logger.debug("Quitting the application - successful test") |
1040 | + self.whenISelectObjectByFilter('GtkButton',BuilderName=u'play-button') |
1041 | + self.keyboard.press_and_release('Ctrl+Q') |
1042 | + |
1043 | |
1044 | === added file 'ubuntu_autopilot_tests/rhythmbox/tests/test_rbox.py' |
1045 | --- ubuntu_autopilot_tests/rhythmbox/tests/test_rbox.py 1970-01-01 00:00:00 +0000 |
1046 | +++ ubuntu_autopilot_tests/rhythmbox/tests/test_rbox.py 2014-03-05 19:06:22 +0000 |
1047 | @@ -0,0 +1,91 @@ |
1048 | +from autopilot.testcase import AutopilotTestCase |
1049 | +from testtools.matchers import Equals, NotEquals, GreaterThan, FileExists, StartsWith, Not |
1050 | +from autopilot.matchers import Eventually |
1051 | +from autopilot.introspection import get_proxy_object_for_existing_process |
1052 | +import subprocess, re, getpass, time, logging, os, sys, shutil |
1053 | +from rhythmbox.emulators import gtk |
1054 | +from rhythmbox.tests.base import RhythmBoxTestCase |
1055 | +from collections import namedtuple |
1056 | + |
1057 | +logger = logging.getLogger('__name__') |
1058 | +logger.setLevel(10) |
1059 | +class RadioTestCase(RhythmBoxTestCase): |
1060 | + |
1061 | + |
1062 | + ############### |
1063 | + #TEST CASES |
1064 | + |
1065 | + def test_window_visible(self, ): |
1066 | + #lets start with asserting we get a visible window |
1067 | + self.assertThat(self.main_window.visible, Equals(True)) |
1068 | + |
1069 | + def test_window_title_is_visible(self, ): |
1070 | + #lets quickly assert window is visible before testing title |
1071 | + self.assertTrue(self.main_window.visible) |
1072 | + self.assertThat(self.main_window.title, Eventually(Equals('Rhythmbox'))) |
1073 | + |
1074 | + |
1075 | + def test_select_radio_station(self, ): |
1076 | + #Test-case name: rhythmbox/rhy-001 |
1077 | + |
1078 | + genre = "Trance (1)" |
1079 | + station = 'HBR1.com - I.D.M. Tranceponder' |
1080 | + self.whenISelectALeftPanelOption('Radio') |
1081 | + |
1082 | + # so now we the radio options come under RBIRadioSource |
1083 | + # so first we select that as our root object for the next part |
1084 | + # to make sure we are only selecting in that part of the tree |
1085 | + radio_source = self.main_window.select_single('RBIRadioSource') |
1086 | + # looking in vis the radio section consists of a radio toolbar and two |
1087 | + # treeviews. lets just focus on the two treeviews for selecting |
1088 | + # channels. |
1089 | + # Using select_many we can get both at once |
1090 | + genre_treeview, title_treeview = radio_source.select_many('GtkTreeView') |
1091 | + |
1092 | + # so lets pick 'Music' genre to start: |
1093 | + trance_genre_item = genre_treeview.select_item(genre) |
1094 | + |
1095 | + try: |
1096 | + self.mouse.click_object(trance_genre_item) |
1097 | + except: |
1098 | + print(traceback.format_exc()) |
1099 | + self.root_obj.print_tree() |
1100 | + raise Exception("Test failed to select the genre, check print_tree in logs") |
1101 | + |
1102 | + #assert we selected it |
1103 | + # for some reason we need to reselect the item :-S I don't get why tbh! |
1104 | + # maybe its refreshing in the background and object id's are changing |
1105 | + item = genre_treeview.select_item(genre) |
1106 | + self.assertTrue(item.selected) |
1107 | + # we need to sleep here, I haven't implemented a wait_select_item() for |
1108 | + # treeview items yet, so we need to wait for the title_treeview |
1109 | + # to update |
1110 | + time.sleep(5) |
1111 | + |
1112 | + #now lets select the radio station from the title treeview |
1113 | + radio_station = title_treeview.select_item(station) |
1114 | + self.mouse.click_object(radio_station) |
1115 | + self.assertThat(radio_station.selected, Eventually(Equals(True))) |
1116 | + |
1117 | + #now press the PLAY button |
1118 | + self.whenISelectObjectByFilter('GtkButton', BuilderName=u'play-button') |
1119 | + |
1120 | + self.thenStatusBarStartsWith(u'1 station') |
1121 | + |
1122 | + |
1123 | + def test_yelp_window_launches(self, ): |
1124 | + self.assertTrue(self.main_window.visible) |
1125 | + # launch yelp |
1126 | + self.keyboard.press_and_release('F1') |
1127 | + app_pid = self.thenGetThisProcessPID('yelp') |
1128 | + # now lets get a proy object for yelp |
1129 | + yelp_proxy = get_proxy_object_for_existing_process(pid=app_pid) |
1130 | + # now lets check we have an object |
1131 | + self.assertThat(yelp_proxy, NotEquals(None)) |
1132 | + # now lets get the yelp app window |
1133 | + yelp = yelp_proxy.select_single('YelpWindow') |
1134 | + #assert the title |
1135 | + self.assertThat(yelp.title, Eventually(Equals('Rhythmbox Music Player'))) |
1136 | + |
1137 | + |
1138 | + |
I'll be away next week and won't have proper time to give this a good review. However, it looks like you are missing the resources with this commit :-)