perf-o-matic: Extract logic to generate JSON responses as classes to unit test
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 21 Feb 2012 22:26:41 +0000 (22:26 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 21 Feb 2012 22:26:41 +0000 (22:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=79018

Rubber-stamped by Adam Birth.

Extracted DashboardJSONGenerator, ManifestJSONGenerator, and RunsJSONGenerator
into json_generators.py from DashboardHandler, ManifestHandler, and RunsHandler
and added unit tests for the former three classes.

Merged the latter three classes into controler.py since they have become too trivial
to have their own files.

* Websites/webkit-perf.appspot.com/controller.py:
(ManifestUpdateHandler):
(ManifestUpdateHandler.get):
(ManifestUpdateHandler.post):
(DashboardUpdateHandler):
(DashboardUpdateHandler.get):
(DashboardUpdateHandler.post):
(_get_test_branch_platform_ids):
(RunsUpdateHandler):
(RunsUpdateHandler.get):
(CachedRunsHandler.get):
* Websites/webkit-perf.appspot.com/dashboard_handler.py: Removed.
* Websites/webkit-perf.appspot.com/json_generators.py: Added.
(JSONGeneratorBase):
(JSONGeneratorBase.to_json):
(DashboardJSONGenerator):
(DashboardJSONGenerator.__init__):
(DashboardJSONGenerator.value):
(ManifestJSONGenerator):
(ManifestJSONGenerator.__init__):
(ManifestJSONGenerator.value):
(RunsJSONGenerator):
(RunsJSONGenerator.__init__):
(RunsJSONGenerator._generate_runs):
(RunsJSONGenerator._entry_from_build_and_result):
(RunsJSONGenerator.value):
* Websites/webkit-perf.appspot.com/json_generators_unittest.py: Added.
(_create_results):
(JSONGeneratorBaseTest):
(JSONGeneratorBaseTest.test_to_json):
(JSONGeneratorBaseTest.test_to_json.AJSONGenerator):
(JSONGeneratorBaseTest.test_to_json.AJSONGenerator.value):
(DashboardJSONGeneratorTest):
(DashboardJSONGeneratorTest.test_value_no_branch):
(DashboardJSONGeneratorTest.test_value_no_plaforms):
(DashboardJSONGeneratorTest.test_value_single_platform):
(DashboardJSONGeneratorTest.test_value_two_platforms):
(ManifestJSONGeneratorTest):
(ManifestJSONGeneratorTest.test_value_no_branch):
(ManifestJSONGeneratorTest.test_value_no_plaforms):
(ManifestJSONGeneratorTest._assert_single_test):
(ManifestJSONGeneratorTest.test_value_single_platform):
(ManifestJSONGeneratorTest.test_value_two_platforms):
(ManifestJSONGeneratorTest.test_value_two_tests):
(RunsJSONGeneratorTest):
(RunsJSONGeneratorTest._create_results):
(RunsJSONGeneratorTest.test_generate_runs):
(RunsJSONGeneratorTest.test_value_without_results):
(RunsJSONGeneratorTest.test_value_with_results):
(RunsJSONGeneratorTest._assert_entry):
(RunsJSONGeneratorTest.test_run_from_build_and_result):
(RunsJSONGeneratorTest.test_run_from_build_and_result.create_build):
* Websites/webkit-perf.appspot.com/main.py:
* Websites/webkit-perf.appspot.com/manifest_handler.py: Removed.
* Websites/webkit-perf.appspot.com/models.py:
(Test.update_or_insert.execute):
(Test):
(TestResult.get_or_insert_from_parsed_json):
* Websites/webkit-perf.appspot.com/models_unittest.py:
(TestModelTests.test_update_or_insert_to_update):
(TestResultTests.test_get_or_insert_stat_value):
* Websites/webkit-perf.appspot.com/runs_handler.py: Removed.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@108399 268f45cc-cd09-0410-ab3c-d52691b4dbfc

ChangeLog
Websites/webkit-perf.appspot.com/controller.py
Websites/webkit-perf.appspot.com/dashboard_handler.py [deleted file]
Websites/webkit-perf.appspot.com/json_generators.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/json_generators_unittest.py [new file with mode: 0644]
Websites/webkit-perf.appspot.com/main.py
Websites/webkit-perf.appspot.com/manifest_handler.py [deleted file]
Websites/webkit-perf.appspot.com/models.py
Websites/webkit-perf.appspot.com/models_unittest.py
Websites/webkit-perf.appspot.com/runs_handler.py [deleted file]

index 55ff883..9fdf30f 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,80 @@
+2012-02-20  Ryosuke Niwa  <rniwa@webkit.org>
+
+        perf-o-matic: Extract logic to generate JSON responses as classes to unit test
+        https://bugs.webkit.org/show_bug.cgi?id=79018
+
+        Rubber-stamped by Adam Birth.
+
+        Extracted DashboardJSONGenerator, ManifestJSONGenerator, and RunsJSONGenerator
+        into json_generators.py from DashboardHandler, ManifestHandler, and RunsHandler
+        and added unit tests for the former three classes.
+
+        Merged the latter three classes into controler.py since they have become too trivial
+        to have their own files.
+
+        * Websites/webkit-perf.appspot.com/controller.py:
+        (ManifestUpdateHandler):
+        (ManifestUpdateHandler.get):
+        (ManifestUpdateHandler.post):
+        (DashboardUpdateHandler):
+        (DashboardUpdateHandler.get):
+        (DashboardUpdateHandler.post):
+        (_get_test_branch_platform_ids):
+        (RunsUpdateHandler):
+        (RunsUpdateHandler.get):
+        (CachedRunsHandler.get):
+        * Websites/webkit-perf.appspot.com/dashboard_handler.py: Removed.
+        * Websites/webkit-perf.appspot.com/json_generators.py: Added.
+        (JSONGeneratorBase):
+        (JSONGeneratorBase.to_json):
+        (DashboardJSONGenerator):
+        (DashboardJSONGenerator.__init__):
+        (DashboardJSONGenerator.value):
+        (ManifestJSONGenerator):
+        (ManifestJSONGenerator.__init__):
+        (ManifestJSONGenerator.value):
+        (RunsJSONGenerator):
+        (RunsJSONGenerator.__init__):
+        (RunsJSONGenerator._generate_runs):
+        (RunsJSONGenerator._entry_from_build_and_result):
+        (RunsJSONGenerator.value):
+        * Websites/webkit-perf.appspot.com/json_generators_unittest.py: Added.
+        (_create_results):
+        (JSONGeneratorBaseTest):
+        (JSONGeneratorBaseTest.test_to_json):
+        (JSONGeneratorBaseTest.test_to_json.AJSONGenerator):
+        (JSONGeneratorBaseTest.test_to_json.AJSONGenerator.value):
+        (DashboardJSONGeneratorTest):
+        (DashboardJSONGeneratorTest.test_value_no_branch):
+        (DashboardJSONGeneratorTest.test_value_no_plaforms):
+        (DashboardJSONGeneratorTest.test_value_single_platform):
+        (DashboardJSONGeneratorTest.test_value_two_platforms):
+        (ManifestJSONGeneratorTest):
+        (ManifestJSONGeneratorTest.test_value_no_branch):
+        (ManifestJSONGeneratorTest.test_value_no_plaforms):
+        (ManifestJSONGeneratorTest._assert_single_test):
+        (ManifestJSONGeneratorTest.test_value_single_platform):
+        (ManifestJSONGeneratorTest.test_value_two_platforms):
+        (ManifestJSONGeneratorTest.test_value_two_tests):
+        (RunsJSONGeneratorTest):
+        (RunsJSONGeneratorTest._create_results):
+        (RunsJSONGeneratorTest.test_generate_runs):
+        (RunsJSONGeneratorTest.test_value_without_results):
+        (RunsJSONGeneratorTest.test_value_with_results):
+        (RunsJSONGeneratorTest._assert_entry):
+        (RunsJSONGeneratorTest.test_run_from_build_and_result):
+        (RunsJSONGeneratorTest.test_run_from_build_and_result.create_build):
+        * Websites/webkit-perf.appspot.com/main.py:
+        * Websites/webkit-perf.appspot.com/manifest_handler.py: Removed.
+        * Websites/webkit-perf.appspot.com/models.py:
+        (Test.update_or_insert.execute):
+        (Test):
+        (TestResult.get_or_insert_from_parsed_json):
+        * Websites/webkit-perf.appspot.com/models_unittest.py:
+        (TestModelTests.test_update_or_insert_to_update):
+        (TestResultTests.test_get_or_insert_stat_value):
+        * Websites/webkit-perf.appspot.com/runs_handler.py: Removed.
+
 2012-02-20  Martin Robinson  <mrobinson@igalia.com>
 
         [GTK] Turn on requestAnimationFrame for release builds
index 1c53608..dbc0819 100644 (file)
@@ -31,8 +31,14 @@ import webapp2
 from google.appengine.api import taskqueue
 from google.appengine.ext import db
 
+from json_generators import DashboardJSONGenerator
+from json_generators import ManifestJSONGenerator
+from json_generators import RunsJSONGenerator
+from models import Branch
+from models import Platform
 from models import Test
 from models import PersistentCache
+from models import model_from_numeric_id
 
 
 def cache_manifest(cache):
@@ -43,6 +49,13 @@ def schedule_manifest_update():
     taskqueue.add(url='/api/test/update')
 
 
+class ManifestUpdateHandler(webapp2.RequestHandler):
+    def post(self):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+        cache_manifest(ManifestJSONGenerator().to_json())
+        self.response.out.write('OK')
+
+
 class CachedManifestHandler(webapp2.RequestHandler):
     def get(self):
         self.response.headers['Content-Type'] = 'application/json'
@@ -61,6 +74,13 @@ def schedule_dashboard_update():
     taskqueue.add(url='/api/test/dashboard/update')
 
 
+class DashboardUpdateHandler(webapp2.RequestHandler):
+    def post(self):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+        cache_dashboard(DashboardJSONGenerator().to_json())
+        self.response.out.write('OK')
+
+
 class CachedDashboardHandler(webapp2.RequestHandler):
     def get(self):
         self.response.headers['Content-Type'] = 'application/json'
@@ -79,20 +99,41 @@ def schedule_runs_update(test_id, branch_id, platform_id):
     taskqueue.add(url='/api/test/runs/update', params={'id': test_id, 'branchid': branch_id, 'platformid': platform_id})
 
 
+def _get_test_branch_platform_ids(handler):
+    try:
+        test_id = int(handler.request.get('id', 0))
+        branch_id = int(handler.request.get('branchid', 0))
+        platform_id = int(handler.request.get('platformid', 0))
+        return test_id, branch_id, platform_id
+    except TypeError:
+        # FIXME: Output an error here
+        return 0, 0, 0
+
+
+class RunsUpdateHandler(webapp2.RequestHandler):
+    def get(self):
+        self.post()
+
+    def get(self):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+        test_id, branch_id, platform_id = _get_test_branch_platform_ids(self)
+
+        branch = model_from_numeric_id(branch_id, Branch)
+        platform = model_from_numeric_id(platform_id, Platform)
+        test = model_from_numeric_id(test_id, Test)
+        assert branch
+        assert platform
+        assert test
+
+        cache_runs(test_id, branch_id, platform_id, RunsJSONGenerator(branch, platform, test.name).to_json())
+        self.response.out.write('OK')
+
+
 class CachedRunsHandler(webapp2.RequestHandler):
     def get(self):
         self.response.headers['Content-Type'] = 'application/json'
 
-        try:
-            test_id = int(self.request.get('id', 0))
-            branch_id = int(self.request.get('branchid', 0))
-            platform_id = int(self.request.get('platformid', 0))
-        except TypeError:
-            # FIXME: Output an error here
-            test_id = 0
-            branch_id = 0
-            platform_id = 0
-
+        test_id, branch_id, platform_id = _get_test_branch_platform_ids(self)
         runs = PersistentCache.get_cache(Test.cache_key(test_id, branch_id, platform_id))
         if runs:
             self.response.out.write(runs)
diff --git a/Websites/webkit-perf.appspot.com/dashboard_handler.py b/Websites/webkit-perf.appspot.com/dashboard_handler.py
deleted file mode 100644 (file)
index 714d23d..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/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 controller import cache_dashboard
-from models import Builder
-from models import Branch
-from models import Platform
-from models import Test
-
-
-class DashboardHandler(webapp2.RequestHandler):
-    def post(self):
-        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
-        webkit_trunk = Branch.get_by_key_name('webkit-trunk')
-        if not webkit_trunk:
-            self.response.out.write("BAD: no webkit-trunk")
-            return
-
-        # FIXME: Determine popular branches, platforms, and tests
-        dashboard = {
-            'defaultBranch': 'WebKit trunk',
-            'branchToId': {webkit_trunk.name: webkit_trunk.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
-
-        cache_dashboard(json.dumps(dashboard))
-        self.response.out.write('OK')
diff --git a/Websites/webkit-perf.appspot.com/json_generators.py b/Websites/webkit-perf.appspot.com/json_generators.py
new file mode 100644 (file)
index 0000000..ffd6558
--- /dev/null
@@ -0,0 +1,176 @@
+#!/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 json
+from time import mktime
+
+from models import Build
+from models import Builder
+from models import Branch
+from models import Platform
+from models import Test
+from models import TestResult
+
+
+class JSONGeneratorBase(object):
+    def to_json(self):
+        return json.dumps(self.value())
+
+
+class DashboardJSONGenerator(JSONGeneratorBase):
+    def __init__(self):
+        # FIXME: Don't hard-code webkit-trunk here; ideally we want this be customizable.
+        webkit_trunk = Branch.get_by_key_name('webkit-trunk')
+        if not webkit_trunk:
+            self._dashboard = None
+            return
+
+        # FIXME: Determine popular branches, platforms, and tests
+        self._dashboard = {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {webkit_trunk.name: webkit_trunk.id},
+            'platformToId': {},
+            'testToId': {},
+        }
+
+        for platform in Platform.all():
+            self._dashboard['platformToId'][platform.name] = platform.id
+
+        for test in Test.all():
+            self._dashboard['testToId'][test.name] = test.id
+
+    def value(self):
+        return self._dashboard
+
+
+class ManifestJSONGenerator(JSONGeneratorBase):
+    def __init__(self):
+        # FIXME: This function is massive. Break it up
+        self._test_map = {}
+        platform_id_map = {}
+        branch_id_map = {}
+        for test in Test.all():
+            branch_ids = [Branch.get(branch_key).id for branch_key in test.branches]
+            platform_ids = [Platform.get(platform_key).id for platform_key in test.platforms]
+            self._test_map[test.id] = {
+                'name': test.name,
+                'branchIds': branch_ids,
+                'platformIds': platform_ids,
+            }
+
+            for platform_id in platform_ids:
+                platform_id_map.setdefault(platform_id, {'tests': [], 'branches': []})
+                platform_id_map[platform_id]['tests'].append(test.id)
+                platform_id_map[platform_id]['branches'] += branch_ids
+
+            for branch_id in branch_ids:
+                branch_id_map.setdefault(branch_id, {'tests': [], 'platforms': []})
+                branch_id_map[branch_id]['tests'].append(test.id)
+                branch_id_map[branch_id]['platforms'] += platform_ids
+
+        self._platform_map = {}
+        for platform in Platform.all():
+            if platform.id not in platform_id_map:
+                continue
+            self._platform_map[platform.id] = {
+                'name': platform.name,
+                'testIds': list(set(platform_id_map[platform.id]['tests'])),
+                'branchIds': list(set(platform_id_map[platform.id]['branches'])),
+            }
+
+        self._branch_map = {}
+        for branch in Branch.all():
+            if branch.id not in branch_id_map:
+                continue
+            self._branch_map[branch.id] = {
+                'name': branch.name,
+                'testIds': list(set(branch_id_map[branch.id]['tests'])),
+                'platformIds': list(set(branch_id_map[branch.id]['platforms'])),
+            }
+
+    def value(self):
+        return {'branchMap': self._branch_map, 'platformMap': self._platform_map, 'testMap': self._test_map}
+
+
+class RunsJSONGenerator(JSONGeneratorBase):
+    def __init__(self, branch, platform, test):
+        self._test_runs = []
+        self._averages = {}
+        values = []
+
+        for build, result in RunsJSONGenerator._generate_runs(branch, platform, test):
+            self._test_runs.append(RunsJSONGenerator._entry_from_build_and_result(build, result))
+            # FIXME: Calculate the average. In practice, we wouldn't have more than one value for a given revision.
+            self._averages[build.revision] = result.value
+            values.append(result.value)
+
+        self._min = min(values) if values else None
+        self._max = max(values) if values else None
+
+    @staticmethod
+    def _generate_runs(branch, platform, test_name):
+        builds = Build.all()
+        builds.filter('branch =', branch)
+        builds.filter('platform =', platform)
+
+        for build in builds:
+            results = TestResult.all()
+            results.filter('name =', test_name)
+            results.filter('build =', build)
+            for result in results:
+                yield build, result
+        raise StopIteration
+
+    @staticmethod
+    def _entry_from_build_and_result(build, result):
+        builder_id = build.builder.key().id()
+        timestamp = mktime(build.timestamp.timetuple())
+        statistics = None
+        supplementary_revisions = None
+
+        if result.valueStdev != None and result.valueMin != None and result.valueMax != None:
+            statistics = {'stdev': result.valueStdev, 'min': result.valueMin, 'max': result.valueMax}
+
+        if build.chromiumRevision != None:
+            supplementary_revisions = {'Chromium': build.chromiumRevision}
+
+        return [result.key().id(),
+            [build.key().id(), build.buildNumber, build.revision, supplementary_revisions],
+            timestamp, result.value, 0,  # runNumber
+            [],  # annotations
+            builder_id, statistics]
+
+    def value(self):
+        return {
+            'test_runs': self._test_runs,
+            'averages': self._averages,
+            'min': self._min,
+            'max': self._max,
+            'date_range': None,  # Never used by common.js.
+            'stat': 'ok'}
diff --git a/Websites/webkit-perf.appspot.com/json_generators_unittest.py b/Websites/webkit-perf.appspot.com/json_generators_unittest.py
new file mode 100644 (file)
index 0000000..4b64a78
--- /dev/null
@@ -0,0 +1,311 @@
+#!/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 json
+import models
+import unittest
+
+from google.appengine.ext import testbed
+from datetime import datetime
+from json_generators import JSONGeneratorBase
+from json_generators import DashboardJSONGenerator
+from json_generators import ManifestJSONGenerator
+from json_generators import RunsJSONGenerator
+from models_unittest import DataStoreTestsBase
+from models import Branch
+from models import Build
+from models import Builder
+from models import Platform
+from models import Test
+from models import TestResult
+
+
+def _create_results(branch, platform, builder, test_name, values):
+    results = []
+    for i, value in enumerate(values):
+        build = Build(branch=branch, platform=platform, builder=builder,
+            buildNumber=i, revision=100 + i, timestamp=datetime.now())
+        build.put()
+        result = TestResult(name=test_name, build=build, value=value)
+        result.put()
+        Test.update_or_insert(test_name, branch, platform)
+        results.append(result)
+    return results
+
+
+class JSONGeneratorBaseTest(unittest.TestCase):
+    def test_to_json(self):
+
+        class AJSONGenerator(JSONGeneratorBase):
+            def value(self):
+                return {'key': 'value'}
+
+        self.assertEqual(AJSONGenerator().value(), {"key": "value"})
+        self.assertEqual(AJSONGenerator().to_json(), '{"key": "value"}')
+
+
+class DashboardJSONGeneratorTest(DataStoreTestsBase):
+    def test_value_no_branch(self):
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertEqual(DashboardJSONGenerator().value(), None)
+
+    def test_value_no_plaforms(self):
+        webkit_trunk = Branch.create_if_possible('webkit-trunk', 'WebKit trunk')
+        self.assertEqual(DashboardJSONGenerator().value(), {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {'WebKit trunk': webkit_trunk.id},
+            'platformToId': {},
+            'testToId': {},
+        })
+
+    def test_value_single_platform(self):
+        webkit_trunk = Branch.create_if_possible('webkit-trunk', 'WebKit trunk')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        self.assertEqual(DashboardJSONGenerator().value(), {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {'WebKit trunk': webkit_trunk.id},
+            'platformToId': {'Some Platform': some_platform.id},
+            'testToId': {},
+        })
+
+        Test.update_or_insert('some-test', webkit_trunk, some_platform)
+        self.assertEqual(DashboardJSONGenerator().value(), {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {'WebKit trunk': webkit_trunk.id},
+            'platformToId': {'Some Platform': some_platform.id},
+            'testToId': {'some-test': Test.get_by_key_name('some-test').id},
+        })
+
+    def test_value_two_platforms(self):
+        webkit_trunk = Branch.create_if_possible('webkit-trunk', 'WebKit trunk')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        other_platform = Platform.create_if_possible('other-platform', 'Other Platform')
+        Test.update_or_insert('some-test', webkit_trunk, some_platform)
+        Test.update_or_insert('some-test', webkit_trunk, other_platform)
+        self.assertEqual(DashboardJSONGenerator().value(), {
+            'defaultBranch': 'WebKit trunk',
+            'branchToId': {'WebKit trunk': webkit_trunk.id},
+            'platformToId': {'Some Platform': some_platform.id, 'Other Platform': other_platform.id},
+            'testToId': {'some-test': Test.get_by_key_name('some-test').id},
+        })
+
+
+class ManifestJSONGeneratorTest(DataStoreTestsBase):
+    def test_value_no_branch(self):
+        self.assertThereIsNoInstanceOf(Branch)
+        self.assertEqual(ManifestJSONGenerator().value(), {'branchMap': {}, 'platformMap': {}, 'testMap': {}})
+
+    def test_value_no_plaforms(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        self.assertEqual(ManifestJSONGenerator().value(), {'branchMap': {}, 'platformMap': {}, 'testMap': {}})
+
+    def _assert_single_test(self, value, branch, platform, test):
+        self.assertEqualUnorderedList(value.keys(), ['branchMap', 'platformMap', 'testMap'])
+        self.assertEqual(value['branchMap'],
+            {branch.id: {'name': branch.name, 'testIds': [test.id], 'platformIds': [platform.id]}})
+        self.assertEqual(value['platformMap'],
+            {platform.id: {'name': platform.name, 'branchIds': [branch.id], 'testIds': [test.id]}})
+        self.assertEqual(value['testMap'],
+            {test.id: {'name': test.name, 'branchIds': [branch.id], 'platformIds': [platform.id]}})
+
+    def test_value_single_platform(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        self.assertEqual(ManifestJSONGenerator().value(), {'branchMap': {}, 'platformMap': {}, 'testMap': {}})
+
+        some_test = Test.update_or_insert('some-test', some_branch, some_platform)
+        self._assert_single_test(ManifestJSONGenerator().value(), some_branch, some_platform, some_test)
+
+    def test_value_two_platforms(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        other_platform = Platform.create_if_possible('other-platform', 'Other Platform')
+        some_test = Test.update_or_insert('some-test', some_branch, some_platform)
+
+        self._assert_single_test(ManifestJSONGenerator().value(), some_branch, some_platform, some_test)
+
+        some_test = Test.update_or_insert('some-test', some_branch, other_platform)
+        self.assertEqualUnorderedList(some_test.platforms, [some_platform.key(), other_platform.key()])
+
+        value = ManifestJSONGenerator().value()
+        expected_platform_ids = [some_platform.id, other_platform.id]
+        self.assertEqualUnorderedList(value.keys(), ['branchMap', 'platformMap', 'testMap'])
+        self.assertEqualUnorderedList(value['branchMap'],
+            {some_branch.id: {'name': some_branch.name, 'testIds': [some_test.id], 'platformIds': expected_platform_ids}})
+        self.assertEqual(value['platformMap'],
+            {some_platform.id: {'name': some_platform.name, 'branchIds': [some_branch.id], 'testIds': [some_test.id]},
+            other_platform.id: {'name': other_platform.name, 'branchIds': [some_branch.id], 'testIds': [some_test.id]}})
+        self.assertEqual(value['testMap'],
+            {some_test.id: {'name': some_test.name, 'branchIds': [some_branch.id], 'platformIds': expected_platform_ids}})
+
+    def test_value_two_tests(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        some_test = Test.update_or_insert('some-test', some_branch, some_platform)
+
+        self._assert_single_test(ManifestJSONGenerator().value(), some_branch, some_platform, some_test)
+
+        other_test = Test.update_or_insert('other-test', some_branch, some_platform)
+
+        value = ManifestJSONGenerator().value()
+        expected_test_ids = [some_test.id, other_test.id]
+        self.assertEqualUnorderedList(value.keys(), ['branchMap', 'platformMap', 'testMap'])
+        self.assertEqualUnorderedList(value['branchMap'],
+            {some_branch.id: {'name': some_branch.name, 'testIds': expected_test_ids, 'platformIds': [some_platform.id]}})
+        self.assertEqual(value['platformMap'],
+            {some_platform.id: {'name': some_platform.name, 'branchIds': [some_branch.id], 'testIds': expected_test_ids}})
+        self.assertEqual(value['testMap'],
+            {some_test.id: {'name': some_test.name, 'branchIds': [some_branch.id], 'platformIds': [some_platform.id]},
+            other_test.id: {'name': other_test.name, 'branchIds': [some_branch.id], 'platformIds': [some_platform.id]}})
+
+
+class RunsJSONGeneratorTest(DataStoreTestsBase):
+    def _create_results(self, branch, platform, builder, test_name, values):
+        results = []
+        for i, value in enumerate(values):
+            build = Build(branch=branch, platform=platform, builder=builder,
+                buildNumber=i, revision=100 + i, timestamp=datetime.now())
+            build.put()
+            result = TestResult(name=test_name, build=build, value=value)
+            result.put()
+            results.append(result)
+        return results
+
+    def test_generate_runs(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        some_builder = Builder.get(Builder.create('some-builder', 'Some Builder'))
+
+        results = self._create_results(some_branch, some_platform, some_builder, 'some-test', [50.0, 51.0, 52.0, 49.0, 48.0])
+        last_i = 0
+        for i, (build, result) in enumerate(RunsJSONGenerator._generate_runs(some_branch, some_platform, "some-test")):
+            self.assertEqual(build.buildNumber, i)
+            self.assertEqual(build.revision, 100 + i)
+            self.assertEqual(result.name, 'some-test')
+            self.assertEqual(result.value, results[i].value)
+            last_i = i
+        self.assertTrue(last_i + 1, len(results))
+
+    def test_value_without_results(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        self.assertThereIsNoInstanceOf(Test)
+        self.assertThereIsNoInstanceOf(TestResult)
+        self.assertEqual(RunsJSONGenerator(some_branch, some_platform, 'some-test').value(), {
+            'test_runs': [],
+            'averages': {},
+            'min': None,
+            'max': None,
+            'date_range': None,
+            'stat': 'ok'})
+
+    def test_value_with_results(self):
+        some_branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        some_platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        some_builder = Builder.get(Builder.create('some-builder', 'Some Builder'))
+        results = self._create_results(some_branch, some_platform, some_builder, 'some-test', [50.0, 51.0, 52.0, 49.0, 48.0])
+
+        value = RunsJSONGenerator(some_branch, some_platform, 'some-test').value()
+        self.assertEqualUnorderedList(value.keys(), ['test_runs', 'averages', 'min', 'max', 'date_range', 'stat'])
+        self.assertEqual(value['stat'], 'ok')
+        self.assertEqual(value['min'], 48.0)
+        self.assertEqual(value['max'], 52.0)
+        self.assertEqual(value['date_range'], None)  # date_range is never given
+
+        self.assertEqual(len(value['test_runs']), len(results))
+        for i, run in enumerate(value['test_runs']):
+            result = results[i]
+            self.assertEqual(run[0], result.key().id())
+            self.assertEqual(run[1][1], i)  # Build number
+            self.assertEqual(run[1][2], 100 + i)  # Revision
+            self.assertEqual(run[1][3], None)  # Supplementary revision
+            self.assertEqual(run[3], result.value)
+            self.assertEqual(run[6], some_builder.key().id())
+            self.assertEqual(run[7], None)  # Statistics
+
+    def _assert_entry(self, entry, build, result, value, statistics=None, supplementary_revisions=None):
+        entry = entry[:]
+        entry[2] = None  # timestamp
+        self.assertEqual(entry, [result.key().id(), [build.key().id(), build.buildNumber, build.revision, supplementary_revisions],
+            None,  # timestamp
+            value, 0,  # runNumber
+            [],  # annotations
+            build.builder.key().id(), statistics])
+
+    def test_run_from_build_and_result(self):
+        branch = Branch.create_if_possible('some-branch', 'Some Branch')
+        platform = Platform.create_if_possible('some-platform', 'Some Platform')
+        builder = Builder.get(Builder.create('some-builder', 'Some Builder'))
+        test_name = ' some-test'
+
+        def create_build(build_number, revision):
+            timestamp = datetime.now().replace(microsecond=0)
+            build = Build(branch=branch, platform=platform, builder=builder, buildNumber=build_number,
+                revision=revision, timestamp=timestamp)
+            build.put()
+            return build
+
+        build = create_build(1, 101)
+        result = TestResult(name=test_name, value=123.0, build=build)
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 123.0)
+
+        build = create_build(2, 102)
+        result = TestResult(name=test_name, value=456.0, valueMedian=789.0, build=build)
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 456.0)
+
+        result.valueStdev = 7.0
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 456.0)
+
+        result.valueStdev = None
+        result.valueMin = 123.0
+        result.valueMax = 789.0
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 456.0)
+
+        result.valueStdev = 8.0
+        result.valueMin = 123.0
+        result.valueMax = 789.0
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 456.0,
+            statistics={'stdev': 8.0, 'min': 123.0, 'max': 789.0})
+
+        result.valueMedian = 345.0  # Median is never used by the frontend.
+        result.valueStdev = 8.0
+        result.valueMin = 123.0
+        result.valueMax = 789.0
+        result.put()
+        self._assert_entry(RunsJSONGenerator._entry_from_build_and_result(build, result), build, result, 456.0,
+            statistics={'stdev': 8.0, 'min': 123.0, 'max': 789.0})
+
+
+if __name__ == '__main__':
+    unittest.main()
index d5602f1..7bba3ad 100644 (file)
@@ -23,14 +23,14 @@ import json
 from controller import CachedDashboardHandler
 from controller import CachedManifestHandler
 from controller import CachedRunsHandler
+from controller import DashboardUpdateHandler
+from controller import ManifestUpdateHandler
+from controller import RunsUpdateHandler
 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 report_process_handler import ReportProcessHandler
 from report_logs_handler import ReportLogsHandler
-from runs_handler import RunsHandler
 from merge_tests_handler import MergeTestsHandler
 
 routes = [
@@ -39,13 +39,13 @@ routes = [
     ('/admin/report-logs/?', ReportLogsHandler),
     ('/admin/create/(.*)', CreateHandler),
     ('/api/test/?', CachedManifestHandler),
-    ('/api/test/update', ManifestHandler),
+    ('/api/test/update', ManifestUpdateHandler),
     ('/api/test/report/?', ReportHandler),
     ('/api/test/report/process', ReportProcessHandler),
     ('/api/test/runs/?', CachedRunsHandler),
-    ('/api/test/runs/update', RunsHandler),
+    ('/api/test/runs/update', RunsUpdateHandler),
     ('/api/test/dashboard/?', CachedDashboardHandler),
-    ('/api/test/dashboard/update', DashboardHandler),
+    ('/api/test/dashboard/update', DashboardUpdateHandler),
 ]
 
 
diff --git a/Websites/webkit-perf.appspot.com/manifest_handler.py b/Websites/webkit-perf.appspot.com/manifest_handler.py
deleted file mode 100644 (file)
index 5906c09..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/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 controller import cache_manifest
-from models import Builder
-from models import Branch
-from models import Platform
-from models import Test
-
-
-class ManifestHandler(webapp2.RequestHandler):
-    def post(self):
-        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
-
-        test_map = {}
-        platform_id_map = {}
-        branch_id_map = {}
-        for test in Test.all():
-            branch_ids = [Branch.get(branch_key).id for branch_key in test.branches]
-            platform_ids = [Platform.get(platform_key).id for platform_key in test.platforms]
-            test_map[test.id] = {
-                'name': test.name,
-                'branchIds': branch_ids,
-                'platformIds': platform_ids,
-            }
-
-            for platform_id in platform_ids:
-                platform_id_map.setdefault(platform_id, {'tests': [], 'branches': []})
-                platform_id_map[platform_id]['tests'].append(test.id)
-                platform_id_map[platform_id]['branches'] += branch_ids
-
-            for branch_id in branch_ids:
-                branch_id_map.setdefault(branch_id, {'tests': [], 'platforms': []})
-                branch_id_map[branch_id]['tests'].append(test.id)
-                branch_id_map[branch_id]['platforms'] += platform_ids
-
-        platform_map = {}
-        for platform in Platform.all():
-            if platform.id not in platform_id_map:
-                continue
-            platform_map[platform.id] = {
-                'name': platform.name,
-                'testIds': list(set(platform_id_map[platform.id]['tests'])),
-                'branchIds': list(set(platform_id_map[platform.id]['branches'])),
-            }
-
-        branch_map = {}
-        for branch in Branch.all():
-            if branch.id not in branch_id_map:
-                continue
-            branch_map[branch.id] = {
-                'name': branch.name,
-                'testIds': list(set(branch_id_map[branch.id]['tests'])),
-                'platformIds': list(set(branch_id_map[branch.id]['platforms'])),
-            }
-
-        cache_manifest(json.dumps({'testMap': test_map, 'platformMap': platform_map, 'branchMap': branch_map}))
-        self.response.out.write('OK')
index 8212a33..ce3fe64 100644 (file)
@@ -160,6 +160,7 @@ class Test(db.Model):
                     test.branches.append(branch.key())
                 if platform.key() not in test.platforms:
                     test.platforms.append(platform.key())
+                test.put()
                 existing_test[0] = test
                 return None
 
@@ -200,20 +201,6 @@ class TestResult(db.Model):
             valueMedian=_float_or_none(result, 'median'), valueStdev=_float_or_none(result, 'stdev'),
             valueMin=_float_or_none(result, 'min'), valueMax=_float_or_none(result, 'max'))
 
-    @staticmethod
-    def generate_runs(branch, platform, test_name):
-        builds = Build.all()
-        builds.filter('branch =', branch)
-        builds.filter('platform =', platform)
-
-        for build in builds:
-            results = TestResult.all()
-            results.filter('name =', test_name)
-            results.filter('build =', build)
-            for result in results:
-                yield build, result
-        raise StopIteration
-
 
 class ReportLog(db.Model):
     timestamp = db.DateTimeProperty(required=True)
index e6c37b1..9c09105 100644 (file)
@@ -250,6 +250,10 @@ class TestModelTests(DataStoreTestsBase):
         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
 
+        test = models.Test.get(test.key())
+        self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
+        self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
+
 
 class TestResultTests(DataStoreTestsBase):
     def _create_build(self):
@@ -285,29 +289,6 @@ class TestResultTests(DataStoreTestsBase):
         self.assertEqual(result.valueMin, 30.5)
         self.assertEqual(result.valueMax, 45)
 
-    def _create_results(self, test_name, values):
-        branch, platform, builder = _create_some_builder()
-        results = []
-        for i, value in enumerate(values):
-            build = models.Build(branch=branch, platform=platform, builder=builder,
-                buildNumber=i, revision=100 + i, timestamp=datetime.now())
-            build.put()
-            result = models.TestResult(name=test_name, build=build, value=value)
-            result.put()
-            results.append(result)
-        return branch, platform, results
-
-    def test_generate_runs(self):
-        branch, platform, results = self._create_results('some-test', [50.0, 51.0, 52.0, 49.0, 48.0])
-        last_i = 0
-        for i, (build, result) in enumerate(models.TestResult.generate_runs(branch, platform, "some-test")):
-            self.assertEqual(build.buildNumber, i)
-            self.assertEqual(build.revision, 100 + i)
-            self.assertEqual(result.name, 'some-test')
-            self.assertEqual(result.value, results[i].value)
-            last_i = i
-        self.assertTrue(last_i + 1, len(results))
-
 
 class ReportLogTests(DataStoreTestsBase):
     def _create_log_with_payload(self, payload):
diff --git a/Websites/webkit-perf.appspot.com/runs_handler.py b/Websites/webkit-perf.appspot.com/runs_handler.py
deleted file mode 100644 (file)
index 5715193..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/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 controller import cache_runs
-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 model_from_numeric_id
-
-
-class RunsHandler(webapp2.RequestHandler):
-    def post(self):
-        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
-
-        try:
-            test_id = int(self.request.get('id', 0))
-            branch_id = int(self.request.get('branchid', 0))
-            platform_id = int(self.request.get('platformid', 0))
-        except TypeError:
-            # FIXME: Output an error here
-            test_id = 0
-            branch_id = 0
-            platform_id = 0
-
-        # FIXME: Just fetch builds specified by "days"
-        # days = self.request.get('days', 365)
-
-        branch = model_from_numeric_id(branch_id, Branch)
-        platform = model_from_numeric_id(platform_id, Platform)
-        test = model_from_numeric_id(test_id, Test)
-        assert branch
-        assert platform
-        assert test
-
-        test_runs = []
-        averages = {}
-        values = []
-        timestamps = []
-
-        for build, result in TestResult.generate_runs(branch, platform, test.name):
-            builderId = build.builder.key().id()
-            posixTimestamp = mktime(build.timestamp.timetuple())
-            statistics = None
-            supplementary_revisions = None
-            if result.valueStdev != None and result.valueMin != None and result.valueMax != None:
-                statistics = {'stdev': result.valueStdev, 'min': result.valueMin, 'max': result.valueMax}
-            if build.chromiumRevision != None:
-                supplementary_revisions = {'Chromium': build.chromiumRevision}
-
-            test_runs.append([result.key().id(),
-                [build.key().id(), build.buildNumber, build.revision, supplementary_revisions],
-                posixTimestamp, result.value, 0,  # runNumber
-                [],  # annotations
-                builderId, statistics])
-
-            # 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)
-
-        result = 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'})
-        cache_runs(test_id, branch_id, platform_id, result)
-        self.response.out.write('OK')