aboutsummaryrefslogtreecommitdiff
path: root/lnt/server/ui/regression_views.py
blob: 60a60236a6eb875bc57da0ddcf3b583f78ec2c13 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
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))