Package tlib :: Package base :: Module WptHelper |
[hide private]
[frames] | no frames]

# Source Code for Module tlib.base.WptHelper

      1  import time
      2  from tlib.asset.wpt_batch_lib import wpt_batch_lib
      3  import xml.etree.ElementTree as elementTree
      4
      5  #Constants used for creating the results object
      6  SUMMARY = 'summary'                 #:Link to the summary report
      7  RUNS = 'runs'                       #:How many test runs were executed
      8  HEADERS_DATA = 'headers'            #:Link to raw headers data
      9  PAGE_DATA = 'pageData'              #:Link to raw page data
     10  REQUESTS_DATA = 'requestsData'      #:Link to raw requests data
     11  UTILIZATION_DATA = 'Utilization'    #:Link to raw utilization data
     12  PAGE_SPEED_DATA = 'pageSpeedData'   #:Link to raw page speed data
     13  FIRST_VIEW = 'firstView'            #:Results for first view (with empty cache)
     14  REPEAT_VIEW = 'repeatView'          #:Results for repeat view (with cache populated)
     15  AVERAGE = 'average'                 #:Response time average
     16  STD_DEV = 'standardDeviation'       #:Response time standard deviation
     17  MEDIAN = 'median'                   #:Response time median
     18  TTFB = 'TTFB'                       #:Time to first byte
     19  REQUESTS = 'requests'               #:Total number of requests
     20  REQUESTS_DOC = 'requestsDoc'        #:Number of requests before document is complete
     21  LOAD_TIME = 'loadTime'              #:Page load time until document is complete
     22  FULLY_LOADED = 'fullyLoaded'        #Page load time until there is no traffic for 2 seconds
     23
     24 -class WptHelper(object):
     25      """
     26      Helper class to send jobs to WebPagetest and get job results
     27      """
     28      _wpt_server = None
     29      _wpt_params = {}
     30      _logger = None
     31
     32 -    def __init__(self, logger, wpt_server, wpt_params):
     33          """
     34          Constructor for class
     35
     36          @param logger: instance of a logging object configured in testing project
     37          @type logger: logging.Logger
     38          @param wpt_server: URL of the WebPagetest server used for testing, e.g. http://172.27.108.148:8080
     39          @type wpt_server: str
     40          @param wpt_params: Dictionarry with test settings. See https://sites.google.com/a/webpagetest.org/docs/advanced-features/webpagetest-restful-apis for more details
     41          @type wpt_params: dict
     42          """
     43          logger.debug("Initializing WptOrchestrator")
     44          logger.debug("WebPagetest server: %s" % wpt_server)
     45          logger.debug("WebPagetest test parameters: %s" % wpt_params)
     46
     47          self._logger = logger
     48          self._wpt_server = wpt_server
     49          self._wpt_params = wpt_params
     50
     51 -    def test_urls_and_wait(self, requested_urls):
     52          """
     53          Submits a test request for a list of URLs and waits until results are returned
     54
     55          @param requested_urls: List of URLs to test
     56          @type requested_urls: list
     57          @return: Results as generated by WebPagetest
     58              For each item in the response, URL is the key and a dom object is the value
     59          @rtype: dict
     60          """
     61          self._logger.debug("Requested URLs\n%s" % requested_urls)
     62
     63          #Submit test request
     64          id_url_dict = wpt_batch_lib.SubmitBatch(requested_urls, self._wpt_params, self._wpt_server)
     65          self._logger.debug("SubmitBatch returned\n%s" % id_url_dict)
     66
     67          #Check all URLs were submitted to test server
     68          submitted_urls = set(id_url_dict.values())
     69          self._logger.debug("Submitted URLs\n%(submitted_urls)s" % {"submitted_urls": submitted_urls})
     70          for url in requested_urls:
     71              if url not in submitted_urls:
     72                  self._logger.error('URL submission failed: %(url)s' % {"url": url})
     73
     74          #Wait until all tests are completed
     75          pending_test_ids = id_url_dict.keys()
     76          results = []
     77          while pending_test_ids:
     78              id_status_dict = wpt_batch_lib.CheckBatchStatus(pending_test_ids, server_url=self._wpt_server)
     79
     80              #Get list of completed tests
     81              completed_test_ids = []
     82              for test_id, test_status in id_status_dict.iteritems():
     83                  # We could get 4 different status codes with different meanings as
     84                  # as follows:
     85                  # 1XX: Test in progress
     86                  # 200: Test complete
     87                  # 4XX: Test request not found
     88                  if int(test_status) >= 400:
     89                      self._logger.error('Tests failed with status %(test_id)s: %(test_status)s' % {'test_id':test_id,'test_status':test_status})
     90                      pending_test_ids.remove(test_id)
     91                      continue
     92
     93                  if int(test_status) == 200:
     94                      self._logger.debug("Test %(test_id)s completed, removing from list of pending tests" % {'test_id': test_id})
     95                      pending_test_ids.remove(test_id)
     96                      completed_test_ids.append(test_id)
     97
     98                  else:
     99                      self._logger.debug("Test %(test_id)s has not completed, status=%(test_status)s" % {'test_id':test_id,'test_status':test_status})
    100
    101              #Get results for completed tests
    102              test_results = wpt_batch_lib.GetXMLResult(completed_test_ids, server_url=self._wpt_server)
    103
    104              #Check we got one result for each of the completed tests
    105              result_test_ids = set(test_results.keys())
    106              for test_id in completed_test_ids:
    107                  if test_id not in result_test_ids:
    108                      self._logger.error('The XML failed to retrieve: %(test_id)s' % {'test_id': test_id})
    109
    110              for test_id, dom in test_results.iteritems():
    111                  self._logger.debug("Test id %(test_id)s result:\n%(xml)s" % {'test_id': test_id, 'xml': dom.toxml()})
    112                  test_result = self._get_test_results(dom.toxml())
    113                  results.append(test_result)
    114
    115              if pending_test_ids:
    116                  time.sleep(5)
    117
    118          return results
    119
    120 -    def test_script_and_wait(self, script):
    121          """
    122          Submits a test request with a script and waits until results are returned
    123
    124          @param script: script
    125          @type script: str
    126          @return: Results as generated by WebPagetest
    127              For each item in the response, URL is the key and a dom object is the value
    128          @rtype: dict
    129          """
    130          self._logger.debug("Script to execute\n%(script)s" % {"script": script})
    131          #Create a new instance of _wpt_params to not change the original parameters
    132          test_params = dict(self._wpt_params)
    133          try:
    134              self._wpt_params['script'] = script
    135              result = self.test_urls_and_wait([""])
    136          finally:
    137              #Restore WPT params
    138              self._wpt_params = dict(test_params)
    139
    140          return result
    141
    142 -    def test_url_and_wait(self, url):
    143          """
    144          Submits a test request for a URL and waits until results are returned
    145
    146          @param url: URLs to test
    147          @type url: str
    148          @return: Results as generated by WebPagetest
    149              For each item in the response, URL is the key and a dom object is the value
    150          @rtype: dict
    151          """
    152          self._logger.debug("URL to test\n%(url)s" % {"url": url})
    153          return self.test_urls_and_wait([url])
    154
    155 -    def _get_test_results(self, xml_text):
    156          """
    157          Extracts global test metrics from DOM object returned by WPT and returns them as a dictionary
    158
    159          @param xml_text: XML returned by WPT server
    160          @type xml_text: str
    161          @return: Returns a dictionary with the metrics extracted from XML
    162          @rtype: dict
    163          """
    164          root = elementTree.fromstring(xml_text)
    165
    166          result = dict()
    167          #Get data node
    168          result[SUMMARY] = root.find("./data/summary").text
    169
    170          #Number of test runs
    171          runs = int(root.find("./data/runs").text)
    172          result[RUNS] = runs
    173
    174          #Get fist view and last view aggregated metrics
    175          #It's possible that RepeatView doesn't exist depending on how test was ran
    176          for view in [FIRST_VIEW, REPEAT_VIEW]:
    177              #Get aggegated metrics
    178              for metric in [AVERAGE, STD_DEV, MEDIAN]:
    179                  view_node = root.find("./data/%(metric)s/%(view)s" % {'metric': metric, 'view': view})
    180                  #Check there is an element "view", otherwise skip
    181                  if view_node is not None:
    182                      result.setdefault(view, {}).setdefault(LOAD_TIME, {})
    183                      result[view][LOAD_TIME][metric] = int(view_node.find("./%(LOAD_TIME)s" % {'LOAD_TIME': LOAD_TIME}).text)
    184
    185                      result.setdefault(view, {}).setdefault(TTFB, {})
    186                      result[view][TTFB][metric] = int(view_node.find("./%(TTFB)s" % {'TTFB': TTFB}).text)
    187
    188                      result.setdefault(view, {}).setdefault(REQUESTS, {})
    189                      result[view][REQUESTS][metric] = int(view_node.find("./%(REQUESTS)s" % {'REQUESTS': REQUESTS}).text)
    190
    191                      result.setdefault(view, {}).setdefault(REQUESTS_DOC, {})
    192                      result[view][REQUESTS_DOC][metric] = int(view_node.find("./%(REQUESTS_DOC)s" % {'REQUESTS_DOC': REQUESTS_DOC}).text)
    193
    194                      result.setdefault(view, {}).setdefault(FULLY_LOADED, {})
    195                      result[view][FULLY_LOADED][metric] = int(view_node.find("./%(FULLY_LOADED)s" % {'FULLY_LOADED': FULLY_LOADED}).text)
    196
    197              #If element "view" doesn't exist, skip getting test runs for that "view"
    198              if view in result:
    199                  #Get results for each run
    200                  run_nodes = root.findall("./data/run")
    201                  for i in range(0, runs):
    202                      #Create missing keys or there will be an exception when assigning values
    203                      result[view].setdefault(RUNS, {}).setdefault(i, {})
    204
    205                      data_node = run_nodes[i].find("./%(view)s" % {'view': view})
    206                      #If there is no 'run'.'view' element, it means that test run failed for that view
    207                      if data_node is not None:
    208                          result[view][RUNS][i][HEADERS_DATA] = data_node.find('./rawData/headers').text
    209                          result[view][RUNS][i][PAGE_DATA] = data_node.find('./rawData/pageData').text
    210                          result[view][RUNS][i][REQUESTS_DATA] = data_node.find('./rawData/requestsData').text
    211                          result[view][RUNS][i][UTILIZATION_DATA] = data_node.find('./rawData/utilization').text
    212                          result[view][RUNS][i][PAGE_SPEED_DATA] = data_node.find('./rawData/PageSpeedData').text
    213                      else:
    214                          result[view][RUNS][i][HEADERS_DATA] = ''
    215                          result[view][RUNS][i][PAGE_DATA] = ''
    216                          result[view][RUNS][i][REQUESTS_DATA] = ''
    217                          result[view][RUNS][i][UTILIZATION_DATA] = ''
    218                          result[view][RUNS][i][PAGE_SPEED_DATA] = ''
    219
    220          self._logger.debug('Result:\n%(result)s' % {'result': result})
    221          return result
    222