#!/usr/bin/python # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Logic for generating random ui actions on the browser. Takes into account the expected state of the browser in order to generate relevant ui actions. """ import random class TabState(object): """A set of properties representing a browser tab.""" def __init__(self, location): """Init for a new tab. Args: location: url string for the initial location of this tab. """ self._history = [location] self._position = 0 @property def navs(self): return self._position @property def backs(self): return len(self._history) - self._position - 1 @property def location(self): return self._history[self._position] def Navigate(self, target): self._history = self._history[:self._position + 1] self._position += 1 self._history.append(target) def Back(self): assert self.navs > 0, 'illegal Back' self._position -= 1 def Forward(self): assert self.backs > 0, 'illegal Forward' self._position += 1 class WindowState(object): """A set of properties representing state of browser window.""" def __init__(self, tab=None, private=False): self._tabs = [] self._saved_position = 0 self._position = 0 self._private = private if tab: self._tabs.append(tab) else: self.NewTab() @property def tab(self): return self._tabs[self._position] @property def num_tabs(self): return len(self._tabs) @property def tab_position(self): return self._position @property def private(self): return self._private def NewTab(self, location='chrome://newtab'): new_tab = TabState(location) self._tabs.append(new_tab) self._saved_position = self._position self._position = len(self._tabs) - 1 def FindTab(self, location): """Return position of first tab at location, or -1""" for position in xrange(self.num_tabs): tab = self._tabs[position] if tab.location == location: return position return -1 def ForgetPosition(self): self._saved_position = None def DragLeft(self): assert self._position > 0, 'illegal DragLeft' tab = self.tab self._position -= 1 self._tabs.remove(tab) self._tabs.insert(self._position, tab) def DragRight(self): assert self._position < self.num_tabs - 1, 'illegal DragRight' tab = self.tab self._position += 1 self._tabs.remove(tab) self._tabs.insert(self._position, tab) def RemoveTab(self): self._tabs.pop(self._position) if self._saved_position != None: self._position = self._saved_position if self._position == self.num_tabs: self._position -= 1 self.ForgetPosition() def RestoreTab(self, tab, position): self._tabs.insert(position, tab) self._position = position self.ForgetPosition() def Focus(self, position): self._position = position self.ForgetPosition() def TabLeft(self): if self._position == 0: self.Focus(self.num_tabs - 1) else: self.Focus(self._position - 1) def TabRight(self): if self._position == self.num_tabs - 1: self.Focus(0) else: self.Focus(self._position + 1) class BrowserState(object): """A set of properties representing browser after an action sequence.""" def __init__(self, advanced_actions=False): self._windows = [] self._focus_stack = [] self._closed = [] blank_tab = TabState('about:blank') self.NewWindow(tab=blank_tab) self.advanced = advanced_actions @property def num_windows(self): return len(self._windows) @property def window(self): return self._focus_stack[self.num_windows - 1] @property def window_position(self): return self._windows.index(self.window) def NewWindow(self, tab=None, private=False): window = WindowState(tab=tab, private=private) self._windows.append(window) self._focus_stack.append(window) def RemoveWindow(self): assert self.num_windows > 1, 'not enough windows to RemoveWindow' window = self._focus_stack.pop() window.ForgetPosition() self._windows.remove(window) self._Remember(window) def _Remember(self, window, tab=None, position=None): if window.private: return self._closed.append((window, tab, position)) if len(self._closed) > 10: self._closed = self._closed[-10:] def _Focus(self, position): window = self._windows[position] self._focus_stack.remove(window) self._focus_stack.append(window) def NewTab(self, location='chrome://newtab'): self.window.NewTab(location) def Downloads(self): position = self.window.FindTab('chrome://downloads') if position >= 0: self.window.Focus(position) else: self.NewTab('chrome://downloads') self.window.ForgetPosition() def RemoveTab(self, destroy_tab=True): """Remove active tab from active window. Args: destroy_tab: boolean, true if the tab is being closed. """ assert self.window.num_tabs > 1 or self.num_windows > 1, 'illegal RemoveTab' if self.window.num_tabs == 1: self.RemoveWindow() return if destroy_tab and not self.window.private: self._Remember(self.window, self.window.tab, self.window.tab_position) self.window.RemoveTab() def CanRestore(self): """Return True if Restore is a valid action.""" if self.window.private: return False if self._closed: return True return False def Restore(self): """Restore a previously removed tab/window. Expected behavior: - If a private window is in focus, you cannot restore tabs. - If the last removed tab was in a different window, that window comes back into focus. - If the window is gone, a new window will come to focus with the restored tab. - Tabs restore to the same index they were removed at, else the last position. """ assert self._closed, 'nothing to Restore' assert not self.window.private, 'cannot Restore (private window)' window, tab, position = self._closed.pop() try: i = self._windows.index(window) self._Focus(i) except ValueError: pass if self.window == window: self.window.RestoreTab(tab, position) else: self._windows.append(window) self._focus_stack.append(window) def DragOut(self): """Drag tab out of window, spawns new window.""" assert self.window.num_tabs > 1, 'not enough tabs to DragOut' tab = self.window.tab self.RemoveTab(destroy_tab=False) self.NewWindow(tab=tab, private=self.window.private) def DragLeft(self): self.window.DragLeft() def DragRight(self): self.window.DragRight() def Navigate(self, target): self.window.ForgetPosition() self.window.tab.Navigate(target) def Back(self): self.window.tab.Back() def Forward(self): self.window.tab.Forward() def NextTab(self): self.window.TabRight() def LastTab(self): self.window.TabLeft() def UpdateState(browser, action): """Return new browser state after performing action. Args: browser: current browser state. action: next action performed. Returns: new browser state. """ a = action.split(';')[0] if a == 'openwindow': browser.NewWindow() elif a == 'goofftherecord': browser.NewWindow(private=True) elif a == 'newtab': browser.NewTab() elif a == 'dragtabout': browser.DragOut() elif a == 'dragtableft': browser.DragLeft() elif a == 'dragtabright': browser.DragRight() elif a == 'closetab': browser.RemoveTab() elif a == 'closewindow': browser.RemoveWindow() elif a == 'restoretab': browser.Restore() elif a == 'navigate': browser.Navigate(action.split(';')[1]) elif a == 'downloads': browser.Downloads() elif a == 'back': browser.Back() elif a == 'forward': browser.Forward() elif a == 'nexttab': browser.NextTab() elif a == 'lasttab': browser.LastTab() return browser def GetRandomAction(browser): """Return a random possible action for a UI sequence in given state. Args: browser: current browser state. Returns: UI action (string). """ possible_actions = [] def AddActionWithWeight(action, weight=1): """Add action to possible actions list with given weight. Args: action: action string. weight: integer weight value. """ for _ in xrange(weight): possible_actions.append(action) AddActionWithWeight('showbookmarks') if browser.num_windows < 6: AddActionWithWeight('openwindow') if browser.window.num_tabs < 10: AddActionWithWeight('newtab', weight=3) # Throw in some navigates to generate a realistic environment. nav_options = ['http://www.craigslist.com', 'http://www.google.com', 'http://www.bing.com'] for location in nav_options: if browser.window.tab.location != location: AddActionWithWeight('navigate;%s' % location) if browser.window.tab.location != 'Downloads': AddActionWithWeight('downloads') # Actions on a web page. if browser.window.tab.navs > 0: AddActionWithWeight('star') AddActionWithWeight('zoomplus') AddActionWithWeight('zoomminus') AddActionWithWeight('pagedown', weight=3) # Other conditional actions. if browser.window.tab.navs > 0: AddActionWithWeight('back', weight=3) if browser.window.tab.backs > 0: AddActionWithWeight('forward', weight=2) if browser.window.num_tabs > 1 or browser.num_windows > 1: AddActionWithWeight('closetab', weight=2) if browser.window.num_tabs > 1: AddActionWithWeight('dragtabout') if browser.window.tab_position > 0: AddActionWithWeight('dragtableft') if browser.window.tab_position < browser.window.num_tabs - 1: AddActionWithWeight('dragtabright') # (v2) actions. separated for backwards compatability. if browser.advanced: if browser.num_windows > 1: AddActionWithWeight('closewindow') #TODO(ace): fix support for restore action. #if browser.CanRestore(): # AddActionWithWeight('restoretab') if browser.window.tab_position > 0: AddActionWithWeight('lasttab') if browser.window.tab_position < browser.window.num_tabs - 1: AddActionWithWeight('nexttab') if browser.num_windows < 6: AddActionWithWeight('goofftherecord') return ChooseFrom(possible_actions) def ChooseFrom(choice_list): """Return a random choice from given list. Args: choice_list: list of possible choices. Returns: One random element from choice_list """ return random.choice(choice_list)