Merge lp:~sil2100/unity/fix-949448-5.0 into lp:unity
- fix-949448-5.0
- Merge into trunk
Proposed by
Łukasz Zemczak
Status: | Superseded |
---|---|
Proposed branch: | lp:~sil2100/unity/fix-949448-5.0 |
Merge into: | lp:unity |
Diff against target: |
701 lines (+664/-0) (has conflicts) 3 files modified
tests/autopilot/autopilot/emulators/bamf.py.OTHER (+411/-0) tests/autopilot/unity/tests/__init__.py (+225/-0) tests/autopilot/unity/tests/test_launcher.py (+28/-0) Conflict adding files to tests/autopilot/autopilot. Created directory. Conflict because tests/autopilot/autopilot is not versioned, but has versioned children. Versioned directory. Conflict adding files to tests/autopilot/autopilot/emulators. Created directory. Conflict because tests/autopilot/autopilot/emulators is not versioned, but has versioned children. Versioned directory. Contents conflict in tests/autopilot/autopilot/emulators/bamf.py Text conflict in tests/autopilot/unity/tests/__init__.py Text conflict in tests/autopilot/unity/tests/test_launcher.py |
To merge this branch: | bzr merge lp:~sil2100/unity/fix-949448-5.0 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Unity Team | Pending | ||
Review via email: mp+106978@code.launchpad.net |
This proposal has been superseded by a proposal from 2012-05-23.
Commit message
Backported andyrock's fix from lp:unity/6.0 - Unmute launcher.
Description of the change
This branch needs to be tested. I see no other dependency changes, but I cannot get it to work with orca somehow. Could anyone else check it? Maybe I'm doing something wrong.
Original MRQ:
Copy-pasted from the original andyrock's MRQ for 6.0:
https:/
== Problem ==
Launcher is silent to screen reader users.
== Fix ==
Fix a couple of regressions. Alan Bell confirmed the fix for the launcher.
== Test ==
Not sure how we could test it.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'launcher/BamfLauncherIcon.cpp' | |||
2 | === modified file 'launcher/LauncherController.cpp' | |||
3 | === modified file 'plugins/unityshell/src/unitya11y.cpp' | |||
4 | === added directory 'tests/autopilot/autopilot' | |||
5 | === added directory 'tests/autopilot/autopilot/emulators' | |||
6 | === added file 'tests/autopilot/autopilot/emulators/bamf.py.OTHER' | |||
7 | --- tests/autopilot/autopilot/emulators/bamf.py.OTHER 1970-01-01 00:00:00 +0000 | |||
8 | +++ tests/autopilot/autopilot/emulators/bamf.py.OTHER 2012-05-23 10:30:27 +0000 | |||
9 | @@ -0,0 +1,411 @@ | |||
10 | 1 | # Copyright 2011 Canonical | ||
11 | 2 | # Author: Thomi Richards | ||
12 | 3 | # | ||
13 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
14 | 5 | # under the terms of the GNU General Public License version 3, as published | ||
15 | 6 | # by the Free Software Foundation. | ||
16 | 7 | |||
17 | 8 | "Various classes for interacting with BAMF." | ||
18 | 9 | |||
19 | 10 | import dbus | ||
20 | 11 | import dbus.glib | ||
21 | 12 | import gio | ||
22 | 13 | import gobject | ||
23 | 14 | import os | ||
24 | 15 | from Xlib import display, X, protocol | ||
25 | 16 | from gtk import gdk | ||
26 | 17 | |||
27 | 18 | from autopilot.emulators.dbus_handler import session_bus | ||
28 | 19 | |||
29 | 20 | __all__ = [ | ||
30 | 21 | "Bamf", | ||
31 | 22 | "BamfApplication", | ||
32 | 23 | "BamfWindow", | ||
33 | 24 | ] | ||
34 | 25 | |||
35 | 26 | _BAMF_BUS_NAME = 'org.ayatana.bamf' | ||
36 | 27 | _X_DISPLAY = display.Display() | ||
37 | 28 | |||
38 | 29 | |||
39 | 30 | def _filter_user_visible(win): | ||
40 | 31 | """Filter out non-user-visible objects. | ||
41 | 32 | |||
42 | 33 | In some cases the DBus method we need to call hasn't been registered yet, | ||
43 | 34 | in which case we do the safe thing and return False. | ||
44 | 35 | |||
45 | 36 | """ | ||
46 | 37 | try: | ||
47 | 38 | return win.user_visible | ||
48 | 39 | except dbus.DBusException: | ||
49 | 40 | return False | ||
50 | 41 | |||
51 | 42 | |||
52 | 43 | class Bamf(object): | ||
53 | 44 | """High-level class for interacting with Bamf from within a test. | ||
54 | 45 | |||
55 | 46 | Use this class to inspect the state of running applications and open | ||
56 | 47 | windows. | ||
57 | 48 | |||
58 | 49 | """ | ||
59 | 50 | |||
60 | 51 | def __init__(self): | ||
61 | 52 | matcher_path = '/org/ayatana/bamf/matcher' | ||
62 | 53 | self.matcher_interface_name = 'org.ayatana.bamf.matcher' | ||
63 | 54 | self.matcher_proxy = session_bus.get_object(_BAMF_BUS_NAME, matcher_path) | ||
64 | 55 | self.matcher_interface = dbus.Interface(self.matcher_proxy, self.matcher_interface_name) | ||
65 | 56 | |||
66 | 57 | def get_running_applications(self, user_visible_only=True): | ||
67 | 58 | """Get a list of the currently running applications. | ||
68 | 59 | |||
69 | 60 | If user_visible_only is True (the default), only applications | ||
70 | 61 | visible to the user in the switcher will be returned. | ||
71 | 62 | |||
72 | 63 | """ | ||
73 | 64 | apps = [BamfApplication(p) for p in self.matcher_interface.RunningApplications()] | ||
74 | 65 | if user_visible_only: | ||
75 | 66 | return filter(_filter_user_visible, apps) | ||
76 | 67 | return apps | ||
77 | 68 | |||
78 | 69 | def get_running_applications_by_desktop_file(self, desktop_file): | ||
79 | 70 | """Return a list of applications that have the desktop file 'desktop_file'`. | ||
80 | 71 | |||
81 | 72 | This method may return an empty list, if no applications | ||
82 | 73 | are found with the specified desktop file. | ||
83 | 74 | |||
84 | 75 | """ | ||
85 | 76 | return [a for a in self.get_running_applications() if a.desktop_file == desktop_file] | ||
86 | 77 | |||
87 | 78 | def get_application_by_xid(self, xid): | ||
88 | 79 | """Return the application that has a child with the requested xid or None.""" | ||
89 | 80 | |||
90 | 81 | app_path = self.matcher_interface.ApplicationForXid(xid) | ||
91 | 82 | if len(app_path): | ||
92 | 83 | return BamfApplication(app_path) | ||
93 | 84 | return None | ||
94 | 85 | |||
95 | 86 | def get_open_windows(self, user_visible_only=True): | ||
96 | 87 | """Get a list of currently open windows. | ||
97 | 88 | |||
98 | 89 | If user_visible_only is True (the default), only applications | ||
99 | 90 | visible to the user in the switcher will be returned. | ||
100 | 91 | |||
101 | 92 | The result is sorted to be in stacking order. | ||
102 | 93 | |||
103 | 94 | """ | ||
104 | 95 | |||
105 | 96 | windows = [BamfWindow(w) for w in self.matcher_interface.WindowStackForMonitor(-1)] | ||
106 | 97 | if user_visible_only: | ||
107 | 98 | windows = filter(_filter_user_visible, windows) | ||
108 | 99 | # Now sort on stacking order. | ||
109 | 100 | return reversed(windows) | ||
110 | 101 | |||
111 | 102 | def get_window_by_xid(self, xid): | ||
112 | 103 | """Get the BamfWindow that matches the provided 'xid'.""" | ||
113 | 104 | windows = [BamfWindow(w) for w in self.matcher_interface.WindowPaths() if BamfWindow(w).x_id == xid] | ||
114 | 105 | return windows[0] if windows else None | ||
115 | 106 | |||
116 | 107 | def wait_until_application_is_running(self, desktop_file, timeout): | ||
117 | 108 | """Wait until a given application is running. | ||
118 | 109 | |||
119 | 110 | 'desktop_file' is the name of the application desktop file. | ||
120 | 111 | 'timeout' is the maximum time to wait, in seconds. If set to | ||
121 | 112 | something less than 0, this method will wait forever. | ||
122 | 113 | |||
123 | 114 | This method returns true once the application is found, or false | ||
124 | 115 | if the application was not found until the timeout was reached. | ||
125 | 116 | """ | ||
126 | 117 | desktop_file = os.path.split(desktop_file)[1] | ||
127 | 118 | # python workaround since you can't assign to variables in the enclosing scope: | ||
128 | 119 | # see on_timeout_reached below... | ||
129 | 120 | found_app = [True] | ||
130 | 121 | |||
131 | 122 | # maybe the app is running already? | ||
132 | 123 | if len(self.get_running_applications_by_desktop_file(desktop_file)) == 0: | ||
133 | 124 | wait_forever = timeout < 0 | ||
134 | 125 | gobject_loop = gobject.MainLoop() | ||
135 | 126 | |||
136 | 127 | # No, so define a callback to watch the ViewOpened signal: | ||
137 | 128 | def on_view_added(bamf_path, name): | ||
138 | 129 | if bamf_path.split('/')[-1].startswith('application'): | ||
139 | 130 | app = BamfApplication(bamf_path) | ||
140 | 131 | if desktop_file == os.path.split(app.desktop_file)[1]: | ||
141 | 132 | gobject_loop.quit() | ||
142 | 133 | |||
143 | 134 | # ...and one for when the user-defined timeout has been reached: | ||
144 | 135 | def on_timeout_reached(): | ||
145 | 136 | gobject_loop.quit() | ||
146 | 137 | found_app[0] = False | ||
147 | 138 | return False | ||
148 | 139 | |||
149 | 140 | # need a timeout? if so, connect it: | ||
150 | 141 | if not wait_forever: | ||
151 | 142 | gobject.timeout_add(timeout * 1000, on_timeout_reached) | ||
152 | 143 | # connect signal handler: | ||
153 | 144 | session_bus.add_signal_receiver(on_view_added, 'ViewOpened') | ||
154 | 145 | # pump the gobject main loop until either the correct signal is emitted, or the | ||
155 | 146 | # timeout happens. | ||
156 | 147 | gobject_loop.run() | ||
157 | 148 | |||
158 | 149 | return found_app[0] | ||
159 | 150 | |||
160 | 151 | def launch_application(self, desktop_file, files=[], wait=True): | ||
161 | 152 | """Launch an application by specifying a desktop file. | ||
162 | 153 | |||
163 | 154 | `files` is a list of files to pass to the application. Not all apps support this. | ||
164 | 155 | |||
165 | 156 | If `wait` is True, this method will block until the application has launched. | ||
166 | 157 | |||
167 | 158 | Returns the Gobject process object. if wait is True (the default), | ||
168 | 159 | this method will not return until an instance of this application | ||
169 | 160 | appears in the BAMF application list. | ||
170 | 161 | """ | ||
171 | 162 | if type(files) is not list: | ||
172 | 163 | raise TypeError("files must be a list.") | ||
173 | 164 | proc = gio.unix.DesktopAppInfo(desktop_file) | ||
174 | 165 | proc.launch_uris(files) | ||
175 | 166 | if wait: | ||
176 | 167 | self.wait_until_application_is_running(desktop_file, -1) | ||
177 | 168 | return proc | ||
178 | 169 | |||
179 | 170 | |||
180 | 171 | class BamfApplication(object): | ||
181 | 172 | """Represents an application, with information as returned by Bamf. | ||
182 | 173 | |||
183 | 174 | Don't instantiate this class yourself. instead, use the methods as | ||
184 | 175 | provided by the Bamf class. | ||
185 | 176 | |||
186 | 177 | """ | ||
187 | 178 | def __init__(self, bamf_app_path): | ||
188 | 179 | self.bamf_app_path = bamf_app_path | ||
189 | 180 | try: | ||
190 | 181 | self._app_proxy = session_bus.get_object(_BAMF_BUS_NAME, bamf_app_path) | ||
191 | 182 | self._view_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.view') | ||
192 | 183 | self._app_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.application') | ||
193 | 184 | except dbus.DBusException, e: | ||
194 | 185 | e.message += 'bamf_app_path=%r' % (bamf_app_path) | ||
195 | 186 | raise | ||
196 | 187 | |||
197 | 188 | @property | ||
198 | 189 | def desktop_file(self): | ||
199 | 190 | """Get the application desktop file""" | ||
200 | 191 | return os.path.split(self._app_iface.DesktopFile())[1] | ||
201 | 192 | |||
202 | 193 | @property | ||
203 | 194 | def name(self): | ||
204 | 195 | """Get the application name. | ||
205 | 196 | |||
206 | 197 | Note: This may change according to the current locale. If you want a unique | ||
207 | 198 | string to match applications against, use the desktop_file instead. | ||
208 | 199 | |||
209 | 200 | """ | ||
210 | 201 | return self._view_iface.Name() | ||
211 | 202 | |||
212 | 203 | @property | ||
213 | 204 | def icon(self): | ||
214 | 205 | """Get the application icon.""" | ||
215 | 206 | return self._view_iface.Icon() | ||
216 | 207 | |||
217 | 208 | @property | ||
218 | 209 | def is_active(self): | ||
219 | 210 | """Is the application active (i.e.- has keyboard focus)?""" | ||
220 | 211 | return self._view_iface.IsActive() | ||
221 | 212 | |||
222 | 213 | @property | ||
223 | 214 | def is_urgent(self): | ||
224 | 215 | """Is the application currently signalling urgency?""" | ||
225 | 216 | return self._view_iface.IsUrgent() | ||
226 | 217 | |||
227 | 218 | @property | ||
228 | 219 | def user_visible(self): | ||
229 | 220 | """Is this application visible to the user? | ||
230 | 221 | |||
231 | 222 | Some applications (such as the panel) are hidden to the user but will | ||
232 | 223 | still be returned by bamf. | ||
233 | 224 | |||
234 | 225 | """ | ||
235 | 226 | return self._view_iface.UserVisible() | ||
236 | 227 | |||
237 | 228 | def get_windows(self): | ||
238 | 229 | """Get a list of the application windows.""" | ||
239 | 230 | return [BamfWindow(w) for w in self._view_iface.Children()] | ||
240 | 231 | |||
241 | 232 | def __repr__(self): | ||
242 | 233 | return "<BamfApplication '%s'>" % (self.name) | ||
243 | 234 | |||
244 | 235 | |||
245 | 236 | class BamfWindow(object): | ||
246 | 237 | """Represents an application window, as returned by Bamf. | ||
247 | 238 | |||
248 | 239 | Don't instantiate this class yourself. Instead, use the appropriate methods | ||
249 | 240 | in BamfApplication. | ||
250 | 241 | |||
251 | 242 | """ | ||
252 | 243 | def __init__(self, window_path): | ||
253 | 244 | self._bamf_win_path = window_path | ||
254 | 245 | self._app_proxy = session_bus.get_object(_BAMF_BUS_NAME, window_path) | ||
255 | 246 | self._window_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.window') | ||
256 | 247 | self._view_iface = dbus.Interface(self._app_proxy, 'org.ayatana.bamf.view') | ||
257 | 248 | |||
258 | 249 | self._xid = int(self._window_iface.GetXid()) | ||
259 | 250 | self._x_root_win = _X_DISPLAY.screen().root | ||
260 | 251 | self._x_win = _X_DISPLAY.create_resource_object('window', self._xid) | ||
261 | 252 | |||
262 | 253 | @property | ||
263 | 254 | def x_id(self): | ||
264 | 255 | """Get the X11 Window Id.""" | ||
265 | 256 | return self._xid | ||
266 | 257 | |||
267 | 258 | @property | ||
268 | 259 | def x_win(self): | ||
269 | 260 | """Get the X11 window object of the underlying window.""" | ||
270 | 261 | return self._x_win | ||
271 | 262 | |||
272 | 263 | @property | ||
273 | 264 | def name(self): | ||
274 | 265 | """Get the window name. | ||
275 | 266 | |||
276 | 267 | Note: This may change according to the current locale. If you want a unique | ||
277 | 268 | string to match windows against, use the x_id instead. | ||
278 | 269 | |||
279 | 270 | """ | ||
280 | 271 | return self._view_iface.Name() | ||
281 | 272 | |||
282 | 273 | @property | ||
283 | 274 | def title(self): | ||
284 | 275 | """Get the window title. | ||
285 | 276 | |||
286 | 277 | This may be different from the application name. | ||
287 | 278 | |||
288 | 279 | Note that this may change depending on the current locale. | ||
289 | 280 | |||
290 | 281 | """ | ||
291 | 282 | return self._getProperty('_NET_WM_NAME') | ||
292 | 283 | |||
293 | 284 | @property | ||
294 | 285 | def geometry(self): | ||
295 | 286 | """Get the geometry for this window. | ||
296 | 287 | |||
297 | 288 | Returns a tuple containing (x, y, width, height). | ||
298 | 289 | |||
299 | 290 | """ | ||
300 | 291 | # FIXME: We need to use the gdk window here to get the real coordinates | ||
301 | 292 | geometry = self._x_win.get_geometry() | ||
302 | 293 | origin = gdk.window_foreign_new(self._xid).get_origin() | ||
303 | 294 | return (origin[0], origin[1], geometry.width, geometry.height) | ||
304 | 295 | |||
305 | 296 | @property | ||
306 | 297 | def is_maximized(self): | ||
307 | 298 | """Is the window maximized? | ||
308 | 299 | |||
309 | 300 | Maximized in this case means both maximized | ||
310 | 301 | vertically and horizontally. If a window is only maximized in one | ||
311 | 302 | direction it is not considered maximized. | ||
312 | 303 | |||
313 | 304 | """ | ||
314 | 305 | win_state = self._get_window_states() | ||
315 | 306 | return '_NET_WM_STATE_MAXIMIZED_VERT' in win_state and \ | ||
316 | 307 | '_NET_WM_STATE_MAXIMIZED_HORZ' in win_state | ||
317 | 308 | |||
318 | 309 | @property | ||
319 | 310 | def application(self): | ||
320 | 311 | """Get the application that owns this window. | ||
321 | 312 | |||
322 | 313 | This method may return None if the window does not have an associated | ||
323 | 314 | application. The 'desktop' window is one such example. | ||
324 | 315 | |||
325 | 316 | """ | ||
326 | 317 | # BAMF returns a list of parents since some windows don't have an | ||
327 | 318 | # associated application. For these windows we return none. | ||
328 | 319 | parents = self._view_iface.Parents() | ||
329 | 320 | if parents: | ||
330 | 321 | return BamfApplication(parents[0]) | ||
331 | 322 | else: | ||
332 | 323 | return None | ||
333 | 324 | |||
334 | 325 | @property | ||
335 | 326 | def user_visible(self): | ||
336 | 327 | """Is this window visible to the user in the switcher?""" | ||
337 | 328 | return self._view_iface.UserVisible() | ||
338 | 329 | |||
339 | 330 | @property | ||
340 | 331 | def is_hidden(self): | ||
341 | 332 | """Is this window hidden? | ||
342 | 333 | |||
343 | 334 | Windows are hidden when the 'Show Desktop' mode is activated. | ||
344 | 335 | |||
345 | 336 | """ | ||
346 | 337 | win_state = self._get_window_states() | ||
347 | 338 | return '_NET_WM_STATE_HIDDEN' in win_state | ||
348 | 339 | |||
349 | 340 | @property | ||
350 | 341 | def is_focused(self): | ||
351 | 342 | """Is this window focused?""" | ||
352 | 343 | win_state = self._get_window_states() | ||
353 | 344 | return '_NET_WM_STATE_FOCUSED' in win_state | ||
354 | 345 | |||
355 | 346 | @property | ||
356 | 347 | def is_valid(self): | ||
357 | 348 | """Is this window object valid? | ||
358 | 349 | |||
359 | 350 | Invalid windows are caused by windows closing during the construction of | ||
360 | 351 | this object instance. | ||
361 | 352 | |||
362 | 353 | """ | ||
363 | 354 | return not self._x_win is None | ||
364 | 355 | |||
365 | 356 | @property | ||
366 | 357 | def monitor(self): | ||
367 | 358 | """Returns the monitor to which the windows belongs to""" | ||
368 | 359 | return self._window_iface.Monitor() | ||
369 | 360 | |||
370 | 361 | @property | ||
371 | 362 | def closed(self): | ||
372 | 363 | """Returns True if the window has been closed""" | ||
373 | 364 | # This will return False when the window is closed and then removed from BUS | ||
374 | 365 | try: | ||
375 | 366 | return (self._window_iface.GetXid() != self.x_id) | ||
376 | 367 | except: | ||
377 | 368 | return True | ||
378 | 369 | |||
379 | 370 | def close(self): | ||
380 | 371 | """Close the window.""" | ||
381 | 372 | |||
382 | 373 | self._setProperty('_NET_CLOSE_WINDOW', [0, 0]) | ||
383 | 374 | |||
384 | 375 | def set_focus(self): | ||
385 | 376 | self._x_win.set_input_focus(X.RevertToParent, X.CurrentTime) | ||
386 | 377 | self._x_win.configure(stack_mode=X.Above) | ||
387 | 378 | |||
388 | 379 | def __repr__(self): | ||
389 | 380 | return "<BamfWindow '%s'>" % (self.title if self._x_win else str(self._xid)) | ||
390 | 381 | |||
391 | 382 | def _getProperty(self, _type): | ||
392 | 383 | """Get an X11 property. | ||
393 | 384 | |||
394 | 385 | _type is a string naming the property type. win is the X11 window object. | ||
395 | 386 | |||
396 | 387 | """ | ||
397 | 388 | atom = self._x_win.get_full_property(_X_DISPLAY.get_atom(_type), X.AnyPropertyType) | ||
398 | 389 | if atom: | ||
399 | 390 | return atom.value | ||
400 | 391 | |||
401 | 392 | def _setProperty(self, _type, data, mask=None): | ||
402 | 393 | if type(data) is str: | ||
403 | 394 | dataSize = 8 | ||
404 | 395 | else: | ||
405 | 396 | # data length must be 5 - pad with 0's if it's short, truncate otherwise. | ||
406 | 397 | data = (data + [0] * (5 - len(data)))[:5] | ||
407 | 398 | dataSize = 32 | ||
408 | 399 | |||
409 | 400 | ev = protocol.event.ClientMessage(window=self._x_win, client_type=_X_DISPLAY.get_atom(_type), data=(dataSize, data)) | ||
410 | 401 | |||
411 | 402 | if not mask: | ||
412 | 403 | mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask) | ||
413 | 404 | self._x_root_win.send_event(ev, event_mask=mask) | ||
414 | 405 | _X_DISPLAY.sync() | ||
415 | 406 | |||
416 | 407 | def _get_window_states(self): | ||
417 | 408 | """Return a list of strings representing the current window state.""" | ||
418 | 409 | |||
419 | 410 | _X_DISPLAY.sync() | ||
420 | 411 | return map(_X_DISPLAY.get_atom_name, self._getProperty('_NET_WM_STATE')) | ||
421 | 0 | 412 | ||
422 | === modified file 'tests/autopilot/unity/emulators/icons.py' | |||
423 | === modified file 'tests/autopilot/unity/emulators/launcher.py' | |||
424 | === modified file 'tests/autopilot/unity/tests/__init__.py' | |||
425 | --- tests/autopilot/unity/tests/__init__.py 2012-05-16 23:02:38 +0000 | |||
426 | +++ tests/autopilot/unity/tests/__init__.py 2012-05-23 10:30:27 +0000 | |||
427 | @@ -122,3 +122,228 @@ | |||
428 | 122 | """ | 122 | """ |
429 | 123 | set_log_severity(component, level) | 123 | set_log_severity(component, level) |
430 | 124 | 124 | ||
431 | 125 | <<<<<<< TREE | ||
432 | 126 | ======= | ||
433 | 127 | |||
434 | 128 | class VideoCapturedTestCase(LoggedTestCase): | ||
435 | 129 | """Video capture autopilot tests, saving the results if the test failed.""" | ||
436 | 130 | |||
437 | 131 | _recording_app = '/usr/bin/recordmydesktop' | ||
438 | 132 | _recording_opts = ['--no-sound', '--no-frame', '-o',] | ||
439 | 133 | |||
440 | 134 | def setUp(self): | ||
441 | 135 | super(VideoCapturedTestCase, self).setUp() | ||
442 | 136 | global video_recording_enabled | ||
443 | 137 | if video_recording_enabled and not self._have_recording_app(): | ||
444 | 138 | video_recording_enabled = False | ||
445 | 139 | logger.warning("Disabling video capture since '%s' is not present", self._recording_app) | ||
446 | 140 | |||
447 | 141 | if video_recording_enabled: | ||
448 | 142 | self._test_passed = True | ||
449 | 143 | self.addOnException(self._on_test_failed) | ||
450 | 144 | self.addCleanup(self._stop_video_capture) | ||
451 | 145 | self._start_video_capture() | ||
452 | 146 | |||
453 | 147 | def _have_recording_app(self): | ||
454 | 148 | return os.path.exists(self._recording_app) | ||
455 | 149 | |||
456 | 150 | def _start_video_capture(self): | ||
457 | 151 | args = self._get_capture_command_line() | ||
458 | 152 | self._capture_file = self._get_capture_output_file() | ||
459 | 153 | self._ensure_directory_exists_but_not_file(self._capture_file) | ||
460 | 154 | args.append(self._capture_file) | ||
461 | 155 | logger.debug("Starting: %r", args) | ||
462 | 156 | self._capture_process = Popen(args, stdout=PIPE, stderr=STDOUT) | ||
463 | 157 | |||
464 | 158 | def _stop_video_capture(self): | ||
465 | 159 | """Stop the video capture. If the test failed, save the resulting file.""" | ||
466 | 160 | |||
467 | 161 | if self._test_passed: | ||
468 | 162 | # We use kill here because we don't want the recording app to start | ||
469 | 163 | # encoding the video file (since we're removing it anyway.) | ||
470 | 164 | self._capture_process.kill() | ||
471 | 165 | self._capture_process.wait() | ||
472 | 166 | else: | ||
473 | 167 | self._capture_process.terminate() | ||
474 | 168 | self._capture_process.wait() | ||
475 | 169 | if self._capture_process.returncode != 0: | ||
476 | 170 | self.addDetail('video capture log', text_content(self._capture_process.stdout.read())) | ||
477 | 171 | self._capture_process = None | ||
478 | 172 | |||
479 | 173 | def _get_capture_command_line(self): | ||
480 | 174 | return [self._recording_app] + self._recording_opts | ||
481 | 175 | |||
482 | 176 | def _get_capture_output_file(self): | ||
483 | 177 | return os.path.join(video_record_directory, '%s.ogv' % (self.shortDescription())) | ||
484 | 178 | |||
485 | 179 | def _ensure_directory_exists_but_not_file(self, file_path): | ||
486 | 180 | dirpath = os.path.dirname(file_path) | ||
487 | 181 | if not os.path.exists(dirpath): | ||
488 | 182 | os.makedirs(dirpath) | ||
489 | 183 | elif os.path.exists(file_path): | ||
490 | 184 | logger.warning("Video capture file '%s' already exists, deleting.", file_path) | ||
491 | 185 | os.remove(file_path) | ||
492 | 186 | |||
493 | 187 | def _on_test_failed(self, ex_info): | ||
494 | 188 | """Called when a test fails.""" | ||
495 | 189 | self._test_passed = False | ||
496 | 190 | |||
497 | 191 | |||
498 | 192 | class AutopilotTestCase(VideoCapturedTestCase, KeybindingsHelper): | ||
499 | 193 | """Wrapper around testtools.TestCase that takes care of some cleaning.""" | ||
500 | 194 | |||
501 | 195 | run_test_with = GlibRunner | ||
502 | 196 | |||
503 | 197 | KNOWN_APPS = { | ||
504 | 198 | 'Character Map' : { | ||
505 | 199 | 'desktop-file': 'gucharmap.desktop', | ||
506 | 200 | 'process-name': 'gucharmap', | ||
507 | 201 | }, | ||
508 | 202 | 'Calculator' : { | ||
509 | 203 | 'desktop-file': 'gcalctool.desktop', | ||
510 | 204 | 'process-name': 'gcalctool', | ||
511 | 205 | }, | ||
512 | 206 | 'Mahjongg' : { | ||
513 | 207 | 'desktop-file': 'mahjongg.desktop', | ||
514 | 208 | 'process-name': 'mahjongg', | ||
515 | 209 | }, | ||
516 | 210 | 'Remmina' : { | ||
517 | 211 | 'desktop-file': 'remmina.desktop', | ||
518 | 212 | 'process-name': 'remmina', | ||
519 | 213 | }, | ||
520 | 214 | 'System Settings' : { | ||
521 | 215 | 'desktop-file': 'gnome-control-center.desktop', | ||
522 | 216 | 'process-name': 'gnome-control-center', | ||
523 | 217 | }, | ||
524 | 218 | 'Text Editor' : { | ||
525 | 219 | 'desktop-file': 'gedit.desktop', | ||
526 | 220 | 'process-name': 'gedit', | ||
527 | 221 | }, | ||
528 | 222 | } | ||
529 | 223 | |||
530 | 224 | def setUp(self): | ||
531 | 225 | super(AutopilotTestCase, self).setUp() | ||
532 | 226 | self.bamf = Bamf() | ||
533 | 227 | self.keyboard = Keyboard() | ||
534 | 228 | self.mouse = Mouse() | ||
535 | 229 | self.dash = Dash() | ||
536 | 230 | self.hud = Hud() | ||
537 | 231 | self.launcher = self._get_launcher_controller() | ||
538 | 232 | self.panels = self._get_panel_controller() | ||
539 | 233 | self.switcher = Switcher() | ||
540 | 234 | self.window_manager = self._get_window_manager() | ||
541 | 235 | self.workspace = WorkspaceManager() | ||
542 | 236 | self.screen_geo = ScreenGeometry() | ||
543 | 237 | self.addCleanup(self.workspace.switch_to, self.workspace.current_workspace) | ||
544 | 238 | self.addCleanup(Keyboard.cleanup) | ||
545 | 239 | self.addCleanup(Mouse.cleanup) | ||
546 | 240 | |||
547 | 241 | def start_app(self, app_name, files=[], locale=None): | ||
548 | 242 | """Start one of the known apps, and kill it on tear down. | ||
549 | 243 | |||
550 | 244 | If files is specified, start the application with the specified files. | ||
551 | 245 | If locale is specified, the locale will be set when the application is launched. | ||
552 | 246 | |||
553 | 247 | The method returns the BamfApplication instance. | ||
554 | 248 | |||
555 | 249 | """ | ||
556 | 250 | if locale: | ||
557 | 251 | os.putenv("LC_ALL", locale) | ||
558 | 252 | self.addCleanup(os.unsetenv, "LC_ALL") | ||
559 | 253 | logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale) | ||
560 | 254 | else: | ||
561 | 255 | logger.info("Starting application '%s' with files %r", app_name, files) | ||
562 | 256 | |||
563 | 257 | app = self.KNOWN_APPS[app_name] | ||
564 | 258 | self.bamf.launch_application(app['desktop-file'], files) | ||
565 | 259 | apps = self.bamf.get_running_applications_by_desktop_file(app['desktop-file']) | ||
566 | 260 | self.addCleanup(call, "kill `pidof %s`" % (app['process-name']), shell=True) | ||
567 | 261 | self.assertThat(len(apps), Equals(1)) | ||
568 | 262 | return apps[0] | ||
569 | 263 | |||
570 | 264 | def close_all_app(self, app_name): | ||
571 | 265 | """Close all instances of the app_name.""" | ||
572 | 266 | app = self.KNOWN_APPS[app_name] | ||
573 | 267 | pids = check_output(["pidof", app['process-name']]).split() | ||
574 | 268 | if len(pids): | ||
575 | 269 | call(["kill"] + pids) | ||
576 | 270 | |||
577 | 271 | def get_app_instances(self, app_name): | ||
578 | 272 | """Get BamfApplication instances for app_name.""" | ||
579 | 273 | desktop_file = self.KNOWN_APPS[app_name]['desktop-file'] | ||
580 | 274 | return self.bamf.get_running_applications_by_desktop_file(desktop_file) | ||
581 | 275 | |||
582 | 276 | def app_is_running(self, app_name): | ||
583 | 277 | """Returns true if an instance of the application is running.""" | ||
584 | 278 | apps = self.get_app_instances(app_name) | ||
585 | 279 | return len(apps) > 0 | ||
586 | 280 | |||
587 | 281 | def call_gsettings_cmd(self, command, schema, *args): | ||
588 | 282 | """Set a desktop wide gsettings option | ||
589 | 283 | |||
590 | 284 | Using the gsettings command because there's a bug with importing | ||
591 | 285 | from gobject introspection and pygtk2 simultaneously, and the Xlib | ||
592 | 286 | keyboard layout bits are very unweildy. This seems like the best | ||
593 | 287 | solution, even a little bit brutish. | ||
594 | 288 | """ | ||
595 | 289 | cmd = ['gsettings', command, schema] + list(args) | ||
596 | 290 | # strip to remove the trailing \n. | ||
597 | 291 | ret = check_output(cmd).strip() | ||
598 | 292 | time.sleep(5) | ||
599 | 293 | reset_display() | ||
600 | 294 | return ret | ||
601 | 295 | |||
602 | 296 | def set_unity_option(self, option_name, option_value): | ||
603 | 297 | """Set an option in the unity compiz plugin options. | ||
604 | 298 | |||
605 | 299 | The value will be set for the current test only. | ||
606 | 300 | |||
607 | 301 | """ | ||
608 | 302 | self.set_compiz_option("unityshell", option_name, option_value) | ||
609 | 303 | |||
610 | 304 | def set_compiz_option(self, plugin_name, setting_name, setting_value): | ||
611 | 305 | """Set setting `setting_name` in compiz plugin `plugin_name` to value `setting_value` | ||
612 | 306 | for one test only. | ||
613 | 307 | """ | ||
614 | 308 | old_value = self._set_compiz_option(plugin_name, setting_name, setting_value) | ||
615 | 309 | self.addCleanup(self._set_compiz_option, plugin_name, setting_name, old_value) | ||
616 | 310 | # Allow unity time to respond to the new setting. | ||
617 | 311 | time.sleep(0.5) | ||
618 | 312 | |||
619 | 313 | def _set_compiz_option(self, plugin_name, option_name, option_value): | ||
620 | 314 | logger.info("Setting compiz option '%s' in plugin '%s' to %r", | ||
621 | 315 | option_name, plugin_name, option_value) | ||
622 | 316 | plugin = Plugin(global_context, plugin_name) | ||
623 | 317 | setting = Setting(plugin, option_name) | ||
624 | 318 | old_value = setting.Value | ||
625 | 319 | setting.Value = option_value | ||
626 | 320 | global_context.Write() | ||
627 | 321 | return old_value | ||
628 | 322 | |||
629 | 323 | def _get_launcher_controller(self): | ||
630 | 324 | controllers = LauncherController.get_all_instances() | ||
631 | 325 | self.assertThat(len(controllers), Equals(1)) | ||
632 | 326 | return controllers[0] | ||
633 | 327 | |||
634 | 328 | def _get_panel_controller(self): | ||
635 | 329 | controllers = PanelController.get_all_instances() | ||
636 | 330 | self.assertThat(len(controllers), Equals(1)) | ||
637 | 331 | return controllers[0] | ||
638 | 332 | |||
639 | 333 | def _get_window_manager(self): | ||
640 | 334 | managers = WindowManager.get_all_instances() | ||
641 | 335 | self.assertThat(len(managers), Equals(1)) | ||
642 | 336 | return managers[0] | ||
643 | 337 | |||
644 | 338 | def assertVisibleWindowStack(self, stack_start): | ||
645 | 339 | """Check that the visible window stack starts with the windows passed in. | ||
646 | 340 | |||
647 | 341 | The start_stack is an iterable of BamfWindow objects. | ||
648 | 342 | Minimised windows are skipped. | ||
649 | 343 | |||
650 | 344 | """ | ||
651 | 345 | stack = [win for win in self.bamf.get_open_windows() if not win.is_hidden] | ||
652 | 346 | for pos, win in enumerate(stack_start): | ||
653 | 347 | self.assertThat(stack[pos].x_id, Equals(win.x_id), | ||
654 | 348 | "%r at %d does not equal %r" % (stack[pos], pos, win)) | ||
655 | 349 | >>>>>>> MERGE-SOURCE | ||
656 | 125 | 350 | ||
657 | === modified file 'tests/autopilot/unity/tests/test_launcher.py' | |||
658 | --- tests/autopilot/unity/tests/test_launcher.py 2012-05-17 22:49:33 +0000 | |||
659 | +++ tests/autopilot/unity/tests/test_launcher.py 2012-05-23 10:30:27 +0000 | |||
660 | @@ -660,6 +660,7 @@ | |||
661 | 660 | logger.info("Checking for duplicated launcher icon for application %s", test_app.name) | 660 | logger.info("Checking for duplicated launcher icon for application %s", test_app.name) |
662 | 661 | self.assertOnlyOneLauncherIcon(desktop_id=test_app.desktop_file) | 661 | self.assertOnlyOneLauncherIcon(desktop_id=test_app.desktop_file) |
663 | 662 | 662 | ||
664 | 663 | <<<<<<< TREE | ||
665 | 663 | def test_killing_bamfdaemon_does_not_duplicate_application_xids(self): | 664 | def test_killing_bamfdaemon_does_not_duplicate_application_xids(self): |
666 | 664 | """Killing bamfdaemon should not duplicate any xid in the model.""" | 665 | """Killing bamfdaemon should not duplicate any xid in the model.""" |
667 | 665 | self.start_test_apps() | 666 | self.start_test_apps() |
668 | @@ -685,6 +686,33 @@ | |||
669 | 685 | 686 | ||
670 | 686 | 687 | ||
671 | 687 | class LauncherCaptureTests(UnityTestCase): | 688 | class LauncherCaptureTests(UnityTestCase): |
672 | 689 | ======= | ||
673 | 690 | def test_killing_bamfdaemon_does_not_duplicate_application_xids(self): | ||
674 | 691 | """Killing bamfdaemon should not duplicate any xid in the model.""" | ||
675 | 692 | self.start_test_apps() | ||
676 | 693 | self.start_desktopless_test_apps() | ||
677 | 694 | self.kill_and_restart_bamfdaemon() | ||
678 | 695 | |||
679 | 696 | test_apps = self.get_test_apps() + self.get_desktopless_test_apps() | ||
680 | 697 | |||
681 | 698 | for test_app in test_apps: | ||
682 | 699 | logger.info("Checking for duplicated launcher icon for application %s", test_app.name) | ||
683 | 700 | test_windows = [w.x_id for w in test_app.get_windows()] | ||
684 | 701 | self.assertOnlyOneLauncherIcon(xids=test_windows) | ||
685 | 702 | |||
686 | 703 | def test_killing_bamfdaemon_does_not_duplicate_any_icon_application_id(self): | ||
687 | 704 | """Killing bamfdaemon should not duplicate any application ids in the model.""" | ||
688 | 705 | self.start_test_apps() | ||
689 | 706 | self.start_desktopless_test_apps() | ||
690 | 707 | self.kill_and_restart_bamfdaemon() | ||
691 | 708 | |||
692 | 709 | for icon in self.launcher.model.get_bamf_launcher_icons(): | ||
693 | 710 | logger.info("Checking for duplicated launcher icon %s", icon.tooltip_text) | ||
694 | 711 | self.assertOnlyOneLauncherIcon(application_id=icon.application_id) | ||
695 | 712 | |||
696 | 713 | |||
697 | 714 | class LauncherCaptureTests(AutopilotTestCase): | ||
698 | 715 | >>>>>>> MERGE-SOURCE | ||
699 | 688 | """Test the launchers ability to capture/not capture the mouse.""" | 716 | """Test the launchers ability to capture/not capture the mouse.""" |
700 | 689 | 717 | ||
701 | 690 | screen_geo = ScreenGeometry() | 718 | screen_geo = ScreenGeometry() |