session_store_test_case.py (17970B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 from urllib.parse import quote 6 7 from marionette_driver import Wait, errors 8 from marionette_driver.keys import Keys 9 from marionette_harness import MarionetteTestCase, WindowManagerMixin 10 11 12 def inline(doc): 13 return f"data:text/html;charset=utf-8,{quote(doc)}" 14 15 16 # Each list element represents a window of tabs loaded at 17 # some testing URL 18 DEFAULT_WINDOWS = set([ 19 # Window 1. Note the comma after the inline call - 20 # this is Python's way of declaring a 1 item tuple. 21 (inline("""<div">Lorem</div>"""),), 22 # Window 2 23 ( 24 inline("""<div">ipsum</div>"""), 25 inline("""<div">dolor</div>"""), 26 ), 27 # Window 3 28 ( 29 inline("""<div">sit</div>"""), 30 inline("""<div">amet</div>"""), 31 ), 32 ]) 33 34 35 class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase): 36 def setUp( 37 self, 38 startup_page=1, 39 include_private=True, 40 restore_on_demand=False, 41 no_auto_updates=True, 42 win_register_restart=False, 43 test_windows=DEFAULT_WINDOWS, 44 taskbartabs_enable=False, 45 ): 46 super().setUp() 47 self.marionette.set_context("chrome") 48 49 platform = self.marionette.session_capabilities["platformName"] 50 self.accelKey = Keys.META if platform == "mac" else Keys.CONTROL 51 52 self.test_windows = test_windows 53 54 self.private_windows = set([ 55 ( 56 inline("""<div">consectetur</div>"""), 57 inline("""<div">ipsum</div>"""), 58 ), 59 ( 60 inline("""<div">adipiscing</div>"""), 61 inline("""<div">consectetur</div>"""), 62 ), 63 ]) 64 65 self.marionette.enforce_gecko_prefs({ 66 # Set browser restore previous session pref, 67 # depending on what the test requires. 68 "browser.startup.page": startup_page, 69 # Make the content load right away instead of waiting for 70 # the user to click on the background tabs 71 "browser.sessionstore.restore_on_demand": restore_on_demand, 72 # Avoid race conditions by having the content process never 73 # send us session updates unless the parent has explicitly asked 74 # for them via the TabStateFlusher. 75 "browser.sessionstore.debug.no_auto_updates": no_auto_updates, 76 # Whether to enable the register application restart mechanism. 77 "toolkit.winRegisterApplicationRestart": win_register_restart, 78 # Whether to enable taskbar tabs for this test 79 "browser.taskbarTabs.enabled": taskbartabs_enable, 80 }) 81 82 self.all_windows = self.test_windows.copy() 83 self.open_windows(self.test_windows) 84 85 if include_private: 86 self.all_windows.update(self.private_windows) 87 self.open_windows(self.private_windows, is_private=True) 88 89 def tearDown(self): 90 try: 91 # Create a fresh profile for subsequent tests. 92 self.marionette.restart(in_app=False, clean=True) 93 finally: 94 super().tearDown() 95 96 def open_windows(self, window_sets, is_private=False): 97 """Open a set of windows with tabs pointing at some URLs. 98 99 @param window_sets (list) 100 A set of URL tuples. Each tuple within window_sets 101 represents a window, and each URL in the URL 102 tuples represents what will be loaded in a tab. 103 104 Note that if is_private is False, then the first 105 URL tuple will be opened in the current window, and 106 subequent tuples will be opened in new windows. 107 108 Example: 109 110 set( 111 (self.marionette.absolute_url('layout/mozilla_1.html'), 112 self.marionette.absolute_url('layout/mozilla_2.html')), 113 114 (self.marionette.absolute_url('layout/mozilla_3.html'), 115 self.marionette.absolute_url('layout/mozilla_4.html')), 116 ) 117 118 This would take the currently open window, and load 119 mozilla_1.html and mozilla_2.html in new tabs. It would 120 then open a new, second window, and load tabs at 121 mozilla_3.html and mozilla_4.html. 122 @param is_private (boolean, optional) 123 Whether or not any new windows should be a private browsing 124 windows. 125 """ 126 if is_private: 127 win = self.open_window(private=True) 128 self.marionette.switch_to_window(win) 129 else: 130 win = self.marionette.current_chrome_window_handle 131 132 for index, urls in enumerate(window_sets): 133 if index > 0: 134 win = self.open_window(private=is_private) 135 self.marionette.switch_to_window(win) 136 self.open_tabs(win, urls) 137 138 # Open a Firefox web app (taskbar tab) window 139 def open_taskbartab_window(self): 140 self.marionette.execute_async_script( 141 """ 142 let [resolve] = arguments; 143 (async () => { 144 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 145 Ci.nsIWritablePropertyBag2 146 ); 147 extraOptions.setPropertyAsBool("taskbartab", true); 148 149 let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 150 args.appendElement(null); 151 args.appendElement(extraOptions); 152 args.appendElement(null); 153 154 // Simulate opening a taskbar tab window 155 let win = Services.ww.openWindow( 156 null, 157 AppConstants.BROWSER_CHROME_URL, 158 "_blank", 159 "chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars", 160 args 161 ); 162 await new Promise(resolve => { 163 win.addEventListener("load", resolve, { once: true }); 164 }); 165 await win.delayedStartupPromise; 166 })().then(resolve); 167 """ 168 ) 169 170 # Helper function for taskbar tabs tests, opens a taskbar tab window, 171 # closes the regular window, and reopens another regular window. 172 # Firefox will then be in a "ready to restore" state 173 def setup_taskbartab_restore_scenario(self): 174 self.open_taskbartab_window() 175 taskbar_tab_window_handle = self.marionette.close_chrome_window()[0] 176 self.marionette.switch_to_window(taskbar_tab_window_handle) 177 self.marionette.open(type="window") 178 179 def open_tabs(self, win, urls): 180 """Open a set of URLs inside a window in new tabs. 181 182 @param win (browser window) 183 The browser window to load the tabs in. 184 @param urls (tuple) 185 A tuple of URLs to load in this window. The 186 first URL will be loaded in the currently selected 187 browser tab. Subsequent URLs will be loaded in 188 new tabs. 189 """ 190 # If there are any remaining URLs for this window, 191 # open some new tabs and navigate to them. 192 with self.marionette.using_context("content"): 193 if isinstance(urls, str): 194 self.marionette.navigate(urls) 195 else: 196 for index, url in enumerate(urls): 197 if index > 0: 198 tab = self.open_tab() 199 self.marionette.switch_to_window(tab) 200 self.marionette.navigate(url) 201 202 def wait_for_windows(self, expected_windows, message, timeout=5): 203 current_windows = None 204 205 def check(_): 206 nonlocal current_windows 207 current_windows = self.convert_open_windows_to_set() 208 return current_windows == expected_windows 209 210 try: 211 wait = Wait(self.marionette, timeout=timeout, interval=0.1) 212 wait.until(check, message=message) 213 except errors.TimeoutException as e: 214 # Update the message to include the most recent list of windows 215 message = ( 216 f"{e.message}. Expected {expected_windows}, got {current_windows}." 217 ) 218 raise errors.TimeoutException(message) 219 220 def get_urls_for_window(self, win): 221 orig_handle = self.marionette.current_chrome_window_handle 222 223 try: 224 with self.marionette.using_context("chrome"): 225 self.marionette.switch_to_window(win) 226 return self.marionette.execute_script( 227 """ 228 return gBrowser.tabs.map(tab => { 229 return tab.linkedBrowser.currentURI.spec; 230 }); 231 """ 232 ) 233 finally: 234 self.marionette.switch_to_window(orig_handle) 235 236 def convert_open_windows_to_set(self): 237 # There's no guarantee that Marionette will return us an 238 # iterator for the opened windows that will match the 239 # order within our window list. Instead, we'll convert 240 # the list of URLs within each open window to a set of 241 # tuples that will allow us to do a direct comparison 242 # while allowing the windows to be in any order. 243 opened_windows = set() 244 for win in self.marionette.chrome_window_handles: 245 urls = tuple(self.get_urls_for_window(win)) 246 opened_windows.add(urls) 247 248 return opened_windows 249 250 def _close_window(self): 251 """Use as a callback to `marionette.quit` in order to close the 252 browser window. 253 254 `marionette.close`/`marionette.close_chrome_window` cannot 255 be used alone because they don't allow closing the last window. 256 """ 257 258 self.marionette.execute_script("window.close()") 259 260 def close_all_tabs_and_restart(self): 261 self.close_all_tabs() 262 self.marionette.quit(callback=self._close_window) 263 self.marionette.start_session() 264 265 def simulate_os_shutdown(self): 266 """Simulate an OS shutdown. 267 268 :raises: Exception: if not supported on the current platform 269 :raises: WindowsError: if a Windows API call failed 270 """ 271 if self.marionette.session_capabilities["platformName"] != "windows": 272 raise Exception("Unsupported platform for simulate_os_shutdown") 273 274 self._shutdown_with_windows_restart_manager(self.marionette.process_id) 275 276 def _shutdown_with_windows_restart_manager(self, pid): 277 """Shut down a process using the Windows Restart Manager. 278 279 When Windows shuts down, it uses a protocol including the 280 WM_QUERYENDSESSION and WM_ENDSESSION messages to give 281 applications a chance to shut down safely. The best way to 282 simulate this is via the Restart Manager, which allows a process 283 (such as an installer) to use the same mechanism to shut down 284 any other processes which are using registered resources. 285 286 This function starts a Restart Manager session, registers the 287 process as a resource, and shuts down the process. 288 289 :param pid: The process id (int) of the process to shutdown 290 291 :raises: WindowsError: if a Windows API call fails 292 """ 293 import ctypes 294 from ctypes import POINTER, WINFUNCTYPE, Structure, WinError, pointer, windll 295 from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR, UINT, ULONG, WCHAR 296 297 # set up Windows SDK types 298 OpenProcess = windll.kernel32.OpenProcess 299 OpenProcess.restype = HANDLE 300 OpenProcess.argtypes = [ 301 DWORD, # dwDesiredAccess 302 BOOL, # bInheritHandle 303 DWORD, 304 ] # dwProcessId 305 PROCESS_QUERY_INFORMATION = 0x0400 306 307 class FILETIME(Structure): 308 _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] 309 310 LPFILETIME = POINTER(FILETIME) 311 312 GetProcessTimes = windll.kernel32.GetProcessTimes 313 GetProcessTimes.restype = BOOL 314 GetProcessTimes.argtypes = [ 315 HANDLE, # hProcess 316 LPFILETIME, # lpCreationTime 317 LPFILETIME, # lpExitTime 318 LPFILETIME, # lpKernelTime 319 LPFILETIME, 320 ] # lpUserTime 321 322 ERROR_SUCCESS = 0 323 324 class RM_UNIQUE_PROCESS(Structure): 325 _fields_ = [("dwProcessId", DWORD), ("ProcessStartTime", FILETIME)] 326 327 RmStartSession = windll.rstrtmgr.RmStartSession 328 RmStartSession.restype = DWORD 329 RmStartSession.argtypes = [ 330 POINTER(DWORD), # pSessionHandle 331 DWORD, # dwSessionFlags 332 POINTER(WCHAR), 333 ] # strSessionKey 334 335 class GUID(ctypes.Structure): 336 _fields_ = [ 337 ("Data1", ctypes.c_ulong), 338 ("Data2", ctypes.c_ushort), 339 ("Data3", ctypes.c_ushort), 340 ("Data4", ctypes.c_ubyte * 8), 341 ] 342 343 CCH_RM_SESSION_KEY = ctypes.sizeof(GUID) * 2 344 345 RmRegisterResources = windll.rstrtmgr.RmRegisterResources 346 RmRegisterResources.restype = DWORD 347 RmRegisterResources.argtypes = [ 348 DWORD, # dwSessionHandle 349 UINT, # nFiles 350 POINTER(LPCWSTR), # rgsFilenames 351 UINT, # nApplications 352 POINTER(RM_UNIQUE_PROCESS), # rgApplications 353 UINT, # nServices 354 POINTER(LPCWSTR), 355 ] # rgsServiceNames 356 357 RM_WRITE_STATUS_CALLBACK = WINFUNCTYPE(None, UINT) 358 RmShutdown = windll.rstrtmgr.RmShutdown 359 RmShutdown.restype = DWORD 360 RmShutdown.argtypes = [ 361 DWORD, # dwSessionHandle 362 ULONG, # lActionFlags 363 RM_WRITE_STATUS_CALLBACK, 364 ] # fnStatus 365 366 RmEndSession = windll.rstrtmgr.RmEndSession 367 RmEndSession.restype = DWORD 368 RmEndSession.argtypes = [DWORD] # dwSessionHandle 369 370 # Get the info needed to uniquely identify the process 371 hProc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid) 372 if not hProc: 373 raise WinError() 374 375 creationTime = FILETIME() 376 exitTime = FILETIME() 377 kernelTime = FILETIME() 378 userTime = FILETIME() 379 if not GetProcessTimes( 380 hProc, 381 pointer(creationTime), 382 pointer(exitTime), 383 pointer(kernelTime), 384 pointer(userTime), 385 ): 386 raise WinError() 387 388 # Start the Restart Manager Session 389 dwSessionHandle = DWORD() 390 sessionKeyType = WCHAR * (CCH_RM_SESSION_KEY + 1) 391 sessionKey = sessionKeyType() 392 if RmStartSession(pointer(dwSessionHandle), 0, sessionKey) != ERROR_SUCCESS: 393 raise WinError() 394 395 try: 396 UProcs_count = 1 397 UProcsArrayType = RM_UNIQUE_PROCESS * UProcs_count 398 UProcs = UProcsArrayType(RM_UNIQUE_PROCESS(pid, creationTime)) 399 400 # Register the process as a resource 401 if ( 402 RmRegisterResources( 403 dwSessionHandle, 0, None, UProcs_count, UProcs, 0, None 404 ) 405 != ERROR_SUCCESS 406 ): 407 raise WinError() 408 409 # Shut down all processes using registered resources 410 if ( 411 RmShutdown( 412 dwSessionHandle, 0, ctypes.cast(None, RM_WRITE_STATUS_CALLBACK) 413 ) 414 != ERROR_SUCCESS 415 ): 416 raise WinError() 417 418 finally: 419 RmEndSession(dwSessionHandle) 420 421 def windows_shutdown_with_variety(self, restart_by_os, expect_restore): 422 """Test restoring windows after Windows shutdown. 423 424 Opens a set of windows, both standard and private, with 425 some number of tabs in them. Once the tabs have loaded, shuts down 426 the browser with the Windows Restart Manager and restarts the browser. 427 428 This specifically exercises the Windows synchronous shutdown mechanism, 429 which terminates the process in response to the Restart Manager's 430 WM_ENDSESSION message. 431 432 If restart_by_os is True, the -os-restarted arg is passed when restarting, 433 simulating being automatically restarted by the Restart Manager. 434 435 If expect_restore is True, this ensures that the standard tabs have been 436 restored, and that the private ones have not. Otherwise it ensures that 437 no tabs and windows have been restored. 438 """ 439 current_windows_set = self.convert_open_windows_to_set() 440 self.assertEqual( 441 current_windows_set, 442 self.all_windows, 443 msg=f"Not all requested windows have been opened. Expected {self.all_windows}, got {current_windows_set}.", 444 ) 445 446 self.marionette.quit(callback=lambda: self.simulate_os_shutdown()) 447 448 saved_args = self.marionette.instance.app_args 449 try: 450 if restart_by_os: 451 self.marionette.instance.app_args = ["-os-restarted"] 452 453 self.marionette.start_session() 454 self.marionette.set_context("chrome") 455 finally: 456 self.marionette.instance.app_args = saved_args 457 458 if expect_restore: 459 self.wait_for_windows( 460 self.test_windows, 461 "Non private browsing windows should have been restored", 462 ) 463 else: 464 self.assertEqual( 465 len(self.marionette.chrome_window_handles), 466 1, 467 msg="Windows from last session shouldn`t have been restored.", 468 ) 469 self.assertEqual( 470 len(self.marionette.window_handles), 471 1, 472 msg="Tabs from last session shouldn`t have been restored.", 473 )