Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(593)

Side by Side Diff: pylib/mozrunner/__init__.py

Issue 6183003: Added third_party python libraries that are needed for browser testing. (Closed) Base URL: svn://svn.chromium.org/native_client/trunk/src/third_party/
Patch Set: Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pylib/mozrunner/README ('k') | pylib/mozrunner/killableprocess.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # ***** BEGIN LICENSE BLOCK *****
2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 #
4 # The contents of this file are subject to the Mozilla Public License Version
5 # 1.1 (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
7 # http://www.mozilla.org/MPL/
8 #
9 # Software distributed under the License is distributed on an "AS IS" basis,
10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 # for the specific language governing rights and limitations under the
12 # License.
13 #
14 # The Original Code is Mozilla Corporation Code.
15 #
16 # The Initial Developer of the Original Code is
17 # Mikeal Rogers.
18 # Portions created by the Initial Developer are Copyright (C) 2008-2009
19 # the Initial Developer. All Rights Reserved.
20 #
21 # Contributor(s):
22 # Mikeal Rogers <mikeal.rogers@gmail.com>
23 # Clint Talbert <ctalbert@mozilla.com>
24 # Henrik Skupin <hskupin@mozilla.com>
25 #
26 # Alternatively, the contents of this file may be used under the terms of
27 # either the GNU General Public License Version 2 or later (the "GPL"), or
28 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 # in which case the provisions of the GPL or the LGPL are applicable instead
30 # of those above. If you wish to allow use of your version of this file only
31 # under the terms of either the GPL or the LGPL, and not to allow others to
32 # use your version of this file under the terms of the MPL, indicate your
33 # decision by deleting the provisions above and replace them with the notice
34 # and other provisions required by the GPL or the LGPL. If you do not delete
35 # the provisions above, a recipient may use your version of this file under
36 # the terms of any one of the MPL, the GPL or the LGPL.
37 #
38 # ***** END LICENSE BLOCK *****
39
40 import os
41 import sys
42 import copy
43 import tempfile
44 import signal
45 import commands
46 import zipfile
47 import optparse
48 import killableprocess
49 import subprocess
50 import platform
51
52 from distutils import dir_util
53 from time import sleep
54 from xml.dom import minidom
55
56 # conditional (version-dependent) imports
57 try:
58 import simplejson
59 except ImportError:
60 import json as simplejson
61
62 import logging
63 logger = logging.getLogger(__name__)
64
65 # Use dir_util for copy/rm operations because shutil is all kinds of broken
66 copytree = dir_util.copy_tree
67 rmtree = dir_util.remove_tree
68
69 def findInPath(fileName, path=os.environ['PATH']):
70 dirs = path.split(os.pathsep)
71 for dir in dirs:
72 if os.path.isfile(os.path.join(dir, fileName)):
73 return os.path.join(dir, fileName)
74 if os.name == 'nt' or sys.platform == 'cygwin':
75 if os.path.isfile(os.path.join(dir, fileName + ".exe")):
76 return os.path.join(dir, fileName + ".exe")
77 return None
78
79 stdout = sys.stdout
80 stderr = sys.stderr
81 stdin = sys.stdin
82
83 def run_command(cmd, env=None, **kwargs):
84 """Run the given command in killable process."""
85 killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin}
86 killable_kwargs.update(kwargs)
87
88 if sys.platform != "win32":
89 return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0),
90 env=env, **killable_kwargs)
91 else:
92 return killableprocess.Popen(cmd, env=env, **killable_kwargs)
93
94 def getoutput(l):
95 tmp = tempfile.mktemp()
96 x = open(tmp, 'w')
97 subprocess.call(l, stdout=x, stderr=x)
98 x.close(); x = open(tmp, 'r')
99 r = x.read() ; x.close()
100 os.remove(tmp)
101 return r
102
103 def get_pids(name, minimun_pid=0):
104 """Get all the pids matching name, exclude any pids below minimum_pid."""
105 if os.name == 'nt' or sys.platform == 'cygwin':
106 import wpk
107
108 pids = wpk.get_pids(name)
109
110 else:
111 # get_pids_cmd = ['ps', 'ax']
112 # h = killableprocess.runCommand(get_pids_cmd, stdout=subprocess.PIPE, u niversal_newlines=True)
113 # h.wait(group=False)
114 # data = h.stdout.readlines()
115 data = getoutput(['ps', 'ax']).splitlines()
116 pids = [int(line.split()[0]) for line in data if line.find(name) is not -1]
117
118 matching_pids = [m for m in pids if m > minimun_pid]
119 return matching_pids
120
121 def kill_process_by_name(name):
122 """Find and kill all processes containing a certain name"""
123
124 pids = get_pids(name)
125
126 if os.name == 'nt' or sys.platform == 'cygwin':
127 for p in pids:
128 import wpk
129
130 wpk.kill_pid(p)
131
132 else:
133 for pid in pids:
134 try:
135 os.kill(pid, signal.SIGTERM)
136 except OSError: pass
137 sleep(.5)
138 if len(get_pids(name)) is not 0:
139 try:
140 os.kill(pid, signal.SIGKILL)
141 except OSError: pass
142 sleep(.5)
143 if len(get_pids(name)) is not 0:
144 logger.error('Could not kill process')
145
146 def makedirs(name):
147
148 head, tail = os.path.split(name)
149 if not tail:
150 head, tail = os.path.split(head)
151 if head and tail and not os.path.exists(head):
152 try:
153 makedirs(head)
154 except OSError, e:
155 pass
156 if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exis ts
157 return
158 try:
159 os.mkdir(name)
160 except:
161 pass
162
163 class Profile(object):
164 """Handles all operations regarding profile. Created new profiles, installs extensions,
165 sets preferences and handles cleanup."""
166
167 def __init__(self, binary=None, profile=None, addons=None,
168 preferences=None):
169
170 self.binary = binary
171
172 self.create_new = not(bool(profile))
173 if profile:
174 self.profile = profile
175 else:
176 self.profile = self.create_new_profile(self.binary)
177
178 self.addons_installed = []
179 self.addons = addons or []
180
181 ### set preferences from class preferences
182 preferences = preferences or {}
183 if hasattr(self.__class__, 'preferences'):
184 self.preferences = self.__class__.preferences.copy()
185 else:
186 self.preferences = {}
187 self.preferences.update(preferences)
188
189 for addon in self.addons:
190 self.install_addon(addon)
191
192 self.set_preferences(self.preferences)
193
194 def create_new_profile(self, binary):
195 """Create a new clean profile in tmp which is a simple empty folder"""
196 profile = tempfile.mkdtemp(suffix='.mozrunner')
197 return profile
198
199 ### methods related to addons
200
201 @classmethod
202 def addon_id(self, addon_path):
203 """
204 return the id for a given addon, or None if not found
205 - addon_path : path to the addon directory
206 """
207
208 def find_id(desc):
209 """finds the addon id give its description"""
210
211 addon_id = None
212 for elem in desc:
213 apps = elem.getElementsByTagName('em:targetApplication')
214 if apps:
215 for app in apps:
216 # remove targetApplication nodes, they contain id's we a ren't interested in
217 elem.removeChild(app)
218 if elem.getElementsByTagName('em:id'):
219 addon_id = str(elem.getElementsByTagName('em:id')[0].fir stChild.data)
220 elif elem.hasAttribute('em:id'):
221 addon_id = str(elem.getAttribute('em:id'))
222 return addon_id
223
224 doc = minidom.parse(os.path.join(addon_path, 'install.rdf'))
225
226 for tag in 'Description', 'RDF:Description':
227 desc = doc.getElementsByTagName(tag)
228 addon_id = find_id(desc)
229 if addon_id:
230 return addon_id
231
232
233 def install_addon(self, path):
234 """Installs the given addon or directory of addons in the profile."""
235
236 # if the addon is a directory, install all addons in it
237 addons = [path]
238 if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, ' install.rdf')):
239 addons = [os.path.join(path, x) for x in os.listdir(path)]
240
241 # install each addon
242 for addon in addons:
243
244 # if the addon is an .xpi, uncompress it to a temporary directory
245 if addon.endswith('.xpi'):
246 tmpdir = tempfile.mkdtemp(suffix = "." + os.path.split(addon)[-1 ])
247 compressed_file = zipfile.ZipFile(addon, "r")
248 for name in compressed_file.namelist():
249 if name.endswith('/'):
250 makedirs(os.path.join(tmpdir, name))
251 else:
252 if not os.path.isdir(os.path.dirname(os.path.join(tmpdir , name))):
253 makedirs(os.path.dirname(os.path.join(tmpdir, name)) )
254 data = compressed_file.read(name)
255 f = open(os.path.join(tmpdir, name), 'wb')
256 f.write(data) ; f.close()
257 addon = tmpdir
258
259 # determine the addon id
260 addon_id = Profile.addon_id(addon)
261 assert addon_id is not None, "The addon id could not be found: %s" % addon
262
263 # copy the addon to the profile
264 addon_path = os.path.join(self.profile, 'extensions', addon_id)
265 copytree(addon, addon_path, preserve_symlinks=1)
266 self.addons_installed.append(addon_path)
267
268 def clean_addons(self):
269 """Cleans up addons in the profile."""
270 for addon in self.addons_installed:
271 if os.path.isdir(addon):
272 rmtree(addon)
273
274 ### methods related to preferences
275
276 def set_preferences(self, preferences):
277 """Adds preferences dict to profile preferences"""
278
279 prefs_file = os.path.join(self.profile, 'user.js')
280
281 # Ensure that the file exists first otherwise create an empty file
282 if os.path.isfile(prefs_file):
283 f = open(prefs_file, 'a+')
284 else:
285 f = open(prefs_file, 'w')
286
287 f.write('\n#MozRunner Prefs Start\n')
288
289 pref_lines = ['user_pref(%s, %s);' %
290 (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in
291 preferences.items()]
292 for line in pref_lines:
293 f.write(line+'\n')
294 f.write('#MozRunner Prefs End\n')
295 f.flush() ; f.close()
296
297 def clean_preferences(self):
298 """Removed preferences added by mozrunner."""
299 lines = open(os.path.join(self.profile, 'user.js'), 'r').read().splitlin es()
300 s = lines.index('#MozRunner Prefs Start') ; e = lines.index('#MozRunner Prefs End')
301 cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
302 f = open(os.path.join(self.profile, 'user.js'), 'w')
303 f.write(cleaned_prefs) ; f.flush() ; f.close()
304
305 ### cleanup
306
307 def cleanup(self):
308 """Cleanup operations on the profile."""
309 if self.create_new:
310 rmtree(self.profile)
311 else:
312 self.clean_preferences()
313 self.clean_addons()
314
315 class FirefoxProfile(Profile):
316 """Specialized Profile subclass for Firefox"""
317 preferences = {# Don't automatically update the application
318 'app.update.enabled' : False,
319 # Don't restore the last open set of tabs if the browser has crashed
320 'browser.sessionstore.resume_from_crash': False,
321 # Don't check for the default web browser
322 'browser.shell.checkDefaultBrowser' : False,
323 # Don't warn on exit when multiple tabs are open
324 'browser.tabs.warnOnClose' : False,
325 # Don't warn when exiting the browser
326 'browser.warnOnQuit': False,
327 # Only install add-ons from the profile and the app folder
328 'extensions.enabledScopes' : 5,
329 # Dont' run the add-on compatibility check during start-up
330 'extensions.showMismatchUI' : False,
331 # Don't automatically update add-ons
332 'extensions.update.enabled' : False,
333 # Don't open a dialog to show available add-on updates
334 'extensions.update.notifyUser' : False,
335 }
336
337 @property
338 def names(self):
339 if sys.platform == 'darwin':
340 return ['firefox', 'minefield', 'shiretoko']
341 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')) :
342 return ['firefox', 'mozilla-firefox', 'iceweasel']
343 if os.name == 'nt' or sys.platform == 'cygwin':
344 return ['firefox']
345
346 class ThunderbirdProfile(Profile):
347 preferences = {'extensions.update.enabled' : False,
348 'extensions.update.notifyUser' : False,
349 'browser.shell.checkDefaultBrowser' : False,
350 'browser.tabs.warnOnClose' : False,
351 'browser.warnOnQuit': False,
352 'browser.sessionstore.resume_from_crash': False,
353 }
354 names = ["thunderbird", "shredder"]
355
356
357 class Runner(object):
358 """Handles all running operations. Finds bins, runs and kills the process."" "
359
360 def __init__(self, binary=None, profile=None, cmdargs=[], env=None,
361 aggressively_kill=['crashreporter'], kp_kwargs={}):
362 if binary is None:
363 self.binary = self.find_binary()
364 elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1:
365 self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.na mes[0])
366 else:
367 self.binary = binary
368
369 if not os.path.exists(self.binary):
370 raise Exception("Binary path does not exist "+self.binary)
371
372 if sys.platform == 'linux2' and self.binary.endswith('-bin'):
373 dirname = os.path.dirname(self.binary)
374 if os.environ.get('LD_LIBRARY_PATH', None):
375 os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRAR Y_PATH'], dirname)
376 else:
377 os.environ['LD_LIBRARY_PATH'] = dirname
378
379 self.profile = profile
380
381 self.cmdargs = cmdargs
382 if env is None:
383 self.env = copy.copy(os.environ)
384 self.env.update({'MOZ_NO_REMOTE':"1",})
385 else:
386 self.env = env
387 self.aggressively_kill = aggressively_kill
388 self.kp_kwargs = kp_kwargs
389
390 def find_binary(self):
391 """Finds the binary for self.names if one was not provided."""
392 binary = None
393 if sys.platform in ('linux2', 'sunos5', 'solaris'):
394 for name in reversed(self.names):
395 binary = findInPath(name)
396 elif os.name == 'nt' or sys.platform == 'cygwin':
397
398 # find the default executable from the windows registry
399 try:
400 # assumes self.app_name is defined, as it should be for
401 # implementors
402 import _winreg
403 app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"Software \Mozilla\Mozilla %s" % self.app_name)
404 version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion")
405 version_key = _winreg.OpenKey(app_key, version + r"\Main")
406 path, _ = _winreg.QueryValueEx(version_key, "PathToExe")
407 return path
408 except: # XXX not sure what type of exception this should be
409 pass
410
411 # search for the binary in the path
412 for name in reversed(self.names):
413 binary = findInPath(name)
414 if sys.platform == 'cygwin':
415 program_files = os.environ['PROGRAMFILES']
416 else:
417 program_files = os.environ['ProgramFiles']
418
419 if binary is None:
420 for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe' ),
421 (os.environ.get("ProgramFiles(x86)"),'Mozilla Fi refox', 'firefox.exe'),
422 (program_files,'Minefield', 'firefox.exe'),
423 (os.environ.get("ProgramFiles(x86)"),'Minefield' , 'firefox.exe')
424 ]:
425 path = os.path.join(*bin)
426 if os.path.isfile(path):
427 binary = path
428 break
429 elif sys.platform == 'darwin':
430 for name in reversed(self.names):
431 appdir = os.path.join('Applications', name.capitalize()+'.app')
432 if os.path.isdir(os.path.join(os.path.expanduser('~/'), appdir)) :
433 binary = os.path.join(os.path.expanduser('~/'), appdir,
434 'Contents/MacOS/'+name+'-bin')
435 elif os.path.isdir('/'+appdir):
436 binary = os.path.join("/"+appdir, 'Contents/MacOS/'+name+'-b in')
437
438 if binary is not None:
439 if not os.path.isfile(binary):
440 binary = binary.replace(name+'-bin', 'firefox-bin')
441 if not os.path.isfile(binary):
442 binary = None
443 if binary is None:
444 raise Exception('Mozrunner could not locate your binary, you will ne ed to set it.')
445 return binary
446
447 @property
448 def command(self):
449 """Returns the command list to run."""
450 cmd = [self.binary, '-profile', self.profile.profile]
451 # On i386 OS X machines, i386+x86_64 universal binaries need to be told
452 # to run as i386 binaries. If we're not running a i386+x86_64 universal
453 # binary, then this command modification is harmless.
454 if sys.platform == 'darwin':
455 if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit':
456 cmd = ['arch', '-i386'] + cmd
457 return cmd
458
459 def get_repositoryInfo(self):
460 """Read repository information from application.ini and platform.ini."""
461 import ConfigParser
462
463 config = ConfigParser.RawConfigParser()
464 dirname = os.path.dirname(self.binary)
465 repository = { }
466
467 for entry in [['application', 'App'], ['platform', 'Build']]:
468 (file, section) = entry
469 config.read(os.path.join(dirname, '%s.ini' % file))
470
471 for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'c hangeset']]:
472 (key, id) = entry
473
474 try:
475 repository['%s_%s' % (file, id)] = config.get(section, key);
476 except:
477 repository['%s_%s' % (file, id)] = None
478
479 return repository
480
481 def start(self):
482 """Run self.command in the proper environment."""
483 if self.profile is None:
484 self.profile = self.profile_class()
485 self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs)
486
487 def wait(self, timeout=None):
488 """Wait for the browser to exit."""
489 self.process_handler.wait(timeout=timeout)
490
491 if sys.platform != 'win32':
492 for name in self.names:
493 for pid in get_pids(name, self.process_handler.pid):
494 self.process_handler.pid = pid
495 self.process_handler.wait(timeout=timeout)
496
497 def kill(self, kill_signal=signal.SIGTERM):
498 """Kill the browser"""
499 if sys.platform != 'win32':
500 self.process_handler.kill()
501 for name in self.names:
502 for pid in get_pids(name, self.process_handler.pid):
503 self.process_handler.pid = pid
504 self.process_handler.kill()
505 else:
506 try:
507 self.process_handler.kill(group=True)
508 except Exception, e:
509 logger.error('Cannot kill process, '+type(e).__name__+' '+e.mess age)
510
511 for name in self.aggressively_kill:
512 kill_process_by_name(name)
513
514 def stop(self):
515 self.kill()
516
517 class FirefoxRunner(Runner):
518 """Specialized Runner subclass for running Firefox."""
519
520 app_name = 'Firefox'
521 profile_class = FirefoxProfile
522
523 @property
524 def names(self):
525 if sys.platform == 'darwin':
526 return ['firefox', 'minefield', 'shiretoko']
527 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')) :
528 return ['firefox', 'mozilla-firefox', 'iceweasel']
529 if os.name == 'nt' or sys.platform == 'cygwin':
530 return ['firefox']
531
532 class ThunderbirdRunner(Runner):
533 """Specialized Runner subclass for running Thunderbird"""
534
535 app_name = 'Thunderbird'
536 profile_class = ThunderbirdProfile
537
538 names = ["thunderbird", "shredder"]
539
540 class CLI(object):
541 """Command line interface."""
542
543 runner_class = FirefoxRunner
544 profile_class = FirefoxProfile
545 module = "mozrunner"
546
547 parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path .",
548 metavar=None, default=None),
549 ('-p', "--profile",): dict(dest="profile", help="Profile p ath.",
550 metavar=None, default=None),
551 ('-a', "--addons",): dict(dest="addons",
552 help="Addons paths to install.",
553 metavar=None, default=None),
554 ("--info",): dict(dest="info", default=False,
555 action="store_true",
556 help="Print module information")
557 }
558
559 def __init__(self):
560 """ Setup command line parser and parse arguments """
561 self.metadata = self.get_metadata_from_egg()
562 self.parser = optparse.OptionParser(version="%prog " + self.metadata["Ve rsion"])
563 for names, opts in self.parser_options.items():
564 self.parser.add_option(*names, **opts)
565 (self.options, self.args) = self.parser.parse_args()
566
567 if self.options.info:
568 self.print_metadata()
569 sys.exit(0)
570
571 # XXX should use action='append' instead of rolling our own
572 try:
573 self.addons = self.options.addons.split(',')
574 except:
575 self.addons = []
576
577 def get_metadata_from_egg(self):
578 import pkg_resources
579 ret = {}
580 dist = pkg_resources.get_distribution(self.module)
581 if dist.has_metadata("PKG-INFO"):
582 for line in dist.get_metadata_lines("PKG-INFO"):
583 key, value = line.split(':', 1)
584 ret[key] = value
585 if dist.has_metadata("requires.txt"):
586 ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
587 return ret
588
589 def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
590 "Author", "Author-email", "License", "Platfor m", "Dependencies")):
591 for key in data:
592 if key in self.metadata:
593 print key + ": " + self.metadata[key]
594
595 def create_runner(self):
596 """ Get the runner object """
597 runner = self.get_runner(binary=self.options.binary)
598 profile = self.get_profile(binary=runner.binary,
599 profile=self.options.profile,
600 addons=self.addons)
601 runner.profile = profile
602 return runner
603
604 def get_runner(self, binary=None, profile=None):
605 """Returns the runner instance for the given command line binary argumen t
606 the profile instance returned from self.get_profile()."""
607 return self.runner_class(binary, profile)
608
609 def get_profile(self, binary=None, profile=None, addons=None, preferences=No ne):
610 """Returns the profile instance for the given command line arguments."""
611 addons = addons or []
612 preferences = preferences or {}
613 return self.profile_class(binary, profile, addons, preferences)
614
615 def run(self):
616 runner = self.create_runner()
617 self.start(runner)
618 runner.profile.cleanup()
619
620 def start(self, runner):
621 """Starts the runner and waits for Firefox to exitor Keyboard Interrupt.
622 Shoule be overwritten to provide custom running of the runner instance." ""
623 runner.start()
624 print 'Started:', ' '.join(runner.command)
625 try:
626 runner.wait()
627 except KeyboardInterrupt:
628 runner.stop()
629
630
631 def cli():
632 CLI().run()
633
634 def print_addon_ids(args=sys.argv[1:]):
635 """print addon ids for testing"""
636 for arg in args:
637 print Profile.addon_id(arg)
638
639
OLDNEW
« no previous file with comments | « pylib/mozrunner/README ('k') | pylib/mozrunner/killableprocess.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698