def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb from internal.ios_device import iOSDevice self.must_exit = False self.options = options self.capture_display = None self.job = None self.task = None self.xvfb = None self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.persistent_work_dir = self.wpt.get_persistent_dir() self.adb = Adb( self.options, self.persistent_work_dir) if self.options.android else None self.ios = iOSDevice(self.options.device) if self.options.iOS else None self.browsers = Browsers(options, browsers, self.adb, self.ios) self.shaper = TrafficShaper(options) atexit.register(self.cleanup) signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) self.image_magick = { 'convert': 'convert', 'compare': 'compare', 'mogrify': 'mogrify' } if platform.system() == "Windows": paths = [os.getenv('ProgramFiles'), os.getenv('ProgramFiles(x86)')] for path in paths: if path is not None and os.path.isdir(path): dirs = sorted(os.listdir(path), reverse=True) for subdir in dirs: if subdir.lower().startswith('imagemagick'): convert = os.path.join(path, subdir, 'convert.exe') compare = os.path.join(path, subdir, 'compare.exe') mogrify = os.path.join(path, subdir, 'mogrify.exe') if os.path.isfile(convert) and \ os.path.isfile(compare) and \ os.path.isfile(mogrify): if convert.find(' ') >= 0: convert = '"{0}"'.format(convert) if compare.find(' ') >= 0: compare = '"{0}"'.format(compare) if mogrify.find(' ') >= 0: mogrify = '"{0}"'.format(mogrify) self.image_magick['convert'] = convert self.image_magick['compare'] = compare self.image_magick['mogrify'] = mogrify break
def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb self.must_exit = False self.options = options self.adb = Adb(self.options) if self.options.android else None self.browsers = Browsers(options, browsers, self.adb) self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.shaper = TrafficShaper(options) self.job = None self.task = None self.xvfb = None atexit.register(self.cleanup) signal.signal(signal.SIGINT, self.signal_handler)
def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb from internal.ios_device import iOSDevice self.must_exit = False self.options = options self.capture_display = None self.job = None self.task = None self.xvfb = None self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.persistent_work_dir = self.wpt.get_persistent_dir() self.adb = Adb( self.options, self.persistent_work_dir) if self.options.android else None self.ios = iOSDevice(self.options.device) if self.options.iOS else None self.browsers = Browsers(options, browsers, self.adb, self.ios) self.shaper = TrafficShaper(options) atexit.register(self.cleanup) signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler)
class WPTAgent(object): """Main agent workflow""" def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb from internal.ios_device import iOSDevice self.must_exit = False self.options = options self.capture_display = None self.job = None self.task = None self.xvfb = None self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.persistent_work_dir = self.wpt.get_persistent_dir() self.adb = Adb(self.options, self.persistent_work_dir) if self.options.android else None self.ios = iOSDevice(self.options.device) if self.options.iOS else None self.browsers = Browsers(options, browsers, self.adb, self.ios) self.shaper = TrafficShaper(options) atexit.register(self.cleanup) signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) self.image_magick = {'convert': 'convert', 'compare': 'compare', 'mogrify': 'mogrify'} if platform.system() == "Windows": paths = [os.getenv('ProgramFiles'), os.getenv('ProgramFiles(x86)')] for path in paths: if path is not None and os.path.isdir(path): dirs = sorted(os.listdir(path), reverse=True) for subdir in dirs: if subdir.lower().startswith('imagemagick'): convert = os.path.join(path, subdir, 'convert.exe') compare = os.path.join(path, subdir, 'compare.exe') mogrify = os.path.join(path, subdir, 'mogrify.exe') if os.path.isfile(convert) and \ os.path.isfile(compare) and \ os.path.isfile(mogrify): if convert.find(' ') >= 0: convert = '"{0}"'.format(convert) if compare.find(' ') >= 0: compare = '"{0}"'.format(compare) if mogrify.find(' ') >= 0: mogrify = '"{0}"'.format(mogrify) self.image_magick['convert'] = convert self.image_magick['compare'] = compare self.image_magick['mogrify'] = mogrify break convert = os.path.join(path, subdir, 'magick.exe') compare = os.path.join(path, subdir, 'magick.exe') mogrify = os.path.join(path, subdir, 'magick.exe') if os.path.isfile(convert) and \ os.path.isfile(compare) and \ os.path.isfile(mogrify): if convert.find(' ') >= 0: convert = '"{0}" convert'.format(convert) if compare.find(' ') >= 0: compare = '"{0}" compare'.format(compare) if mogrify.find(' ') >= 0: mogrify = '"{0}" mogrify'.format(mogrify) self.image_magick['convert'] = convert self.image_magick['compare'] = compare self.image_magick['mogrify'] = mogrify break def run_testing(self): """Main testing flow""" import monotonic start_time = monotonic.monotonic() browser = None exit_file = os.path.join(self.root_path, 'exit') message_server = None if not self.options.android and not self.options.iOS: from internal.message_server import MessageServer message_server = MessageServer() message_server.start() if not message_server.is_ok(): logging.error("Unable to start the local message server") return while not self.must_exit: try: self.alive() if os.path.isfile(exit_file): try: os.remove(exit_file) except Exception: pass self.must_exit = True break if message_server is not None and self.options.exit > 0 and \ not message_server.is_ok(): logging.error("Message server not responding, exiting") break if self.browsers.is_ready(): self.job = self.wpt.get_test(self.browsers.browsers) if self.job is not None: self.job['image_magick'] = self.image_magick self.job['message_server'] = message_server self.job['capture_display'] = self.capture_display self.job['shaper'] = self.shaper self.task = self.wpt.get_task(self.job) while self.task is not None: start = monotonic.monotonic() try: self.task['running_lighthouse'] = False if self.job['type'] != 'lighthouse': self.run_single_test() self.wpt.get_bodies(self.task) if self.task['run'] == 1 and not self.task['cached'] and \ self.job['warmup'] <= 0 and \ self.task['error'] is None and \ 'lighthouse' in self.job and self.job['lighthouse']: if 'page_result' not in self.task or \ self.task['page_result'] is None or \ self.task['page_result'] == 0 or \ self.task['page_result'] == 99999: self.task['running_lighthouse'] = True self.wpt.running_another_test(self.task) self.run_single_test() elapsed = monotonic.monotonic() - start logging.debug('Test run time: %0.3f sec', elapsed) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception running test: '\ '{0}'.format(msg) logging.exception("Unhandled exception running test: %s", msg) traceback.print_exc(file=sys.stdout) self.wpt.upload_task_result(self.task) # Set up for the next run self.task = self.wpt.get_task(self.job) if self.job is not None: self.job = None else: self.sleep(self.options.polling) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() if self.task is not None: self.task['error'] = 'Unhandled exception preparing test: '\ '{0}'.format(msg) logging.exception("Unhandled exception: %s", msg) traceback.print_exc(file=sys.stdout) if browser is not None: browser.on_stop_capture(None) browser.on_stop_recording(None) browser = None if self.options.exit > 0: run_time = (monotonic.monotonic() - start_time) / 60.0 if run_time > self.options.exit: break # Exit if adb is having issues (will cause a reboot after several tries) if self.adb is not None and self.adb.needs_exit: break self.cleanup() def run_single_test(self): """Run a single test run""" self.alive() browser = self.browsers.get_browser(self.job['browser'], self.job) if browser is not None: browser.prepare(self.job, self.task) browser.launch(self.job, self.task) try: if self.task['running_lighthouse']: self.task['lighthouse_log'] = \ 'Lighthouse testing is not supported with this browser.' try: browser.run_lighthouse_test(self.task) except Exception: pass if self.task['lighthouse_log']: log_file = os.path.join(self.task['dir'], 'lighthouse.log.gz') with gzip.open(log_file, 'wb', 7) as f_out: f_out.write(self.task['lighthouse_log']) else: browser.run_task(self.task) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception in test run: '\ '{0}'.format(msg) logging.exception("Unhandled exception in test run: %s", msg) traceback.print_exc(file=sys.stdout) browser.stop(self.job, self.task) # Delete the browser profile if needed if self.task['cached'] or self.job['fvonly']: browser.clear_profile(self.task) else: err = "Invalid browser - {0}".format(self.job['browser']) logging.critical(err) self.task['error'] = err browser = None def signal_handler(self, *_): """Ctrl+C handler""" if self.must_exit: exit(1) if self.job is None: print "Exiting..." else: print "Will exit after test completes. Hit Ctrl+C again to exit immediately" self.must_exit = True def cleanup(self): """Do any cleanup that needs to be run regardless of how we exit""" logging.debug('Cleaning up') self.shaper.remove() if self.xvfb is not None: self.xvfb.stop() if self.adb is not None: self.adb.stop() if self.ios is not None: self.ios.disconnect() def sleep(self, seconds): """Sleep wrapped in an exception handler to properly deal with Ctrl+C""" try: time.sleep(seconds) except IOError: pass def wait_for_idle(self, timeout=30): """Wait for the system to go idle for at least 2 seconds""" import monotonic import psutil logging.debug("Waiting for Idle...") cpu_count = psutil.cpu_count() if cpu_count > 0: target_pct = 50. / float(cpu_count) idle_start = None end_time = monotonic.monotonic() + timeout idle = False while not idle and monotonic.monotonic() < end_time: self.alive() check_start = monotonic.monotonic() pct = psutil.cpu_percent(interval=0.5) if pct <= target_pct: if idle_start is None: idle_start = check_start if monotonic.monotonic() - idle_start > 2: idle = True else: idle_start = None def alive(self): """Touch a watchdog file indicating we are still alive""" if self.options.alive: with open(self.options.alive, 'a'): os.utime(self.options.alive, None) def requires(self, module, module_name=None): """Try importing a module and installing it if it isn't available""" ret = False if module_name is None: module_name = module try: __import__(module) ret = True except ImportError: pass if not ret: from internal.os_util import run_elevated logging.debug('Trying to install %s...', module_name) subprocess.call([sys.executable, '-m', 'pip', 'uninstall', '-y', module_name]) run_elevated(sys.executable, '-m pip uninstall -y {0}'.format(module_name)) subprocess.call([sys.executable, '-m', 'pip', 'install', module_name]) run_elevated(sys.executable, '-m pip install {0}'.format(module_name)) try: __import__(module) ret = True except ImportError: pass if not ret: print "Missing {0} module. Please run 'pip install {1}'".format(module, module_name) return ret def startup(self): """Validate that all of the external dependencies are installed""" ret = True # default /tmp/wptagent as an alive file on Linux if self.options.alive is None: if platform.system() == "Linux": self.options.alive = '/tmp/wptagent' else: self.options.alive = os.path.join(os.path.dirname(__file__), 'wptagent.alive') self.alive() ret = self.requires('dns', 'dnspython') and ret ret = self.requires('monotonic') and ret ret = self.requires('PIL', 'pillow') and ret ret = self.requires('psutil') and ret ret = self.requires('requests') and ret if not self.options.android and not self.options.iOS: ret = self.requires('tornado') and ret # Windows-specific imports if platform.system() == "Windows": ret = self.requires('win32api', 'pywin32') and ret # Optional imports self.requires('brotli') self.requires('fontTools', 'fonttools') # Try patching ws4py with a faster lib try: self.requires('wsaccel') import wsaccel wsaccel.patch_ws4py() except Exception: pass try: subprocess.check_output(['python', '--version']) except Exception: print "Make sure python 2.7 is available in the path." ret = False try: subprocess.check_output('{0} -version'.format(self.image_magick['convert']), shell=True) except Exception: print "Missing convert utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False try: subprocess.check_output('{0} -version'.format(self.image_magick['mogrify']), shell=True) except Exception: print "Missing mogrify utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False if platform.system() == "Linux": try: subprocess.check_output(['traceroute', '--version']) except Exception: logging.debug("Traceroute is missing, installing...") subprocess.call(['sudo', 'apt-get', '-yq', 'install', 'traceroute']) # If we are on Linux and there is no display, enable xvfb by default if platform.system() == "Linux" and not self.options.android and \ not self.options.iOS and 'DISPLAY' not in os.environ: self.options.xvfb = True if self.options.xvfb: ret = self.requires('xvfbwrapper') and ret if ret: from xvfbwrapper import Xvfb self.xvfb = Xvfb(width=1920, height=1200, colordepth=24) self.xvfb.start() # Figure out which display to capture from if platform.system() == "Linux" and 'DISPLAY' in os.environ: logging.debug('Display: %s', os.environ['DISPLAY']) self.capture_display = os.environ['DISPLAY'] elif platform.system() == "Darwin": proc = subprocess.Popen('ffmpeg -f avfoundation -list_devices true -i ""', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) _, err = proc.communicate() for line in err.splitlines(): matches = re.search(r'\[(\d+)\] Capture screen', line) if matches: self.capture_display = matches.group(1) break elif platform.system() == "Windows": self.capture_display = 'desktop' if self.options.throttle: try: subprocess.check_output('sudo cgset -h', shell=True) except Exception: print "Missing cgroups, make sure cgroup-tools is installed." ret = False # Fix Lighthouse install permissions if platform.system() != "Windows": from internal.os_util import run_elevated run_elevated('chmod', '-R 777 ~/.config/configstore/') try: import getpass run_elevated('chown', '-R {0}:{0} ~/.config'.format(getpass.getuser())) except Exception: pass # Check for Node 10+ if self.get_node_version() < 10.0: if platform.system() == "Linux": # This only works on debian-based systems logging.debug('Updating Node.js to 10.x') subprocess.call('curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -', shell=True) subprocess.call(['sudo', 'apt-get', 'install', '-y', 'nodejs']) if self.get_node_version() < 10.0: logging.warning("Node.js 10 or newer is required for Lighthouse testing") # Check the iOS install if self.ios is not None: ret = self.ios.check_install() if not self.options.android and not self.options.iOS and not self.options.noidle: self.wait_for_idle(300) if self.adb is not None: if not self.adb.start(): print "Error configuring adb. Make sure it is installed and in the path." ret = False self.shaper.remove() if not self.shaper.install(): if platform.system() == "Windows": print "Error configuring traffic shaping, make sure secure boot is disabled." else: print "Error configuring traffic shaping, make sure it is installed." ret = False # Update the Windows root certs if platform.system() == "Windows": self.update_windows_certificates() return ret def get_node_version(self): """Get the installed version of Node.js""" version = 0 try: stdout = subprocess.check_output(['node', '--version']) matches = re.match(r'^v(\d+\.\d+)', stdout) if matches: version = float(matches.group(1)) except Exception: pass return version def update_windows_certificates(self): """ Update the root Windows certificates""" try: cert_file = os.path.join(self.persistent_work_dir, 'root_certs.sst') if not os.path.isdir(self.persistent_work_dir): os.makedirs(self.persistent_work_dir) needs_update = True if os.path.isfile(cert_file): days = (time.time() - os.path.getmtime(cert_file)) / 86400 if days < 5: needs_update = False if needs_update: logging.debug("Updating Windows root certificates...") if os.path.isfile(cert_file): os.unlink(cert_file) from internal.os_util import run_elevated run_elevated('certutil.exe', '-generateSSTFromWU "{0}"'.format(cert_file)) if os.path.isfile(cert_file): run_elevated('certutil.exe', '-addstore -f Root "{0}"'.format(cert_file)) except Exception: pass
class WPTAgent(object): """Main agent workflow""" def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb self.must_exit = False self.options = options self.adb = Adb(self.options) if self.options.android else None self.browsers = Browsers(options, browsers, self.adb) self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.shaper = TrafficShaper(options) self.job = None self.task = None self.xvfb = None atexit.register(self.cleanup) signal.signal(signal.SIGINT, self.signal_handler) def run_testing(self): """Main testing flow""" import monotonic start_time = monotonic.monotonic() browser = None while not self.must_exit: try: if self.browsers.is_ready(): self.job = self.wpt.get_test() if self.job is not None: self.task = self.wpt.get_task(self.job) while self.task is not None: start = monotonic.monotonic() try: self.task['running_lighthouse'] = False if self.job['type'] != 'lighthouse': self.run_single_test() if self.task['run'] == 1 and not self.task['cached'] and \ 'lighthouse' in self.job and self.job['lighthouse']: self.task['running_lighthouse'] = True self.run_single_test() elapsed = monotonic.monotonic() - start logging.debug('Test run time: %0.3f sec', elapsed) except Exception as err: msg = '' if err is not None and err.__str__( ) is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception running test: '\ '{0}'.format(msg) logging.critical( "Unhandled exception running test: %s", msg) traceback.print_exc(file=sys.stdout) self.wpt.upload_task_result(self.task) # Set up for the next run self.task = self.wpt.get_task(self.job) if self.job is not None: self.job = None else: self.sleep(5) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() if self.task is not None: self.task['error'] = 'Unhandled exception preparing test: '\ '{0}'.format(msg) logging.critical("Unhandled exception: %s", msg) traceback.print_exc(file=sys.stdout) if browser is not None: browser.on_stop_recording(None) browser = None if self.options.exit > 0: run_time = (monotonic.monotonic() - start_time) / 60.0 if run_time > self.options.exit: break def run_single_test(self): """Run a single test run""" browser = self.browsers.get_browser(self.job['browser'], self.job) if browser is not None: browser.prepare(self.job, self.task) browser.launch(self.job, self.task) if self.shaper.configure(self.job): try: if self.task['running_lighthouse']: browser.run_lighthouse_test(self.task) else: browser.run_task(self.task) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception in test run: '\ '{0}'.format(msg) logging.critical("Unhandled exception in test run: %s", msg) traceback.print_exc(file=sys.stdout) else: self.task['error'] = "Error configuring traffic-shaping" self.shaper.reset() browser.stop(self.job, self.task) else: err = "Invalid browser - {0}".format(self.job['browser']) logging.critical(err) self.task['error'] = err # Delete the browser profile if needed if self.task['cached'] or self.job['fvonly']: browser.clear_profile(self.task) browser = None def signal_handler(self, *_): """Ctrl+C handler""" if self.must_exit: exit(1) if self.job is None: print "Exiting..." else: print "Will exit after test completes. Hit Ctrl+C again to exit immediately" self.must_exit = True def cleanup(self): """Do any cleanup that needs to be run regardless of how we exit.""" logging.debug('Cleaning up') self.shaper.remove() if self.xvfb is not None: self.xvfb.stop() def sleep(self, seconds): """Sleep wrapped in an exception handler to properly deal with Ctrl+C""" try: time.sleep(seconds) except IOError: pass def wait_for_idle(self, timeout=30): """Wait for the system to go idle""" import monotonic import psutil logging.debug("Waiting for Idle...") cpu_count = psutil.cpu_count() if cpu_count > 0: target_pct = 20. / float(cpu_count) idle_start = None end_time = monotonic.monotonic() + timeout idle = False while not idle and monotonic.monotonic() < end_time: check_start = monotonic.monotonic() pct = psutil.cpu_percent(interval=1) if pct <= target_pct: if idle_start is None: idle_start = check_start if monotonic.monotonic() - idle_start > 2: idle = True else: idle_start = None def startup(self): """Validate that all of the external dependencies are installed""" ret = True try: import dns.resolver as _ except ImportError: print "Missing dns module. Please run 'pip install dnspython'" ret = False try: import monotonic as _ except ImportError: print "Missing monotonic module. Please run 'pip install monotonic'" ret = False try: from PIL import Image as _ except ImportError: print "Missing PIL module. Please run 'pip install pillow'" ret = False try: import psutil as _ except ImportError: print "Missing psutil module. Please run 'pip install psutil'" ret = False try: import requests as _ except ImportError: print "Missing requests module. Please run 'pip install requests'" ret = False try: import ujson as _ except ImportError: print "Missing ujson parser. Please run 'pip install ujson'" ret = False try: subprocess.check_output(['python', '--version']) except Exception: print "Make sure python 2.7 is available in the path." ret = False try: subprocess.check_output('convert -version', shell=True) except Exception: print "Missing convert utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False try: subprocess.check_output('mogrify -version', shell=True) except Exception: print "Missing mogrify utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False if self.options.xvfb: try: from xvfbwrapper import Xvfb self.xvfb = Xvfb(width=1920, height=1200, colordepth=24) self.xvfb.start() except ImportError: print "Missing xvfbwrapper module. Please run 'pip install xvfbwrapper'" ret = False # Windows-specific imports if platform.system() == "Windows": try: import win32api as _ import win32process as _ except ImportError: print "Missing pywin32 module. Please run 'python -m pip install pypiwin32'" ret = False if not self.options.android: self.wait_for_idle(300) self.shaper.remove() if not self.shaper.install(): print "Error configuring traffic shaping, make sure it is installed." ret = False if self.adb is not None: if not self.adb.start(): print "Error configuring adb. Make sure it is installed and in the path." ret = False return ret
class WPTAgent(object): """Main agent workflow""" def __init__(self, options, browsers): from internal.browsers import Browsers from internal.webpagetest import WebPageTest from internal.traffic_shaping import TrafficShaper from internal.adb import Adb from internal.ios_device import iOSDevice self.must_exit = False self.options = options self.capture_display = None self.job = None self.task = None self.xvfb = None self.root_path = os.path.abspath(os.path.dirname(__file__)) self.wpt = WebPageTest(options, os.path.join(self.root_path, "work")) self.persistent_work_dir = self.wpt.get_persistent_dir() self.adb = Adb( self.options, self.persistent_work_dir) if self.options.android else None self.ios = iOSDevice(self.options.device) if self.options.iOS else None self.browsers = Browsers(options, browsers, self.adb, self.ios) self.shaper = TrafficShaper(options) atexit.register(self.cleanup) signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) def run_testing(self): """Main testing flow""" import monotonic start_time = monotonic.monotonic() browser = None exit_file = os.path.join(self.root_path, 'exit') message_server = None if not self.options.android and not self.options.iOS: message_server = MessageServer() message_server.start() while not self.must_exit: try: if os.path.isfile(exit_file): try: os.remove(exit_file) except Exception: pass self.must_exit = True break if message_server is not None and self.options.exit > 0 and \ not message_server.is_ok(): logging.error("Message server not responding, exiting") break if self.browsers.is_ready(): self.job = self.wpt.get_test() if self.job is not None: self.job['message_server'] = message_server self.job['capture_display'] = self.capture_display self.task = self.wpt.get_task(self.job) while self.task is not None: start = monotonic.monotonic() try: self.task['running_lighthouse'] = False if self.job['type'] != 'lighthouse': self.run_single_test() if self.task['run'] == 1 and not self.task['cached'] and \ self.task['error'] is None and \ 'lighthouse' in self.job and self.job['lighthouse']: if 'page_result' not in self.task or \ self.task['page_result'] is None or \ self.task['page_result'] == 0 or \ self.task['page_result'] == 99999: self.task['running_lighthouse'] = True self.wpt.running_another_test( self.task) self.run_single_test() elapsed = monotonic.monotonic() - start logging.debug('Test run time: %0.3f sec', elapsed) except Exception as err: msg = '' if err is not None and err.__str__( ) is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception running test: '\ '{0}'.format(msg) logging.exception( "Unhandled exception running test: %s", msg) traceback.print_exc(file=sys.stdout) self.wpt.upload_task_result(self.task) # Set up for the next run self.task = self.wpt.get_task(self.job) if self.job is not None: self.job = None else: self.sleep(self.options.polling) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() if self.task is not None: self.task['error'] = 'Unhandled exception preparing test: '\ '{0}'.format(msg) logging.exception("Unhandled exception: %s", msg) traceback.print_exc(file=sys.stdout) if browser is not None: browser.on_stop_recording(None) browser = None if self.options.exit > 0: run_time = (monotonic.monotonic() - start_time) / 60.0 if run_time > self.options.exit: break def run_single_test(self): """Run a single test run""" self.alive() browser = self.browsers.get_browser(self.job['browser'], self.job) if browser is not None: browser.prepare(self.job, self.task) browser.launch(self.job, self.task) if self.shaper.configure(self.job): try: if self.task['running_lighthouse']: browser.run_lighthouse_test(self.task) else: browser.run_task(self.task) except Exception as err: msg = '' if err is not None and err.__str__() is not None: msg = err.__str__() self.task['error'] = 'Unhandled exception in test run: '\ '{0}'.format(msg) logging.exception("Unhandled exception in test run: %s", msg) traceback.print_exc(file=sys.stdout) else: self.task['error'] = "Error configuring traffic-shaping" self.shaper.reset() browser.stop(self.job, self.task) # Delete the browser profile if needed if self.task['cached'] or self.job['fvonly']: browser.clear_profile(self.task) else: err = "Invalid browser - {0}".format(self.job['browser']) logging.critical(err) self.task['error'] = err browser = None def signal_handler(self, *_): """Ctrl+C handler""" if self.must_exit: exit(1) if self.job is None: print "Exiting..." else: print "Will exit after test completes. Hit Ctrl+C again to exit immediately" self.must_exit = True def cleanup(self): """Do any cleanup that needs to be run regardless of how we exit.""" logging.debug('Cleaning up') self.shaper.remove() if self.xvfb is not None: self.xvfb.stop() if self.adb is not None: self.adb.stop() if self.ios is not None: self.ios.disconnect() def sleep(self, seconds): """Sleep wrapped in an exception handler to properly deal with Ctrl+C""" try: time.sleep(seconds) except IOError: pass def wait_for_idle(self, timeout=30): """Wait for the system to go idle for at least 2 seconds""" import monotonic import psutil logging.debug("Waiting for Idle...") cpu_count = psutil.cpu_count() if cpu_count > 0: target_pct = 50. / float(cpu_count) idle_start = None end_time = monotonic.monotonic() + timeout idle = False while not idle and monotonic.monotonic() < end_time: self.alive() check_start = monotonic.monotonic() pct = psutil.cpu_percent(interval=0.5) if pct <= target_pct: if idle_start is None: idle_start = check_start if monotonic.monotonic() - idle_start > 2: idle = True else: idle_start = None def alive(self): """Touch a watchdog file indicating we are still alive""" if self.options.alive: with open(self.options.alive, 'a'): os.utime(self.options.alive, None) def startup(self): """Validate that all of the external dependencies are installed""" ret = True self.alive() try: import dns.resolver as _ except ImportError: print "Missing dns module. Please run 'pip install dnspython'" ret = False try: import monotonic as _ except ImportError: print "Missing monotonic module. Please run 'pip install monotonic'" ret = False try: from PIL import Image as _ except ImportError: print "Missing PIL module. Please run 'pip install pillow'" ret = False try: import psutil as _ except ImportError: print "Missing psutil module. Please run 'pip install psutil'" ret = False try: import requests as _ except ImportError: print "Missing requests module. Please run 'pip install requests'" ret = False try: subprocess.check_output(['python', '--version']) except Exception: print "Make sure python 2.7 is available in the path." ret = False try: subprocess.check_output('convert -version', shell=True) except Exception: print "Missing convert utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False try: subprocess.check_output('mogrify -version', shell=True) except Exception: print "Missing mogrify utility. Please install ImageMagick " \ "and make sure it is in the path." ret = False if platform.system() == "Linux": try: subprocess.check_output(['traceroute', '--version']) except Exception: logging.debug("Traceroute is missing, installing...") subprocess.call( ['sudo', 'apt-get', '-yq', 'install', 'traceroute']) # if we are on Linux and there is no display, enable xvfb by default if platform.system() == "Linux" and not self.options.android and \ not self.options.iOS and 'DISPLAY' not in os.environ: self.options.xvfb = True if self.options.xvfb: try: from xvfbwrapper import Xvfb self.xvfb = Xvfb(width=1920, height=1200, colordepth=24) self.xvfb.start() except ImportError: print "Missing xvfbwrapper module. Please run 'pip install xvfbwrapper'" ret = False # Figure out which display to capture from if platform.system() == "Linux" and 'DISPLAY' in os.environ: logging.debug('Display: %s', os.environ['DISPLAY']) self.capture_display = os.environ['DISPLAY'] elif platform.system() == "Darwin": proc = subprocess.Popen( 'ffmpeg -f avfoundation -list_devices true -i ""', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) _, err = proc.communicate() for line in err.splitlines(): matches = re.search(r'\[(\d+)\] Capture screen', line) if matches: self.capture_display = matches.group(1) break elif platform.system() == "Windows": self.capture_display = 'desktop' if self.options.throttle: try: subprocess.check_output('sudo cgset -h', shell=True) except Exception: print "Missing cgroups, make sure cgroup-tools is installed." ret = False # Windows-specific imports if platform.system() == "Windows": try: import win32api as _ import win32process as _ except ImportError: print "Missing pywin32 module. Please run 'python -m pip install pypiwin32'" ret = False # Check the iOS install if self.ios is not None: ret = self.ios.check_install() if not self.options.android and not self.options.iOS: self.wait_for_idle(300) self.shaper.remove() if not self.shaper.install(): if platform.system() == "Windows": print "Error configuring traffic shaping, make sure secure boot is disabled." else: print "Error configuring traffic shaping, make sure it is installed." ret = False if self.adb is not None: if not self.adb.start(): print "Error configuring adb. Make sure it is installed and in the path." ret = False return ret