[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