| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Logic for generating random ui actions on the browser. | |
| 6 | |
| 7 Takes into account the expected state of the browser in order to generate | |
| 8 relevant ui actions. | |
| 9 """ | |
| 10 | |
| 11 import random | |
| 12 | |
| 13 | |
| 14 class TabState(object): | |
| 15 """A set of properties representing a browser tab.""" | |
| 16 | |
| 17 def __init__(self, location): | |
| 18 """Init for a new tab. | |
| 19 | |
| 20 Args: | |
| 21 location: url string for the initial location of this tab. | |
| 22 """ | |
| 23 self._history = [location] | |
| 24 self._position = 0 | |
| 25 | |
| 26 @property | |
| 27 def navs(self): | |
| 28 return self._position | |
| 29 | |
| 30 @property | |
| 31 def backs(self): | |
| 32 return len(self._history) - self._position - 1 | |
| 33 | |
| 34 @property | |
| 35 def location(self): | |
| 36 return self._history[self._position] | |
| 37 | |
| 38 def Navigate(self, target): | |
| 39 self._history = self._history[:self._position + 1] | |
| 40 self._position += 1 | |
| 41 self._history.append(target) | |
| 42 | |
| 43 def Back(self): | |
| 44 assert self.navs > 0, 'illegal Back' | |
| 45 self._position -= 1 | |
| 46 | |
| 47 def Forward(self): | |
| 48 assert self.backs > 0, 'illegal Forward' | |
| 49 self._position += 1 | |
| 50 | |
| 51 | |
| 52 class WindowState(object): | |
| 53 """A set of properties representing state of browser window.""" | |
| 54 | |
| 55 def __init__(self, tab=None, private=False): | |
| 56 self._tabs = [] | |
| 57 self._saved_position = 0 | |
| 58 self._position = 0 | |
| 59 self._private = private | |
| 60 if tab: | |
| 61 self._tabs.append(tab) | |
| 62 else: | |
| 63 self.NewTab() | |
| 64 | |
| 65 @property | |
| 66 def tab(self): | |
| 67 return self._tabs[self._position] | |
| 68 | |
| 69 @property | |
| 70 def num_tabs(self): | |
| 71 return len(self._tabs) | |
| 72 | |
| 73 @property | |
| 74 def tab_position(self): | |
| 75 return self._position | |
| 76 | |
| 77 @property | |
| 78 def private(self): | |
| 79 return self._private | |
| 80 | |
| 81 def NewTab(self, location='chrome://newtab'): | |
| 82 new_tab = TabState(location) | |
| 83 self._tabs.append(new_tab) | |
| 84 self._saved_position = self._position | |
| 85 self._position = len(self._tabs) - 1 | |
| 86 | |
| 87 def FindTab(self, location): | |
| 88 """Return position of first tab at location, or -1""" | |
| 89 for position in xrange(self.num_tabs): | |
| 90 tab = self._tabs[position] | |
| 91 if tab.location == location: | |
| 92 return position | |
| 93 return -1 | |
| 94 | |
| 95 def ForgetPosition(self): | |
| 96 self._saved_position = None | |
| 97 | |
| 98 def DragLeft(self): | |
| 99 assert self._position > 0, 'illegal DragLeft' | |
| 100 tab = self.tab | |
| 101 self._position -= 1 | |
| 102 self._tabs.remove(tab) | |
| 103 self._tabs.insert(self._position, tab) | |
| 104 | |
| 105 def DragRight(self): | |
| 106 assert self._position < self.num_tabs - 1, 'illegal DragRight' | |
| 107 tab = self.tab | |
| 108 self._position += 1 | |
| 109 self._tabs.remove(tab) | |
| 110 self._tabs.insert(self._position, tab) | |
| 111 | |
| 112 def RemoveTab(self): | |
| 113 self._tabs.pop(self._position) | |
| 114 if self._saved_position != None: | |
| 115 self._position = self._saved_position | |
| 116 if self._position == self.num_tabs: | |
| 117 self._position -= 1 | |
| 118 self.ForgetPosition() | |
| 119 | |
| 120 def RestoreTab(self, tab, position): | |
| 121 self._tabs.insert(position, tab) | |
| 122 self._position = position | |
| 123 self.ForgetPosition() | |
| 124 | |
| 125 def Focus(self, position): | |
| 126 self._position = position | |
| 127 self.ForgetPosition() | |
| 128 | |
| 129 def TabLeft(self): | |
| 130 if self._position == 0: | |
| 131 self.Focus(self.num_tabs - 1) | |
| 132 else: | |
| 133 self.Focus(self._position - 1) | |
| 134 | |
| 135 def TabRight(self): | |
| 136 if self._position == self.num_tabs - 1: | |
| 137 self.Focus(0) | |
| 138 else: | |
| 139 self.Focus(self._position + 1) | |
| 140 | |
| 141 | |
| 142 class BrowserState(object): | |
| 143 """A set of properties representing browser after an action sequence.""" | |
| 144 | |
| 145 def __init__(self, advanced_actions=False): | |
| 146 self._windows = [] | |
| 147 self._focus_stack = [] | |
| 148 self._closed = [] | |
| 149 blank_tab = TabState('about:blank') | |
| 150 self.NewWindow(tab=blank_tab) | |
| 151 self.advanced = advanced_actions | |
| 152 | |
| 153 @property | |
| 154 def num_windows(self): | |
| 155 return len(self._windows) | |
| 156 | |
| 157 @property | |
| 158 def window(self): | |
| 159 return self._focus_stack[self.num_windows - 1] | |
| 160 | |
| 161 @property | |
| 162 def window_position(self): | |
| 163 return self._windows.index(self.window) | |
| 164 | |
| 165 def NewWindow(self, tab=None, private=False): | |
| 166 window = WindowState(tab=tab, private=private) | |
| 167 self._windows.append(window) | |
| 168 self._focus_stack.append(window) | |
| 169 | |
| 170 def RemoveWindow(self): | |
| 171 assert self.num_windows > 1, 'not enough windows to RemoveWindow' | |
| 172 window = self._focus_stack.pop() | |
| 173 window.ForgetPosition() | |
| 174 self._windows.remove(window) | |
| 175 self._Remember(window) | |
| 176 | |
| 177 def _Remember(self, window, tab=None, position=None): | |
| 178 if window.private: | |
| 179 return | |
| 180 self._closed.append((window, tab, position)) | |
| 181 if len(self._closed) > 10: | |
| 182 self._closed = self._closed[-10:] | |
| 183 | |
| 184 def _Focus(self, position): | |
| 185 window = self._windows[position] | |
| 186 self._focus_stack.remove(window) | |
| 187 self._focus_stack.append(window) | |
| 188 | |
| 189 def NewTab(self, location='chrome://newtab'): | |
| 190 self.window.NewTab(location) | |
| 191 | |
| 192 def Downloads(self): | |
| 193 position = self.window.FindTab('chrome://downloads') | |
| 194 if position >= 0: | |
| 195 self.window.Focus(position) | |
| 196 else: | |
| 197 self.NewTab('chrome://downloads') | |
| 198 self.window.ForgetPosition() | |
| 199 | |
| 200 def RemoveTab(self, destroy_tab=True): | |
| 201 """Remove active tab from active window. | |
| 202 | |
| 203 Args: | |
| 204 destroy_tab: boolean, true if the tab is being closed. | |
| 205 """ | |
| 206 assert self.window.num_tabs > 1 or self.num_windows > 1, 'illegal RemoveTab' | |
| 207 if self.window.num_tabs == 1: | |
| 208 self.RemoveWindow() | |
| 209 return | |
| 210 if destroy_tab and not self.window.private: | |
| 211 self._Remember(self.window, self.window.tab, self.window.tab_position) | |
| 212 self.window.RemoveTab() | |
| 213 | |
| 214 def CanRestore(self): | |
| 215 """Return True if Restore is a valid action.""" | |
| 216 if self.window.private: | |
| 217 return False | |
| 218 if self._closed: | |
| 219 return True | |
| 220 return False | |
| 221 | |
| 222 def Restore(self): | |
| 223 """Restore a previously removed tab/window. | |
| 224 | |
| 225 Expected behavior: | |
| 226 - If a private window is in focus, you cannot restore tabs. | |
| 227 - If the last removed tab was in a different window, that window comes back | |
| 228 into focus. | |
| 229 - If the window is gone, a new window will come to focus with the restored | |
| 230 tab. | |
| 231 - Tabs restore to the same index they were removed at, else the last | |
| 232 position. | |
| 233 """ | |
| 234 assert self._closed, 'nothing to Restore' | |
| 235 assert not self.window.private, 'cannot Restore (private window)' | |
| 236 window, tab, position = self._closed.pop() | |
| 237 try: | |
| 238 i = self._windows.index(window) | |
| 239 self._Focus(i) | |
| 240 except ValueError: | |
| 241 pass | |
| 242 if self.window == window: | |
| 243 self.window.RestoreTab(tab, position) | |
| 244 else: | |
| 245 self._windows.append(window) | |
| 246 self._focus_stack.append(window) | |
| 247 | |
| 248 def DragOut(self): | |
| 249 """Drag tab out of window, spawns new window.""" | |
| 250 assert self.window.num_tabs > 1, 'not enough tabs to DragOut' | |
| 251 tab = self.window.tab | |
| 252 self.RemoveTab(destroy_tab=False) | |
| 253 self.NewWindow(tab=tab, private=self.window.private) | |
| 254 | |
| 255 def DragLeft(self): | |
| 256 self.window.DragLeft() | |
| 257 | |
| 258 def DragRight(self): | |
| 259 self.window.DragRight() | |
| 260 | |
| 261 def Navigate(self, target): | |
| 262 self.window.ForgetPosition() | |
| 263 self.window.tab.Navigate(target) | |
| 264 | |
| 265 def Back(self): | |
| 266 self.window.tab.Back() | |
| 267 | |
| 268 def Forward(self): | |
| 269 self.window.tab.Forward() | |
| 270 | |
| 271 def NextTab(self): | |
| 272 self.window.TabRight() | |
| 273 | |
| 274 def LastTab(self): | |
| 275 self.window.TabLeft() | |
| 276 | |
| 277 | |
| 278 def UpdateState(browser, action): | |
| 279 """Return new browser state after performing action. | |
| 280 | |
| 281 Args: | |
| 282 browser: current browser state. | |
| 283 action: next action performed. | |
| 284 | |
| 285 Returns: | |
| 286 new browser state. | |
| 287 """ | |
| 288 a = action.split(';')[0] | |
| 289 if a == 'openwindow': | |
| 290 browser.NewWindow() | |
| 291 elif a == 'goofftherecord': | |
| 292 browser.NewWindow(private=True) | |
| 293 elif a == 'newtab': | |
| 294 browser.NewTab() | |
| 295 elif a == 'dragtabout': | |
| 296 browser.DragOut() | |
| 297 elif a == 'dragtableft': | |
| 298 browser.DragLeft() | |
| 299 elif a == 'dragtabright': | |
| 300 browser.DragRight() | |
| 301 elif a == 'closetab': | |
| 302 browser.RemoveTab() | |
| 303 elif a == 'closewindow': | |
| 304 browser.RemoveWindow() | |
| 305 elif a == 'restoretab': | |
| 306 browser.Restore() | |
| 307 elif a == 'navigate': | |
| 308 browser.Navigate(action.split(';')[1]) | |
| 309 elif a == 'downloads': | |
| 310 browser.Downloads() | |
| 311 elif a == 'back': | |
| 312 browser.Back() | |
| 313 elif a == 'forward': | |
| 314 browser.Forward() | |
| 315 elif a == 'nexttab': | |
| 316 browser.NextTab() | |
| 317 elif a == 'lasttab': | |
| 318 browser.LastTab() | |
| 319 return browser | |
| 320 | |
| 321 | |
| 322 def GetRandomAction(browser): | |
| 323 """Return a random possible action for a UI sequence in given state. | |
| 324 | |
| 325 Args: | |
| 326 browser: current browser state. | |
| 327 | |
| 328 Returns: | |
| 329 UI action (string). | |
| 330 """ | |
| 331 possible_actions = [] | |
| 332 | |
| 333 def AddActionWithWeight(action, weight=1): | |
| 334 """Add action to possible actions list with given weight. | |
| 335 | |
| 336 Args: | |
| 337 action: action string. | |
| 338 weight: integer weight value. | |
| 339 """ | |
| 340 for _ in xrange(weight): | |
| 341 possible_actions.append(action) | |
| 342 | |
| 343 AddActionWithWeight('showbookmarks') | |
| 344 if browser.num_windows < 6: | |
| 345 AddActionWithWeight('openwindow') | |
| 346 if browser.window.num_tabs < 10: | |
| 347 AddActionWithWeight('newtab', weight=3) | |
| 348 | |
| 349 # Throw in some navigates to generate a realistic environment. | |
| 350 nav_options = ['http://www.craigslist.com', | |
| 351 'http://www.google.com', | |
| 352 'http://www.bing.com'] | |
| 353 for location in nav_options: | |
| 354 if browser.window.tab.location != location: | |
| 355 AddActionWithWeight('navigate;%s' % location) | |
| 356 | |
| 357 if browser.window.tab.location != 'Downloads': | |
| 358 AddActionWithWeight('downloads') | |
| 359 | |
| 360 # Actions on a web page. | |
| 361 if browser.window.tab.navs > 0: | |
| 362 AddActionWithWeight('star') | |
| 363 AddActionWithWeight('zoomplus') | |
| 364 AddActionWithWeight('zoomminus') | |
| 365 AddActionWithWeight('pagedown', weight=3) | |
| 366 | |
| 367 # Other conditional actions. | |
| 368 if browser.window.tab.navs > 0: | |
| 369 AddActionWithWeight('back', weight=3) | |
| 370 if browser.window.tab.backs > 0: | |
| 371 AddActionWithWeight('forward', weight=2) | |
| 372 if browser.window.num_tabs > 1 or browser.num_windows > 1: | |
| 373 AddActionWithWeight('closetab', weight=2) | |
| 374 if browser.window.num_tabs > 1: | |
| 375 AddActionWithWeight('dragtabout') | |
| 376 if browser.window.tab_position > 0: | |
| 377 AddActionWithWeight('dragtableft') | |
| 378 if browser.window.tab_position < browser.window.num_tabs - 1: | |
| 379 AddActionWithWeight('dragtabright') | |
| 380 | |
| 381 # (v2) actions. separated for backwards compatability. | |
| 382 if browser.advanced: | |
| 383 if browser.num_windows > 1: | |
| 384 AddActionWithWeight('closewindow') | |
| 385 #TODO(ace): fix support for restore action. | |
| 386 #if browser.CanRestore(): | |
| 387 # AddActionWithWeight('restoretab') | |
| 388 if browser.window.tab_position > 0: | |
| 389 AddActionWithWeight('lasttab') | |
| 390 if browser.window.tab_position < browser.window.num_tabs - 1: | |
| 391 AddActionWithWeight('nexttab') | |
| 392 if browser.num_windows < 6: | |
| 393 AddActionWithWeight('goofftherecord') | |
| 394 | |
| 395 return ChooseFrom(possible_actions) | |
| 396 | |
| 397 | |
| 398 def Seed(seed=None): | |
| 399 """Seed random for reproducible command sequences. | |
| 400 | |
| 401 Args: | |
| 402 seed: optional long int input for random.seed. If none is given, | |
| 403 one is generated. | |
| 404 | |
| 405 Returns: | |
| 406 The given or generated seed. | |
| 407 """ | |
| 408 if not seed: | |
| 409 random.seed() | |
| 410 seed = random.getrandbits(64) | |
| 411 random.seed(seed) | |
| 412 return seed | |
| 413 | |
| 414 | |
| 415 def ChooseFrom(choice_list): | |
| 416 """Return a random choice from given list. | |
| 417 | |
| 418 Args: | |
| 419 choice_list: list of possible choices. | |
| 420 | |
| 421 Returns: | |
| 422 One random element from choice_list | |
| 423 """ | |
| 424 return random.choice(choice_list) | |
| OLD | NEW |