[LNT] r249770 - Prototype of regression tracking feature for LNT

Chris Matthews via llvm-commits llvm-commits at lists.llvm.org
Thu Oct 8 16:12:05 PDT 2015


Author: cmatthews
Date: Thu Oct  8 18:12:04 2015
New Revision: 249770

URL: http://llvm.org/viewvc/llvm-project?rev=249770&view=rev
Log:
Prototype of regression tracking feature for LNT

This patch corrosponds to my mailing list discussion about a regression tracking
feature for LNT.  FieldChanges are used as the base for detected changes.  The
feature introduces 3 new pages in LNT, a triage page showing recent performance
changes and creating regressions, a tracking page, showing all tracked
regressions, and a details page to show more about each regression, and its
current status.

The FieldChange table is dropped (just renamed actually), the current field
change system was producing bad data.  New tables store regression, changes
that we are interested in, changes to ignore.  Graphs load through via JQuery
Ajax calls.

Added:
    lnt/trunk/lnt/server/db/migrations/upgrade_7_to_8.py
    lnt/trunk/lnt/server/ui/regression_views.py
    lnt/trunk/lnt/server/ui/templates/v4_new_regressions.html
    lnt/trunk/lnt/server/ui/templates/v4_regression_detail.html
    lnt/trunk/lnt/server/ui/templates/v4_regression_list.html
Modified:
    lnt/trunk/lnt/server/db/fieldchange.py
    lnt/trunk/lnt/server/db/migrations/upgrade_0_to_1.py
    lnt/trunk/lnt/server/db/migrations/upgrade_2_to_3.py
    lnt/trunk/lnt/server/db/testsuitedb.py
    lnt/trunk/lnt/server/db/v4db.py
    lnt/trunk/lnt/server/reporting/analysis.py
    lnt/trunk/lnt/server/ui/app.py
    lnt/trunk/lnt/server/ui/templates/layout.html
    lnt/trunk/lnt/server/ui/templates/utils.html
    lnt/trunk/requirements.txt
    lnt/trunk/tests/server/db/CreateV4TestSuiteInstance.py
    lnt/trunk/tests/server/ui/test_api.py

Modified: lnt/trunk/lnt/server/db/fieldchange.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/fieldchange.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/fieldchange.py (original)
+++ lnt/trunk/lnt/server/db/fieldchange.py Thu Oct  8 18:12:04 2015
@@ -4,6 +4,15 @@ import sqlalchemy.sql
 import lnt.server.reporting.analysis
 from lnt.testing.util.commands import warning
 
+
+# How many runs backwards to use in the previous run set.
+# More runs are slower (more DB access), but may provide
+# more accurate results.
+FIELD_CHANGE_LOOKBACK = 10
+
+from lnt.testing.util.commands import note
+
+
 def regenerate_fieldchanges_for_run(ts, run):
     """Regenerate the set of FieldChange objects for the given run.
     """
@@ -11,10 +20,14 @@ def regenerate_fieldchanges_for_run(ts,
     # Allow for potentially a few different runs, previous_runs, next_runs
     # all with the same order_id which we will aggregate together to make
     # our comparison result.
-    runs = ts.query(ts.Run).filter(ts.Run.order_id == run.order_id).all()
-    previous_runs = ts.get_previous_runs_on_machine(run, 1)
-    next_runs = ts.get_next_runs_on_machine(run, 1)
-    
+
+    runs = ts.query(ts.Run). \
+        filter(ts.Run.order_id == run.order_id). \
+        filter(ts.Run.machine_id == run.machine_id). \
+        all()
+    previous_runs = ts.get_previous_runs_on_machine(run, FIELD_CHANGE_LOOKBACK)
+    next_runs = ts.get_next_runs_on_machine(run, FIELD_CHANGE_LOOKBACK)
+
     # Find our start/end order.
     if previous_runs != []:
         start_order = previous_runs[0].order
@@ -26,7 +39,7 @@ def regenerate_fieldchanges_for_run(ts,
         end_order = run.order
     
     # Load our run data for the creation of the new fieldchanges.
-    runs_to_load = [r.id for r in (runs + previous_runs + next_runs)]
+    runs_to_load = [r.id for r in (runs + previous_runs)]
 
     # When the same rev is submitted many times, the database accesses here
     # can be huge, and it is almost always an error to have the same rev
@@ -35,32 +48,49 @@ def regenerate_fieldchanges_for_run(ts,
     if run_size > 50:
         warning("Generating field changes for {} runs."
                 "That will be very slow.".format(run_size))
-
     runinfo = lnt.server.reporting.analysis.RunInfo(ts, runs_to_load)
 
     # Only store fieldchanges for "metric" samples like execution time;
     # not for fields with other data, e.g. hash of a binary
     for field in list(ts.Sample.get_metric_fields()):
         for test_id in runinfo.test_ids:
+            f = None
             result = runinfo.get_comparison_result(runs, previous_runs,
                                                    test_id, field)
-            if result.is_result_interesting():
+            # Try and find a matching FC and update, else create one.
+            try:
+                f = ts.query(ts.FieldChange) \
+                    .filter(ts.FieldChange.start_order == start_order) \
+                    .filter(ts.FieldChange.end_order == end_order) \
+                    .filter(ts.FieldChange.test_id == test_id) \
+                    .filter(ts.FieldChange.machine == run.machine) \
+                    .filter(ts.FieldChange.field == field) \
+                    .one()
+            except sqlalchemy.orm.exc.NoResultFound:
+                f = None
+
+            if not result.is_result_performance_change() and f:
+                # With more data, its not a regression. Kill it!
+                note("Removing field change: {}".format(f.id))
+                ts.delete(f)
+                continue
+
+            if result.is_result_performance_change() and not f:
+
                 f = ts.FieldChange(start_order=start_order,
                                    end_order=run.order,
-                                   test=None,
                                    machine=run.machine,
-                                   field=field)
-                f.test_id = test_id
-                ts.add(f)
-            
-            result = runinfo.get_comparison_result(runs, next_runs,
-                                                   test_id, field)
-            if result.is_result_interesting():
-                f = ts.FieldChange(start_order=run.order,
-                                   end_order=end_order,
                                    test=None,
-                                   machine=run.machine,
                                    field=field)
                 f.test_id = test_id
                 ts.add(f)
 
+                note("Found field change: {}".format(run.machine))
+
+                ts.commit()
+
+            # Always update FCs with new values.
+            if f:
+                f.old_value = result.previous
+                f.new_value = result.current
+                f.run = run

Modified: lnt/trunk/lnt/server/db/migrations/upgrade_0_to_1.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/migrations/upgrade_0_to_1.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/migrations/upgrade_0_to_1.py (original)
+++ lnt/trunk/lnt/server/db/migrations/upgrade_0_to_1.py Thu Oct  8 18:12:04 2015
@@ -257,18 +257,19 @@ def get_base_for_testsuite(test_suite):
         class_dict = locals()
         for item in test_suite.sample_fields:
             if item.name in class_dict:
-                raise ValueError,"test suite defines reserved key %r" % (
-                    name,)
+                raise ValueError("test suite defines reserved key {}"
+                                 .format(name))
 
             if item.type.name == 'Real':
                 item.column = Column(item.name, Float)
             elif item.type.name == 'Status':
                 item.column = Column(item.name, Integer, ForeignKey(
                         StatusKind.id))
+            elif item.type.name == 'Hash':
+                continue
             else:
-                raise ValueError,(
-                    "test suite defines unknown sample type %r" (
-                        item.type.name,))
+                raise ValueError("test suite defines unknown sample type {}"
+                                 .format(item.type.name))
 
             class_dict[item.name] = item.column
 

Modified: lnt/trunk/lnt/server/db/migrations/upgrade_2_to_3.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/migrations/upgrade_2_to_3.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/migrations/upgrade_2_to_3.py (original)
+++ lnt/trunk/lnt/server/db/migrations/upgrade_2_to_3.py Thu Oct  8 18:12:04 2015
@@ -15,6 +15,10 @@ import lnt.server.db.migrations.upgrade_
 
 ###
 # Upgrade TestSuite
+def get_base(test_suite):
+    """Return the schema base with field changes added."""
+    return add_fieldchange(test_suite)
+
 
 def add_fieldchange(test_suite):
     # Grab the Base for the previous schema so that we have all
@@ -66,4 +70,3 @@ def upgrade(engine):
     # Create our FieldChangeField table and commit.
     upgrade_testsuite(engine, session, 'nts')
     upgrade_testsuite(engine, session, 'compile')
-

Added: lnt/trunk/lnt/server/db/migrations/upgrade_7_to_8.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/migrations/upgrade_7_to_8.py?rev=249770&view=auto
==============================================================================
--- lnt/trunk/lnt/server/db/migrations/upgrade_7_to_8.py (added)
+++ lnt/trunk/lnt/server/db/migrations/upgrade_7_to_8.py Thu Oct  8 18:12:04 2015
@@ -0,0 +1,118 @@
+# Version 8 of the database updates FieldChanges as well as adds tables
+# for Regression Tracking features.
+
+import os
+import sys
+
+import sqlalchemy
+from sqlalchemy import *
+from sqlalchemy.schema import Index
+from sqlalchemy.orm import relation
+
+# Import the original schema from upgrade_0_to_1 since upgrade_1_to_2 does not
+# change the actual schema, but rather adds functionality vis-a-vis orders.
+import lnt.server.db.migrations.upgrade_0_to_1 as upgrade_0_to_1
+import lnt.server.db.migrations.upgrade_2_to_3 as upgrade_2_to_3
+
+
+###
+# Upgrade TestSuite
+def add_regressions(test_suite):
+    """Given a test suite with a database connection and a test-suite
+    name, make the regression sqalchmey database objects for that test-suite.
+    """
+    # Grab the Base for the previous schema so that we have all
+    # the definitions we need.
+    Base = upgrade_2_to_3.get_base(test_suite)
+    # Grab our db_key_name for our test suite so we can properly
+    # prefix our fields/table names.
+
+    db_key_name = test_suite.db_key_name
+    # Replace the field change definition with a new one, the old table
+    # is full of bad data.
+    Base.metadata.remove(Base.metadata.tables["{}_FieldChange".format(db_key_name)])
+
+    class FieldChange(Base):
+        """FieldChange represents a change in between the values
+        of the same field belonging to two samples from consecutive runs."""
+
+        __tablename__ = db_key_name + '_FieldChangeV2'
+        id = Column("ID", Integer, primary_key = True)
+        old_value = Column("OldValue", Float)
+        new_value = Column("NewValue", Float)
+        start_order_id = Column("StartOrderID", Integer,
+                                ForeignKey("%s_Order.ID" % db_key_name))
+        end_order_id = Column("EndOrderID", Integer,
+                              ForeignKey("%s_Order.ID" % db_key_name))
+        test_id = Column("TestID", Integer,
+                         ForeignKey("%s_Test.ID" % db_key_name))
+        machine_id = Column("MachineID", Integer,
+                            ForeignKey("%s_Machine.ID" % db_key_name))
+        field_id = Column("FieldID", Integer,
+                           ForeignKey(upgrade_0_to_1.SampleField.id))
+        # Could be from many runs, but most recent one is interesting.
+        run_id = Column("RunID", Integer,
+                            ForeignKey("%s_Run.ID" % db_key_name))
+
+
+    class Regression(Base):
+        """Regession hold data about a set of RegressionIndicies."""
+
+        __tablename__ = db_key_name + '_Regression'
+        id = Column("ID", Integer, primary_key=True)
+        title = Column("Title", String(256), unique=False, index=False)
+        bug = Column("BugLink", String(256), unique=False, index=False)
+        state = Column("State", Integer)
+
+    class RegressionIndicator(Base):
+        """"""
+        __tablename__ = db_key_name + '_RegressionIndicator'
+        id = Column("ID", Integer, primary_key=True)
+        regression_id = Column("RegressionID", Integer,
+                               ForeignKey("%s_Regression.ID" % db_key_name))
+
+        field_change_id = Column("FieldChangeID", Integer,
+                        ForeignKey("%s_FieldChangeV2.ID" % db_key_name))
+
+
+    class ChangeIgnore(Base):
+        """Changes to ignore in the web interface."""
+
+        __tablename__ = db_key_name + '_ChangeIgnore'
+        id = Column("ID", Integer, primary_key=True)
+
+        field_change_id = Column("ChangeIgnoreID", Integer,
+                             ForeignKey("%s_FieldChangeV2.ID" % db_key_name))
+
+    return Base
+
+
+
+def upgrade_testsuite(engine, session, name):
+    # Grab Test Suite.
+    test_suite = session.query(upgrade_0_to_1.TestSuite).\
+                 filter_by(name=name).first()
+    assert(test_suite is not None)
+    
+    # Add FieldChange to the test suite.
+    Base = add_regressions(test_suite)
+
+    # Create tables. We commit now since databases like Postgres run
+    # into deadlocking issues due to previous queries that we have run
+    # during the upgrade process. The commit closes all of the
+    # relevant transactions allowing us to then perform our upgrade.
+    session.commit()
+    Base.metadata.create_all(engine)
+    # Commit changes (also closing all relevant transactions with
+    # respect to Postgres like databases).
+    session.commit()
+    
+
+
+def upgrade(engine):
+    # Create a session.
+    session = sqlalchemy.orm.sessionmaker(engine)()
+    
+    # Create our FieldChangeField table and commit.
+    upgrade_testsuite(engine, session, 'nts')
+    upgrade_testsuite(engine, session, 'compile')

Modified: lnt/trunk/lnt/server/db/testsuitedb.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/testsuitedb.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/testsuitedb.py (original)
+++ lnt/trunk/lnt/server/db/testsuitedb.py Thu Oct  8 18:12:04 2015
@@ -390,8 +390,10 @@ class TestSuiteDB(object):
             """FieldChange represents a change in between the values
             of the same field belonging to two samples from consecutive runs."""
             
-            __tablename__ = db_key_name + '_FieldChange'
+            __tablename__ = db_key_name + '_FieldChangeV2'
             id = Column("ID", Integer, primary_key = True)
+            old_value = Column("OldValue", Float)
+            new_value = Column("NewValue", Float)
             start_order_id = Column("StartOrderID", Integer,
                                     ForeignKey("%s_Order.ID" % db_key_name))
             end_order_id = Column("EndOrderID", Integer,
@@ -402,6 +404,9 @@ class TestSuiteDB(object):
                                 ForeignKey("%s_Machine.ID" % db_key_name))
             field_id = Column("FieldID", Integer,
                               ForeignKey(self.v4db.SampleField.id))
+            # Could be from many runs, but most recent one is interesting.
+            run_id = Column("RunID", Integer,
+                                ForeignKey("%s_Run.ID" % db_key_name))
             
             start_order = sqlalchemy.orm.relation(Order,
                                                   primaryjoin='FieldChange.'\
@@ -415,27 +420,88 @@ class TestSuiteDB(object):
                                             primaryjoin= \
                                               self.v4db.SampleField.id == \
                                               field_id)
-            
-            def __init__(self, start_order, end_order, test, machine,
-                         field):
+            run = sqlalchemy.orm.relation(Run)
+
+            def __init__(self, start_order, end_order, machine,
+                         test, field):
                 self.start_order = start_order
                 self.end_order = end_order
-                self.test = test
                 self.machine = machine
                 self.field = field
+                self.test = test
 
             def __repr__(self):
                 return '%s_%s%r' % (db_key_name, self.__class__.__name__,
                                     (self.start_order, self.end_order,
                                      self.test, self.machine, self.field))
 
+        class Regression(self.base, ParameterizedMixin):
+            """Regession hold data about a set of RegressionIndicies."""
+
+            __tablename__ = db_key_name + '_Regression'
+            id = Column("ID", Integer, primary_key=True)
+            title = Column("Title", String(256), unique=False, index=False)
+            bug = Column("BugLink", String(256), unique=False, index=False)
+            state = Column("State", Integer)
+
+            def __init__(self, title, bug):
+                self.title = title
+                self.bug = bug
+
+            def __repr__(self):
+                return '%s_%s:"%s"' % (db_key_name, self.__class__.__name__,
+                                    self.title)
+
+        class RegressionIndicator(self.base, ParameterizedMixin):
+            """"""
+
+            __tablename__ = db_key_name + '_RegressionIndicator'
+            id = Column("ID", Integer, primary_key=True)
+            regression_id = Column("RegressionID", Integer,
+                                   ForeignKey("%s_Regression.ID" % db_key_name))
+
+            field_change_id = Column("FieldChangeID", Integer,
+                            ForeignKey("%s_FieldChangeV2.ID" % db_key_name))
+
+            regression = sqlalchemy.orm.relation(Regression)
+            field_change = sqlalchemy.orm.relation(FieldChange)
+
+            def __init__(self, regression, field_change):
+                self.regression = regression
+                self.field_change = field_change
+
+            def __repr__(self):
+                return '%s_%s%r' % (db_key_name, self.__class__.__name__,(
+                        self.id, self.regression, self.field_change))
+
+        class ChangeIgnore(self.base, ParameterizedMixin):
+            """Changes to ignore in the web interface."""
+
+            __tablename__ = db_key_name + '_ChangeIgnore'
+            id = Column("ID", Integer, primary_key=True)
+
+            field_change_id = Column("ChangeIgnoreID", Integer,
+                                     ForeignKey("%s_FieldChangeV2.ID" % db_key_name))
+
+            field_change = sqlalchemy.orm.relation(FieldChange)
+
+            def __init__(self, field_change):
+                self.field_change = field_change
+
+            def __repr__(self):
+                return '%s_%s%r' % (db_key_name, self.__class__.__name__,(
+                                    self.id, self.field_change))
+
         self.Machine = Machine
         self.Run = Run
         self.Test = Test
         self.Sample = Sample
         self.Order = Order
         self.FieldChange = FieldChange
-        
+        self.Regression = Regression
+        self.RegressionIndicator = RegressionIndicator
+        self.ChangeIgnore = ChangeIgnore
+
         # Create the compound index we cannot declare inline.
         sqlalchemy.schema.Index("ix_%s_Sample_RunID_TestID" % db_key_name,
                                 Sample.run_id, Sample.test_id)
@@ -450,6 +516,7 @@ class TestSuiteDB(object):
         # Add several shortcut aliases, similar to the ones on the v4db.
         self.session = self.v4db.session
         self.add = self.v4db.add
+        self.delete = self.v4db.delete
         self.commit = self.v4db.commit
         self.query = self.v4db.query
         self.rollback = self.v4db.rollback

Modified: lnt/trunk/lnt/server/db/v4db.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/db/v4db.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/db/v4db.py (original)
+++ lnt/trunk/lnt/server/db/v4db.py Thu Oct  8 18:12:04 2015
@@ -94,6 +94,7 @@ class V4DB(object):
 
         # Add several shortcut aliases.
         self.add = self.session.add
+        self.delete = self.session.delete
         self.commit = self.session.commit
         self.query = self.session.query
         self.rollback = self.session.rollback

Modified: lnt/trunk/lnt/server/reporting/analysis.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/reporting/analysis.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/reporting/analysis.py (original)
+++ lnt/trunk/lnt/server/reporting/analysis.py Thu Oct  8 18:12:04 2015
@@ -120,6 +120,12 @@ class ComparisonResult:
                           self.confidence_lv,
                           bool(self.bigger_is_better))
 
+    def is_result_performance_change(self):
+        """Check if we think there was a performance change."""
+        if self.get_value_status() in (REGRESSED, IMPROVED):
+            return True
+        return False
+
     def is_result_interesting(self):
         """is_result_interesting() -> bool
 
@@ -220,7 +226,12 @@ class ComparisonResult:
 
 class RunInfo(object):
     def __init__(self, testsuite, runs_to_load,
-                 aggregation_fn=stats.safe_min, confidence_lv=.05):
+                 aggregation_fn=stats.safe_min, confidence_lv=.05,
+                 only_tests=None):
+        """Get all the samples needed to build a CR.
+        runs_to_load are the run IDs of the runs to get the samples from.
+        if only_tests is passed, only samples form those test IDs are fetched.
+        """
         self.testsuite = testsuite
         self.aggregation_fn = aggregation_fn
         self.confidence_lv = confidence_lv
@@ -228,7 +239,7 @@ class RunInfo(object):
         self.sample_map = util.multidict()
         self.loaded_run_ids = set()
 
-        self._load_samples_for_runs(runs_to_load)
+        self._load_samples_for_runs(runs_to_load, only_tests)
 
     @property
     def test_ids(self):
@@ -321,7 +332,7 @@ class RunInfo(object):
                                 confidence_lv=0,
                                 bigger_is_better=field.bigger_is_better)
 
-    def _load_samples_for_runs(self, run_ids):
+    def _load_samples_for_runs(self, run_ids, only_tests):
         # Find the set of new runs to load.
         to_load = set(run_ids) - self.loaded_run_ids
         if not to_load:
@@ -335,6 +346,8 @@ class RunInfo(object):
                    self.testsuite.Sample.test_id]
         columns.extend(f.column for f in self.testsuite.sample_fields)
         q = self.testsuite.query(*columns)
+        if only_tests:
+            q = q.filter(self.testsuite.Sample.test_id.in_(only_tests))
         q = q.filter(self.testsuite.Sample.run_id.in_(to_load))
         for data in q:
             run_id = data[0]

Modified: lnt/trunk/lnt/server/ui/app.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/app.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/app.py (original)
+++ lnt/trunk/lnt/server/ui/app.py Thu Oct  8 18:12:04 2015
@@ -19,6 +19,8 @@ import lnt.server.instance
 import lnt.server.ui.filters
 import lnt.server.ui.globals
 import lnt.server.ui.views
+
+import lnt.server.ui.regression_views
 from lnt.server.ui.api import load_api_resources
 
 

Added: lnt/trunk/lnt/server/ui/regression_views.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/regression_views.py?rev=249770&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/regression_views.py (added)
+++ lnt/trunk/lnt/server/ui/regression_views.py Thu Oct  8 18:12:04 2015
@@ -0,0 +1,266 @@
+import datetime
+from flask import g
+from flask import render_template
+from flask import request
+from flask import make_response
+from flask import flash
+from flask import redirect
+
+# import sqlalchemy.sql
+# from sqlalchemy.orm.exc import NoResultFound
+
+from lnt.server.ui.decorators import v4_route
+from lnt.server.reporting.analysis import RunInfo
+import lnt.server.reporting.analysis
+from lnt.server.ui.globals import db_url_for, v4_url_for
+
+from collections import namedtuple
+from loremipsum import get_sentences
+from random import randint
+from sqlalchemy import desc, asc
+from lnt.server.ui.util import FLASH_DANGER, FLASH_INFO, FLASH_SUCCESS
+from lnt.server.reporting.analysis import REGRESSED
+from wtforms import SelectMultipleField, StringField, widgets
+from flask_wtf import Form
+from wtforms.validators import DataRequired
+
+
+class MultiCheckboxField(SelectMultipleField):
+    """
+    A multiple-select, except displays a list of checkboxes.
+
+    Iterating the field will produce subfields, allowing custom rendering of
+    the enclosed checkbox fields.
+    """
+    widget = widgets.ListWidget(prefix_label=False)
+    option_widget = widgets.CheckboxInput()
+
+
+class TriagePageSelectedForm(Form):
+    field_changes = MultiCheckboxField("Changes", coerce=int)
+    name = StringField('name', validators=[DataRequired()])
+
+
+ChangeData = namedtuple("ChangeData", ["ri", "cr", "run", "latest_cr"])
+
+
+def get_fieldchange(ts, id):
+    return ts.query(ts.FieldChange).filter(ts.FieldChange.id == id).one()
+
+
+class PrecomputedCR():
+    """Make a thing that looks like a comprison result, that is derived
+    from a field change."""
+    previous = 0
+    current = 0
+    pct_delta = 0.00
+    bigger_is_better = False
+
+    def __init__(self, old, new, bigger_is_better):
+        self.previous = old
+        self.current = new
+        self.delta = new - old
+        self.pct_delta = self.delta / old
+
+    def get_test_status(self):
+        return True
+
+    def get_value_status(self, ignore_small=True):
+        return REGRESSED
+
+
+def new_regression(ts, field_changes):
+    """Make a new regression and add to DB."""
+    today = datetime.date.today()
+    MSG = "Regression of {} benchmarks on {}"
+    title = MSG.format(len(field_changes),
+                       today.strftime('%b %d %Y'))
+    regression = ts.Regression(title, "")
+    ts.add(regression)
+    for fc_id in field_changes:
+        fc = get_fieldchange(ts, fc_id)
+        ri1 = ts.RegressionIndicator(regression, fc)
+        ts.add(ri1)
+    ts.commit()
+    return regression
+
+
+ at v4_route("/regressions/new", methods=["GET", "POST"])
+def v4_new_regressions():
+    form = TriagePageSelectedForm(request.form)
+    ts = request.get_testsuite()
+    if request.method == 'POST' and request.form['btn'] == "Create New Regression":
+        regression = new_regression(ts, form.field_changes.data)
+        flash("Created " + regression.title, FLASH_SUCCESS)
+        return redirect(v4_url_for("v4_regression_list",
+                        highlight=regression.id))
+    if request.method == 'POST' and request.form['btn'] == "Ignore Changes":
+        msg = "Ignoring changes: "
+        ignored = []
+        for fc_id in form.field_changes.data:
+            ignored.append(str(fc_id))
+            fc = get_fieldchange(ts, fc_id)
+            ignored_change = ts.ChangeIgnore(fc)
+            ts.add(ignored_change)
+        ts.commit()
+        flash(msg + ", ".join(ignored), FLASH_SUCCESS)
+
+#    d = datetime.datetime.now()
+#    two_weeks_ago = d - datetime.timedelta(days=14)
+    recent_fieldchange = ts.query(ts.FieldChange) \
+        .join(ts.Test) \
+        .outerjoin(ts.ChangeIgnore) \
+        .filter(ts.ChangeIgnore.id == None) \
+        .outerjoin(ts.RegressionIndicator) \
+        .filter(ts.RegressionIndicator.id == None) \
+        .order_by(desc(ts.FieldChange.id)) \
+        .limit(500) \
+        .all()
+    crs = []
+
+    form.field_changes.choices = list()
+    for fc in recent_fieldchange:
+        if fc.old_value is None:
+            cr, key_run = get_cr_for_field_change(ts, fc)
+        else:
+            cr = PrecomputedCR(fc.old_value, fc.new_value, fc.field.bigger_is_better)
+            key_run = get_first_runs_of_fieldchange(ts, fc)
+        current_cr, _ = get_cr_for_field_change(ts, fc, current=True)
+        crs.append(ChangeData(fc, cr, key_run, current_cr))
+        form.field_changes.choices.append((fc.id, 1,))
+    return render_template("v4_new_regressions.html",
+                           testsuite_name=g.testsuite_name,
+                           changes=crs, analysis=lnt.server.reporting.analysis,
+                           form=form)
+
+
+ChangeRuns = namedtuple("ChangeRuns", ["before", "after"])
+
+
+def get_runs_for_order_and_machine(ts, order_id, machine_id):
+    """Collect all the runs for a particular order/machine combo."""
+    runs = ts.query(ts.Run) \
+        .filter(ts.Run.machine_id == machine_id) \
+        .filter(ts.Run.order_id == order_id) \
+        .all()
+    return runs
+
+
+def get_runs_of_fieldchange(ts, fc):
+    before_runs = get_runs_for_order_and_machine(ts, fc.start_order_id,
+                                                 fc.machine_id)
+    after_runs = get_runs_for_order_and_machine(ts, fc.end_order_id,
+                                                fc.machine_id)
+    return ChangeRuns(before_runs, after_runs)
+
+
+def get_current_runs_of_fieldchange(ts, fc):
+    before_runs = get_runs_for_order_and_machine(ts, fc.start_order_id,
+                                                 fc.machine_id)
+    newest_order = get_all_orders_for_machine(ts, fc.machine_id)[-1]
+
+    after_runs = get_runs_for_order_and_machine(ts, newest_order.id,
+                                                fc.machine_id)
+    return ChangeRuns(before_runs, after_runs)
+
+
+def get_first_runs_of_fieldchange(ts, fc):
+    # import ipdb; ipdb.set_trace()
+    run = ts.query(ts.Run) \
+        .filter(ts.Run.machine_id == fc.machine_id) \
+        .filter(ts.Run.order_id == fc.end_order_id) \
+        .first()
+    return run
+
+
+def get_all_orders_for_machine(ts, machine):
+    """Get all the oredrs for this sa machine."""
+    return ts.query(ts.Order) \
+        .join(ts.Run) \
+        .filter(ts.Run.machine_id == machine) \
+        .order_by(asc(ts.Order.llvm_project_revision)) \
+        .all()
+
+
+def get_cr_for_field_change(ts, field_change, current=False):
+    """Given a filed_change, calculate a comparison result for that change. 
+    And the last run."""
+    if current:
+        runs = get_current_runs_of_fieldchange(ts, field_change)
+    else:
+        runs = get_runs_of_fieldchange(ts, field_change)
+    runs_all = list(runs.before)
+    runs_all.extend(runs.after)
+    ri = RunInfo(ts, [r.id for r in runs_all], only_tests=[field_change.test_id])
+    cr = ri.get_comparison_result(runs.after, runs.before,
+                                  field_change.test.id, field_change.field)
+    return cr, runs.after[0]
+
+
+ at v4_route("/regressions/")
+def v4_regression_list():
+    ts = request.get_testsuite()
+
+    regression_info = ts.query(ts.Regression) \
+        .all()[::-1]
+
+    return render_template("v4_regression_list.html",
+                           testsuite_name=g.testsuite_name,
+                           regressions=regression_info,
+                           highlight=request.args.get('highlight'))
+
+
+class EditRegressionForm(Form):
+    title = StringField(u'Title', validators=[DataRequired()])
+    bug = StringField(u'Bug', validators=[DataRequired()])
+    field_changes = MultiCheckboxField("Changes", coerce=int)
+
+
+ at v4_route("/regressions/<int:id>",  methods=["GET", "POST"])
+def v4_regression_detail(id):
+    ts = request.get_testsuite()
+    form = EditRegressionForm(request.form)
+
+    regression_info = ts.query(ts.Regression) \
+        .filter(ts.Regression.id == id) \
+        .one()
+    if request.method == 'POST' and request.form['save_btn'] == "Save Changes":
+        regression_info.title = form.title.data
+        regression_info.bug = form.bug.data
+        ts.commit()
+        flash("Updated " + regression_info.title, FLASH_SUCCESS)
+        return redirect(v4_url_for("v4_regression_list",
+                        highlight=regression_info.id))
+    if request.method == 'POST' and request.form['save_btn'] == "Split Regression":
+        # For each of the regression indicators, grab their field ids.
+
+        res_inds = ts.query(ts.RegressionIndicator) \
+            .filter(ts.RegressionIndicator.id.in_(form.field_changes.data)) \
+            .all()
+        fc_ids = [x.field_change_id for x in res_inds]
+        second_regression = new_regression(ts, fc_ids)
+        # Now remove our links to this regression.
+        for res_ind in res_inds:
+            ts.delete(res_ind)
+        ts.commit()
+        flash("Split " + second_regression.title, FLASH_SUCCESS)
+        return redirect(v4_url_for("v4_regression_list",
+                        highlight=second_regression.id))
+    form.field_changes.choices = list()
+    regression_indicators = ts.query(ts.RegressionIndicator) \
+        .filter(ts.RegressionIndicator.regression_id == id) \
+        .all()
+    indicators = []
+    for regression in regression_indicators:
+        fc = regression.field_change
+        cr, key_run = get_cr_for_field_change(ts, fc)
+        latest_cr, _ = get_cr_for_field_change(ts, fc, current=True)
+        indicators.append(ChangeData(fc, cr, key_run, latest_cr))
+        form.field_changes.choices.append((regression.id, 1,))
+    form.title.data = regression_info.title
+    form.bug.data = regression_info.bug
+
+    return render_template("v4_regression_detail.html",
+                           testsuite_name=g.testsuite_name,
+                           regression=regression_info, indicators=indicators,
+                           form=form, analysis=lnt.server.reporting.analysis)

Modified: lnt/trunk/lnt/server/ui/templates/layout.html
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/templates/layout.html?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/templates/layout.html (original)
+++ lnt/trunk/lnt/server/ui/templates/layout.html Thu Oct  8 18:12:04 2015
@@ -19,6 +19,14 @@
   <script src="{{ url_for('.static', filename='timedate.js')
                }}"></script>
 
+  <!-- DataTables CSS -->
+  <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.8/css/jquery.dataTables.css">
+                 
+   <!-- DataTables -->
+   <script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10.8/js/jquery.dataTables.js">
+   </script>
+
+
   <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
 
@@ -117,6 +125,8 @@
               <li><a href="{{ v4_url_for('v4_global_status') }}">Global Status</a></li>
               <li><a href="{{ v4_url_for('v4_daily_report_overview') }}">Daily Report</a></li>
               <li><a href="{{ v4_url_for('v4_machines') }}">All Machines</a></li>
+              <li><a href="{{ v4_url_for('v4_new_regressions') }}">Triage</a></li>
+              <li><a href="{{ v4_url_for('v4_regression_list') }}">Tracking</a></li>
               <li class="divider"></li>
               <li class="disabled"><a href="#">Summary Report</a></li>
               {#"{{ v4_url_for('v4_summary_report') }}"#}

Modified: lnt/trunk/lnt/server/ui/templates/utils.html
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/templates/utils.html?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/lnt/server/ui/templates/utils.html (original)
+++ lnt/trunk/lnt/server/ui/templates/utils.html Thu Oct  8 18:12:04 2015
@@ -22,6 +22,20 @@
 <a href="{{v4_url_for('v4_machine', id=m.id)}}">{{m.name}}:{{m.id}}</a>
 {%- endmacro %}
 
+{% macro render_regression(regress) -%}
+<a href="{{v4_url_for('v4_regression_detail', id=regress.id)}}">{{regress.title}}:{{regress.id}}</a>
+{%- endmacro %}
+
+{% macro render_bug(bug) -%}
+<a href="{{bug}}">{{bug}}</a>
+{%- endmacro %}
+
+{% macro render_order_link(order) -%}
+<a href="{{v4_url_for('v4_order', id=order.id)}}">m{{order.llvm_project_revision}}</a> <a href="https://smooshbase.apple.com/am/r/{{order.llvm_project_revision}}">
+<i class="icon-share-alt"></i></a>
+{%- endmacro %}
+
+
 {% macro regex_filter_box(input_id, selector, placeholder,
                           selector_part_to_search=None) -%}
 <div class="input-group"> <span class="input-group-addon">Filter</span>
@@ -48,3 +62,27 @@ $(document).ready(function() {
 });
 </script>
 {%- endmacro %}
+
+{% macro get_regression_cell_value(cr, analysis) %}
+  {% set test_status = cr.get_test_status() %}
+  {% set value_status = cr.get_value_status(ignore_small=True) %}
+  {% set run_cell_value = "None" if cr.current is none else "%.4f" % cr.current %}
+
+
+  {% set cell_color = none %}
+  {% if test_status == analysis.REGRESSED %}
+    {% set cell_color = (233,128,128) %}
+  {% elif test_status == analysis.IMPROVED %}
+    {% set cell_color = (143,223,95) %}
+  {% elif test_status == analysis.UNCHANGED_FAIL %}
+    {% set cell_color = (255,195,67) %}
+  {% endif %}
+
+  {% if (value_status == analysis.REGRESSED or
+         value_status == analysis.IMPROVED) %}
+    {{ cr.pct_delta|aspctcell(reverse=cr.bigger_is_better)|safe }}
+  {% else %}
+    {{ cr.pct_delta|aspctcell(reverse=cr.bigger_is_better)|safe}}
+  {% endif %}
+
+{% endmacro %}

Added: lnt/trunk/lnt/server/ui/templates/v4_new_regressions.html
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/templates/v4_new_regressions.html?rev=249770&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/templates/v4_new_regressions.html (added)
+++ lnt/trunk/lnt/server/ui/templates/v4_new_regressions.html Thu Oct  8 18:12:04 2015
@@ -0,0 +1,75 @@
+{% set nosidebar = True %}
+{% import "utils.html" as utils %}
+{% set db = request.get_db() %}
+
+
+{% extends "layout.html" %}
+{% set components = [(testsuite_name, v4_url_for("v4_recent_activity"))] %}
+{% block title %}Regression Triage{% endblock %}
+
+{% block body %}
+
+
+<section id="Changes" />
+<h3>Recent Changes</h3>
+
+<form method="POST" action="{{ v4_url_for("v4_new_regressions") }}">
+    {{ form.hidden_tag() }}
+
+<table id="changes_table" class="display">
+  
+  <thead>
+  <tr>
+    <th>X</th>
+    <th>Machine</th>
+    <th>Metric</th>
+    <th>Test</th>
+    <th>Good, Bad</th>
+    <th>Old</th><th>New</th>
+    <th>%Δ</th>
+    <th>Now</th>
+    <th>Curernt</th>
+    <th>Age</th>
+
+  </tr>
+  </thead>
+  <tbody>
+    {% set graph_base=v4_url_for('v4_graph') %}
+    {# Show the active submissions. #}
+    {% for form_change in form.field_changes%}
+        {% set fc = changes[loop.index -1] %}
+    <tr>
+        <td>{{ form_change }}</td>
+        <td>{{utils.render_machine(fc.ri.machine)}}</td>
+        <td> {{ fc.ri.field.name }} </td>
+         {% set graph_base=v4_url_for('v4_graph', highlight_run=fc.run.id) %}
+        <td><a href="{{graph_base}}&plot.{{fc.ri.test.id}}={{ fc.ri.machine.id}}.{{fc.ri.test.id}}.{{fc.ri.field.index}}">{{ fc.ri.test.name }}</a></td>
+        <td>m{{ fc.ri.start_order.llvm_project_revision }}, {{utils.render_order_link(fc.ri.end_order)}}</td>
+        <td>{{ fc.cr.previous }}</td><td>{{ fc.cr.current }}</td>
+        {{ utils.get_regression_cell_value(fc.cr, analysis)}}
+        <td>{{ fc.latest_cr.current }}</td>
+
+        {{ utils.get_regression_cell_value(fc.latest_cr, analysis)}}
+
+        <td><span class="reltime" data-toggle="tooltip" title="{{fc.run.end_time}}">{{ fc.run.end_time.isoformat() }}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+
+    <input name="btn" type="submit" value="Create New Regression">
+    <input name="btn" type="submit" value="Ignore Changes">
+
+</form>
+
+
+<script type="text/javascript">
+$(document).ready( function () {
+    $('#changes_table').DataTable({
+    "sDom": '<"top"if>rt<"bottom"Flp>'
+  });
+} );
+</script>
+
+
+{% endblock %}

Added: lnt/trunk/lnt/server/ui/templates/v4_regression_detail.html
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/templates/v4_regression_detail.html?rev=249770&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/templates/v4_regression_detail.html (added)
+++ lnt/trunk/lnt/server/ui/templates/v4_regression_detail.html Thu Oct  8 18:12:04 2015
@@ -0,0 +1,95 @@
+{% import "utils.html" as utils %}
+{% set db = request.get_db() %}
+
+{% extends "layout.html" %}
+{% set components = [(testsuite_name, v4_url_for("v4_recent_activity")),
+                      ("Tracking", v4_url_for("v4_regression_list"))] %}
+{% block title %}Regression Details{% endblock %}
+
+{% block body %}
+<section id="Regression Detail" />
+<h3>Regression: {{regression.title}}</h3>
+<a href="{{regression.bug}}">{{regression.bug}}</a>
+<form method="POST" action="">
+<table class="table table-striped table-hover table-condensed">
+  <caption>This needs to be dynamically queried</caption>
+
+  <thead>
+  <tr>
+    <th>X</th>
+    <th>Machine</th>
+    <th>Metric</th>
+    <th>Test</th>
+    <th>Detected</th>
+    <th>Last Good Rev.</th>
+    <th>First Bad Rev.</th>
+    <th>Change</th>
+    <th>Current</th>
+
+  </tr>
+  </thead>
+  <tbody>
+    {% for form_change in form.field_changes%}
+        {% set ind = indicators[loop.index -1] %}
+    <tr>
+        <td>{{form_change}}</td>
+         {% set graph_base=v4_url_for('v4_graph', highlight_run=ind.run.id) %}
+        <td>{{utils.render_machine(ind.ri.machine)}}</td>
+        <td>{{ind.ri.field.name}}</td>
+        <td><a href="{{graph_base}}&plot.{{ind.ri.test.id}}={{ ind.ri.machine.id}}.{{ind.ri.test.id}}.{{ind.ri.field.index}}">{{ ind.ri.test.name }}</a></td>
+        <td><span class="reltime" data-toggle="tooltip" title="{{ind.run.end_time}}">{{ ind.run.end_time.isoformat() }}</td>
+
+        <td>{{utils.render_order_link(ind.ri.start_order)}}</td>
+        <td>{{utils.render_order_link(ind.ri.end_order)}}</td>
+        {{utils.get_regression_cell_value(ind.cr, analysis)}}
+        {{utils.get_regression_cell_value(ind.latest_cr, analysis)}}
+
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+
+<!-- Button to trigger modal -->
+<a href="#editRegressionModal" role="button" class="btn" data-toggle="modal">Edit</a>
+ 
+<!-- Modal -->
+<div id="editRegressionModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="editRegressionModal" aria-hidden="true">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+    <h3 id="editRegressionModal">Edit Regression</h3>
+  </div>
+
+     
+      <div class="modal-body">
+          <p>{{ form.title.label }}{{ form.title }}</p>
+          <p>{{ form.bug.label }}{{form.bug }}</p>
+      </div>
+      <div class="modal-footer">
+          <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
+          <input  name="save_btn" class="btn btn-primary" type="submit" value="Save Changes">
+
+  </div>
+</div>
+
+
+<!-- Button to trigger modal -->
+<a href="#splitRegressionModal" role="button" class="btn" data-toggle="modal">Split</a>
+ 
+<!-- Modal -->
+<div id="splitRegressionModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="splitRegressionModal" aria-hidden="true">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+    <h3 id="editRegressionModal">Split Regression</h3>
+  </div>
+     
+      <div class="modal-body">
+      Split selected changes into new regression. Are you sure?
+      </div>
+      <div class="modal-footer">
+          <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
+          <input name="save_btn" class="btn btn-primary" type="submit" value="Split Regression">
+
+  </div>
+</div>
+</form> 
+{% endblock %}

Added: lnt/trunk/lnt/server/ui/templates/v4_regression_list.html
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/server/ui/templates/v4_regression_list.html?rev=249770&view=auto
==============================================================================
--- lnt/trunk/lnt/server/ui/templates/v4_regression_list.html (added)
+++ lnt/trunk/lnt/server/ui/templates/v4_regression_list.html Thu Oct  8 18:12:04 2015
@@ -0,0 +1,31 @@
+{% import "utils.html" as utils %}
+{% set db = request.get_db() %}
+
+{% extends "layout.html" %}
+{% set components = [(testsuite_name, v4_url_for("v4_recent_activity"))] %}
+{% block title %}Regression List{% endblock %}
+
+{% block body %}
+<section id="Regressions" />
+
+<table class="table table-striped table-hover table-condensed">
+  <caption>This needs to be dynamically queried</caption>
+
+  <thead>
+  <tr>
+    <th>Title</th>
+    <th>Bug</th>
+  </tr>
+  </thead>
+  <tbody>
+    {% for regress in regressions %}
+    <tr>
+        <td>{{utils.render_regression(regress)}} {% if regress.id|int == highlight|int %} <span class="label label-success">Updated</span> {% endif %} </td>
+        <td>{{utils.render_bug(regress.bug)}}</td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+
+
+{% endblock %}

Modified: lnt/trunk/requirements.txt
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/requirements.txt?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/requirements.txt (original)
+++ lnt/trunk/requirements.txt Thu Oct  8 18:12:04 2015
@@ -18,3 +18,5 @@ pytz==2015.4
 requests==2.3.0
 six==1.9.0
 wsgiref==0.1.2
+WTForms==2.0.2
+Flask-WTF==0.12

Modified: lnt/trunk/tests/server/db/CreateV4TestSuiteInstance.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/db/CreateV4TestSuiteInstance.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/tests/server/db/CreateV4TestSuiteInstance.py (original)
+++ lnt/trunk/tests/server/db/CreateV4TestSuiteInstance.py Thu Oct  8 18:12:04 2015
@@ -25,21 +25,51 @@ end_time = datetime.datetime.utcnow()
 machine = ts_db.Machine("test-machine")
 machine.os = "test-os"
 order = ts_db.Order()
-order.llvm_project_revision = "test-revision"
+order.llvm_project_revision = "1234"
+
+order2 = ts_db.Order()
+order2.llvm_project_revision = "1235"
+
+order3 = ts_db.Order()
+order3.llvm_project_revision = "1236"
+
+
 run = ts_db.Run(machine, order, start_time, end_time)
 test = ts_db.Test("test-a")
-sample = ts_db.Sample(run, test)
-sample.compile_time = 1.0
-sample.score = 4.2
-sample.mem_bytes = 58093568
 
+sample = ts_db.Sample(run, test, compile_time=1.0, score=4.2)
+sample.mem_bytes = 58093568
 # Add and commit.
 ts_db.add(machine)
 ts_db.add(order)
+ts_db.add(order2)
+ts_db.add(order3)
+
+
 ts_db.add(run)
 ts_db.add(test)
 ts_db.add(sample)
+field_change = ts_db.FieldChange(order, order2, machine, test,
+                                 list(sample.get_primary_fields())[0])
+
+ts_db.add(field_change)
+
+field_change2 = ts_db.FieldChange(order2, order3, machine, test,
+                                  list(sample.get_primary_fields())[1])
+ts_db.add(field_change2)
+
+TEST_TITLE = "Some regression title"
+regression = ts_db.Regression(TEST_TITLE, "PR1234")
+ts_db.add(regression)
+
+regression_indicator1 = ts_db.RegressionIndicator(regression, field_change)
+regression_indicator2 = ts_db.RegressionIndicator(regression, field_change2)
+
+ts_db.add(regression_indicator1)
+ts_db.add(regression_indicator2)
+
 ts_db.commit()
+
 del machine, order, run, test, sample
 
 # Fetch the added objects.
@@ -48,7 +78,7 @@ assert len(machines) == 1
 machine = machines[0]
 
 orders = ts_db.query(ts_db.Order).all()
-assert len(orders) == 1
+assert len(orders) == 3
 order = orders[0]
 
 runs = ts_db.query(ts_db.Run).all()
@@ -63,6 +93,12 @@ samples = ts_db.query(ts_db.Sample).all(
 assert len(samples) == 1
 sample = samples[0]
 
+regression_indicators = ts_db.query(ts_db.RegressionIndicator).all()
+assert len(regression_indicators) == 2
+ri = regression_indicators[0]
+
+assert ri.regression.title == TEST_TITLE
+
 # Audit the various fields.
 assert machine.name == "test-machine"
 assert machine.os == "test-os"

Modified: lnt/trunk/tests/server/ui/test_api.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/server/ui/test_api.py?rev=249770&r1=249769&r2=249770&view=diff
==============================================================================
--- lnt/trunk/tests/server/ui/test_api.py (original)
+++ lnt/trunk/tests/server/ui/test_api.py Thu Oct  8 18:12:04 2015
@@ -65,6 +65,7 @@ graph_data = [{u'time': u'2012-05-01T16:
                u'rev': u'152293',
                u'val': 10.0}]
 
+
 class JSONAPITester(unittest.TestCase):
     """Test the REST api."""
 




More information about the llvm-commits mailing list