1 # Copyright (C) 2010 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
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
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.
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.
35 from google.appengine.api import users
36 from google.appengine.ext.webapp import template
37 from google.appengine.ext import db
41 from model.jsonresults import JsonResults
42 from model.testfile import TestFile
44 PARAM_MASTER = "master"
45 PARAM_BUILDER = "builder"
46 PARAM_BUILD_NUMBER = "buildnumber"
50 PARAM_BEFORE = "before"
51 PARAM_NUM_FILES = "numfiles"
53 PARAM_TEST_TYPE = "testtype"
54 PARAM_TEST_LIST_JSON = "testlistjson"
55 PARAM_CALLBACK = "callback"
58 def _replace_jsonp_callback(json, callback_name):
59 if callback_name and re.search(r"^[A-Za-z0-9_]+$", callback_name):
60 if re.search(r"^[A-Za-z0-9_]+[(]", json):
61 return re.sub(r"^[A-Za-z0-9_]+[(]", callback_name + "(", json)
62 return callback_name + "(" + json + ")"
67 class DeleteFile(webapp2.RequestHandler):
68 """Delete test file for a given builder and name from datastore."""
71 key = self.request.get(PARAM_KEY)
72 # Intentionally don't munge the master from deprecated names here.
73 # Assume anyone deleting files wants explicit control.
74 master = self.request.get(PARAM_MASTER)
75 builder = self.request.get(PARAM_BUILDER)
76 test_type = self.request.get(PARAM_TEST_TYPE)
77 build_number = self.request.get(PARAM_BUILD_NUMBER, default_value=None)
78 name = self.request.get(PARAM_NAME)
79 num_files = self.request.get(PARAM_NUM_FILES)
80 before = self.request.get(PARAM_BEFORE)
83 "Deleting File, master: %s, builder: %s, test_type: %s, build_number: %s, name: %s, before: %s, key: %s.",
84 master, builder, test_type, build_number, name, before, key)
86 limit = int(num_files) if num_files else 1
87 num_deleted = TestFile.delete_file(key, master, builder, test_type, build_number, name, before, limit)
89 self.response.set_status(200)
90 self.response.out.write("Deleted %d files." % num_deleted)
93 class GetFile(webapp2.RequestHandler):
94 """Get file content or list of files for given builder and name."""
96 def _get_file_list(self, master, builder, test_type, build_number, name, before, limit, callback_name=None):
97 """Get and display a list of files that matches builder and file name.
100 builder: builder name
101 test_type: type of the test
105 files = TestFile.get_files(
106 master, builder, test_type, build_number, name, before, load_data=False, limit=limit)
108 logging.info("File not found, master: %s, builder: %s, test_type: %s, build_number: %s, name: %s.",
109 master, builder, test_type, build_number, name)
110 self.response.out.write("File not found")
114 "admin": users.is_current_user_admin(),
117 "test_type": test_type,
118 "build_number": build_number,
123 json = template.render("templates/showfilelist.jsonp", template_values)
124 self._serve_json(_replace_jsonp_callback(json, callback_name), files[0].date)
126 self.response.out.write(template.render("templates/showfilelist.html",
129 def _get_file_content(self, master, builder, test_type, build_number, name):
130 """Return content of the file that matches builder and file name.
133 builder: builder name
134 test_type: type of the test
135 build_number: build number, or 'latest'
139 files = TestFile.get_files(
140 master, builder, test_type, build_number, name, load_data=True, limit=1)
142 logging.info("File not found, master %s, builder: %s, test_type: %s, build_number: %s, name: %s.",
143 master, builder, test_type, build_number, name)
146 return files[0].data, files[0].date
148 def _get_file_content_from_key(self, key):
152 logging.info("File not found, key %s.", key)
156 return file.data, file.date
158 def _serve_json(self, json, modified_date):
160 if "If-Modified-Since" in self.request.headers:
161 old_date = self.request.headers["If-Modified-Since"]
162 if time.strptime(old_date, '%a, %d %b %Y %H:%M:%S %Z') == modified_date.utctimetuple():
163 self.response.set_status(304)
166 # The appengine datetime objects are naive, so they lack a timezone.
167 # In practice, appengine seems to use GMT.
168 self.response.headers["Last-Modified"] = modified_date.strftime('%a, %d %b %Y %H:%M:%S') + ' GMT'
169 self.response.headers["Content-Type"] = "application/json"
170 self.response.headers["Access-Control-Allow-Origin"] = "*"
171 self.response.out.write(json)
176 key = self.request.get(PARAM_KEY)
177 master = self.request.get(PARAM_MASTER)
178 builder = self.request.get(PARAM_BUILDER)
179 test_type = self.request.get(PARAM_TEST_TYPE)
180 build_number = self.request.get(PARAM_BUILD_NUMBER, default_value=None)
181 name = self.request.get(PARAM_NAME)
182 before = self.request.get(PARAM_BEFORE)
183 num_files = self.request.get(PARAM_NUM_FILES)
184 test_list_json = self.request.get(PARAM_TEST_LIST_JSON)
185 callback_name = self.request.get(PARAM_CALLBACK)
188 "Getting files, master %s, builder: %s, test_type: %s, build_number: %s, name: %s, before: %s.",
189 master, builder, test_type, build_number, name, before)
192 json, date = self._get_file_content_from_key(key)
193 elif num_files or not master or not builder or not test_type or (not build_number and not JsonResults.is_aggregate_file(name)) or not name:
194 limit = int(num_files) if num_files else 100
195 self._get_file_list(master, builder, test_type, build_number, name, before, limit, callback_name)
198 # FIXME: Stop using the old master name style after all files have been updated.
199 master_data = master_config.getMaster(master)
201 master_data = master_config.getMasterByMasterName(master)
206 json, date = self._get_file_content(master_data['url_name'], builder, test_type, build_number, name)
208 json, date = self._get_file_content(master_data['name'], builder, test_type, build_number, name)
210 if json and test_list_json:
211 json = JsonResults.get_test_list(builder, json)
214 json = _replace_jsonp_callback(json, callback_name)
216 self._serve_json(json, date)
219 class Upload(webapp2.RequestHandler):
220 """Upload test results file to datastore."""
223 file_params = self.request.POST.getall(PARAM_FILE)
225 self.response.out.write("FAIL: missing upload file field.")
228 builder = self.request.get(PARAM_BUILDER)
230 self.response.out.write("FAIL: missing builder parameter.")
233 master_parameter = self.request.get(PARAM_MASTER)
235 master_data = master_config.getMasterByMasterName(master_parameter)
237 deprecated_master = master_parameter
238 master = master_data['url_name']
240 deprecated_master = None
241 master = master_parameter
243 test_type = self.request.get(PARAM_TEST_TYPE)
246 "Processing upload request, master: %s, builder: %s, test_type: %s.",
247 master, builder, test_type)
249 # There are two possible types of each file_params in the request:
250 # one file item or a list of file items.
251 # Normalize file_params to a file item list.
253 logging.debug("test: %s, type:%s", file_params, type(file_params))
254 for item in file_params:
255 if not isinstance(item, list) and not isinstance(item, tuple):
260 final_status_code = 200
262 file_json = JsonResults._load_json(file.value)
263 if file.filename == "incremental_results.json":
264 # FIXME: Ferret out and eliminate remaining incremental_results.json producers.
265 logging.info("incremental_results.json received from master: %s, builder: %s, test_type: %s.",
266 master, builder, test_type)
267 status_string, status_code = JsonResults.update(master, builder, test_type, file_json,
268 deprecated_master=deprecated_master, is_full_results_format=False)
271 build_number = int(file_json.get('build_number', 0))
272 status_string, status_code = TestFile.add_file(master, builder, test_type, build_number, file.filename, file.value)
273 except (ValueError, TypeError):
275 status_string = 'Could not cast the build_number field in the json to an integer.'
277 if status_code == 200:
278 logging.info(status_string)
280 logging.error(status_string)
281 errors.append(status_string)
282 final_status_code = status_code
284 if status_code == 200 and file.filename == "full_results.json":
285 status_string, status_code = JsonResults.update(master, builder, test_type, file_json,
286 deprecated_master=deprecated_master, is_full_results_format=True)
288 if status_code == 200:
289 logging.info(status_string)
291 logging.error(status_string)
292 errors.append(status_string)
293 final_status_code = status_code
296 messages = "FAIL: " + "; ".join(errors)
297 self.response.set_status(final_status_code, messages)
298 self.response.out.write(messages)
300 self.response.set_status(200)
301 self.response.out.write("OK")
304 class UploadForm(webapp2.RequestHandler):
305 """Show a form so user can upload a file."""
309 "upload_url": "/testfile/upload",
311 self.response.out.write(template.render("templates/uploadform.html",