diff options
Diffstat (limited to 'tools/tryselect/selectors/chooser')
-rw-r--r-- | tools/tryselect/selectors/chooser/.eslintrc.js | 16 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/__init__.py | 120 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/app.py | 176 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/static/filter.js | 116 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/static/select.js | 46 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/static/style.css | 107 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/templates/chooser.html | 78 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/templates/close.html | 11 | ||||
-rw-r--r-- | tools/tryselect/selectors/chooser/templates/layout.html | 71 |
9 files changed, 741 insertions, 0 deletions
diff --git a/tools/tryselect/selectors/chooser/.eslintrc.js b/tools/tryselect/selectors/chooser/.eslintrc.js new file mode 100644 index 0000000000..861d6bafc2 --- /dev/null +++ b/tools/tryselect/selectors/chooser/.eslintrc.js @@ -0,0 +1,16 @@ +/* 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/. */ + +"use strict"; + +module.exports = { + env: { + jquery: true, + }, + globals: { + apply: true, + applyChunks: true, + tasks: true, + }, +}; diff --git a/tools/tryselect/selectors/chooser/__init__.py b/tools/tryselect/selectors/chooser/__init__.py new file mode 100644 index 0000000000..d6a32e08d0 --- /dev/null +++ b/tools/tryselect/selectors/chooser/__init__.py @@ -0,0 +1,120 @@ +# 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/. + +import multiprocessing +import os +import time +import webbrowser +from threading import Timer + +from gecko_taskgraph.target_tasks import filter_by_uncommon_try_tasks + +from tryselect.cli import BaseTryParser +from tryselect.push import ( + check_working_directory, + generate_try_task_config, + push_to_try, +) +from tryselect.tasks import generate_tasks + +here = os.path.abspath(os.path.dirname(__file__)) + + +class ChooserParser(BaseTryParser): + name = "chooser" + arguments = [] + common_groups = ["push", "task"] + task_configs = [ + "artifact", + "browsertime", + "chemspill-prio", + "disable-pgo", + "env", + "existing-tasks", + "gecko-profile", + "path", + "pernosco", + "rebuild", + "worker-overrides", + ] + + +def run( + update=False, + query=None, + try_config_params=None, + full=False, + parameters=None, + save=False, + preset=None, + mod_presets=False, + stage_changes=False, + dry_run=False, + message="{msg}", + closed_tree=False, + push_to_lando=False, +): + from .app import create_application + + push = not stage_changes and not dry_run + check_working_directory(push) + + tg = generate_tasks(parameters, full) + + # Remove tasks that are not to be shown unless `--full` is specified. + if not full: + excluded_tasks = [ + label + for label in tg.tasks.keys() + if not filter_by_uncommon_try_tasks(label) + ] + for task in excluded_tasks: + tg.tasks.pop(task) + + queue = multiprocessing.Queue() + + 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 = create_application(tg, queue) + app.run() + return + + # give app a second to start before opening the browser + url = "http://127.0.0.1:5000" + Timer(1, lambda: webbrowser.open(url)).start() + print("Starting trychooser on {}".format(url)) + process = multiprocessing.Process( + target=create_and_run_application, args=(tg, queue) + ) + process.start() + + selected = queue.get() + + # Allow the close page to render before terminating the process. + time.sleep(1) + process.terminate() + 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), + try_task_config=generate_try_task_config( + "chooser", selected, params=try_config_params + ), + stage_changes=stage_changes, + dry_run=dry_run, + closed_tree=closed_tree, + push_to_lando=push_to_lando, + ) + + +def create_and_run_application(tg, queue: multiprocessing.Queue): + from .app import create_application + + app = create_application(tg, queue) + + app.run() diff --git a/tools/tryselect/selectors/chooser/app.py b/tools/tryselect/selectors/chooser/app.py new file mode 100644 index 0000000000..99d63cd37f --- /dev/null +++ b/tools/tryselect/selectors/chooser/app.py @@ -0,0 +1,176 @@ +# 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/. + +import multiprocessing +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"] + + def labelfn(self, task): + suite = task["unittest_suite"].replace(" ", "-") + + if suite.endswith("-chunked"): + suite = suite[: -len("-chunked")] + + 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", "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,hazard" + 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, queue: multiprocessing.Queue): + 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) + + queue.put(app.tasks) + return render_template("close.html") + + return app diff --git a/tools/tryselect/selectors/chooser/static/filter.js b/tools/tryselect/selectors/chooser/static/filter.js new file mode 100644 index 0000000000..2d8731e61f --- /dev/null +++ b/tools/tryselect/selectors/chooser/static/filter.js @@ -0,0 +1,116 @@ +/* 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/. */ + +const selection = $("#selection")[0]; +const count = $("#selection-count")[0]; +const pluralize = (count, noun, suffix = "s") => + `${count} ${noun}${count !== 1 ? suffix : ""}`; + +var selected = []; + +var updateLabels = () => { + $(".tab-pane.active > .filter-label").each(function (index) { + let box = $("#" + this.htmlFor)[0]; + let method = box.checked ? "add" : "remove"; + $(this)[method + "Class"]("is-checked"); + }); +}; + +var apply = () => { + let filters = {}; + let kinds = []; + + $(".filter:checked").each(function (index) { + for (let kind of this.name.split(",")) { + if (!kinds.includes(kind)) { + kinds.push(kind); + } + } + + // Checkbox element values are generated by Section.get_context() in app.py + let attrs = JSON.parse(this.value); + for (let attr in attrs) { + if (!(attr in filters)) { + filters[attr] = []; + } + + let values = attrs[attr]; + filters[attr] = filters[attr].concat(values); + } + }); + updateLabels(); + + if ( + !Object.keys(filters).length || + (Object.keys(filters).length == 1 && "build_type" in filters) + ) { + selection.value = ""; + count.innerHTML = "0 tasks selected"; + return; + } + + var taskMatches = label => { + let task = tasks[label]; + + // If no box for the given kind has been checked, this task is + // automatically not selected. + if (!kinds.includes(task.kind)) { + return false; + } + + for (let attr in filters) { + let values = filters[attr]; + if (!(attr in task) || values.includes(task[attr])) { + continue; + } + return false; + } + return true; + }; + + selected = Object.keys(tasks).filter(taskMatches); + applyChunks(); +}; + +var applyChunks = () => { + // For tasks that have a chunk filter applied, we handle that here. + let filters = {}; + $(".filter:text").each(function (index) { + let value = $(this).val(); + if (value === "") { + return; + } + + let attrs = JSON.parse(this.name); + let key = `${attrs.unittest_suite}-${attrs.unittest_flavor}`; + if (!(key in filters)) { + filters[key] = []; + } + + // Parse the chunk strings. These are formatted like printer page setups, e.g: "1,4-6,9" + for (let item of value.split(",")) { + if (!item.includes("-")) { + filters[key].push(parseInt(item)); + continue; + } + + let [start, end] = item.split("-"); + for (let i = parseInt(start); i <= parseInt(end); ++i) { + filters[key].push(i); + } + } + }); + + let chunked = selected.filter(function (label) { + let task = tasks[label]; + let key = task.unittest_suite + "-" + task.unittest_flavor; + if (key in filters && !filters[key].includes(parseInt(task.test_chunk))) { + return false; + } + return true; + }); + + selection.value = chunked.join("\n"); + count.innerText = pluralize(chunked.length, "task") + " selected"; +}; diff --git a/tools/tryselect/selectors/chooser/static/select.js b/tools/tryselect/selectors/chooser/static/select.js new file mode 100644 index 0000000000..8a315c0a52 --- /dev/null +++ b/tools/tryselect/selectors/chooser/static/select.js @@ -0,0 +1,46 @@ +/* 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/. */ + +const labels = $("label.multiselect"); +const boxes = $("label.multiselect input:checkbox"); +var lastChecked = {}; + +// implements shift+click +labels.click(function (e) { + if (e.target.tagName === "INPUT") { + return; + } + + let box = $("#" + this.htmlFor)[0]; + let activeSection = $("div.tab-pane.active")[0].id; + + if (activeSection in lastChecked) { + // Bug 559506 - In Firefox shift/ctrl/alt+clicking a label doesn't check the box. + let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1; + + if (e.shiftKey) { + if (isFirefox) { + box.checked = !box.checked; + } + + let start = boxes.index(box); + let end = boxes.index(lastChecked[activeSection]); + + boxes + .slice(Math.min(start, end), Math.max(start, end) + 1) + .prop("checked", box.checked); + apply(); + } + } + + lastChecked[activeSection] = box; +}); + +function selectAll(btn) { + let checked = !!btn.value; + $("div.active label.filter-label").each(function (index) { + $(this).find("input:checkbox")[0].checked = checked; + }); + apply(); +} diff --git a/tools/tryselect/selectors/chooser/static/style.css b/tools/tryselect/selectors/chooser/static/style.css new file mode 100644 index 0000000000..6b2f96935b --- /dev/null +++ b/tools/tryselect/selectors/chooser/static/style.css @@ -0,0 +1,107 @@ +/* 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/. */ + +body { + padding-top: 70px; +} + +/* Tabs */ + +#tabbar .nav-link { + color: #009570; + font-size: 18px; + padding-bottom: 15px; + padding-top: 15px; +} + +#tabbar .nav-link.active { + color: #212529; +} + +#tabbar .nav-link:hover { + color: #0f5a3a; +} + +/* Sections */ + +.tab-content button { + font-size: 14px; + margin-bottom: 5px; + margin-top: 10px; +} + +.filter-label { + display: block; + font-size: 16px; + position: relative; + padding-left: 15px; + padding-right: 15px; + padding-top: 10px; + padding-bottom: 10px; + margin-bottom: 0; + user-select: none; + vertical-align: middle; +} + +.filter-label span { + display: flex; + min-height: 34px; + align-items: center; + justify-content: space-between; +} + +.filter-label input[type="checkbox"] { + position: absolute; + opacity: 0; + height: 0; + width: 0; +} + +.filter-label input[type="text"] { + width: 50px; +} + +.filter-label:hover { + background-color: #91a0b0; +} + +.filter-label.is-checked:hover { + background-color: #91a0b0; +} + +.filter-label.is-checked { + background-color: #404c59; + color: white; +} + +/* Preview pane */ + +#preview { + position: fixed; + height: 100vh; + margin-left: 66%; + width: 100%; +} + +#submit-tasks { + display: flex; + flex-direction: column; + height: 80%; +} + +#buttons { + display: flex; + justify-content: space-between; +} + +#push { + background-color: #00e9b7; + margin-left: 5px; + width: 100%; +} + +#selection { + height: 100%; + width: 100%; +} diff --git a/tools/tryselect/selectors/chooser/templates/chooser.html b/tools/tryselect/selectors/chooser/templates/chooser.html new file mode 100644 index 0000000000..4e009d94ac --- /dev/null +++ b/tools/tryselect/selectors/chooser/templates/chooser.html @@ -0,0 +1,78 @@ +<!-- 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/. --> + +{% extends 'layout.html' %} +{% block content %} +<div class="container-fluid"> + <div class="row"> + <div class="col-8"> + <div class="form-group form-inline"> + <span class="col-form-label col-md-2 pt-1">Build Type</span> + <div class="form-check form-check-inline"> + <input id="both" class="filter form-check-input" type="radio" name="buildtype" value='{}' onchange="apply();" checked> + <label for="both" class="form-check-label">both</label> + </div> + {% for type in ["opt", "debug"] %} + <div class="form-check form-check-inline"> + <input id="{{ type }}" class="filter form-check-input" type="radio" name="buildtype" value='{"build_type": "{{ type }}"}' onchange="apply();"> + <label for={{ type }} class="form-check-label">{{ type }}</label> + </div> + {% endfor %} + </div> + <ul class="nav nav-tabs" id="tabbar" role="tablist"> + {% for section in sections %} + <li class="nav-item"> + {% if loop.first %} + <a class="nav-link active" id="{{ section.name }}-tab" data-toggle="tab" href="#{{section.name }}" role="tab" aria-controls="{{ section.name }}" aria-selected="true">{{ section.title }}</a> + {% else %} + <a class="nav-link" id="{{ section.name }}-tab" data-toggle="tab" href="#{{section.name }}" role="tab" aria-controls="{{ section.name }}" aria-selected="false">{{ section.title }}</a> + {% endif %} + </li> + {% endfor %} + </ul> + <div class="tab-content"> + <button type="button" class="btn btn-secondary" value="true" onclick="selectAll(this);">Select All</button> + <button type="button" class="btn btn-secondary" onclick="selectAll(this);">Deselect All</button> + {% for section in sections %} + {% if loop.first %} + <div class="tab-pane show active" id="{{ section.name }}" role="tabpanel" aria-labelledby="{{ section.name }}-tab"> + {% else %} + <div class="tab-pane" id="{{ section.name }}" role="tabpanel" aria-labelledby="{{ section.name }}-tab"> + {% endif %} + {% for label, meta in section.labels|dictsort %} + <label class="multiselect filter-label" for={{ label }}> + <span> + {{ label }} + <input class="filter" type="checkbox" id={{ label }} name="{{ section.kind }}" value='{{ meta.attrs|tojson|safe }}' onchange="console.log('checkbox onchange triggered');apply();"> + {% if meta.max_chunk > 1 %} + <input class="filter" type="text" pattern="[0-9][0-9,\-]*" placeholder="1-{{ meta.max_chunk }}" name='{{ meta.attrs|tojson|safe }}' oninput="applyChunks();"> + {% endif %} + </span> + </label> + {% endfor %} + </div> + {% endfor %} + </div> + </div> + <div class="col-4" id="preview"> + <form id="submit-tasks" action="" method="POST"> + <textarea id="selection" name="selected-tasks" wrap="off"></textarea> + <span id="selection-count">0 tasks selected</span><br> + <span id="buttons"> + <input id="cancel" class="btn btn-default" type="submit" name="action" value="Cancel"> + <input id="push" class="btn btn-default" type="submit" name="action" value="Push"> + </span> + </form> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> + const tasks = {{ tasks|tojson|safe }}; +</script> +<script src="{{ url_for('static', filename='filter.js') }}"></script> +<script src="{{ url_for('static', filename='select.js') }}"></script> +{% endblock %} diff --git a/tools/tryselect/selectors/chooser/templates/close.html b/tools/tryselect/selectors/chooser/templates/close.html new file mode 100644 index 0000000000..9dc0a161f3 --- /dev/null +++ b/tools/tryselect/selectors/chooser/templates/close.html @@ -0,0 +1,11 @@ +<!-- 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/. --> + +{% extends 'layout.html' %} {% block content %} +<div class="container-fluid"> + <div class="alert alert-primary" role="alert"> + You may now close this page. + </div> +</div> +{% endblock %} diff --git a/tools/tryselect/selectors/chooser/templates/layout.html b/tools/tryselect/selectors/chooser/templates/layout.html new file mode 100644 index 0000000000..8553ae94df --- /dev/null +++ b/tools/tryselect/selectors/chooser/templates/layout.html @@ -0,0 +1,71 @@ +<!-- 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/. --> + +<html> + <head> + <meta charset="utf-8" /> + <title>Try Chooser Enhanced</title> + <link + rel="stylesheet" + href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" + /> + <link + rel="stylesheet" + href="{{ url_for('static', filename='style.css') }}" + /> + </head> + <body> + <nav class="navbar navbar-default fixed-top navbar-dark bg-dark"> + <div class="container-fluid"> + <span class="navbar-brand mb-0 h1">Try Chooser Enhanced</span> + <button + class="navbar-toggler" + type="button" + data-toggle="collapse" + data-target="#navbarSupportedContent" + aria-controls="navbarSupportedContent" + aria-expanded="false" + aria-label="Toggle navigation" + > + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <a + class="nav-link" + href="https://firefox-source-docs.mozilla.org/tools/try/index.html" + >Documentation</a + > + </li> + <li class="nav-item"> + <a + class="nav-link" + href="https://treeherder.mozilla.org/#/jobs?repo=try" + >Treeherder</a + > + </li> + </ul> + </div> + </div> + </nav> + {% block content %}{% endblock %} + <script + src="https://code.jquery.com/jquery-3.3.1.slim.min.js" + integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" + crossorigin="anonymous" + ></script> + <script + src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" + integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" + crossorigin="anonymous" + ></script> + <script + src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" + integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" + crossorigin="anonymous" + ></script> + {% block scripts %}{% endblock %} + </body> +</html> |