[LNT] r242206 - Reapply "Start adding a simple REST API to LNT""

Chris Matthews cmatthews5 at apple.com
Tue Jul 14 14:45:59 PDT 2015


Author: cmatthews
Date: Tue Jul 14 16:45:59 2015
New Revision: 242206

URL: http://llvm.org/viewvc/llvm-project?rev=242206&view=rev
Log:
Reapply "Start adding a simple REST API to LNT""

This reverts commit r242149 to reapply r242070

Added:
    lnt/trunk/lnt/server/ui/api.py
    lnt/trunk/tests/server/ui/test_api.py
Modified:
    lnt/trunk/lnt/server/ui/app.py
    lnt/trunk/setup.py
    lnt/trunk/tests/server/ui/V4Pages.py

Added: lnt/trunk/lnt/server/ui/api.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/api.py?rev=242206&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/api.py (added)
+++ lnt/trunk/lnt/server/ui/api.py Tue Jul 14 16:45:59 2015
@@ -0,0 +1,161 @@
+from flask import current_app, g
+from flask import request
+from sqlalchemy.orm.exc import NoResultFound
+from flask_restful import Resource, reqparse, fields, marshal_with, abort
+
+parser = reqparse.RequestParser()
+parser.add_argument('db', type=str)
+
+
+def in_db(func):
+    """Extract the database information off the request and attach to
+    particular test suite and database."""
+    def wrap(*args, **kwargs):
+        db = kwargs.pop('db')
+        ts = kwargs.pop('ts')
+        g.db_name = db
+        g.testsuite_name = ts
+        g.db_info = current_app.old_config.databases.get(g.db_name)
+        if g.db_info is None:
+            abort(404, message="Invalid database.")
+        # Compute result.
+        result = func(*args, **kwargs)
+
+        # Make sure that any transactions begun by this request are finished.
+        request.get_db().rollback()
+        return result
+    return wrap
+
+
+def ts_path(path):
+    """Make a URL path with a database and test suite embedded in them."""
+    return "/api/db_<string:db>/v4/<string:ts>/" + path
+
+
+def with_ts(obj):
+    """For Url type fields to work, the objects we return must have a test-suite
+    and database attribute set, the function attempts to set them."""
+    if type(obj) == list:
+        # For lists, set them on all elements.
+        return [with_ts(x) for x in obj]
+    if type(obj) == dict:
+        # If already a dict, just add the fields.
+        new_obj = obj
+    else:
+        # Sqlalcamey objects are read-only and store their attributes in a
+        # sub-dict.  Make a copy so we can edit it.
+        new_obj = obj.__dict__.copy()
+
+    new_obj['db'] = g.db_name
+    new_obj['ts'] = g.testsuite_name
+    return new_obj
+
+
+# This date format is what the JavaScript in the LNT frontend likes.
+DATE_FORMAT = "iso8601"
+
+
+machines_fields = {
+    'id': fields.Integer,
+    'name': fields.String,
+    'os': fields.String,
+    'hardware': fields.String,
+}
+
+
+class Machines(Resource):
+    """List all the machines and give summary information."""
+    method_decorators = [in_db]
+
+    @marshal_with(machines_fields)
+    def get(self):
+        ts = request.get_testsuite()
+        changes = ts.query(ts.Machine).all()
+        return changes
+
+
+machine_fields = {
+    'id': fields.Integer,
+    'name': fields.String,
+    'os': fields.String,
+    'hardware': fields.String,
+    'runs': fields.List(fields.Url('runs')),
+}
+
+
+class Machine(Resource):
+    """Detailed results about a particular machine, including runs on it."""
+    method_decorators = [in_db]
+
+    @marshal_with(machine_fields)
+    def get(self, machine_id):
+
+        ts = request.get_testsuite()
+        try:
+            machine = ts.query(ts.Machine).filter(
+                    ts.Machine.id == machine_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid machine.")
+
+        machine = with_ts(machine)
+        machine_runs = ts.query(ts.Run.id).join(ts.Machine).filter(
+                ts.Machine.id == machine_id).all()
+
+        machine['runs'] = with_ts([dict(run_id=x[0]) for x in machine_runs])
+        print machine['runs']
+        return machine
+
+
+run_fields = {
+    'id': fields.Integer,
+    'start_time': fields.DateTime(dt_format=DATE_FORMAT),
+    'end_time': fields.DateTime(dt_format=DATE_FORMAT),
+    'machine_id': fields.Integer,
+    'machine': fields.Url("machine"),
+    'order_id': fields.Integer,
+    'order': fields.Url("order"),
+}
+
+
+class Runs(Resource):
+    method_decorators = [in_db]
+
+    @marshal_with(run_fields)
+    def get(self, run_id):
+        ts = request.get_testsuite()
+        try:
+            changes = ts.query(ts.Run).join(ts.Machine).filter(
+                ts.Run.id == run_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid run.")
+
+        changes = with_ts(changes)
+        return changes
+
+
+order_fields = {
+    'id': fields.Integer,
+    'llvm_project_revision': fields.String,
+    'next_order_id': fields.Integer,
+    'previous_order_id': fields.Integer,
+}
+
+
+class Order(Resource):
+    method_decorators = [in_db]
+
+    @marshal_with(order_fields)
+    def get(self, order_id):
+        ts = request.get_testsuite()
+        try:
+            changes = ts.query(ts.Order).filter(ts.Order.id == order_id).one()
+        except NoResultFound:
+            abort(404, message="Invalid order.")
+        return changes
+
+
+def load_api_resources(api):
+    api.add_resource(Machines, ts_path("machines"))
+    api.add_resource(Machine, ts_path("machine/<int:machine_id>"))
+    api.add_resource(Runs, ts_path("run/<int:run_id>"))
+    api.add_resource(Order, ts_path("order/<int:order_id>"))

Modified: lnt/trunk/lnt/server/ui/app.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/app.py?rev=242206&r1=242205&r2=242206&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/app.py (original)
+++ lnt/trunk/lnt/server/ui/app.py Tue Jul 14 16:45:59 2015
@@ -10,6 +10,8 @@ import flask
 from flask import current_app
 from flask import g
 from flask import url_for
+from flask import Flask
+from flask_restful import Resource, Api
 
 import lnt
 import lnt.server.db.v4db
@@ -17,6 +19,8 @@ import lnt.server.instance
 import lnt.server.ui.filters
 import lnt.server.ui.globals
 import lnt.server.ui.views
+from lnt.server.ui.api import load_api_resources
+
 
 class RootSlashPatchMiddleware(object):
     def __init__(self, app):
@@ -98,7 +102,11 @@ class App(flask.Flask):
 
         # Load the application routes.
         app.register_module(lnt.server.ui.views.frontend)
-                        
+
+        # Load the flaskRESTful API.
+        app.api = Api(app)
+        load_api_resources(app.api)
+
         return app
 
     @staticmethod

Modified: lnt/trunk/setup.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/setup.py?rev=242206&r1=242205&r2=242206&view=diff
==============================================================================
--- lnt/trunk/setup.py (original)
+++ lnt/trunk/setup.py Tue Jul 14 16:45:59 2015
@@ -90,5 +90,5 @@ http://llvm.org/svn/llvm-project/lnt/tru
             'lnt = lnt.lnttool:main',
             ],
         },
-    install_requires=['SQLAlchemy', 'Flask'],
+    install_requires=['SQLAlchemy', 'Flask', 'flask-restful'],
 )

Modified: lnt/trunk/tests/server/ui/V4Pages.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/ui/V4Pages.py?rev=242206&r1=242205&r2=242206&view=diff
==============================================================================
--- lnt/trunk/tests/server/ui/V4Pages.py (original)
+++ lnt/trunk/tests/server/ui/V4Pages.py Tue Jul 14 16:45:59 2015
@@ -15,16 +15,26 @@ from htmlentitydefs import name2codepoin
 
 import lnt.server.db.migrate
 import lnt.server.ui.app
+import json
 
 logging.basicConfig(level=logging.DEBUG)
 
 
-def check_code(client, url, expected_code=200):
-    resp = client.get(url, follow_redirects=False)
+def check_code(client, url, expected_code=200, data_to_send=None):
+    """Call a flask url, and make sure the return code is good."""
+    resp = client.get(url, follow_redirects=False, data=data_to_send)
     assert resp.status_code == expected_code, \
         "Call to %s returned: %d, not the expected %d"%(url, resp.status_code, expected_code)
     return resp
 
+
+def check_json(client, url, expected_code=200, data_to_send=None):
+    """Call a flask url, make sure the return code is good,
+    and grab reply data from the json payload."""
+    return json.loads(check_code(client, url, expected_code,
+                      data_to_send=data_to_send).data)
+
+
 def check_redirect(client, url, expected_redirect_regex):
     resp = client.get(url, follow_redirects=False)
     assert resp.status_code == 302, \

Added: lnt/trunk/tests/server/ui/test_api.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/ui/test_api.py?rev=242206&view=auto
==============================================================================
--- lnt/trunk/tests/server/ui/test_api.py (added)
+++ lnt/trunk/tests/server/ui/test_api.py Tue Jul 14 16:45:59 2015
@@ -0,0 +1,100 @@
+# Check that the LNT REST JSON API is working.
+# create temporary instance
+# RUN: rm -rf %t.instance
+# RUN: python %{shared_inputs}/create_temp_instance.py \
+# RUN:     %{shared_inputs}/SmallInstance \
+# RUN:     %t.instance %S/Inputs/V4Pages_extra_records.sql
+#
+# RUN: python %s %t.instance
+
+import unittest
+import logging
+import sys
+
+import lnt.server.db.migrate
+import lnt.server.ui.app
+
+from V4Pages import check_json
+logging.basicConfig(level=logging.DEBUG)
+
+machines_expected_response = [{u'hardware': u'x86_64',
+                               u'os': u'Darwin 11.3.0',
+                               u'id': 1,
+                               u'name': u'localhost__clang_DEV__x86_64'},
+                              {u'hardware': u'AArch64',
+                               u'os': u'linux',
+                               u'id': 2,
+                               u'name': u'machine2'},
+                              {u'hardware': u'AArch64',
+                               u'os': u'linux',
+                               u'id': 3,
+                               u'name': u'machine3'}]
+
+# Machine add some extra fields, so add them.
+machine_expected_response = list(machines_expected_response)
+machine_expected_response[0] = machines_expected_response[0].copy()
+machine_expected_response[0][u'runs'] = [u'/api/db_default/v4/nts/run/1',
+                                         u'/api/db_default/v4/nts/run/2']
+
+machine_expected_response[1] = machines_expected_response[1].copy()
+machine_expected_response[1][u'runs'] = [u'/api/db_default/v4/nts/run/3']
+
+machine_expected_response[2] = machines_expected_response[2].copy()
+machine_expected_response[2][u'runs'] = [u'/api/db_default/v4/nts/run/4']
+
+
+run_expected_response = [{u'end_time': u'2012-04-11T16:28:58',
+                          u'id': 1,
+                          u'machine_id': 1,
+                          u'machine': u'/api/db_default/v4/nts/machine/1',
+                          u'order_id': 3,
+                          u'order': u'/api/db_default/v4/nts/order/3',
+                          u'start_time': u'2012-04-11T16:28:23'}]
+
+order_expected_response = {u'id': 3,
+                           u'llvm_project_revision': "154331",
+                           u'next_order_id': 0,
+                           u'previous_order_id': 4}
+
+
+class JSONAPITester(unittest.TestCase):
+    """Test the REST api."""
+
+    def setUp(self):
+        """Bind to the LNT test instance."""
+        _, instance_path = sys.argv
+        app = lnt.server.ui.app.App.create_standalone(instance_path)
+        app.testing = True
+        self.client = app.test_client()
+
+    def test_machine_api(self):
+        """Check /machines and /machine/n return expected results from
+        testdb.
+        """
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/machines')
+        self.assertEquals(j, machines_expected_response)
+        for i in xrange(0, len(machine_expected_response)):
+            j = check_json(client, 'api/db_default/v4/nts/machine/' +
+                           str(i + 1))
+            self.assertEquals(j, machine_expected_response[i])
+
+    def test_run_api(self):
+        """Check /run/n returns expected run information."""
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/run/1')
+        self.assertEquals(j, run_expected_response[0])
+
+        for i in xrange(0, len(run_expected_response)):
+            j = check_json(client, 'api/db_default/v4/nts/run/' + str(i + 1))
+            self.assertEquals(j, run_expected_response[i])
+
+    def test_order_api(self):
+        """ Check /order/n returns the expected order information."""
+        client = self.client
+        j = check_json(client, 'api/db_default/v4/nts/order/3')
+        self.assertEquals(j, order_expected_response)
+
+
+if __name__ == '__main__':
+    unittest.main(argv=[sys.argv[0], ])





More information about the llvm-commits mailing list