1 # Copyright (C) 2010 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the name of Google Inc. nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 import time 30 import logging 31 import re 32 import urllib 33 import webapp2 34 35 from google.appengine.api import users 36 from google.appengine.ext.webapp import template 37 from google.appengine.ext import db 38 39 from model.jsonresults import JsonResults 40 from model.testfile import TestFile 41 42 PARAM_MASTER = "master" 43 PARAM_BUILDER = "builder" 44 PARAM_DIR = "dir" 45 PARAM_FILE = "file" 46 PARAM_NAME = "name" 47 PARAM_BEFORE = "before" 48 PARAM_NUM_FILES = "numfiles" 49 PARAM_KEY = "key" 50 PARAM_TEST_TYPE = "testtype" 51 PARAM_TEST_LIST_JSON = "testlistjson" 52 PARAM_CALLBACK = "callback" 53 54 55 def _replace_jsonp_callback(json, callback_name): 56 if callback_name and re.search(r"^[A-Za-z0-9_]+$", callback_name): 57 if re.search(r"^[A-Za-z0-9_]+[(]", json): 58 return re.sub(r"^[A-Za-z0-9_]+[(]", callback_name + "(", json) 59 return callback_name + "(" + json + ")" 60 61 return json 62 63 64 class DeleteFile(webapp2.RequestHandler): 65 """Delete test file for a given builder and name from datastore.""" 66 67 def get(self): 68 key = self.request.get(PARAM_KEY) 69 master = self.request.get(PARAM_MASTER) 70 builder = self.request.get(PARAM_BUILDER) 71 test_type = self.request.get(PARAM_TEST_TYPE) 72 name = self.request.get(PARAM_NAME) 73 num_files = self.request.get(PARAM_NUM_FILES) 74 before = self.request.get(PARAM_BEFORE) 75 76 logging.debug( 77 "Deleting File, master: %s, builder: %s, test_type: %s, name: %s, before: %s, key: %s.", 78 master, builder, test_type, name, before, key) 79 80 limit = int(num_files) if num_files else 1 81 num_deleted = TestFile.delete_file(key, master, builder, test_type, name, before, limit) 82 83 self.response.set_status(200) 84 self.response.out.write("Deleted %d files." % num_deleted) 85 86 87 class GetFile(webapp2.RequestHandler): 88 """Get file content or list of files for given builder and name.""" 89 90 def _get_file_list(self, master, builder, test_type, name, before, limit, callback_name=None): 91 """Get and display a list of files that matches builder and file name. 92 93 Args: 94 builder: builder name 95 test_type: type of the test 96 name: file name 97 """ 98 99 files = TestFile.get_files( 100 master, builder, test_type, name, before, load_data=False, limit=limit) 101 if not files: 102 logging.info("File not found, master: %s, builder: %s, test_type: %s, name: %s.", 103 master, builder, test_type, name) 104 self.response.out.write("File not found") 105 return 106 107 template_values = { 108 "admin": users.is_current_user_admin(), 109 "master": master, 110 "builder": builder, 111 "test_type": test_type, 112 "name": name, 113 "files": files, 114 } 115 if callback_name: 116 json = template.render("templates/showfilelist.jsonp", template_values) 117 self._serve_json(_replace_jsonp_callback(json, callback_name), files[0].date) 118 return 119 self.response.out.write(template.render("templates/showfilelist.html", 120 template_values)) 121 122 def _get_file_content(self, master, builder, test_type, name): 123 """Return content of the file that matches builder and file name. 124 125 Args: 126 builder: builder name 127 test_type: type of the test 128 name: file name 129 """ 130 131 files = TestFile.get_files( 132 master, builder, test_type, name, load_data=True, limit=1) 133 if not files: 134 logging.info("File not found, master %s, builder: %s, test_type: %s, name: %s.", 135 master, builder, test_type, name) 136 return None, None 137 138 return files[0].data, files[0].date 139 140 def _get_file_content_from_key(self, key): 141 file = db.get(key) 142 143 if not file: 144 logging.info("File not found, key %s.", key) 145 return None 146 147 file.load_data() 148 return file.data, file.date 149 150 def _get_test_list_json(self, master, builder, test_type): 151 """Return json file with test name list only, do not include test 152 results and other non-test-data . 153 154 Args: 155 builder: builder name. 156 test_type: type of test results. 157 """ 158 159 json, date = self._get_file_content(master, builder, test_type, "results.json") 160 if not json: 161 return None 162 163 return JsonResults.get_test_list(builder, json), date 164 165 def _serve_json(self, json, modified_date): 166 if json: 167 if "If-Modified-Since" in self.request.headers: 168 old_date = self.request.headers["If-Modified-Since"] 169 if time.strptime(old_date, '%a, %d %b %Y %H:%M:%S %Z') == modified_date.utctimetuple(): 170 self.response.set_status(304) 171 return 172 173 # The appengine datetime objects are naive, so they lack a timezone. 174 # In practice, appengine seems to use GMT. 175 self.response.headers["Last-Modified"] = modified_date.strftime('%a, %d %b %Y %H:%M:%S') + ' GMT' 176 self.response.headers["Content-Type"] = "application/json" 177 self.response.headers["Access-Control-Allow-Origin"] = "*" 178 self.response.out.write(json) 179 else: 180 self.error(404) 181 182 def get(self): 183 key = self.request.get(PARAM_KEY) 184 master = self.request.get(PARAM_MASTER) 185 builder = self.request.get(PARAM_BUILDER) 186 test_type = self.request.get(PARAM_TEST_TYPE) 187 name = self.request.get(PARAM_NAME) 188 before = self.request.get(PARAM_BEFORE) 189 num_files = self.request.get(PARAM_NUM_FILES) 190 test_list_json = self.request.get(PARAM_TEST_LIST_JSON) 191 callback_name = self.request.get(PARAM_CALLBACK) 192 193 logging.debug( 194 "Getting files, master %s, builder: %s, test_type: %s, name: %s, before: %s.", 195 master, builder, test_type, name, before) 196 197 if key: 198 json, date = self._get_file_content_from_key(key) 199 elif test_list_json: 200 json, date = self._get_test_list_json(master, builder, test_type) 201 elif num_files or not master or not builder or not test_type or not name: 202 limit = int(num_files) if num_files else 100 203 self._get_file_list(master, builder, test_type, name, before, limit, callback_name) 204 return 205 else: 206 json, date = self._get_file_content(master, builder, test_type, name) 207 208 if json: 209 json = _replace_jsonp_callback(json, callback_name) 210 211 self._serve_json(json, date) 212 213 214 class Upload(webapp2.RequestHandler): 215 """Upload test results file to datastore.""" 216 217 def post(self): 218 file_params = self.request.POST.getall(PARAM_FILE) 219 if not file_params: 220 self.response.out.write("FAIL: missing upload file field.") 221 return 222 223 builder = self.request.get(PARAM_BUILDER) 224 if not builder: 225 self.response.out.write("FAIL: missing builder parameter.") 226 return 227 228 master = self.request.get(PARAM_MASTER) 229 test_type = self.request.get(PARAM_TEST_TYPE) 230 231 logging.debug( 232 "Processing upload request, master: %s, builder: %s, test_type: %s.", 233 master, builder, test_type) 234 235 # There are two possible types of each file_params in the request: 236 # one file item or a list of file items. 237 # Normalize file_params to a file item list. 238 files = [] 239 logging.debug("test: %s, type:%s", file_params, type(file_params)) 240 for item in file_params: 241 if not isinstance(item, list) and not isinstance(item, tuple): 242 item = [item] 243 files.extend(item) 244 245 errors = [] 246 final_status_code = 200 247 for file in files: 248 if file.filename == "incremental_results.json": 249 status_string, status_code = JsonResults.update(master, builder, test_type, file.value, is_full_results_format=False) 250 elif file.filename == "times_ms.json": 251 # We never look at historical times_ms.json files, so we can overwrite the existing one if it exists. 252 status_string, status_code = TestFile.overwrite_or_add_file(master, builder, test_type, file.filename, file.value) 253 else: 254 status_string, status_code = TestFile.add_file(master, builder, test_type, file.filename, file.value) 255 # FIXME: Upload full_results.json files for non-layout tests as well and stop supporting the 256 # incremental_results.json file format. 257 if status_code == 200 and file.filename == "full_results.json": 258 status_string, status_code = JsonResults.update(master, builder, test_type, file.value, is_full_results_format=True) 259 260 if status_code == 200: 261 logging.info(status_string) 262 else: 263 logging.error(status_string) 264 errors.append(status_string) 265 final_status_code = status_code 266 267 if errors: 268 messages = "FAIL: " + "; ".join(errors) 269 self.response.set_status(final_status_code, messages) 270 self.response.out.write(messages) 271 else: 272 self.response.set_status(200) 273 self.response.out.write("OK") 274 275 276 class UploadForm(webapp2.RequestHandler): 277 """Show a form so user can upload a file.""" 278 279 def get(self): 280 template_values = { 281 "upload_url": "/testfile/upload", 282 } 283 self.response.out.write(template.render("templates/uploadform.html", 284 template_values)) 285