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

Source Code for Module tlib.base.WptHelper

  1  import time 
  2  import wpt_batch_lib 
  3  import xml.etree.ElementTree as ET 
  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 result = {} 63 64 #Submit test request 65 id_url_dict = wpt_batch_lib.SubmitBatch(requested_urls, self._wpt_params, self._wpt_server) 66 self._logger.debug("SubmitBatch returned\n%s" % id_url_dict) 67 68 #Check all URLs were submitted to test server 69 submitted_urls = set(id_url_dict.values()) 70 self._logger.debug("Submitted URLs\n%(submitted_urls)s" % {"submitted_urls": submitted_urls}) 71 for url in requested_urls: 72 if url not in submitted_urls: 73 self._logger.error('URL submission failed: %(url)s' % {"url": url}) 74 75 #Wait until all tests are completed 76 pending_test_ids = id_url_dict.keys() 77 results = [] 78 while pending_test_ids: 79 id_status_dict = wpt_batch_lib.CheckBatchStatus(pending_test_ids, server_url=self._wpt_server) 80 81 #Get list of completed tests 82 completed_test_ids = [] 83 for test_id, test_status in id_status_dict.iteritems(): 84 # We could get 4 different status codes with different meanings as 85 # as follows: 86 # 1XX: Test in progress 87 # 200: Test complete 88 # 4XX: Test request not found 89 if int(test_status) >= 400: 90 self._logger.error('Tests failed with status %(test_id)s: %(test_status)s' % {'test_id':test_id,'test_status':test_status}) 91 pending_test_ids.remove(test_id) 92 continue 93 94 if int(test_status) == 200: 95 self._logger.debug("Test %(test_id)s completed, removing from list of pending tests" % {'test_id': test_id}) 96 pending_test_ids.remove(test_id) 97 completed_test_ids.append(test_id) 98 99 else: 100 self._logger.debug("Test %(test_id)s has not completed, status=%(test_status)s" % {'test_id':test_id,'test_status':test_status}) 101 102 #Get results for completed tests 103 test_results = wpt_batch_lib.GetXMLResult(completed_test_ids, server_url=self._wpt_server) 104 105 #Check we got one result for each of the completed tests 106 result_test_ids = set(test_results.keys()) 107 for test_id in completed_test_ids: 108 if test_id not in result_test_ids: 109 self._logger.error('The XML failed to retrieve: %(test_id)s' % {'test_id': test_id}) 110 111 for test_id, dom in test_results.iteritems(): 112 self._logger.debug("Test id %(test_id)s result:\n%(xml)s" % {'test_id': test_id, 'xml': dom.toxml()}) 113 test_result = self._get_test_results(dom.toxml()) 114 results.append(test_result) 115 116 if pending_test_ids: 117 time.sleep(5) 118 119 return results
120
121 - def test_script_and_wait(self, script):
122 """ 123 Submits a test request with a script and waits until results are returned 124 125 @param script: script 126 @type script: str 127 @return: Results as generated by WebPagetest 128 For each item in the response, URL is the key and a dom object is the value 129 @rtype: dict 130 """ 131 self._logger.debug("Script to execute\n%(script)s" % {"script": script}) 132 #Create a new instance of _wpt_params to not change the original parameters 133 result = {} 134 test_params = dict(self._wpt_params) 135 try: 136 self._wpt_params['script'] = script 137 result = self.test_urls_and_wait([""]) 138 finally: 139 #Restore WPT params 140 self._wpt_params = dict(test_params) 141 142 return result
143
144 - def test_url_and_wait(self, url):
145 """ 146 Submits a test request for a URL and waits until results are returned 147 148 @param url: URLs to test 149 @type url: str 150 @return: Results as generated by WebPagetest 151 For each item in the response, URL is the key and a dom object is the value 152 @rtype: dict 153 """ 154 self._logger.debug("URL to test\n%(url)s" % {"url": url}) 155 return self.test_urls_and_wait([url])
156
157 - def _get_test_results(self, xml_text):
158 """ 159 Extracts global test metrics from DOM object returned by WPT and returns them as a dictionary 160 161 @param xml_text: XML returned by WPT server 162 @type xml_text: str 163 @return: Returns a dictionary with the metrics extracted from XML 164 @rtype: dict 165 """ 166 root = ET.fromstring(xml_text) 167 node = root.find("./data/summary") 168 169 170 result = {} 171 #Get data node 172 result[SUMMARY] = root.find("./data/summary").text 173 174 #Number of test runs 175 runs = int(root.find("./data/runs").text) 176 result[RUNS] = runs 177 178 #Get fist view and last view aggregated metrics 179 #It's possible that RepeatView doesn't exist depending on how test was ran 180 for view in [FIRST_VIEW, REPEAT_VIEW]: 181 #Get aggegated metrics 182 for metric in [AVERAGE, STD_DEV, MEDIAN]: 183 view_node = root.find("./data/%(metric)s/%(view)s" % {'metric': metric, 'view': view}) 184 #Check there is an element "view", otherwise skip 185 if view_node is not None: 186 result.setdefault(view, {}).setdefault(LOAD_TIME, {}) 187 result[view][LOAD_TIME][metric] = int(view_node.find("./%(LOAD_TIME)s" % {'LOAD_TIME': LOAD_TIME}).text) 188 189 result.setdefault(view, {}).setdefault(TTFB, {}) 190 result[view][TTFB][metric] = int(view_node.find("./%(TTFB)s" % {'TTFB': TTFB}).text) 191 192 result.setdefault(view, {}).setdefault(REQUESTS, {}) 193 result[view][REQUESTS][metric] = int(view_node.find("./%(REQUESTS)s" % {'REQUESTS': REQUESTS}).text) 194 195 result.setdefault(view, {}).setdefault(REQUESTS_DOC, {}) 196 result[view][REQUESTS_DOC][metric] = int(view_node.find("./%(REQUESTS_DOC)s" % {'REQUESTS_DOC': REQUESTS_DOC}).text) 197 198 result.setdefault(view, {}).setdefault(FULLY_LOADED, {}) 199 result[view][FULLY_LOADED][metric] = int(view_node.find("./%(FULLY_LOADED)s" % {'FULLY_LOADED': FULLY_LOADED}).text) 200 201 #If element "view" doesn't exist, skip getting test runs for that "view" 202 if view in result: 203 #Get results for each run 204 run_nodes = root.findall("./data/run") 205 for i in range(0, runs): 206 #Create missing keys or there will be an exception when assigning values 207 result[view].setdefault(RUNS, {}).setdefault(i, {}) 208 209 data_node = run_nodes[i].find("./%(view)s" % {'view': view}) 210 #If there is no 'run'.'view' element, it means that test run failed for that view 211 if data_node is not None: 212 result[view][RUNS][i][HEADERS_DATA] = data_node.find('./rawData/headers').text 213 result[view][RUNS][i][PAGE_DATA] = data_node.find('./rawData/pageData').text 214 result[view][RUNS][i][REQUESTS_DATA] = data_node.find('./rawData/requestsData').text 215 result[view][RUNS][i][UTILIZATION_DATA] = data_node.find('./rawData/utilization').text 216 result[view][RUNS][i][PAGE_SPEED_DATA] = data_node.find('./rawData/PageSpeedData').text 217 else: 218 result[view][RUNS][i][HEADERS_DATA] = '' 219 result[view][RUNS][i][PAGE_DATA] = '' 220 result[view][RUNS][i][REQUESTS_DATA] = '' 221 result[view][RUNS][i][UTILIZATION_DATA] = '' 222 result[view][RUNS][i][PAGE_SPEED_DATA] = '' 223 224 self._logger.debug('Result:\n%(result)s' % {'result': result}) 225 return result
226