Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Bug 1499822 - [tryselect] Implement |mach try chooser| r=sclements
Usage:

  $ ./mach try chooser

Will start a local flask server and server a "trychooser-like" page
that is dynamically generated from the taskgraph.

Differential Revision: https://phabricator.services.mozilla.com/D14903

--HG--
extra : rebase_source : dafe631a4ed9850afdda3b3d91a67643802360a9
extra : absorb_source : 3cb0544110796ae38816cd6c0c20022816a3def1
extra : source : 31e16c2d94299dcc7076b024981b1997a264e5e1
  • Loading branch information
ahal committed Jan 9, 2019
1 parent 853adfb commit 290a403
Show file tree
Hide file tree
Showing 13 changed files with 670 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -418,6 +418,7 @@ toolkit/components/urlformatter/nsURLFormatter.js
toolkit/modules/AppConstants.jsm
toolkit/mozapps/downloads/nsHelperAppDlg.js
toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
tools/tryselect/selectors/chooser/templates/chooser.html

# Third party
toolkit/modules/third_party/**
Expand Down
33 changes: 33 additions & 0 deletions tools/tryselect/docs/selectors/chooser.rst
@@ -0,0 +1,33 @@
Chooser Selector
================

When pushing to try, there are a very large amount of builds and tests to choose from. Often too
many to remember, making it easy to forget a set of tasks which should otherwise have been run.

This selector allows you to select tasks from a web interface that lists all the possible build and
test tasks and allows you to select them from a list. It is similar in concept to the old `try
syntax chooser`_ page, except that the values are dynamically generated using the `taskgraph`_ as an
input. This ensures that it will never be out of date.

To use:

.. code-block:: shell
$ mach try chooser
This will spin up a local web server (using Flask) which serves the chooser app. After making your
selection, simply press ``Push`` and the rest will be handled from there. No need to copy/paste any
syntax strings or the like.

You can run:

.. code-block:: shell
$ mach try chooser --full
To generate the interface using the full taskgraph instead. This will include tasks that don't run
on mozilla-central.


.. _try syntax chooser: https://mozilla-releng.net/trychooser
.. _taskgraph: https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/index.html
2 changes: 2 additions & 0 deletions tools/tryselect/docs/selectors/index.rst
Expand Up @@ -3,6 +3,7 @@ Selectors

These are the currently implemented try selectors:

* :doc:`chooser <chooser>`: Select tasks using a web interface.
* :doc:`fuzzy <fuzzy>`: Select tasks using a fuzzy finding algorithm and
a terminal interface.
* :doc:`again <again>`: Re-run a previous ``try_task_config.json`` based
Expand Down Expand Up @@ -30,6 +31,7 @@ See selector specific options by running:
:maxdepth: 1
:hidden:

Chooser <chooser>
Fuzzy <fuzzy>
Empty <empty>
Syntax <syntax>
Expand Down
20 changes: 20 additions & 0 deletions tools/tryselect/mach_commands.py
Expand Up @@ -149,6 +149,26 @@ def try_fuzzy(self, **kwargs):
from tryselect.selectors.fuzzy import run_fuzzy_try
return run_fuzzy_try(**kwargs)

@SubCommand('try',
'chooser',
description='Schedule tasks by selecting them from a web '
'interface.',
parser=get_parser('chooser'))
def try_chooser(self, **kwargs):
"""Push tasks selected from a web interface to try.
This selector will build the taskgraph and spin up a dynamically
created 'trychooser-like' web-page on the localhost. After a selection
has been made, pressing the 'Push' button will automatically push the
selection to try.
"""
self._activate_virtualenv()
self.virtualenv_manager.install_pip_package('flask')
self.virtualenv_manager.install_pip_package('flask-wtf')

from tryselect.selectors.chooser import run_try_chooser
return run_try_chooser(**kwargs)

@SubCommand('try',
'again',
description='Schedule a previously generated (non try syntax) '
Expand Down
12 changes: 12 additions & 0 deletions tools/tryselect/selectors/chooser/.eslintrc.js
@@ -0,0 +1,12 @@
"use strict";

module.exports = {
env: {
"jquery": true
},
globals: {
"apply": true,
"applyChunks": true,
"tasks": true
}
};
50 changes: 50 additions & 0 deletions tools/tryselect/selectors/chooser/__init__.py
@@ -0,0 +1,50 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import, print_function, unicode_literals

import os
import webbrowser
from threading import Timer

from tryselect.cli import BaseTryParser
from tryselect.tasks import generate_tasks
from tryselect.push import check_working_directory, push_to_try, vcs

here = os.path.abspath(os.path.dirname(__file__))


class ChooserParser(BaseTryParser):
name = 'chooser'
arguments = []
common_groups = ['push', 'task']
templates = ['artifact', 'env', 'rebuild', 'chemspill-prio', 'talos-profile']


def run_try_chooser(update=False, query=None, templates=None, full=False, parameters=None,
save=False, preset=None, mod_presets=False, push=True, message='{msg}',
**kwargs):
from .app import create_application
check_working_directory(push)

tg = generate_tasks(parameters, full, root=vcs.path)
app = create_application(tg)

if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
# we are in the reloader process, don't open the browser or do any try stuff
app.run()
return

# give app a second to start before opening the browser
Timer(1, lambda: webbrowser.open('http://127.0.0.1:5000')).start()
app.run()

selected = app.tasks
if not selected:
print("no tasks selected")
return

msg = "Try Chooser Enhanced ({} tasks selected)".format(len(selected))
return push_to_try('chooser', message.format(msg=msg), selected, templates, push=push,
closed_tree=kwargs["closed_tree"])
190 changes: 190 additions & 0 deletions tools/tryselect/selectors/chooser/app.py
@@ -0,0 +1,190 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import, print_function

from abc import ABCMeta, abstractproperty
from collections import defaultdict

from flask import (
Flask,
render_template,
request,
)

SECTIONS = []
SUPPORTED_KINDS = set()


def register_section(cls):
assert issubclass(cls, Section)
instance = cls()
SECTIONS.append(instance)
SUPPORTED_KINDS.update(instance.kind.split(','))


class Section(object):
__metaclass__ = ABCMeta

@abstractproperty
def name(self):
pass

@abstractproperty
def kind(self):
pass

@abstractproperty
def title(self):
pass

@abstractproperty
def attrs(self):
pass

def contains(self, task):
return task.kind in self.kind.split(',')

def get_context(self, tasks):
labels = defaultdict(lambda: {'max_chunk': 0, 'attrs': defaultdict(list)})

for task in tasks.values():
if not self.contains(task):
continue

task = task.attributes
label = labels[self.labelfn(task)]
for attr in self.attrs:
if attr in task and task[attr] not in label['attrs'][attr]:
label['attrs'][attr].append(task[attr])

if 'test_chunk' in task:
label['max_chunk'] = max(label['max_chunk'], int(task['test_chunk']))

return {
'name': self.name,
'kind': self.kind,
'title': self.title,
'labels': labels,
}


@register_section
class Platform(Section):
name = 'platform'
kind = 'build'
title = 'Platforms'
attrs = ['build_platform']

def labelfn(self, task):
return task['build_platform']

def contains(self, task):
if not Section.contains(self, task):
return False

# android-stuff tasks aren't actual platforms
return task.task['tags'].get('android-stuff', False) != "true"


@register_section
class Test(Section):
name = 'test'
kind = 'test'
title = 'Test Suites'
attrs = ['unittest_suite', 'unittest_flavor']

def labelfn(self, task):
suite = task['unittest_suite'].replace(' ', '-')
flavor = task['unittest_flavor'].replace(' ', '-')

if flavor.endswith('chunked'):
flavor = flavor[:-len('chunked')]

if flavor.startswith(suite):
flavor = flavor[len(suite):]
flavor = flavor.strip('-')

if flavor in ('crashtest', 'jsreftest'):
return flavor

if flavor:
return '{}-{}'.format(suite, flavor)
return suite

def contains(self, task):
if not Section.contains(self, task):
return False
return task.attributes['unittest_suite'] not in ('raptor', 'talos')


@register_section
class Perf(Section):
name = 'perf'
kind = 'test'
title = 'Performance'
attrs = ['unittest_suite', 'unittest_flavor', 'raptor_try_name', 'talos_try_name']

def labelfn(self, task):
suite = task['unittest_suite']
label = task['{}_try_name'.format(suite)]

if not label.startswith(suite):
label = '{}-{}'.format(suite, label)

if label.endswith('-e10s'):
label = label[:-len('-e10s')]

return label

def contains(self, task):
if not Section.contains(self, task):
return False
return task.attributes['unittest_suite'] in ('raptor', 'talos')


@register_section
class Analysis(Section):
name = 'analysis'
kind = 'build,static-analysis-autotest'
title = 'Analysis'
attrs = ['build_platform']

def labelfn(self, task):
return task['build_platform']

def contains(self, task):
if not Section.contains(self, task):
return False
if task.kind == 'build':
return task.task['tags'].get('android-stuff', False) == "true"
return True


def create_application(tg):
tasks = {l: t for l, t in tg.tasks.items() if t.kind in SUPPORTED_KINDS}
sections = [s.get_context(tasks) for s in SECTIONS]
context = {
'tasks': {l: t.attributes for l, t in tasks.items()},
'sections': sections,
}

app = Flask(__name__)
app.env = 'development'
app.tasks = []

@app.route('/', methods=['GET', 'POST'])
def chooser():
if request.method == 'GET':
return render_template('chooser.html', **context)

if request.form['action'] == 'Push':
labels = request.form['selected-tasks'].splitlines()
app.tasks.extend(labels)

shutdown = request.environ.get('werkzeug.server.shutdown')
shutdown()
return render_template('close.html')

return app

0 comments on commit 290a403

Please sign in to comment.