diff options
author | Steve Block <steveblock@google.com> | 2010-04-27 16:31:00 +0100 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2010-05-11 14:42:12 +0100 |
commit | dcc8cf2e65d1aa555cce12431a16547e66b469ee (patch) | |
tree | 92a8d65cd5383bca9749f5327fb5e440563926e6 /WebKitTools/TestResultServer | |
parent | ccac38a6b48843126402088a309597e682f40fe6 (diff) | |
download | external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.zip external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.tar.gz external_webkit-dcc8cf2e65d1aa555cce12431a16547e66b469ee.tar.bz2 |
Merge webkit.org at r58033 : Initial merge by git
Change-Id: If006c38561af287c50cd578d251629b51e4d8cd1
Diffstat (limited to 'WebKitTools/TestResultServer')
18 files changed, 1032 insertions, 0 deletions
diff --git a/WebKitTools/TestResultServer/app.yaml b/WebKitTools/TestResultServer/app.yaml new file mode 100644 index 0000000..e51af84 --- /dev/null +++ b/WebKitTools/TestResultServer/app.yaml @@ -0,0 +1,19 @@ +application: test-results +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /stylesheets + static_dir: stylesheets + +- url: /testfile/delete + script: main.py + login: admin + +- url: /dashboards/delete + script: main.py + login: admin + +- url: /.* + script: main.py diff --git a/WebKitTools/TestResultServer/handlers/__init__.py b/WebKitTools/TestResultServer/handlers/__init__.py new file mode 100644 index 0000000..ef65bee --- /dev/null +++ b/WebKitTools/TestResultServer/handlers/__init__.py @@ -0,0 +1 @@ +# Required for Python to search this directory for module files diff --git a/WebKitTools/TestResultServer/handlers/dashboardhandler.py b/WebKitTools/TestResultServer/handlers/dashboardhandler.py new file mode 100644 index 0000000..45bc471 --- /dev/null +++ b/WebKitTools/TestResultServer/handlers/dashboardhandler.py @@ -0,0 +1,120 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import logging +import mimetypes +import urllib2 + +from google.appengine.api import users +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + +from model.dashboardfile import DashboardFile + +PARAM_FILE = "file" + +def get_content_type(filename): + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + + +class GetDashboardFile(webapp.RequestHandler): + def get(self, resource): + if not resource: + logging.debug("Getting dashboard file list.") + return self._get_file_list() + + filename = str(urllib2.unquote(resource)) + + logging.debug("Getting dashboard file: %s", filename) + + files = DashboardFile.get_files(filename) + if not files: + logging.error("Failed to find dashboard file: %s, request: %s", + filename, self.request) + self.response.set_status(404) + return + + content_type = "%s; charset=utf-8" % get_content_type(filename) + logging.info("content type: %s", content_type) + self.response.headers["Content-Type"] = content_type + self.response.out.write(files[0].data) + + def _get_file_list(self): + logging.info("getting dashboard file list.") + + files = DashboardFile.get_files("", 100) + if not files: + logging.info("Failed to find dashboard files.") + self.response.set_status(404) + return + + template_values = { + "admin": users.is_current_user_admin(), + "files": files, + } + self.response.out.write( + template.render("templates/dashboardfilelist.html", + template_values)) + + +class UpdateDashboardFile(webapp.RequestHandler): + def get(self): + files = self.request.get_all(PARAM_FILE) + if not files: + files = ["flakiness_dashboard.html", + "dashboard_base.js", + "aggregate_results.html"] + + errors = [] + for file in files: + if not DashboardFile.update_file(file): + errors.append("Failed to update file: %s" % file) + + if errors: + messages = "; ".join(errors) + logging.warning(messages) + self.response.set_status(500, messages) + self.response.out.write("FAIL") + else: + self.response.set_status(200) + self.response.out.write("OK") + + +class DeleteDashboardFile(webapp.RequestHandler): + def get(self): + files = self.request.get_all(PARAM_FILE) + if not files: + logging.warning("No dashboard file to delete.") + self.response.set_status(400) + return + + for file in files: + DashboardFile.delete_file(file) + + # Display dashboard file list after deleting the file. + self.redirect("/dashboards/") diff --git a/WebKitTools/TestResultServer/handlers/menu.py b/WebKitTools/TestResultServer/handlers/menu.py new file mode 100644 index 0000000..ad2599d --- /dev/null +++ b/WebKitTools/TestResultServer/handlers/menu.py @@ -0,0 +1,64 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from google.appengine.api import users +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + +menu = [ + ["List of test files", "/testfile"], + ["List of results.json files", "/testfile?name=results.json"], + ["List of expectations.json files", "/testfile?name=expectations.json"], + ["Upload test file", "/testfile/uploadform"], + ["List of dashboard files", "/dashboards/"], + ["Update dashboard files", "/dashboards/update"], +] + + +class Menu(webapp.RequestHandler): + def get(self): + user = users.get_current_user() + if user: + user_email = user.email() + login_text = "Sign out" + login_url = users.create_logout_url(self.request.uri) + else: + user_email = "" + login_text = "Sign in" + login_url = users.create_login_url(self.request.uri) + + template_values = { + "user_email": user_email, + "login_text": login_text, + "login_url": login_url, + "menu": menu, + } + + self.response.out.write( + template.render("templates/menu.html", template_values)) + diff --git a/WebKitTools/TestResultServer/handlers/testfilehandler.py b/WebKitTools/TestResultServer/handlers/testfilehandler.py new file mode 100644 index 0000000..972b606 --- /dev/null +++ b/WebKitTools/TestResultServer/handlers/testfilehandler.py @@ -0,0 +1,221 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import logging +import urllib + +from google.appengine.api import users +from google.appengine.ext import blobstore +from google.appengine.ext import webapp +from google.appengine.ext.webapp import blobstore_handlers +from google.appengine.ext.webapp import template + +from model.testfile import TestFile + +PARAM_BUILDER = "builder" +PARAM_DIR = "dir" +PARAM_FILE = "file" +PARAM_NAME = "name" +PARAM_KEY = "key" +PARAM_TEST_TYPE = "testtype" + + +class DeleteFile(webapp.RequestHandler): + """Delete test file for a given builder and name from datastore (metadata) and blobstore (file data).""" + + def get(self): + key = self.request.get(PARAM_KEY) + builder = self.request.get(PARAM_BUILDER) + test_type = self.request.get(PARAM_TEST_TYPE) + name = self.request.get(PARAM_NAME) + + logging.debug( + "Deleting File, builder: %s, test_type: %s, name: %s, blob key: %s.", + builder, test_type, name, key) + + TestFile.delete_file(key, builder, test_type, name, 100) + + # Display file list after deleting the file. + self.redirect("/testfile?builder=%s&testtype=%s&name=%s" + % (builder, test_type, name)) + + +class GetFile(blobstore_handlers.BlobstoreDownloadHandler): + """Get file content or list of files for given builder and name.""" + + def _get_file_list(self, builder, test_type, name): + """Get and display a list of files that matches builder and file name. + + Args: + builder: builder name + test_type: type of the test + name: file name + """ + + files = TestFile.get_files(builder, test_type, name, 100) + if not files: + logging.info("File not found, builder: %s, test_type: %s, name: %s.", + builder, test_type, name) + self.response.out.write("File not found") + return + + template_values = { + "admin": users.is_current_user_admin(), + "builder": builder, + "test_type": test_type, + "name": name, + "files": files, + } + self.response.out.write(template.render("templates/showfilelist.html", + template_values)) + + def _get_file_content(self, builder, test_type, name): + """Return content of the file that matches builder and file name. + + Args: + builder: builder name + test_type: type of the test + name: file name + """ + + files = TestFile.get_files(builder, test_type, name, 1) + if not files: + logging.info("File not found, builder: %s, test_type: %s, name: %s.", + builder, test_type, name) + return + + blob_key = files[0].blob_key + blob_info = blobstore.get(blob_key) + if blob_info: + self.send_blob(blob_info, "text/plain") + + def get(self): + builder = self.request.get(PARAM_BUILDER) + test_type = self.request.get(PARAM_TEST_TYPE) + name = self.request.get(PARAM_NAME) + dir = self.request.get(PARAM_DIR) + + logging.debug( + "Getting files, builder: %s, test_type: %s, name: %s.", + builder, test_type, name) + + # If parameter "dir" is specified or there is no builder or filename + # specified in the request, return list of files, otherwise, return + # file content. + if dir or not builder or not name: + return self._get_file_list(builder, test_type, name) + else: + return self._get_file_content(builder, test_type, name) + + +class GetUploadUrl(webapp.RequestHandler): + """Get an url for uploading file to blobstore. A special url is required for each blobsotre upload.""" + + def get(self): + upload_url = blobstore.create_upload_url("/testfile/upload") + logging.info("Getting upload url: %s.", upload_url) + self.response.out.write(upload_url) + + +class Upload(blobstore_handlers.BlobstoreUploadHandler): + """Upload file to blobstore.""" + + def post(self): + uploaded_files = self.get_uploads("file") + if not uploaded_files: + return self._upload_done([("Missing upload file field.")]) + + builder = self.request.get(PARAM_BUILDER) + if not builder: + for blob_info in uploaded_files: + blob_info.delete() + + return self._upload_done([("Missing builder parameter in upload request.")]) + + test_type = self.request.get(PARAM_TEST_TYPE) + + logging.debug( + "Processing upload request, builder: %s, test_type: %s.", + builder, test_type) + + errors = [] + for blob_info in uploaded_files: + tf = TestFile.update_file(builder, test_type, blob_info) + if not tf: + errors.append( + "Upload failed, builder: %s, test_type: %s, name: %s." % + (builder, test_type, blob_info.filename)) + blob_info.delete() + + return self._upload_done(errors) + + def _upload_done(self, errors): + logging.info("upload done.") + + error_messages = [] + for error in errors: + logging.info(error) + error_messages.append("error=%s" % urllib.quote(error)) + + if error_messages: + redirect_url = "/uploadfail?%s" % "&".join(error_messages) + else: + redirect_url = "/uploadsuccess" + + logging.info(redirect_url) + # BlobstoreUploadHandler requires redirect at the end. + self.redirect(redirect_url) + + +class UploadForm(webapp.RequestHandler): + """Show a form so user can submit a file to blobstore.""" + + def get(self): + upload_url = blobstore.create_upload_url("/testfile/upload") + template_values = { + "upload_url": upload_url, + } + self.response.out.write(template.render("templates/uploadform.html", + template_values)) + +class UploadStatus(webapp.RequestHandler): + """Return status of file uploading""" + + def get(self): + logging.debug("Update status") + + if self.request.path == "/uploadsuccess": + self.response.set_status(200) + self.response.out.write("OK") + else: + errors = self.request.params.getall("error") + if errors: + messages = "FAIL: " + "; ".join(errors) + logging.warning(messages) + self.response.set_status(500, messages) + self.response.out.write("FAIL") diff --git a/WebKitTools/TestResultServer/index.yaml b/WebKitTools/TestResultServer/index.yaml new file mode 100644 index 0000000..50284dc --- /dev/null +++ b/WebKitTools/TestResultServer/index.yaml @@ -0,0 +1,50 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: DashboardFile + properties: + - name: name + - name: date + direction: desc + +- kind: TestFile + properties: + - name: builder + - name: date + direction: desc + +- kind: TestFile + properties: + - name: builder + - name: name + - name: date + direction: desc + +- kind: TestFile + properties: + - name: builder + - name: name + - name: test_type + - name: date + direction: desc + +- kind: TestFile + properties: + - name: name + - name: date + direction: desc + +- kind: TestFile + properties: + - name: test_type + - name: date + direction: desc diff --git a/WebKitTools/TestResultServer/main.py b/WebKitTools/TestResultServer/main.py new file mode 100644 index 0000000..7a0d237 --- /dev/null +++ b/WebKitTools/TestResultServer/main.py @@ -0,0 +1,60 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Request a modern Django +from google.appengine.dist import use_library +use_library('django', '1.1') + +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import run_wsgi_app + +from handlers import dashboardhandler +from handlers import menu +from handlers import testfilehandler + +routes = [ + ('/dashboards/delete', dashboardhandler.DeleteDashboardFile), + ('/dashboards/update', dashboardhandler.UpdateDashboardFile), + ('/dashboards/([^?]+)?', dashboardhandler.GetDashboardFile), + ('/testfile/delete', testfilehandler.DeleteFile), + ('/testfile/uploadurl', testfilehandler.GetUploadUrl), + ('/testfile/upload', testfilehandler.Upload), + ('/testfile/uploadform', testfilehandler.UploadForm), + ('/testfile/?', testfilehandler.GetFile), + ('/uploadfail', testfilehandler.UploadStatus), + ('/uploadsuccess', testfilehandler.UploadStatus), + ('/*|/menu', menu.Menu), +] + +application = webapp.WSGIApplication(routes, debug=True) + +def main(): + run_wsgi_app(application) + +if __name__ == "__main__": + main() diff --git a/WebKitTools/TestResultServer/model/__init__.py b/WebKitTools/TestResultServer/model/__init__.py new file mode 100644 index 0000000..ef65bee --- /dev/null +++ b/WebKitTools/TestResultServer/model/__init__.py @@ -0,0 +1 @@ +# Required for Python to search this directory for module files diff --git a/WebKitTools/TestResultServer/model/dashboardfile.py b/WebKitTools/TestResultServer/model/dashboardfile.py new file mode 100644 index 0000000..c74f071 --- /dev/null +++ b/WebKitTools/TestResultServer/model/dashboardfile.py @@ -0,0 +1,116 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from datetime import datetime +import logging +import urllib +import urllib2 + +from google.appengine.ext import db + +SVN_PATH_DASHBOARD = ("http://src.chromium.org/viewvc/chrome/trunk/tools/" + "dashboards/") + +class DashboardFile(db.Model): + name = db.StringProperty() + data = db.BlobProperty() + date = db.DateTimeProperty(auto_now_add=True) + + @classmethod + def get_files(cls, name, limit=1): + query = DashboardFile.all() + if name: + query = query.filter("name =", name) + return query.order("-date").fetch(limit) + + @classmethod + def add_file(cls, name, data): + file = DashboardFile() + file.name = name + file.data = db.Blob(data) + file.put() + + logging.debug("Dashboard file saved, name: %s.", name) + + return file + + @classmethod + def grab_file_from_svn(cls, name): + logging.debug("Grab file from SVN, name: %s.", name) + + url = SVN_PATH_DASHBOARD + urllib.quote_plus(name) + + logging.info("Grab file from SVN, url: %s.", url) + try: + file = urllib2.urlopen(url) + if not file: + logging.error("Failed to grab dashboard file: %s.", url) + return None + + return file.read() + except urllib2.HTTPError, e: + logging.error("Failed to grab dashboard file: %s", str(e)) + except urllib2.URLError, e: + logging.error("Failed to grab dashboard file: %s", str(e)) + + return None + + @classmethod + def update_file(cls, name): + data = cls.grab_file_from_svn(name) + if not data: + return None + + logging.info("Got file from SVN.") + + files = cls.get_files(name) + if not files: + logging.info("No existing file, added as new file.") + return cls.add_file(name, data) + + logging.debug("Updating existing file.") + file = files[0] + file.data = data + file.date = datetime.now() + file.put() + + logging.info("Dashboard file replaced, name: %s.", name) + + return file + + @classmethod + def delete_file(cls, name): + files = cls.get_files(name) + if not files: + logging.warning("File not found, name: %s.", name) + return False + + for file in files: + file.delete() + + return True diff --git a/WebKitTools/TestResultServer/model/testfile.py b/WebKitTools/TestResultServer/model/testfile.py new file mode 100644 index 0000000..35ab967 --- /dev/null +++ b/WebKitTools/TestResultServer/model/testfile.py @@ -0,0 +1,122 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from datetime import datetime +import logging + +from google.appengine.ext import blobstore +from google.appengine.ext import db + + +class TestFile(db.Model): + builder = db.StringProperty() + name = db.StringProperty() + test_type = db.StringProperty() + blob_key = db.StringProperty() + date = db.DateTimeProperty(auto_now_add=True) + + @classmethod + def delete_file(cls, key, builder, test_type, name, limit): + if key: + file = db.get(key) + if not file: + logging.warning("File not found, key: %s.", key) + return False + + file._delete_all() + else: + files = cls.get_files(builder, test_type, name, limit) + if not files: + logging.warning( + "File not found, builder: %s, test_type:%s, name: %s.", + builder, test_type, name) + return False + + for file in files: + file._delete_all() + + return True + + @classmethod + def get_files(cls, builder, test_type, name, limit): + query = TestFile.all() + if builder: + query = query.filter("builder =", builder) + if test_type: + query = query.filter("test_type =", test_type) + if name: + query = query.filter("name =", name) + + return query.order("-date").fetch(limit) + + @classmethod + def add_file(cls, builder, test_type, blob_info): + file = TestFile() + file.builder = builder + file.test_type = test_type + file.name = blob_info.filename + file.blob_key = str(blob_info.key()) + file.put() + + logging.info( + "File saved, builder: %s, test_type: %s, name: %s, blob key: %s.", + builder, test_type, file.name, file.blob_key) + + return file + + @classmethod + def update_file(cls, builder, test_type, blob_info): + files = cls.get_files(builder, test_type, blob_info.filename, 1) + if not files: + return cls.add_file(builder, test_type, blob_info) + + file = files[0] + old_blob_info = blobstore.BlobInfo.get(file.blob_key) + if old_blob_info: + old_blob_info.delete() + + file.builder = builder + file.test_type = test_type + file.name = blob_info.filename + file.blob_key = str(blob_info.key()) + file.date = datetime.now() + file.put() + + logging.info( + "File replaced, builder: %s, test_type: %s, name: %s, blob key: %s.", + builder, test_type, file.name, file.blob_key) + + return file + + def _delete_all(self): + if self.blob_key: + blob_info = blobstore.BlobInfo.get(self.blob_key) + if blob_info: + blob_info.delete() + + self.delete() diff --git a/WebKitTools/TestResultServer/stylesheets/dashboardfile.css b/WebKitTools/TestResultServer/stylesheets/dashboardfile.css new file mode 100644 index 0000000..1b0921c --- /dev/null +++ b/WebKitTools/TestResultServer/stylesheets/dashboardfile.css @@ -0,0 +1,30 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + padding: 0px; + color: #444; +} +h1 { + color: #444; + font-size: 14pt; + font-style: italic; + margin: 0px; + padding: 5px; +} +table { + border-spacing: 0px; +} +th { + background-color: #AAA; + color: white; + text-align: left; + padding: 5px; + font-size: 12pt; +} +td { + font-size: 11pt; + padding: 3px; + text-align: left; +} +tr:hover { + background-color: #EEE; +} diff --git a/WebKitTools/TestResultServer/stylesheets/form.css b/WebKitTools/TestResultServer/stylesheets/form.css new file mode 100644 index 0000000..b8f367d --- /dev/null +++ b/WebKitTools/TestResultServer/stylesheets/form.css @@ -0,0 +1,26 @@ +body { + font-family: Verdana; + padding: 0px; + color: #444; +} +h1 { + color: #444; + font-size: 14pt; + font-style: italic; + margin: 0px; + padding: 5px; +} +.label { + margin: 1px; + padding: 5px; + font-size: 11pt; + width: 90px; +} +.inputtext { + font-size: 11pt; +} +.button { + margin: 1px; + padding: 1px; + font-size: 11pt; +} diff --git a/WebKitTools/TestResultServer/stylesheets/menu.css b/WebKitTools/TestResultServer/stylesheets/menu.css new file mode 100644 index 0000000..9948605 --- /dev/null +++ b/WebKitTools/TestResultServer/stylesheets/menu.css @@ -0,0 +1,28 @@ +body { + font-family: Verdana, Helvetica, sans-serif; +} +h1 { + background-color: #EEE; + color: #444; + font-size: 14pt; + font-style: italic; + margin: 0px; + padding: 5px; +} +ul { + margin: 0px; + padding: 20px; + list-style: none; +} +li { + padding: 5px; +} +li:hover { + background-color: #EEE; +} +.login { + font-size: 8pt; + text-align: right; + width: 100%; +} + diff --git a/WebKitTools/TestResultServer/stylesheets/testfile.css b/WebKitTools/TestResultServer/stylesheets/testfile.css new file mode 100644 index 0000000..1b0921c --- /dev/null +++ b/WebKitTools/TestResultServer/stylesheets/testfile.css @@ -0,0 +1,30 @@ +body { + font-family: Verdana, Helvetica, sans-serif; + padding: 0px; + color: #444; +} +h1 { + color: #444; + font-size: 14pt; + font-style: italic; + margin: 0px; + padding: 5px; +} +table { + border-spacing: 0px; +} +th { + background-color: #AAA; + color: white; + text-align: left; + padding: 5px; + font-size: 12pt; +} +td { + font-size: 11pt; + padding: 3px; + text-align: left; +} +tr:hover { + background-color: #EEE; +} diff --git a/WebKitTools/TestResultServer/templates/dashboardfilelist.html b/WebKitTools/TestResultServer/templates/dashboardfilelist.html new file mode 100644 index 0000000..818cb91 --- /dev/null +++ b/WebKitTools/TestResultServer/templates/dashboardfilelist.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> +<title>Dashboard Files</title> +<link type="text/css" rel="stylesheet" href="/stylesheets/dashboardfile.css" /> +</head> +<body> +<h1>Dashboard Files +</h1> +<div> + <table> + <tr> + <th>File</th> + <th>Date</th> + {% if admin %} + <th></th> + {% endif %} + {% for file in files %} + <tr>{% if file.name %} + <td><a href="/dashboards/{{ file.name }}" > + {{ file.name }} + </a> + </td> + <td>{{ file.date|date:"d-M-Y H:i:s" }} + </td> + {% if admin %} + <td><a href="/dashboards/delete?file={{ file.name }}" > + Delete + </a> + </td> + {% endif %} + {% endif %} + </tr> + {% endfor %} + </table> +</div> +</body> +</html> diff --git a/WebKitTools/TestResultServer/templates/menu.html b/WebKitTools/TestResultServer/templates/menu.html new file mode 100644 index 0000000..1ad9f4d --- /dev/null +++ b/WebKitTools/TestResultServer/templates/menu.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test Result Server</title> +<table class=login> + <tr> + <td> + {% if user_email %} + <span>{{ user_email }}</span> + {% endif %} + <span><a href="{{ login_url }}">{{ login_text }}</a></span> + </td> + </tr> +</table> +<link type="text/css" rel="stylesheet" href="/stylesheets/menu.css" /> +</head> +<body> +<h1>Test Result Server</h1> +<div> + <ul>{% for title,link in menu %} + <li> + <a href="{{ link }}" >{{ title }}</a> + </li>{% endfor %} + </ul> +</div> +</body> +</html> diff --git a/WebKitTools/TestResultServer/templates/showfilelist.html b/WebKitTools/TestResultServer/templates/showfilelist.html new file mode 100644 index 0000000..fa72b7f --- /dev/null +++ b/WebKitTools/TestResultServer/templates/showfilelist.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test Results</title> +<link type="text/css" rel="stylesheet" href="/stylesheets/testfile.css" /> +</head> +<body> +<h1>Test Results +{% if builder or test_type or name %} +- {{ builder }} {{test_type }} {{ name }} +{% endif %} +</h1> +<div> + <table> + <tr> + <th>Builder</th> + <th>Test Type</th> + <th>File</th> + <th>Date</th> + {% if admin %} + <th></th> + {% endif %} + {% for file in files %} + <tr>{% if file.builder and file.name %} + <td><a href="/testfile?builder={{ file.builder }}" > + {{ file.builder }} + </a> + </td> + <td>{% if file.test_type %} + <a href="/testfile?testtype={{ file.test_type }}" > + {{ file.test_type }} + </a> + {% endif %} + </td> + <td><a href="/testfile?builder={{ file.builder }}&name={{ file.name }}" > + {{ file.name }} + </a> + </td> + <td>{{ file.date|date:"d-M-Y H:i:s" }} + </td> + {% if admin %} + <td><a href="/testfile/delete?key={{ file.key }}&builder={{ builder }}&name={{ name }}" > + Delete + </a> + </td> + {% endif %} + {% endif %} + </tr> + {% endfor %} + </table> +</div> +</body> +</html> diff --git a/WebKitTools/TestResultServer/templates/uploadform.html b/WebKitTools/TestResultServer/templates/uploadform.html new file mode 100644 index 0000000..933f9f5 --- /dev/null +++ b/WebKitTools/TestResultServer/templates/uploadform.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<title>Upload Test Result File</title> +<link type="text/css" rel="stylesheet" href="/stylesheets/form.css" /> +</head> +<body> +<h1>Upload Test Result File</h1> +<form id="uploadForm" name="test_result_upload" accept="text/html" action="{{ upload_url }}" enctype="multipart/form-data" method="post"> + <br> + <table> + <tr> + <td class=label><label>Builder:</label></td> + <td><input class=inputtext type="text" name="builder" value="Webkit"/></td> + </tr> + <tr> + <td class=label><label>Test Type:</label></td> + <td><input class=inputtext type="text" name="testtype" value=""/></td> + </tr> + </table> + <div><input class=button type="file" name="file" multiple></div> + <br> + <div><input class=button type="submit" value="Upload"></div> +</form> +</body> +</html> |