summaryrefslogtreecommitdiffstats
path: root/WebKitTools/TestResultServer
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/TestResultServer')
-rw-r--r--WebKitTools/TestResultServer/app.yaml19
-rw-r--r--WebKitTools/TestResultServer/handlers/__init__.py1
-rw-r--r--WebKitTools/TestResultServer/handlers/dashboardhandler.py120
-rw-r--r--WebKitTools/TestResultServer/handlers/menu.py64
-rw-r--r--WebKitTools/TestResultServer/handlers/testfilehandler.py221
-rw-r--r--WebKitTools/TestResultServer/index.yaml50
-rw-r--r--WebKitTools/TestResultServer/main.py60
-rw-r--r--WebKitTools/TestResultServer/model/__init__.py1
-rw-r--r--WebKitTools/TestResultServer/model/dashboardfile.py116
-rw-r--r--WebKitTools/TestResultServer/model/testfile.py122
-rw-r--r--WebKitTools/TestResultServer/stylesheets/dashboardfile.css30
-rw-r--r--WebKitTools/TestResultServer/stylesheets/form.css26
-rw-r--r--WebKitTools/TestResultServer/stylesheets/menu.css28
-rw-r--r--WebKitTools/TestResultServer/stylesheets/testfile.css30
-rw-r--r--WebKitTools/TestResultServer/templates/dashboardfilelist.html38
-rw-r--r--WebKitTools/TestResultServer/templates/menu.html27
-rw-r--r--WebKitTools/TestResultServer/templates/showfilelist.html53
-rw-r--r--WebKitTools/TestResultServer/templates/uploadform.html26
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>