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 |