+2012-01-24 Ryosuke Niwa <rniwa@webkit.org>
+
+ Port Mozilla's Graph Server
+ https://bugs.webkit.org/show_bug.cgi?id=76312
+
+ Reviewed by Adam Barth.
+
+ Add the app engine backend for the Mozilla's graph server used on perf-webkit.appspot.com.
+
+ To deploy webkit-perf.appspot.com, you also need to pull index.html, embed.html, graph.html, jq,
+ js (except config.js), and css (except title.png) from https://github.com/mozilla/graphs.
+
+ * Websites/perf-webkit.appspot.com: Added.
+ * Websites/perf-webkit.appspot.com/app.yaml: Added.
+ * Websites/perf-webkit.appspot.com/create_handler.py: Added.
+ (CreateHandler):
+ (CreateHandler.post):
+ (CreateHandler._createBuilder):
+ (CreateHandler._createBuilder.execute):
+ (CreateHandler._createBranch):
+ (CreateHandler._createBranch.execute):
+ (CreateHandler._createPlatform):
+ (CreateHandler._createPlatform.execute):
+ * Websites/perf-webkit.appspot.com/dashboard_handler.py: Added.
+ (DashboardHandler):
+ (DashboardHandler.get):
+ * Websites/perf-webkit.appspot.com/index.yaml: Added.
+ * Websites/perf-webkit.appspot.com/main.py: Added.
+ (main):
+ * Websites/perf-webkit.appspot.com/manifest_handler.py: Added.
+ (ManifestHandler):
+ (ManifestHandler.get):
+ * Websites/perf-webkit.appspot.com/models.py: Added.
+ (NumericIdHolder):
+ (NumericIdHolder.whose):
+ (createInTransactionWithNumericIdHolder):
+ (modelFromNumericId):
+ (Branch):
+ (Platform):
+ (Builder):
+ (Builder.authenticate):
+ (Builder.hashedPassword):
+ (Build):
+ (Test):
+ (TestResult):
+ (ReportLog):
+ * Websites/perf-webkit.appspot.com/report_handler.py: Added.
+ (ReportHandler):
+ (ReportHandler.post):
+ (ReportHandler._modelByKeyNameInBodyOrError):
+ (ReportHandler._integerInBody):
+ (ReportHandler._timestampInBody):
+ (ReportHandler._output):
+ (ReportHandler._resultsAreValid):
+ (ReportHandler._createBuildIfPossible):
+ (ReportHandler._createBuildIfPossible.execute):
+ (ReportHandler._addTestIfNeeded):
+ (ReportHandler._addTestIfNeeded.execute):
+ * Websites/perf-webkit.appspot.com/runs_handler.py: Added.
+ (RunsHandler):
+ (RunsHandler.get):
+ * Websites/perf-webkit.appspot.com/static: Added.
+ * Websites/perf-webkit.appspot.com/static/create-models.html: Added.
+ * Websites/perf-webkit.appspot.com/static/manual-submit.html: Added.
+
2012-01-25 Hajime Morita <morrita@google.com>>
ENABLE_SHADOW_DOM should be available via build-webkit --shadow-dom
--- /dev/null
+application: webkit-perf
+version: 8
+runtime: python27
+api_version: 1
+threadsafe: false
+
+handlers:
+- url: /favicon\.ico
+ static_files: favicon.ico
+ upload: favicon\.ico
+
+- url: /admin/((.+)\.html)
+ static_files: static/\1
+ upload: static
+ secure: always
+ login: admin
+
+- url: /
+ static_files: index.html
+ upload: index.html
+
+- url: /(.+\.html)
+ static_files: \1
+ upload: (.+\.html)
+
+- url: /css
+ static_dir: css
+
+- url: /js
+ static_dir: js
+
+- url: /jq
+ static_dir: jq
+
+- url: /api/test/report
+ script: main.py
+ secure: always
+
+- url: /admin/report
+ script: main.py
+ secure: always
+ login: admin
+
+- url: /api/create/(\w+)
+ script: main.py
+ secure: always
+ login: admin
+
+- url: .*
+ script: main.py
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+from google.appengine.ext import db
+
+import json
+
+from models import Builder
+from models import Branch
+from models import NumericIdHolder
+from models import Platform
+from models import createInTransactionWithNumericIdHolder
+
+
+class CreateHandler(webapp2.RequestHandler):
+ def post(self, model):
+ self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
+
+ try:
+ payload = json.loads(self.request.body)
+ key = payload.get('key', '')
+ name = payload.get('name', '')
+ password = payload.get('password', '')
+ except:
+ self.response.out.write("Failed to parse the payload: %s" % self.request.body)
+ return
+
+ if model == 'builder':
+ error = self._createBuilder(name, password)
+ elif model == 'branch':
+ error = self._createBranch(key, name)
+ elif model == 'platform':
+ error = self._createPlatform(key, name)
+ else:
+ error = "Unknown model type: %s\n" % model
+
+ self.response.out.write(error + '\n' if error else 'OK')
+
+ def _createBuilder(self, name, password):
+ if not name or not password:
+ return 'Invalid name or password'
+
+ password = Builder.hashedPassword(password)
+
+ def execute():
+ message = None
+ bot = Builder.get_by_key_name(name)
+ if bot:
+ message = 'Updating the password since bot "%s" already exists' % name
+ bot.password = password
+ else:
+ bot = Builder(name=name, password=password, key_name=name)
+ bot.put()
+ return message
+
+ return db.run_in_transaction(execute)
+
+ def _createBranch(self, key, name):
+ if not key or not name:
+ return 'Invalid key or name'
+
+ error = [None]
+
+ def execute(id):
+ if Branch.get_by_key_name(key):
+ error[0] = 'Branch "%s" already exists' % key
+ return
+ branch = Branch(id=id, name=name, key_name=key)
+ branch.put()
+ return branch
+
+ createInTransactionWithNumericIdHolder(execute)
+ return error[0]
+
+ def _createPlatform(self, key, name):
+ if not key or not name:
+ return 'Invalid key name'
+
+ error = [None]
+
+ def execute(id):
+ if Platform.get_by_key_name(key):
+ error[0] = 'Platform "%s" already exists' % key
+ return
+ platform = Platform(id=id, name=name, key_name=key)
+ platform.put()
+ return platform
+
+ createInTransactionWithNumericIdHolder(execute)
+ return error[0]
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2011 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 webapp2
+
+import json
+
+from models import Builder
+from models import Branch
+from models import Platform
+from models import Test
+
+
+class DashboardHandler(webapp2.RequestHandler):
+ def get(self):
+ webkitTrunk = Branch.get_by_key_name('webkit-trunk')
+
+ # FIXME: Determine popular branches, platforms, and tests
+ dashboard = {
+ 'defaultBranch': 'WebKit trunk',
+ 'branchToId': {webkitTrunk.name: webkitTrunk.id},
+ 'platformToId': {},
+ 'testToId': {},
+ }
+
+ for platform in Platform.all():
+ dashboard['platformToId'][platform.name] = platform.id
+
+ for test in Test.all():
+ dashboard['testToId'][test.name] = test.id
+
+ self.response.headers['Content-Type'] = 'application/json; charset=utf-8';
+ self.response.out.write(json.dumps(dashboard))
--- /dev/null
+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.
--- /dev/null
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var USE_GENERATED_IMAGES_IN_DASHBOARD = false;
+var MAX_GRAPHS = 6;
+var MAX_CSETS = 100;
+var DAY = 86400000;
+
+var COLORS = ['#e7454c', '#6dba4b', '#4986cf', '#f5983d', '#884e9f', '#bf5c41'];
+
+// server for JSON performance data
+var SERVER = location.protocol.indexOf('http') == 0 ? location.protocol + '//' + location.host : 'http://webkit-perf.appspot.com';
+
+// server for static dashboard images
+var IMAGE_SERVER = SERVER;
+
+var LIGHT_COLORS = $.map(COLORS, function(color) {
+ return $.color.parse(color).add('a', -.5).toString();
+});
+
+var PLOT_OPTIONS = {
+ xaxis: { mode: 'time' },
+ yaxis: { min: 0 },
+ selection: { mode: 'x', color: '#97c6e5' },
+ series: { shadowSize: 0 },
+ lines: { show: false },
+ points: { show: true },
+ grid: {
+ color: '#cdd6df',
+ borderWidth: 2,
+ backgroundColor: '#fff',
+ hoverable: true,
+ clickable: true,
+ autoHighlight: false
+ }
+};
+
+var OVERVIEW_OPTIONS = {
+ xaxis: { mode: 'time' },
+ yaxis: { min: 0 },
+ selection: { mode: 'x', color: '#97c6e5' },
+ series: {
+ lines: { show: true, lineWidth: 1 },
+ shadowSize: 0
+ },
+ grid: {
+ color: '#cdd6df',
+ borderWidth: 2,
+ backgroundColor: '#fff',
+ tickColor: 'rgba(0,0,0,0)'
+ }
+};
+
+function urlForChangeset(branch, changeset)
+{
+ return 'http://trac.webkit.org/changeset/' + changeset;
+}
+
+function urlForChangesetList(branch, changesetList)
+{
+ var min = Math.min.apply(Math, changesetList);
+ var max = Math.max.apply(Math, changesetList);
+ return 'http://trac.webkit.org/log/?rev=' + max + '&stop_rev=' + min + '&verbose=on';
+}
+
+// FIXME move this back to dashboard.js once the bug 718925 is fixed
+function fetchDashboardManifest(callback)
+{
+ $.ajaxSetup({
+ 'error': function(xhr, e, message) {
+ error('Could not download dashboard data from server', e);
+ },
+ cache: true,
+ });
+
+ $.getJSON(SERVER + '/api/test/dashboard', callback);
+}
--- /dev/null
+#!/usr/bin/env python
+#
+# Copyright 2007, 2011 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import webapp2
+from google.appengine.ext.webapp import util
+
+import json
+
+from create_handler import CreateHandler
+from dashboard_handler import DashboardHandler
+from manifest_handler import ManifestHandler
+from report_handler import ReportHandler
+from report_handler import AdminReportHandler
+from runs_handler import RunsHandler
+
+routes = [
+ ('/api/create/(.*)', CreateHandler),
+ ('/api/test/?', ManifestHandler),
+ ('/api/test/report/?', ReportHandler),
+ ('/admin/report/?', AdminReportHandler),
+ ('/api/test/runs/?', RunsHandler),
+ ('/api/test/dashboard/?', DashboardHandler),
+]
+
+
+def main():
+ application = webapp2.WSGIApplication(routes, debug=True)
+ util.run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2011 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 webapp2
+
+import json
+
+from models import Builder
+from models import Branch
+from models import Platform
+from models import Test
+
+
+class ManifestHandler(webapp2.RequestHandler):
+ def get(self):
+ self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
+ self.response.out.write('{"testMap":')
+
+ testMap = {}
+ platformIdMap = {}
+ branchIdMap = {}
+ for test in Test.all():
+ branchIds = [Branch.get(branchKey).id for branchKey in test.branches]
+ platformIds = [Platform.get(platformKey).id for platformKey in test.platforms]
+ testMap[test.id] = {
+ 'name': test.name,
+ 'branchIds': branchIds,
+ 'platformIds': platformIds,
+ }
+
+ for platformId in platformIds:
+ platformIdMap.setdefault(platformId, {'tests': [], 'branches': []})
+ platformIdMap[platformId]['tests'].append(test.id)
+ platformIdMap[platformId]['branches'] += branchIds
+
+ for branchId in branchIds:
+ branchIdMap.setdefault(branchId, {'tests': [], 'platforms': []})
+ branchIdMap[branchId]['tests'].append(test.id)
+ branchIdMap[branchId]['platforms'] += platformIds
+
+ self.response.out.write(json.dumps(testMap))
+ self.response.out.write(',"platformMap":')
+
+ platformMap = {}
+ for platform in Platform.all():
+ if platform.id not in platformIdMap:
+ continue
+ platformMap[platform.id] = {
+ 'name': platform.name,
+ 'testIds': list(set(platformIdMap[platform.id]['tests'])),
+ 'branchIds': list(set(platformIdMap[platform.id]['branches'])),
+ }
+
+ self.response.out.write(json.dumps(platformMap))
+ self.response.out.write(',"branchMap":')
+
+ branchMap = {}
+ for branch in Branch.all():
+ if branch.id not in branchIdMap:
+ continue
+ branchMap[branch.id] = {
+ 'name': branch.name,
+ 'testIds': list(set(branchIdMap[branch.id]['tests'])),
+ 'platformIds': list(set(branchIdMap[branch.id]['platforms'])),
+ }
+
+ self.response.out.write(json.dumps(branchMap))
+ self.response.out.write('}')
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2012 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 hashlib
+import re
+
+from google.appengine.ext import db
+
+
+class NumericIdHolder(db.Model):
+ owner = db.ReferenceProperty()
+ # Dummy class whose sole purpose is to generate key().id()
+
+
+def createInTransactionWithNumericIdHolder(callback):
+ idHolder = NumericIdHolder()
+ idHolder.put()
+ idHolder = NumericIdHolder.get(idHolder.key())
+ owner = db.run_in_transaction(callback, idHolder.key().id())
+ if owner:
+ idHolder.owner = owner
+ idHolder.put()
+ else:
+ idHolder.delete()
+ return owner
+
+
+def modelFromNumericId(id, expectedKind):
+ idHolder = NumericIdHolder.get_by_id(id)
+ return idHolder.owner if idHolder and idHolder.owner and isinstance(idHolder.owner, expectedKind) else None
+
+
+class Branch(db.Model):
+ id = db.IntegerProperty(required=True)
+ name = db.StringProperty(required=True)
+
+
+class Platform(db.Model):
+ id = db.IntegerProperty(required=True)
+ name = db.StringProperty(required=True)
+
+
+class Builder(db.Model):
+ name = db.StringProperty(required=True)
+ password = db.StringProperty(required=True)
+
+ def authenticate(self, rawPassword):
+ return self.password == hashlib.sha256(rawPassword).hexdigest()
+
+ @staticmethod
+ def hashedPassword(rawPassword):
+ return hashlib.sha256(rawPassword).hexdigest()
+
+
+class Build(db.Model):
+ branch = db.ReferenceProperty(Branch, required=True, collection_name='build_branch')
+ platform = db.ReferenceProperty(Platform, required=True, collection_name='build_platform')
+ builder = db.ReferenceProperty(Builder, required=True, collection_name='builder_key')
+ buildNumber = db.IntegerProperty(required=True)
+ revision = db.IntegerProperty(required=True)
+ timestamp = db.DateTimeProperty(required=True)
+
+
+# Used to generate TestMap in the manifest efficiently
+class Test(db.Model):
+ id = db.IntegerProperty(required=True)
+ name = db.StringProperty(required=True)
+ branches = db.ListProperty(db.Key)
+ platforms = db.ListProperty(db.Key)
+
+
+class TestResult(db.Model):
+ name = db.StringProperty(required=True)
+ build = db.ReferenceProperty(Build, required=True)
+ value = db.FloatProperty(required=True)
+ valueMedian = db.FloatProperty()
+ valueStdev = db.FloatProperty()
+ valueMin = db.FloatProperty()
+ valueMax = db.FloatProperty()
+
+
+# Temporarily log reports sent by bots
+class ReportLog(db.Model):
+ timestamp = db.DateTimeProperty(required=True)
+ headers = db.TextProperty()
+ payload = db.TextProperty()
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+from google.appengine.ext import db
+
+import json
+import re
+import time
+from datetime import datetime
+
+from models import Builder
+from models import Branch
+from models import Build
+from models import NumericIdHolder
+from models import Platform
+from models import ReportLog
+from models import Test
+from models import TestResult
+from models import createInTransactionWithNumericIdHolder
+
+
+class ReportHandler(webapp2.RequestHandler):
+ def post(self):
+ self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+
+ headers = "\n".join([key + ': ' + value for key, value in self.request.headers.items()])
+
+ # Do as best as we can to remove the password
+ request_body_without_password = re.sub(r'"password"\s*:\s*".+?",', '', self.request.body)
+ log = ReportLog(timestamp=datetime.now(), headers=headers, payload=request_body_without_password)
+ log.put()
+
+ try:
+ self._body = json.loads(self.request.body)
+ except ValueError:
+ return self._output('Failed to parse the payload as a json. Report key: %d' % log.key().id())
+
+ builder = self._modelByKeyNameInBodyOrError(Builder, 'builder-name')
+ branch = self._modelByKeyNameInBodyOrError(Branch, 'branch')
+ platform = self._modelByKeyNameInBodyOrError(Platform, 'platform')
+ buildNumber = self._integerInBody('build-number')
+ revision = self._integerInBody('revision')
+ timestamp = self._timestampInBody()
+
+ failed = False
+ if builder and not (self.bypassAuthentication() or builder.authenticate(self._body.get('password', ''))):
+ self._output('Authentication failed')
+ failed = True
+
+ if not self._resultsAreValid():
+ self._output("The payload doesn't contain results or results are malformed")
+ failed = True
+
+ if not (builder and branch and platform and buildNumber and revision and timestamp) or failed:
+ return
+
+ build = self._createBuildIfPossible(builder, buildNumber, branch, platform, revision, timestamp)
+ if not build:
+ return
+
+ for test, result in self._body['results'].iteritems():
+ self._addTestIfNeeded(test, branch, platform)
+ if isinstance(result, dict):
+ TestResult(name=test, build=build, value=float(result.get('avg', 0)), valueMedian=float(result.get('median', 0)),
+ valueStdev=float(result.get('stdev', 0)), valueMin=float(result.get('min', 0)), valueMax=float(result.get('max', 0))).put()
+ else:
+ TestResult(name=test, build=build, value=float(result)).put()
+
+ log = ReportLog.get(log.key())
+ log.delete()
+
+ return self._output('OK')
+
+ def _modelByKeyNameInBodyOrError(self, model, keyName):
+ key = self._body.get(keyName, '')
+ instance = key and model.get_by_key_name(key)
+ if not instance:
+ self._output('There are no %s named "%s"' % (model.__name__.lower(), key))
+ return instance
+
+ def _integerInBody(self, key):
+ value = self._body.get(key, '')
+ try:
+ return int(value)
+ except:
+ return self._output('Invalid %s: "%s"' % (key.replace('-', ' '), value))
+
+ def _timestampInBody(self):
+ value = self._body.get('timestamp', '')
+ try:
+ return datetime.fromtimestamp(int(value))
+ except:
+ return self._output('Failed to parse the timestamp: %s' % value)
+
+ def _output(self, message):
+ self.response.out.write(message + '\n')
+
+ def bypassAuthentication(self):
+ return False
+
+ def _resultsAreValid(self):
+
+ def _isFloatConvertible(value):
+ try:
+ float(value)
+ return True
+ except TypeError:
+ return False
+
+ if 'results' not in self._body or not isinstance(self._body['results'], dict):
+ return False
+
+ for testResult in self._body['results'].values():
+ if isinstance(testResult, dict):
+ for value in testResult.values():
+ if not _isFloatConvertible(value):
+ return False
+ if 'avg' not in testResult:
+ return False
+ continue
+ if not _isFloatConvertible(testResult):
+ return False
+
+ return True
+
+ def _createBuildIfPossible(self, builder, buildNumber, branch, platform, revision, timestamp):
+ key_name = builder.name + ':' + str(int(time.mktime(timestamp.timetuple())))
+
+ def execute():
+ build = Build.get_by_key_name(key_name)
+ if build:
+ return self._output('The build at %s already exists for %s' % (str(timestamp), builder.name))
+
+ return Build(branch=branch, platform=platform, builder=builder, buildNumber=buildNumber,
+ timestamp=timestamp, revision=revision, key_name=key_name).put()
+ return db.run_in_transaction(execute)
+
+ def _addTestIfNeeded(self, testName, branch, platform):
+
+ def execute(id):
+ test = Test.get_by_key_name(testName)
+ returnValue = None
+ if not test:
+ test = Test(id=id, name=testName, key_name=testName)
+ returnValue = test
+ if branch.key() not in test.branches:
+ test.branches.append(branch.key())
+ if platform.key() not in test.platforms:
+ test.platforms.append(platform.key())
+ test.put()
+ return returnValue
+ createInTransactionWithNumericIdHolder(execute)
+
+
+class AdminReportHandler(ReportHandler):
+ def bypassAuthentication(self):
+ return True
--- /dev/null
+#!/usr/bin/env python
+# Copyright (C) 2012 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 webapp2
+
+import json
+from time import mktime
+from datetime import datetime
+
+from models import Build
+from models import Builder
+from models import Branch
+from models import NumericIdHolder
+from models import Platform
+from models import Test
+from models import TestResult
+from models import modelFromNumericId
+
+
+class RunsHandler(webapp2.RequestHandler):
+ def get(self):
+ try:
+ testId = int(self.request.get('id', 0))
+ branchId = int(self.request.get('branchid', 0))
+ platformId = int(self.request.get('platformid', 0))
+ except TypeError:
+ # FIXME: Output an error here
+ testId = 0
+ branchId = 0
+ platformId = 0
+
+ # FIXME: Just fetch builds specified by "days"
+ # days = self.request.get('days', 365)
+
+ builds = Build.all()
+ builds.filter('branch =', modelFromNumericId(branchId, Branch))
+ builds.filter('platform =', modelFromNumericId(platformId, Platform))
+
+ test = modelFromNumericId(testId, Test)
+ testName = test.name if test else None
+ test_runs = []
+ averages = {}
+ values = []
+ timestamps = []
+
+ for build in builds:
+ results = TestResult.all()
+ results.filter('name =', testName)
+ results.filter('build =', build)
+ for result in results:
+ builderId = build.builder.key().id()
+ posixTimestamp = mktime(build.timestamp.timetuple())
+ test_runs.append([result.key().id(),
+ [build.key().id(), build.buildNumber, build.revision],
+ posixTimestamp, result.value, 0, [], builderId])
+ # FIXME: Calculate the average; in practice, we wouldn't have more than one value for a given revision
+ averages[build.revision] = result.value
+ values.append(result.value)
+ timestamps.append(posixTimestamp)
+
+ self.response.headers['Content-Type'] = 'application/json; charset=utf-8';
+ self.response.out.write(json.dumps({
+ 'test_runs': test_runs,
+ 'averages': averages,
+ 'min': min(values) if values else None,
+ 'max': max(values) if values else None,
+ 'date_range': [min(timestamps), max(timestamps)] if timestamps else None,
+ 'stat': 'ok'}))
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<title>Create new models</title>
+<style type="text/css">
+em { font-style: normal; color: red; }
+pre {border: solid 1px black; padding: 5px;}
+h3 {font-size: 1em;}
+</style>
+</head>
+<body>
+<h1>Create new models</h1>
+
+<p>Key: canonicalized name used by build bots and storage. Name: human friendly name</p>
+
+<h2>Builder</h2>
+<form method="post" action="/api/create/builder" onsubmit="return submitByXHR(this, event)">
+<label for="name">Name/Key</label><input type="text" name="name">
+<label for="password">Password</label><input type="password" name="password">
+<button type="submit">Create</button>
+</form>
+
+<h2>Branch</h2>
+<form method="post" action="/api/create/branch" onsubmit="return submitByXHR(this, event);">
+<label for="key">Key</label><input type="text" name="key">
+<label for="name">Name</label><input type="text" name="name">
+<button type="submit">Create</button>
+</form>
+
+<h2>Platform</h2>
+<form method="post" action="/api/create/platform" onsubmit="return submitByXHR(this, event)">
+<label for="key">Key</label><input type="text" name="key">
+<label for="name">Name</label><input type="text" name="name">
+<button type="submit">Create</button>
+</form>
+
+<h2>Result:</h2>
+<h3>Status code</h3>
+<p id="status"></p>
+<h3>Headers</h3>
+<pre id="headers"></pre>
+<h3>Response</h3>
+<pre id="response" name="response"></pre>
+<script>
+
+$ = function (id) { return document.getElementById(id); }
+
+function submitByXHR(form, event) {
+ event.preventDefault();
+
+ var contents = {}
+ for (var i = 0; i < form.elements.length; i++)
+ contents[form.elements[i].name] = form.elements[i].value;
+
+ var xhr = new XMLHttpRequest;
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState != 4)
+ return;
+ $('status').innerText = xhr.status;
+ $('headers').innerText = xhr.getAllResponseHeaders();
+ $('response').innerHTML = xhr.responseText;
+ }
+ xhr.open(form.method, form.action, true);
+ xhr.send(JSON.stringify(contents));
+}
+
+</script>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test submission of a build report</title>
+<style type="text/css">
+em { font-style: normal; color: red; }
+pre {border: solid 1px black; padding: 5px;}
+h3 {font-size: 1em;}
+</style>
+</head>
+<body>
+<h1>Test submission of a build report</h1>
+<p>Specify the payload and submit:</p>
+<textarea id="data" name="data" rows="20" cols="100"></textarea><br>
+<em id="json_error"></em><br>
+<button type="submit" onclick="submit()">Submit</button>
+
+<h2>Result:</h2>
+<h3>Status code</h3>
+<p id="status"></p>
+<h3>Headers</h3>
+<pre id="headers"></pre>
+<h3>Response</h3>
+<pre id="response" name="response"></pre>
+<script>
+
+$ = function (id) { return document.getElementById(id); }
+
+$('data').oninput = function () {
+ var payload = $('data').value;
+ try {
+ JSON.parse(payload);
+ $('json_error').innerText = '';
+ } catch (error) {
+ $('json_error').innerText = error;
+ }
+}
+
+function submit() {
+ var xhr = new XMLHttpRequest;
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState != 4)
+ return;
+ $('status').innerText = xhr.status;
+ $('headers').innerText = xhr.getAllResponseHeaders();
+ $('response').innerHTML = xhr.responseText;
+ }
+ xhr.open('POST','/admin/report/', true);
+ xhr.send($('data').value);
+}
+
+$('data').value = JSON.stringify({
+ 'branch': 'webkit-trunk',
+ 'platform': 'chromium-mac',
+ 'builder-name': 'google-mac-2',
+ 'build-number': '123',
+ 'timestamp': parseInt(Date.now() / 1000),
+ 'revision': 104856,
+ 'results':
+ {
+ 'webkit_style_test': {'avg': 100, 'median': 102, 'stdev': 5, 'min': 90, 'max': 110},
+ 'some_test': 54,
+ },
+}, null, ' ');
+
+</script>
+</body>
+</html>