| import flask |
| import sqlalchemy |
| import json |
| from flask import g |
| from flask import abort |
| from flask import render_template |
| from flask import request |
| from flask import flash |
| from flask import redirect |
| from sqlalchemy import desc |
| from sqlalchemy.orm.exc import NoResultFound |
| from wtforms import SelectMultipleField, StringField, widgets, SelectField |
| from wtforms import HiddenField |
| from flask_wtf import Form |
| from wtforms.validators import DataRequired |
| |
| from lnt.server.ui.decorators import v4_route |
| import lnt.server.reporting.analysis |
| from lnt.server.ui.globals import v4_url_for |
| from lnt.server.ui.views import ts_data |
| |
| from lnt.util import logger |
| from lnt.server.ui.util import FLASH_DANGER, FLASH_SUCCESS, PrecomputedCR |
| import lnt.server.db.fieldchange |
| from lnt.server.db.regression import RegressionState, new_regression |
| from lnt.server.db.regression import get_first_runs_of_fieldchange |
| from lnt.server.db.regression import get_cr_for_field_change |
| from lnt.server.db.regression import ChangeData |
| from lnt.server.db import rules_manager as rule_hooks |
| |
| |
| 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()]) |
| |
| |
| def get_fieldchange(session, ts, id): |
| return session.query(ts.FieldChange) \ |
| .filter(ts.FieldChange.id == id) \ |
| .one() |
| |
| |
| @v4_route("/regressions/new", methods=["GET", "POST"]) |
| def v4_new_regressions(): |
| form = TriagePageSelectedForm(request.form) |
| session = request.session |
| ts = request.get_testsuite() |
| if request.method == 'POST' and \ |
| request.form['btn'] == "Create New Regression": |
| regression, _ = new_regression(session, 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(session, ts, fc_id) |
| ignored_change = ts.ChangeIgnore(fc) |
| session.add(ignored_change) |
| session.commit() |
| flash(msg + ", ".join(ignored), FLASH_SUCCESS) |
| |
| # d = datetime.datetime.now() |
| # two_weeks_ago = d - datetime.timedelta(days=14) |
| recent_fieldchange = session.query(ts.FieldChange) \ |
| .join(ts.Test) \ |
| .outerjoin(ts.ChangeIgnore) \ |
| .filter(ts.ChangeIgnore.id.is_(None)) \ |
| .outerjoin(ts.RegressionIndicator) \ |
| .filter(ts.RegressionIndicator.id.is_(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(session, ts, fc) |
| else: |
| cr = PrecomputedCR(fc.old_value, fc.new_value, |
| fc.field.bigger_is_better) |
| key_run = get_first_runs_of_fieldchange(session, ts, fc) |
| current_cr, _, _ = get_cr_for_field_change(session, 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, **ts_data(ts)) |
| |
| |
| def calc_impact(session, ts, fcs): |
| crs = [] |
| for fc in fcs: |
| if fc is None: |
| continue |
| if fc.old_value is None: |
| cr, _, _ = get_cr_for_field_change(session, ts, fc) |
| else: |
| cr = PrecomputedCR(fc.old_value, fc.new_value, |
| fc.field.bigger_is_better) |
| crs.append(cr) |
| if crs: |
| olds = sum([x.previous for x in crs if x.previous]) |
| news = sum([x.current for x in crs if x.current]) |
| if olds and news: |
| new_cr = PrecomputedCR(olds, news, crs[0].bigger_is_better) |
| # TODO both directions |
| return new_cr |
| |
| return PrecomputedCR(1, 1, True) |
| |
| |
| class MergeRegressionForm(Form): |
| regression_checkboxes = MultiCheckboxField("regression_checkboxes", |
| coerce=int) |
| |
| |
| class EmptyDate(object): |
| def isoformat(self): |
| return "-" |
| |
| def strftime(self, _): |
| return "0" |
| |
| |
| @v4_route("/regressions/", methods=["GET", "POST"]) |
| def v4_regression_list(): |
| session = request.session |
| ts = request.get_testsuite() |
| form = MergeRegressionForm(request.form) |
| machine_filter = request.args.get('machine_filter') |
| state_filter = int(request.args.get('state', RegressionState.ACTIVE)) |
| # Merge requested regressions. |
| if request.method == 'POST' and \ |
| request.form['merge_btn'] == "Merge Regressions": |
| reg_inds, regressions = _get_regressions_from_selected_form(session, |
| form, ts) |
| links = [] |
| target = 0 |
| for i, r in enumerate(regressions): |
| if r.bug: |
| target = i |
| links.append(r.bug) |
| |
| new_regress, _ = new_regression(session, ts, |
| [x.field_change_id for x in reg_inds]) |
| new_regress.state = regressions[target].state |
| new_regress.title = regressions[target].title |
| new_regress.bug = ' '.join(links) |
| for r in regressions: |
| r.bug = v4_url_for(".v4_regression_detail", id=new_regress.id) |
| r.title = "Merged into Regression " + str(new_regress.id) |
| r.state = RegressionState.IGNORED |
| [session.delete(x) for x in reg_inds] |
| |
| session.commit() |
| flash("Created: " + new_regress.title, FLASH_SUCCESS) |
| return redirect(v4_url_for(".v4_regression_detail", id=new_regress.id)) |
| # Delete requested regressions. |
| if request.method == 'POST' and \ |
| request.form['merge_btn'] == "Delete Regressions": |
| reg_inds, regressions = _get_regressions_from_selected_form(session, |
| form, ts) |
| titles = [r.title for r in regressions] |
| for res_ind in reg_inds: |
| session.delete(res_ind) |
| for reg in regressions: |
| session.delete(reg) |
| session.commit() |
| flash(' Deleted: '.join(titles), FLASH_SUCCESS) |
| return redirect(v4_url_for(".v4_regression_list", state=state_filter)) |
| |
| q = session.query(ts.Regression) |
| title = "All Regressions" |
| if state_filter != -1: |
| q = q.filter(ts.Regression.state == state_filter) |
| title = RegressionState.names[state_filter] |
| regression_info = q.all()[::-1] |
| |
| form.regression_checkboxes.choices = list() |
| regression_sizes = [] |
| impacts = [] |
| ages = [] |
| |
| filtered_regressions = [] |
| for regression in regression_info: |
| reg_inds = session.query(ts.RegressionIndicator) \ |
| .filter(ts.RegressionIndicator.regression_id == |
| regression.id) \ |
| .all() |
| if machine_filter: |
| machine_names = \ |
| set([x.field_change.machine.name for x in reg_inds]) |
| if machine_filter in machine_names: |
| filtered_regressions.append(regression) |
| else: |
| continue |
| else: |
| filtered_regressions.append(regression) |
| form.regression_checkboxes.choices.append((regression.id, 1,)) |
| |
| regression_sizes.append(len(reg_inds)) |
| impacts.append(calc_impact(session, ts, |
| [x.field_change for x in reg_inds])) |
| # Now guess the regression age: |
| if len(reg_inds) and reg_inds[0].field_change and \ |
| reg_inds[0].field_change.run: |
| age = reg_inds[0].field_change.run.end_time |
| else: |
| age = EmptyDate() |
| ages.append(age) |
| |
| return render_template("v4_regression_list.html", |
| testsuite_name=g.testsuite_name, |
| regressions=filtered_regressions, |
| highlight=request.args.get('highlight'), |
| title=title, |
| RegressionState=RegressionState, |
| state_filter=state_filter, |
| form=form, |
| sizes=regression_sizes, |
| impacts=impacts, |
| ages=ages, |
| analysis=lnt.server.reporting.analysis, |
| **ts_data(ts)) |
| |
| |
| def _get_regressions_from_selected_form(session, form, ts): |
| regressions_id_to_merge = form.regression_checkboxes.data |
| regressions = session.query(ts.Regression) \ |
| .filter(ts.Regression.id.in_(regressions_id_to_merge)).all() |
| reg_inds = session.query(ts.RegressionIndicator) \ |
| .filter(ts.RegressionIndicator.regression_id.in_( |
| regressions_id_to_merge)) \ |
| .all() |
| return reg_inds, regressions |
| |
| |
| class EditRegressionForm(Form): |
| title = StringField(u'Title', validators=[DataRequired()]) |
| bug = StringField(u'Bug', validators=[DataRequired()]) |
| field_changes = MultiCheckboxField("Changes", coerce=int) |
| choices = list(RegressionState.names.items()) |
| state = SelectField(u'State', choices=choices) |
| edit_state = HiddenField(u'EditState', validators=[DataRequired()]) |
| |
| |
| def name(cls): |
| """Get a nice name for this object.""" |
| return cls.__class__.__name__ |
| |
| |
| class LNTEncoder(flask.json.JSONEncoder): |
| """Encode all the common LNT objects.""" |
| def default(self, obj): |
| # Most of our objects have a __json__ defined. |
| if hasattr(obj, "__json__"): |
| return obj.__json__() |
| # From sqlalchemy, when we encounter ignore. |
| if name(obj) == "InstanceState": |
| return |
| if name(obj) == "SampleField": |
| return obj.name |
| return flask.json.JSONEncoder.default(self, obj) |
| |
| |
| @v4_route("/regressions/<int:id>", methods=["GET", "POST"]) |
| def v4_regression_detail(id): |
| session = request.session |
| ts = request.get_testsuite() |
| form = EditRegressionForm(request.form) |
| |
| try: |
| regression_info = session.query(ts.Regression) \ |
| .filter(ts.Regression.id == id) \ |
| .one() |
| except NoResultFound as e: |
| abort(404) |
| if request.method == 'POST' and request.form['save_btn'] == "Save Changes": |
| regression_info.title = form.title.data |
| regression_info.bug = form.bug.data |
| regression_info.state = form.state.data |
| session.commit() |
| flash("Updated " + regression_info.title, FLASH_SUCCESS) |
| return redirect(v4_url_for(".v4_regression_list", |
| highlight=regression_info.id, |
| state=int(form.edit_state.data))) |
| if request.method == 'POST' and \ |
| request.form['save_btn'] == "Split Regression": |
| # For each of the regression indicators, grab their field ids. |
| res_inds = session.query(ts.RegressionIndicator) \ |
| .filter(ts.RegressionIndicator.field_change_id.in_( |
| form.field_changes.data)) \ |
| .all() |
| fc_ids = [x.field_change_id for x in res_inds] |
| second_regression, _ = new_regression(session, ts, fc_ids) |
| second_regression.state = regression_info.state |
| |
| # Now remove our links to this regression. |
| for res_ind in res_inds: |
| session.delete(res_ind) |
| lnt.server.db.fieldchange.rebuild_title(session, ts, regression_info) |
| session.commit() |
| flash("Split " + second_regression.title, FLASH_SUCCESS) |
| return redirect(v4_url_for(".v4_regression_list", |
| highlight=second_regression.id, |
| state=int(form.edit_state.data))) |
| if request.method == 'POST' and request.form['save_btn'] == "Delete": |
| # For each of the regression indicators, grab their field ids. |
| title = regression_info.title |
| res_inds = session.query(ts.RegressionIndicator) \ |
| .filter( |
| ts.RegressionIndicator.regression_id == regression_info.id) \ |
| .all() |
| # Now remove our links to this regression. |
| for res_ind in res_inds: |
| session.delete(res_ind) |
| session.delete(regression_info) |
| session.commit() |
| flash("Deleted " + title, FLASH_SUCCESS) |
| return redirect(v4_url_for(".v4_regression_list", |
| state=int(form.edit_state.data))) |
| form.field_changes.choices = list() |
| form.state.default = regression_info.state |
| form.process() |
| form.edit_state.data = regression_info.state |
| form.title.data = regression_info.title |
| form.bug.data = regression_info.bug |
| regression_indicators = session.query(ts.RegressionIndicator) \ |
| .filter(ts.RegressionIndicator.regression_id == id) \ |
| .all() |
| |
| crs = [] |
| |
| test_suite_versions = set() |
| form.field_changes.choices = list() |
| # If we have more than 10 regressions, don't graph any by default. |
| checkbox_state = 1 |
| if len(regression_indicators) >= 10: |
| checkbox_state = 0 |
| |
| for regression in regression_indicators: |
| fc = regression.field_change |
| if fc is None: |
| continue |
| if fc.old_value is None: |
| cr, key_run, all_runs = get_cr_for_field_change(session, ts, fc) |
| else: |
| cr = PrecomputedCR(fc.old_value, fc.new_value, |
| fc.field.bigger_is_better) |
| key_run = get_first_runs_of_fieldchange(session, ts, fc) |
| current_cr, _, all_runs = get_cr_for_field_change(session, ts, fc, |
| current=True) |
| crs.append(ChangeData(fc, cr, key_run, current_cr)) |
| form.field_changes.choices.append((fc.id, checkbox_state,)) |
| for run in all_runs: |
| ts_rev = key_run.parameters.get('test_suite_revision') |
| if ts_rev and ts_rev != u'None': |
| test_suite_versions.add(ts_rev) |
| |
| if len(test_suite_versions) > 1: |
| revs = ', '.join(list(test_suite_versions)) |
| flash("More than one test-suite version: " + revs, |
| FLASH_DANGER) |
| |
| if request.args.get('json'): |
| return json.dumps({u'Regression': regression_info, |
| u'Changes': crs}, |
| cls=LNTEncoder) |
| |
| return render_template("v4_regression_detail.html", |
| testsuite_name=g.testsuite_name, |
| regression=regression_info, changes=crs, |
| form=form, analysis=lnt.server.reporting.analysis, |
| check_all=checkbox_state, |
| **ts_data(ts)) |
| |
| |
| @v4_route("/hook", methods=["GET"]) |
| def v4_hook(): |
| session = request.session |
| ts = request.get_testsuite() |
| rule_hooks.post_submission_hooks(session, ts, 0) |
| abort(400) |
| |
| |
| @v4_route("/regressions/new_from_graph/<int:machine_id>/<int:test_id>" |
| "/<int:field_index>/<int:run_id>", methods=["GET"]) |
| def v4_make_regression(machine_id, test_id, field_index, run_id): |
| """This function is called to make a new regression from a graph data point. |
| |
| It is not nessessarly the case that there will be a real change there, |
| so we must create a regression, bypassing the normal analysis. |
| |
| """ |
| session = request.session |
| ts = request.get_testsuite() |
| field = ts.sample_fields[field_index] |
| new_regression_id = 0 |
| run = session.query(ts.Run).get(run_id) |
| |
| runs = session.query(ts.Run). \ |
| filter(ts.Run.order_id == run.order_id). \ |
| filter(ts.Run.machine_id == run.machine_id). \ |
| all() |
| |
| if len(runs) == 0: |
| abort(404) |
| |
| previous_runs = ts.get_previous_runs_on_machine(session, run, 1) |
| |
| # Find our start/end order. |
| if previous_runs != []: |
| start_order = previous_runs[0].order |
| else: |
| start_order = run.order |
| 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)] |
| |
| runinfo = lnt.server.reporting.analysis.RunInfo(session, ts, runs_to_load) |
| |
| result = runinfo.get_comparison_result( |
| runs, previous_runs, test_id, field, |
| ts.Sample.get_hash_of_binary_field()) |
| |
| # Try and find a matching FC and update, else create one. |
| try: |
| f = session.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_id == field.id) \ |
| .one() |
| except sqlalchemy.orm.exc.NoResultFound: |
| # Create one |
| test = session.query(ts.Test).filter(ts.Test.id == test_id).one() |
| f = ts.FieldChange(start_order=start_order, |
| end_order=run.order, |
| machine=run.machine, |
| test=test, |
| field_id=field.id) |
| session.add(f) |
| |
| # Always update FCs with new values. |
| if f: |
| f.old_value = result.previous |
| f.new_value = result.current |
| f.run = run |
| session.commit() |
| |
| # Make new regressions. |
| regression, _ = new_regression(session, ts, [f.id]) |
| regression.state = RegressionState.ACTIVE |
| |
| session.commit() |
| logger.info("Manually created new regressions: {}".format(regression.id)) |
| flash("Created " + regression.title, FLASH_SUCCESS) |
| |
| return redirect(v4_url_for(".v4_regression_detail", id=regression.id)) |